From 6470152d4d02eb8fb1faeff376539b9546efba98 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 15 Jul 2025 08:25:40 +0300 Subject: [PATCH 01/79] feat: Start recode BREAKING CHANGE: Removed all old classes for the recode --- build.gradle | 183 ------ bukkit/build.gradle | 22 - .../skinoverlay/SkinOverlayBukkit.java | 219 ------- .../handler/handlers/SkinHandler_Legacy.java | 295 --------- .../bukkit/DeveloperInformListener.java | 19 - .../bukkit/PaperPlayerListeners.java | 33 - .../listeners/bukkit/PlayerListeners.java | 108 ---- .../utilities/BukkitPluginMessageUtils.java | 18 - .../utilities/player/PlayerObjectBukkit.java | 79 --- bungee/build.gradle | 10 - .../skinoverlay/SkinOverlayBungee.java | 195 ------ .../handlers/SkinHandler_BungeeCord.java | 72 --- .../handler/profile/SGameProfileBungee.java | 38 -- .../bungee/DeveloperInformListener.java | 19 - .../listeners/bungee/PlayerListeners.java | 65 -- .../BungeeCordPluginMessageUtils.java | 23 - .../utilities/player/PlayerObjectBungee.java | 78 --- core/build.gradle | 3 - .../georgev22/skinoverlay/SkinOverlay.java | 576 ------------------ .../commands/SkinOverlayCommand.java | 253 -------- .../georgev22/skinoverlay/event/Event.java | 58 -- .../skinoverlay/event/EventManager.java | 157 ----- .../skinoverlay/event/EventPriority.java | 62 -- .../skinoverlay/event/HandlerList.java | 264 -------- .../skinoverlay/event/ListenerWrapper.java | 116 ---- .../event/annotations/EventHandler.java | 42 -- .../player/PlayerObjectConnectionEvent.java | 65 -- .../events/player/PlayerObjectEvent.java | 72 --- .../events/player/PlayerObjectUserEvent.java | 74 --- .../PlayerSkinPartOptionsChangedEvent.java | 36 -- .../skin/PlayerObjectPreUpdateSkinEvent.java | 68 --- .../skin/PlayerObjectUpdateSkinEvent.java | 103 ---- .../events/profile/ProfileCreatedEvent.java | 86 --- .../profile/property/SPropertyAddEvent.java | 98 --- .../property/SPropertyRemoveEvent.java | 80 --- .../event/events/user/UserEvent.java | 74 --- .../events/user/data/UserModifyDataEvent.java | 57 -- .../user/data/add/UserAddDataEvent.java | 79 --- .../user/data/load/UserPostLoadEvent.java | 57 -- .../user/data/load/UserPreLoadEvent.java | 70 --- .../event/interfaces/Cancellable.java | 22 - .../event/interfaces/EventListener.java | 8 - .../exceptions/EventException.java | 20 - .../skinoverlay/exceptions/SkinException.java | 20 - .../skinoverlay/exceptions/UserException.java | 21 - .../skinoverlay/handler/SGameProfile.java | 187 ------ .../skinoverlay/handler/SProperty.java | 109 ---- .../skinoverlay/handler/SkinHandler.java | 575 ----------------- .../handlers/SkinHandler_Unsupported.java | 66 -- .../handler/profile/SGameProfileBukkit.java | 32 - .../handler/skin/MinecraftSkinRenderer.java | 31 - .../skinoverlay/handler/skin/Part.java | 94 --- .../skinoverlay/handler/skin/Section.java | 475 --------------- .../skinoverlay/handler/skin/SkinParts.java | 310 ---------- .../georgev22/skinoverlay/hook/SkinHook.java | 28 - .../skinoverlay/hook/hooks/SkinHookImpl.java | 43 -- .../hook/hooks/SkinsRestorerHook.java | 65 -- .../skinoverlay/listeners/DebugListeners.java | 125 ---- .../listeners/ObservableListener.java | 13 - .../listeners/PlayerListeners.java | 31 - .../skinoverlay/storage/data/Data.java | 53 -- .../skinoverlay/storage/data/Skin.java | 221 ------- .../skinoverlay/storage/data/User.java | 120 ---- .../storage/gsonAdapters/PartTypeAdapter.java | 87 --- .../gsonAdapters/SPropertyTypeAdapter.java | 79 --- .../SerializableBufferedImageTypeAdapter.java | 82 --- .../gsonAdapters/SkinPartsTypeAdapter.java | 78 --- .../storage/gsonAdapters/SkinTypeAdapter.java | 87 --- .../storage/gsonAdapters/UserTypeAdapter.java | 93 --- .../storage/manager/SkinManager.java | 254 -------- .../storage/manager/UserManager.java | 256 -------- .../skinoverlay/utilities/Locale.java | 75 --- .../utilities/PluginMessageUtils.java | 94 --- .../utilities/SerializableBufferedImage.java | 152 ----- .../skinoverlay/utilities/Updater.java | 124 ---- .../skinoverlay/utilities/Utilities.java | 311 ---------- .../utilities/config/FileManager.java | 27 - .../utilities/config/MessagesUtil.java | 149 ----- .../utilities/config/OptionsUtil.java | 204 ------- .../utilities/config/OverlayOptionsUtil.java | 140 ----- .../config/SkinConfigurationFile.java | 64 -- .../utilities/config/SkinFileCache.java | 154 ----- .../utilities/interfaces/ImageSupplier.java | 18 - .../utilities/interfaces/SkinOverlayImpl.java | 147 ----- .../utilities/player/PlayerObject.java | 453 -------------- gradle.properties | 28 - gradle/jacoco.gradle | 10 - gradle/publish.gradle | 103 ---- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 6 - gradlew | 244 -------- gradlew.bat | 92 --- multiver/build.gradle | 19 - multiver/mc_1_17_R1/build.gradle | 14 - .../handler/handlers/SkinHandler_1_17_R1.java | 122 ---- multiver/mc_1_18_R1/build.gradle | 14 - .../handler/handlers/SkinHandler_1_18_R1.java | 122 ---- multiver/mc_1_18_R2/build.gradle | 14 - .../handler/handlers/SkinHandler_1_18_R2.java | 122 ---- multiver/mc_1_19_R1/build.gradle | 16 - .../handler/handlers/SkinHandler_1_19_R1.java | 139 ----- multiver/mc_1_19_R2/build.gradle | 16 - .../handler/handlers/SkinHandler_1_19_R2.java | 137 ----- multiver/mc_1_19_R3/build.gradle | 16 - .../handler/handlers/SkinHandler_1_19_R3.java | 122 ---- multiver/mc_1_20_R1/build.gradle | 16 - .../handler/handlers/SkinHandler_1_20_R1.java | 126 ---- multiver/mc_1_20_R2/build.gradle | 16 - .../handler/handlers/SkinHandler_1_20_R2.java | 120 ---- multiver/mc_1_20_R3/build.gradle | 16 - .../handler/handlers/SkinHandler_1_20_R3.java | 121 ---- settings.gradle | 35 -- src/main/resources/bungee.yml | 7 - src/main/resources/config.yml | 134 ---- src/main/resources/messages_el.yml | 16 - src/main/resources/messages_en_US.yml | 16 - src/main/resources/messages_tr.yml | 16 - src/main/resources/plugin.yml | 21 - src/main/resources/skins-cfg/alley.yml | 15 - .../resources/skins-cfg/bubbo_transparent.yml | 15 - src/main/resources/skins-cfg/fire_demon.yml | 15 - src/main/resources/skins-cfg/flame.yml | 15 - src/main/resources/skins-cfg/frog_pijama.yml | 15 - src/main/resources/skins-cfg/glare.yml | 15 - src/main/resources/skins-cfg/hoodie.yml | 15 - src/main/resources/skins-cfg/migrator.yml | 15 - src/main/resources/skins-cfg/mustache.yml | 15 - src/main/resources/skins-cfg/pirate.yml | 15 - src/main/resources/skins-cfg/policeman.yml | 15 - src/main/resources/skins-cfg/smoking.yml | 15 - src/main/resources/skins/alley.png | Bin 2030 -> 0 bytes .../resources/skins/bubbo_transparent.png | Bin 720 -> 0 bytes src/main/resources/skins/fire_demon.png | Bin 3049 -> 0 bytes src/main/resources/skins/flame.png | Bin 1150 -> 0 bytes src/main/resources/skins/frog_pijama.png | Bin 1001 -> 0 bytes src/main/resources/skins/glare.png | Bin 1253 -> 0 bytes src/main/resources/skins/hoodie.png | Bin 1831 -> 0 bytes src/main/resources/skins/migrator.png | Bin 2551 -> 0 bytes src/main/resources/skins/mustache.png | Bin 575 -> 0 bytes src/main/resources/skins/pirate.png | Bin 2643 -> 0 bytes src/main/resources/skins/policeman.png | Bin 1792 -> 0 bytes src/main/resources/skins/smoking.png | Bin 900 -> 0 bytes velocity/build.gradle | 18 - .../skinoverlay/SkinOverlayVelocity.java | 239 -------- .../handlers/SkinHandler_Velocity.java | 72 --- .../handler/profile/SGameProfileVelocity.java | 36 -- .../velocity/DeveloperInformListener.java | 18 - .../listeners/velocity/PlayerListeners.java | 65 -- .../utilities/VelocityPluginMessageUtils.java | 24 - .../player/PlayerObjectVelocity.java | 274 --------- 150 files changed, 12765 deletions(-) delete mode 100644 build.gradle delete mode 100644 bukkit/build.gradle delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/DeveloperInformListener.java delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PaperPlayerListeners.java delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitPluginMessageUtils.java delete mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBukkit.java delete mode 100644 bungee/build.gradle delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/SkinOverlayBungee.java delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_BungeeCord.java delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBungee.java delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/DeveloperInformListener.java delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/listeners/bungee/PlayerListeners.java delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/utilities/BungeeCordPluginMessageUtils.java delete mode 100644 bungee/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectBungee.java delete mode 100644 core/build.gradle delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/commands/SkinOverlayCommand.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/Event.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/EventManager.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/EventPriority.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/HandlerList.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/ListenerWrapper.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/annotations/EventHandler.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectConnectionEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerObjectUserEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerSkinPartOptionsChangedEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectPreUpdateSkinEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/player/skin/PlayerObjectUpdateSkinEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/profile/ProfileCreatedEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyAddEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/profile/property/SPropertyRemoveEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/user/UserEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/UserModifyDataEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/add/UserAddDataEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPostLoadEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/events/user/data/load/UserPreLoadEvent.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/interfaces/Cancellable.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/event/interfaces/EventListener.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/exceptions/EventException.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/exceptions/UserException.java delete mode 100755 core/src/main/java/com/georgev22/skinoverlay/handler/SGameProfile.java delete mode 100755 core/src/main/java/com/georgev22/skinoverlay/handler/SProperty.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/SkinHandler.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Unsupported.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileBukkit.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/skin/MinecraftSkinRenderer.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/skin/Part.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/skin/Section.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/handler/skin/SkinParts.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/hook/SkinHook.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinHookImpl.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinsRestorerHook.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/listeners/DebugListeners.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/listeners/ObservableListener.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/data/Data.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/data/User.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/PartTypeAdapter.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinManager.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/storage/manager/UserManager.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/PluginMessageUtils.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/Updater.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/Utilities.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/SkinOverlayImpl.java delete mode 100644 core/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObject.java delete mode 100644 gradle.properties delete mode 100644 gradle/jacoco.gradle delete mode 100644 gradle/publish.gradle delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat delete mode 100644 multiver/build.gradle delete mode 100644 multiver/mc_1_17_R1/build.gradle delete mode 100644 multiver/mc_1_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.java delete mode 100644 multiver/mc_1_18_R1/build.gradle delete mode 100644 multiver/mc_1_18_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.java delete mode 100644 multiver/mc_1_18_R2/build.gradle delete mode 100644 multiver/mc_1_18_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R2.java delete mode 100644 multiver/mc_1_19_R1/build.gradle delete mode 100644 multiver/mc_1_19_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R1.java delete mode 100644 multiver/mc_1_19_R2/build.gradle delete mode 100644 multiver/mc_1_19_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R2.java delete mode 100644 multiver/mc_1_19_R3/build.gradle delete mode 100644 multiver/mc_1_19_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_19_R3.java delete mode 100644 multiver/mc_1_20_R1/build.gradle delete mode 100644 multiver/mc_1_20_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java delete mode 100644 multiver/mc_1_20_R2/build.gradle delete mode 100644 multiver/mc_1_20_R2/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R2.java delete mode 100644 multiver/mc_1_20_R3/build.gradle delete mode 100644 multiver/mc_1_20_R3/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R3.java delete mode 100644 settings.gradle delete mode 100644 src/main/resources/bungee.yml delete mode 100644 src/main/resources/config.yml delete mode 100644 src/main/resources/messages_el.yml delete mode 100644 src/main/resources/messages_en_US.yml delete mode 100644 src/main/resources/messages_tr.yml delete mode 100644 src/main/resources/plugin.yml delete mode 100644 src/main/resources/skins-cfg/alley.yml delete mode 100644 src/main/resources/skins-cfg/bubbo_transparent.yml delete mode 100644 src/main/resources/skins-cfg/fire_demon.yml delete mode 100644 src/main/resources/skins-cfg/flame.yml delete mode 100644 src/main/resources/skins-cfg/frog_pijama.yml delete mode 100644 src/main/resources/skins-cfg/glare.yml delete mode 100644 src/main/resources/skins-cfg/hoodie.yml delete mode 100644 src/main/resources/skins-cfg/migrator.yml delete mode 100644 src/main/resources/skins-cfg/mustache.yml delete mode 100644 src/main/resources/skins-cfg/pirate.yml delete mode 100644 src/main/resources/skins-cfg/policeman.yml delete mode 100644 src/main/resources/skins-cfg/smoking.yml delete mode 100644 src/main/resources/skins/alley.png delete mode 100644 src/main/resources/skins/bubbo_transparent.png delete mode 100644 src/main/resources/skins/fire_demon.png delete mode 100644 src/main/resources/skins/flame.png delete mode 100644 src/main/resources/skins/frog_pijama.png delete mode 100644 src/main/resources/skins/glare.png delete mode 100644 src/main/resources/skins/hoodie.png delete mode 100644 src/main/resources/skins/migrator.png delete mode 100644 src/main/resources/skins/mustache.png delete mode 100644 src/main/resources/skins/pirate.png delete mode 100644 src/main/resources/skins/policeman.png delete mode 100644 src/main/resources/skins/smoking.png delete mode 100644 velocity/build.gradle delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Velocity.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/handler/profile/SGameProfileVelocity.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/DeveloperInformListener.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/utilities/VelocityPluginMessageUtils.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/utilities/player/PlayerObjectVelocity.java 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/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/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java deleted file mode 100644 index fabb11f9..00000000 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ /dev/null @@ -1,219 +0,0 @@ -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.listeners.bukkit.PlayerListeners; -import com.georgev22.skinoverlay.utilities.BukkitPluginMessageUtils; -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.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.library.minecraft.BukkitMinecraftUtils.MinecraftVersion.*; - -@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; - - @Override - public void onLoad() { - try { - if (getCurrentVersion().isBelow(V1_16_R3)) { - this.libraryLoader = new LibraryLoader(this.getClassLoader(), this.getDataFolder()); - this.libraryLoader.loadAll(this, true); - } - } catch (InvalidDependencyException | UnknownDependencyException e) { - throw new RuntimeException(e); - } - skinOverlay = new SkinOverlay(this); - skinOverlay.onLoad(); - } - - @Override - public void onEnable() { - if (BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion().isBelow(V1_16_R1)) - this.adventure = BukkitAudiences.create(this); - if (isFolia()) { - this.scheduler = new MinecraftFoliaScheduler(); - } else { - this.scheduler = new MinecraftBukkitScheduler(); - } - this.skinOverlay.setMinecraftScheduler(this.scheduler); - switch (getCurrentVersion()) { - case V1_17_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_17_R1()); - case V1_18_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_18_R1()); - case V1_18_R2 -> skinOverlay.setSkinHandler(new SkinHandler_1_18_R2()); - case V1_19_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_19_R1()); - case V1_19_R2 -> skinOverlay.setSkinHandler(new SkinHandler_1_19_R2()); - case V1_19_R3 -> skinOverlay.setSkinHandler(new SkinHandler_1_19_R3()); - case V1_20_R1 -> skinOverlay.setSkinHandler(new SkinHandler_1_20_R1()); - case V1_20_R2 -> skinOverlay.setSkinHandler(new SkinHandler_1_20_R2()); - case V1_20_R3 -> skinOverlay.setSkinHandler(new SkinHandler_1_20_R3()); - case UNKNOWN -> skinOverlay.setSkinHandler(new SkinHandler_Unsupported()); - default -> skinOverlay.setSkinHandler(new SkinHandler_Legacy()); - } - skinOverlay.setCommandManager(new PaperCommandManager(this)); - skinOverlay.onEnable(); - BukkitMinecraftUtils.registerListeners(this, new PlayerListeners(), new DeveloperInformListener()); - if (PaperLib.isPaper() & getCurrentVersion().isAboveOrEqual(V1_15_R1)) - BukkitMinecraftUtils.registerListeners(this, new PaperPlayerListeners()); - if (OptionsUtil.PROXY.getBooleanValue()) { - skinOverlay.setPluginMessageUtils(new BukkitPluginMessageUtils()); - Bukkit.getServer().getMessenger().registerIncomingPluginChannel(this, "skinoverlay:bungee", new PlayerListeners()); - Bukkit.getServer().getMessenger().registerOutgoingPluginChannel(this, "skinoverlay:message"); - } - if (OptionsUtil.METRICS.getBooleanValue()) - new Metrics(this, 17474); - if (!PaperLib.isPaper()) - PaperLib.suggestPaper(this); - } - - @Override - public void onDisable() { - Bukkit.getServer().getMessenger().unregisterIncomingPluginChannel(this); - Bukkit.getServer().getMessenger().unregisterOutgoingPluginChannel(this); - skinOverlay.onDisable(); - if (this.adventure != null) { - this.adventure.close(); - this.adventure = null; - } - if (getCurrentVersion().isBelow(V1_16_R3)) { - try { - this.libraryLoader.unloadAll(); - } catch (InvalidDependencyException e) { - throw new RuntimeException(e); - } - } - } - - - @Override - public Type type() { - return Type.BUKKIT; - } - - @Override - public File dataFolder() { - return this.getDataFolder(); - } - - @Override - public Logger logger() { - return this.getLogger(); - } - - @Override - public Description description() { - return new Description(this.getName(), this.getDescription().getVersion(), this.getDescription().getMain(), this.getDescription().getAuthors()); - } - - @Override - public boolean enable(boolean enable) { - this.setEnabled(enable); - return enabled(); - } - - @Override - public boolean enabled() { - return this.isEnabled(); - } - - @Override - public void saveResource(@NotNull String resource, boolean replace) { - super.saveResource(resource, replace); - } - - @Override - public boolean onlineMode() { - return Bukkit.getOnlineMode(); - } - - private final ObservableObjectMap players = new ObservableObjectMap<>(); - - @Override - public ObservableObjectMap onlinePlayers() { - for (Player player : Bukkit.getOnlinePlayers()) { - if (players.containsKey(player.getUniqueId())) { - continue; - } - players.append(player.getUniqueId(), new PlayerObjectBukkit(player)); - } - 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(); - } - - @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; - } - } -} \ 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/handler/handlers/SkinHandler_Legacy.java deleted file mode 100644 index 000118b4..00000000 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_Legacy.java +++ /dev/null @@ -1,295 +0,0 @@ -package com.georgev22.skinoverlay.handler.handlers; - -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.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.NotNull; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -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.*; - -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinHandler_Legacy extends SkinHandler_Unsupported { - private final Class playOutRespawn; - private final Class playOutPlayerInfo; - private final Class playOutPosition; - private final Class packet; - private final Class playOutHeldItemSlot; - private final Method getHandleMethod; - private Enum removePlayerEnum; - private Enum addPlayerEnum; - - public SkinHandler_Legacy() { - try { - packet = getNMSClass("Packet", "net.minecraft.network.protocol.Packet"); - playOutHeldItemSlot = getNMSClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot"); - playOutPosition = getNMSClass("PacketPlayOutPosition", "net.minecraft.network.protocol.game.PacketPlayOutPosition"); - playOutPlayerInfo = getNMSClass("PacketPlayOutPlayerInfo", "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo"); - playOutRespawn = getNMSClass("PacketPlayOutRespawn", "net.minecraft.network.protocol.game.PacketPlayOutRespawn"); - - try { - removePlayerEnum = getEnum(playOutPlayerInfo, "EnumPlayerInfoAction", "REMOVE_PLAYER"); - addPlayerEnum = getEnum(playOutPlayerInfo, "EnumPlayerInfoAction", "ADD_PLAYER"); - } catch (Exception e1) { - try { - Class enumPlayerInfoActionClass = Class.forName("net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo$EnumPlayerInfoAction"); - - removePlayerEnum = getEnum(enumPlayerInfoActionClass, 4); - addPlayerEnum = getEnum(enumPlayerInfoActionClass, 0); - } catch (Exception e2) { - try { - removePlayerEnum = getEnum(playOutPlayerInfo, "Action", "REMOVE_PLAYER"); - addPlayerEnum = getEnum(playOutPlayerInfo, "Action", "ADD_PLAYER"); - } catch (Exception e3) { - try { - Class enumPlayerInfoAction = getNMSClass("EnumPlayerInfoAction", null); - - removePlayerEnum = getEnum(enumPlayerInfoAction, "REMOVE_PLAYER"); - addPlayerEnum = getEnum(enumPlayerInfoAction, "ADD_PLAYER"); - } catch (Exception e4) { - //ignore - } - } - } - } - - getHandleMethod = getOBCClass("entity.CraftPlayer").getDeclaredMethod("getHandle"); - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - - @Override - public CompletableFuture updateSkin(@NotNull PlayerObject playerObject, @NotNull Skin skin) { - return CompletableFuture.supplyAsync(() -> { - try { - Player player = playerObject.player(); - - final Object entityPlayer = getHandleMethod.invoke(player); - Object removePlayer; - Object addPlayer; - try { - removePlayer = invokeConstructor(playOutPlayerInfo, removePlayerEnum, ImmutableList.of(entityPlayer)); - 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); - } - - Object world; - try { - world = fetchMethodAndInvoke(entityPlayer.getClass(), "getWorld", entityPlayer, new Object[0], new Class[0]); - } catch (Exception ignore) { - world = fetchMethodAndInvoke(entityPlayer.getClass(), "getWorldServer", entityPlayer, new Object[0], new Class[0]); - } - Object difficulty; - try { - difficulty = fetchMethodAndInvoke(world.getClass(), "getDifficulty", world, new Object[0], new Class[0]); - } catch (Exception e) { - try { - difficulty = fetchField(world.getClass(), world, "difficulty"); - } catch (NoSuchFieldException ignore) { - skinOverlay.getLogger().info(world.getClass().getSimpleName() + " does not have getDifficulty method or difficulty field!!"); - difficulty = null; - } - - } - - Object worldData = null; - try { - worldData = fetchMethodAndInvoke(world.getClass(), "getWorldData", world, new Object[0], new Class[0]); - } catch (Exception ignored) { - try { - worldData = fetchField(world.getClass(), world, "worldData"); - } catch (Exception ignored2) { - skinOverlay.getLogger().info(world.getClass().getSimpleName() + " does not have getWorldData method or worldData field!!"); - } - } - - Object worldType = null; - try { - worldType = fetchMethodAndInvoke(Objects.requireNonNull(worldData).getClass(), "getType", worldData, new Object[0], new Class[0]); - } catch (Exception ignored) { - try { - worldType = fetchMethodAndInvoke(Objects.requireNonNull(worldData).getClass(), "getGameType", worldData, new Object[0], new Class[0]); - } catch (Exception ignored2) { - } - } - - 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(); - - Object respawn; - try { - respawn = invokeConstructor(playOutRespawn, dimension, difficulty, worldType, enumGamemode); - } catch (Exception ignored) { - Object worldObject = getFieldByType(entityPlayer, "World"); - Object dimensionManager = null; - try { - dimensionManager = getFieldByType(worldObject, "DimensionManager"); - } catch (ReflectionException e) { - try { - Class dimensionManagerClass = getNMSClass("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager"); - - for (Method m : dimensionManagerClass.getDeclaredMethods()) { - if (m.getReturnType() == dimensionManagerClass && m.getParameterCount() == 1 && m.getParameterTypes()[0] == Integer.TYPE) { - m.setAccessible(true); - dimensionManager = m.invoke(null, dimension); - } - } - } catch (InvocationTargetException | IllegalAccessException | ClassNotFoundException e2) { - throw new RuntimeException(e2); - } - } - - if (dimensionManager == null) { - throw new ReflectionException("Could not get DimensionManager from " + worldObject.getClass().getSimpleName()); - } - - try { - respawn = invokeConstructor(playOutRespawn, dimensionManager, difficulty, worldType, enumGamemode); - } catch (Exception ignored2) { - try { - respawn = invokeConstructor(playOutRespawn, dimensionManager, worldType, enumGamemode); - } catch (Exception ignored3) { - long seedEncrypted = Hashing.sha256().hashString(String.valueOf(player.getWorld().getSeed()), StandardCharsets.UTF_8).asLong(); - try { - respawn = invokeConstructor(playOutRespawn, dimensionManager, seedEncrypted, worldType, enumGamemode); - } catch (Exception ignored5) { - Object dimensionKey = fetchMethodAndInvoke(worldObject.getClass(), "getDimensionKey", worldObject, new Object[0], new Class[0]); - boolean debug = (boolean) fetchMethodAndInvoke(worldObject.getClass(), "isDebugWorld", worldObject, new Object[0], new Class[0]); - boolean flat = (boolean) fetchMethodAndInvoke(worldObject.getClass(), "isFlatWorld", worldObject, new Object[0], new Class[0]); - List gameModeList = getFieldByTypeList(playerIntManager, "EnumGamemode"); - - Enum enumGamemodePrevious = null; - for (Object obj : gameModeList) { - if (obj != enumGamemode) - enumGamemodePrevious = (Enum) obj; - } - - try { - Object typeKey = fetchMethodAndInvoke(worldObject.getClass(), "getTypeKey", worldObject, new Object[0], new Class[0]); - - respawn = invokeConstructor(playOutRespawn, typeKey, dimensionKey, seedEncrypted, enumGamemode, enumGamemodePrevious, debug, flat, true); - } catch (Exception ignored6) { - respawn = invokeConstructor(playOutRespawn, dimensionManager, dimensionKey, seedEncrypted, enumGamemode, enumGamemodePrevious, debug, flat, true); - } - } - } - } - } - - Location l = player.getLocation(); - Object pos; - try { - pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), new HashSet>(), 0); - } catch (Exception e1) { - try { - pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), new HashSet>()); - } catch (Exception e3) { - pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), false); - } - } - - Object slot = invokeConstructor(playOutHeldItemSlot, player.getInventory().getHeldItemSlot()); - Object playerConnection = getFieldByType(entityPlayer, "PlayerConnection"); - - sendPacket(playerConnection, removePlayer); - sendPacket(playerConnection, addPlayer); - - sendPacket(playerConnection, respawn); - - fetchMethodAndInvoke(entityPlayer.getClass(), "updateAbilities", entityPlayer, new Object[0], new Class[0]); - - sendPacket(playerConnection, pos); - sendPacket(playerConnection, slot); - - fetchMethodAndInvoke(player.getClass(), "updateScaledHealth", player, new Object[0], new Class[0]); - player.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); - }); - } - return true; - } catch (ReflectionException | InvocationTargetException | IllegalAccessException | NoSuchMethodException | - NoSuchFieldException e) { - throw new RuntimeException(e); - } - }, runnable -> this.skinOverlay.getMinecraftScheduler().runTask(this.skinOverlay.getSkinOverlay().plugin(), 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/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 deleted file mode 100644 index b46eba1f..00000000 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java +++ /dev/null @@ -1,108 +0,0 @@ -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 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; - -import static com.georgev22.skinoverlay.utilities.Utilities.decrypt; - -@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; - } - 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); - } - } - } - -} - 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/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/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/SkinHandler.java b/core/src/main/java/com/georgev22/skinoverlay/handler/SkinHandler.java deleted file mode 100644 index eb254fd9..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/SkinHandler.java +++ /dev/null @@ -1,575 +0,0 @@ -package com.georgev22.skinoverlay.handler; - -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.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.georgev22.skinoverlay.utilities.Utilities; -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.google.gson.*; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.mineskin.MineskinClient; -import org.mineskin.data.Texture; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.Base64; -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 { - - protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - 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()); - } - - /** - * 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 { - 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; - } - } - - 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; - } - - FileConfiguration fileConfiguration = skinConfigurationFile.getFileConfiguration(); - - BufferedImage currentSkin = this.getSkinImage(this.getProfileBytes(playerObject, this.skinOverlay.getSkinHook().getProperty(playerObject))); - - 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")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_JACKET.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Hat")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_HAT.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Left_Sleeve")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_LEFT_SLEEVE.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Right_Sleeve")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_RIGHT_SLEEVE.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Left_Pants")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_LEFT_PANTS.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Right_Pants")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_RIGHT_PANTS.getBooleanValue(fileConfiguration)) { - continue; - } - } - overlaySkinParts.add(part); - } - - List newSkinParts = new ArrayList<>(); - for (Part part : currentSkinParts.getParts().values()) { - if (part.name().startsWith("Jacket")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_JACKET.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Hat")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_HAT.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Left_Sleeve")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_LEFT_SLEEVE.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Right_Sleeve")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_RIGHT_SLEEVE.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Left_Pants")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_LEFT_PANTS.getBooleanValue(fileConfiguration)) { - continue; - } - } - if (part.name().startsWith("Right_Pants")) { - if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_RIGHT_PANTS.getBooleanValue(fileConfiguration)) { - continue; - } - } - newSkinParts.add(part); - } - - MinecraftSkinRenderer defaultSkinRenderer = new MinecraftSkinRenderer(newSkinParts.toArray(Part[]::new)); - defaultSkinRenderer.createFullSkinImage(); - - MinecraftSkinRenderer overlaySkinRenderer = new MinecraftSkinRenderer(overlaySkinParts.toArray(Part[]::new)); - overlaySkinRenderer.createFullSkinImage(); - - BufferedImage skinToBeGenerated = new BufferedImage(currentSkin.getWidth(), currentSkin.getHeight(), 2); - Graphics2D canvas = skinToBeGenerated.createGraphics(); - - canvas.drawImage(defaultSkinRenderer.getFullSkinImage().getBufferedImage(), 0, 0, null); - 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; - } - skinOverlay.getSkinManager().save(skin); - return skin; - } catch (IOException | ExecutionException | InterruptedException | RuntimeException exception) { - skinOverlay.getLogger().log(Level.SEVERE, "Error generating or retrieving the skin:", exception); - return null; - } - } - }); - } - - 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 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[] getProfileBytes(@NotNull final PlayerObject playerObject, @Nullable SProperty property) throws IOException, ExecutionException, InterruptedException { - return playerObject.isBedrock() ? this.getBedrockProfileBytes(playerObject, property) : this.getJavaProfileBytes(playerObject, property); - } - - /** - * Retrieves Java {@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[] getJavaProfileBytes(@NotNull final PlayerObject playerObject, @Nullable SProperty property) throws IOException, ExecutionException, InterruptedException { - return property != null ? - new ByteArrayInputStream(this.createJsonFromProperty(playerObject, property) - .getAsJsonObject().toString().getBytes()).readAllBytes() : - new Utilities.Request() - .openConnection( - String.format( - "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false", - (skinOverlay.isOnlineMode() ? - playerObject.playerUUID() : - getUUID(playerObject.playerName())) - .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 - * - * @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 - */ - 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); - } - - /** - * 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} - * @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); - } - - /** - * Retrieves a skin image from the provided SProperty. - * - * @param sProperty The SProperty containing the skin information. - * @return The BufferedImage representing the player's skin. - * @throws IOException If an I/O error occurs while fetching the image. - */ - public BufferedImage getSkinImage(final @NotNull SProperty sProperty) throws IOException { - 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 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. - * - * @param profileBytes The profile bytes containing skin information. - * @return The BufferedImage representing the player's skin. - * @throws IOException If an I/O error occurs while fetching the image. - */ - public BufferedImage getSkinImage(final byte @NotNull [] profileBytes) throws IOException { - JsonElement json = JsonParser.parseString(new String(profileBytes)); - JsonArray properties = json.getAsJsonObject().getAsJsonArray("properties"); - - JsonElement textures = findTexturesProperty(properties); - if (textures == null) { - skinOverlay.getLogger().log(Level.SEVERE, "Property object 'textures' not found", new SkinException("Property object 'textures' not found")); - return null; - } - - String base64Texture = textures.getAsJsonObject().get("value").getAsString(); - String decodedTexture = new String(Base64.getDecoder().decode(base64Texture)); - - JsonElement textureJson = JsonParser.parseString(decodedTexture); - String skinUrl = textureJson.getAsJsonObject() - .getAsJsonObject("textures") - .getAsJsonObject("SKIN") - .get("url") - .getAsString(); - - return ImageIO.read(new URL(skinUrl)); - } - - /** - * Find the 'textures' property within a JsonArray of properties. - * - * @param properties The JsonArray of properties to search for 'textures'. - * @return The JsonElement representing the 'textures' property, or null if not found. - */ - public JsonElement findTexturesProperty(@NotNull JsonArray properties) { - for (JsonElement property : properties) { - if (property.isJsonObject() && "textures".equals(property.getAsJsonObject().get("name").getAsString())) { - return property; - } - } - return null; - } - - /** - * This method checks if a Minecraft username is a premium account. - * A premium account is one that has paid for the game. - * - * @param username The Minecraft username to check. - * @return True if the username is a premium account, false otherwise. - */ - public boolean isUsernamePremium(String username) { - try { - URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + username); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("GET"); - con.setConnectTimeout(5000); - con.setReadTimeout(5000); - - int status = con.getResponseCode(); - if (status == 200) { - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuilder content = new StringBuilder(); - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - } - in.close(); - - Gson gson = new Gson(); - JsonObject jsonObject = gson.fromJson(content.toString(), JsonObject.class); - return jsonObject != null && jsonObject.has("id") && jsonObject.has("name"); - } else { - con.disconnect(); - return false; - } - } catch (Exception e) { - skinOverlay.getLogger().log(Level.SEVERE, "Unable to check if username " + username + " is a premium account.", e); - return false; - } - } -} 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/Part.java b/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Part.java deleted file mode 100644 index c9ee4841..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/Part.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.georgev22.skinoverlay.handler.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; -import org.jetbrains.annotations.NotNull; - -import java.util.HashMap; -import java.util.Map; - -/** - * 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 { - - /** - * Returns a string representation of the Part object. - * - * @return A string representation of the Part object. - */ - @Contract(pure = true) - @Override - public @NotNull String toString() { - return "Part{" + - "name='" + name + '\'' + - ", image=" + image + - ", x=" + x + - ", y=" + y + - ", width=" + width + - ", height=" + height + - ", isEmpty=" + isEmpty + - '}'; - } - - /** - * Serializes the Part object to a map for YAML serialization. - * - * @return A map containing the serialized data. - */ - @Override - public @NotNull Map serialize() { - Map map = new HashMap<>(); - map.put("name", name); - map.put("bufferedImage", image); - map.put("x", x); - map.put("y", y); - map.put("width", width); - map.put("height", height); - map.put("isEmpty", isEmpty); - return map; - } - - /** - * Deserializes the YAML-compatible map to a Part object. - * - * @param map The YAML-compatible map containing part information. - * @return The deserialized Part object. - */ - @Contract("_ -> new") - public static @NotNull Part deserialize(@NotNull Map map) { - return new Part( - (String) map.get("name"), - (SerializableBufferedImage) map.get("bufferedImage"), - (int) map.get("x"), - (int) map.get("y"), - (int) map.get("width"), - (int) map.get("height"), - (boolean) map.get("isEmpty") - ); - } - - /** - * Serializes the Part 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 Part object. - * - * @param json The JSON string to deserialize. - * @return The deserialized Part object. - */ - public static Part fromJson(String json) { - return SkinOverlay.getInstance().getGson().fromJson(json, Part.class); - } -} \ No newline at end of file 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/handler/skin/SkinParts.java b/core/src/main/java/com/georgev22/skinoverlay/handler/skin/SkinParts.java deleted file mode 100644 index c0b2a7ec..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/handler/skin/SkinParts.java +++ /dev/null @@ -1,310 +0,0 @@ -package com.georgev22.skinoverlay.handler.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.utilities.SerializableBufferedImage; -import lombok.Getter; -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 { - - private static final SerializableBufferedImage steveSkin; - - static { - try { - URL url = new URL("https://s.namemc.com/i/12b92a9206470fe2.png"); - 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); - } - } catch (IOException e) { - steveSkin = null; - throw new RuntimeException(e); - } - steveSkin = new SerializableBufferedImage(ImageIO.read(new ByteArrayInputStream(output.toByteArray()))); - - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private SerializableBufferedImage fullSkin; - - private String skinName; - - private final Map parts; - - /** - * Default constructor for SkinParts using the Steve skin. - */ - public SkinParts() { - this(steveSkin, "Steve"); - } - - /** - * Constructor for SkinParts with a specified full skin and skin name. - * - * @param fullSkin The full skin image. - * @param skinName The name of the skin. - */ - public SkinParts(SerializableBufferedImage fullSkin, String skinName) { - this.parts = new HashMap<>(); - if (fullSkin != null) { - this.fullSkin = fullSkin; - } - if (skinName != null) { - this.skinName = skinName; - } - createParts(); - } - - /** - * Creates different parts based on the full skin image. - */ - public void createParts() { - if (fullSkin == null) { - return; - } - if (isOldSkin()) { - 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())); - } - - /** - * Creates a part image based on the specified section. - * - * @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(); - - BufferedImage partImage = fullSkin.getBufferedImage().getSubimage(x, y, width, height); - int[] pixelData = partImage.getRGB(0, 0, width, height, null, 0, width); - - boolean isAreaTransparent = true; - for (int pixel : pixelData) { - int alpha = (pixel >> 24) & 0xFF; - if (alpha > 0) { - isAreaTransparent = false; - break; - } - } - - String partName = section.getClass().getSimpleName(); - return new Part(partName, new SerializableBufferedImage(partImage), x, y, width, height, isAreaTransparent); - } - - /** - * Retrieves a specific part by name. - * - * @param partName The name of the part. - * @return The Part object corresponding to the specified name. - */ - public Part getPart(String partName) { - return parts.get(partName); - } - - /** - * Saves images of all parts to the specified output directory. - * - * @param outputDirectory The directory to save the part images. - */ - public void savePartImages(File outputDirectory) { - for (Part part : parts.values()) { - String fileName = part.name() + ".png"; - try { - ImageIO.write(part.image().getBufferedImage(), "png", new File(outputDirectory, fileName)); - } catch (IOException e) { - System.out.println("Failed to save part image: " + fileName); - System.out.println(e.getMessage()); - } - } - } - - /** - * Checks if the skin is an old format (64x32). - * - * @return True if the skin is in the old format, false otherwise. - */ - public boolean isOldSkin() { - BufferedImage image = this.fullSkin.getBufferedImage(); - return image.getWidth() == 64 && image.getHeight() == 32; - } - - /** - * Converts an old format skin to the new format (64x64). - * - * @param original The original skin image in old format. - * @return The converted skin image in new format. - */ - public SerializableBufferedImage convertSkin(@NotNull SerializableBufferedImage original) { - BufferedImage orig = original.getBufferedImage(); - BufferedImage newImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); - - // Copy top half of the original image - newImage.getGraphics().drawImage(orig, 0, 0, 64, 32, null); - - // Copy left leg from the original image - BufferedImage leftLeg = orig.getSubimage(0, 16, 16, 16); - newImage.getGraphics().drawImage(leftLeg, 16, 48, null); - - // Copy right leg from the original image - BufferedImage rightLeg = orig.getSubimage(40, 16, 16, 16); - newImage.getGraphics().drawImage(rightLeg, 32, 48, null); - - return new SerializableBufferedImage(newImage); - } - - - /** - * 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); - } - - /** - * Deserializes the YAML-compatible map to a SkinParts object. - * - * @param map The YAML-compatible map containing skin information. - * @return The deserialized SkinParts object. - */ - public static @NotNull SkinParts deserialize(@NotNull Map map) { - return new SkinParts((SerializableBufferedImage) map.get("bufferedImage"), (String) map.get("skinName")); - } - - - /** - * Serializes the SkinParts 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 SkinParts object. - * - * @param json The JSON string to deserialize. - * @return The deserialized SkinParts object. - */ - public static SkinParts fromJson(String json) { - return SkinOverlay.getInstance().getGson().fromJson(json, SkinParts.class); - } - - /** - * Returns a string representation of the SkinParts object. - * - * @return A string representation of the SkinParts object. - */ - @Override - public String toString() { - return "SkinParts{" + - "fullSkin=" + fullSkin + - ", parts=" + parts + - '}'; - } -} 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/hook/hooks/SkinsRestorerHook.java b/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinsRestorerHook.java deleted file mode 100644 index b9472041..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/hook/hooks/SkinsRestorerHook.java +++ /dev/null @@ -1,65 +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 net.skinsrestorer.api.SkinsRestorer; -import net.skinsrestorer.api.SkinsRestorerProvider; -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 - * data for players using the SkinsRestorer plugin. - */ -@ApiStatus.Internal -@ApiStatus.NonExtendable -public class SkinsRestorerHook implements SkinHook { - /** - * SkinsRestorerAPI object for accessing the SkinsRestorer API. - */ - private final SkinsRestorer skinsRestorerAPI; - - /** - * SkinOverlay object for accessing the SkinOverlay plugin. - */ - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - /** - * Constructor for SkinsRestorerHook class. - * Initializes the SkinsRestorerAPI object. - */ - public SkinsRestorerHook() { - skinsRestorerAPI = SkinsRestorerProvider.get(); - } - - /** - * Retrieves the skin property data for the given player using the SkinsRestorer plugin. - * 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 - * @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 { - 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); - } - } - -} 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/gsonAdapters/PartTypeAdapter.java b/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/PartTypeAdapter.java deleted file mode 100644 index 2ca4071b..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/PartTypeAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; - -import com.georgev22.skinoverlay.handler.skin.Part; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.google.gson.*; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Type; - -/** - * Gson TypeAdapter for serializing and deserializing {@link Part} objects. - *

- * This adapter is responsible for converting {@link Part} objects to and from JSON format. - * It handles the serialization and deserialization of the part's name, image, position, dimensions, and emptiness status. - */ -public class PartTypeAdapter implements JsonSerializer, JsonDeserializer { - - /** - * Deserializes the JSON element to a Part object. - * - * @param jsonElement The JSON element containing part information. - * @param typeOfT The type of the Object to deserialize to. - * @param jsonDeserializationContext The context for deserialization. - * @return The deserialized Part object. - * @throws JsonParseException If there is an issue during the deserialization process. - */ - @Override - public Part deserialize(@NotNull JsonElement jsonElement, Type typeOfT, @NotNull JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - - String name = jsonObject.get("name").getAsString(); - SerializableBufferedImage image = jsonDeserializationContext.deserialize(jsonObject.get("bufferedImage"), SerializableBufferedImage.class); - int x = jsonObject.get("x").getAsInt(); - int y = jsonObject.get("y").getAsInt(); - int width = jsonObject.get("width").getAsInt(); - int height = jsonObject.get("height").getAsInt(); - boolean isEmpty = jsonObject.get("isEmpty").getAsBoolean(); - - return new Part(name, image, x, y, width, height, isEmpty); - } - - /** - * Serializes the Part object to a JSON element. - * - * @param src The Part object to serialize. - * @param typeOfSrc The actual type (fully genericized version) of the source object. - * @param jsonSerializationContext The context for serialization. - * @return The serialized JSON element. - */ - @Override - public JsonElement serialize(@NotNull Part src, Type typeOfSrc, @NotNull JsonSerializationContext jsonSerializationContext) { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("name", src.name()); - jsonObject.add("bufferedImage", jsonSerializationContext.serialize(src.image())); - jsonObject.addProperty("x", src.x()); - jsonObject.addProperty("y", src.y()); - jsonObject.addProperty("width", src.width()); - jsonObject.addProperty("height", src.height()); - jsonObject.addProperty("isEmpty", src.isEmpty()); - return jsonObject; - } - - /** - * Converts a {@link Part} object to its JSON representation. - * - * @param part The {@link Part} object to be converted. - * @return A JSON string representing the serialized {@link Part} object. - * @throws IllegalArgumentException If the provided {@link Part} object is null. - */ - public static String toJson(Part part) { - if (part == null) throw new IllegalArgumentException("Part cannot be null"); - return part.toJson(); - } - - /** - * Converts a JSON string to a {@link Part} object. - * - * @param json The JSON string representing the serialized {@link Part} object. - * @return A {@link Part} object deserialized from the provided JSON string. - * @throws IllegalArgumentException If the provided JSON string is null or empty. - */ - public static Part fromJson(String json) { - if (json == null) throw new IllegalArgumentException("Json cannot be null"); - if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); - return Part.fromJson(json); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java b/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java deleted file mode 100644 index 33fba3c3..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SPropertyTypeAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; - -import com.georgev22.skinoverlay.handler.SProperty; -import com.google.gson.*; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Type; - -/** - * Gson TypeAdapter for serializing and deserializing {@link SProperty} objects. - *

- * This adapter is responsible for converting {@link SProperty} objects to and from JSON format. - * It handles the serialization and deserialization of the property's name, value, and signature. - */ -public class SPropertyTypeAdapter implements - JsonSerializer, - JsonDeserializer { - - /** - * Deserializes the JSON element to an {@code SProperty}. - * - * @param jsonElement The JSON element containing name, value, and signature information. - * @param typeOfT The type of the Object to deserialize to. - * @param jsonDeserializationContext The context for deserialization. - * @return The deserialized {@code SProperty}. - */ - @Override - 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); - } - - /** - * Serializes the {@code SProperty} to a JSON element. - * - * @param src The {@code SProperty} to serialize. - * @param typeOfSrc The actual type (fully genericized version) of the source object. - * @param jsonSerializationContext The context for serialization. - * @return The serialized JSON element. - */ - @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; - } - - /** - * Converts a JSON string to an {@code SProperty} object. - * - * @param json The JSON string representing the serialized {@link SProperty} object. - * @return An {@link SProperty} object deserialized from the provided JSON string. - * @throws IllegalArgumentException If the provided JSON string is null or empty. - */ - 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); - } - - /** - * Converts an {@code SProperty} object to its JSON representation. - * - * @param sProperty The {@code SProperty} object to be converted. - * @return A JSON string representing the serialized {@code SProperty} object. - * @throws IllegalArgumentException If the provided {@code SProperty} object is null. - */ - public static String toJson(SProperty sProperty) { - if (sProperty == null) throw new IllegalArgumentException("SProperty cannot be null"); - return sProperty.toJson(); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java b/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java deleted file mode 100644 index f8aec797..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SerializableBufferedImageTypeAdapter.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; - -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.google.gson.*; -import org.jetbrains.annotations.NotNull; - -import java.awt.image.BufferedImage; -import java.lang.reflect.Type; -import java.util.Base64; - -/** - * Gson TypeAdapter for serializing and deserializing {@link SerializableBufferedImage} objects. - *

- * This adapter is responsible for converting {@link SerializableBufferedImage} objects to and from JSON format. - * It handles the serialization and deserialization of the image's width, height, and pixel information. - */ -public class SerializableBufferedImageTypeAdapter implements - JsonSerializer, - JsonDeserializer { - - /** - * Deserializes the JSON element to a {@link SerializableBufferedImage}. - * - * @param jsonElement The JSON element containing width, height, and pixel information. - * @param type The type to deserialize. - * @param jsonDeserializationContext The context for deserialization. - * @return The deserialized {@link SerializableBufferedImage}. - */ - @Override - public @NotNull SerializableBufferedImage deserialize(@NotNull JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - - int width = jsonObject.get("width").getAsInt(); - int height = jsonObject.get("height").getAsInt(); - String encodedPixels = jsonObject.get("pixels").getAsString(); - int[] pixels = SerializableBufferedImage.fromByteArray(Base64.getDecoder().decode(encodedPixels)); - - BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - bufferedImage.setRGB(0, 0, width, height, pixels, 0, width); - - return new SerializableBufferedImage(bufferedImage); - } - - /** - * Serializes the {@link SerializableBufferedImage} to a JSON element. - * - * @param serializableBufferedImage The {@link SerializableBufferedImage} to serialize. - * @param type The type to serialize. - * @param jsonSerializationContext The context for serialization. - * @return The serialized JSON element. - */ - @Override - public JsonElement serialize(@NotNull SerializableBufferedImage serializableBufferedImage, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { - return jsonSerializationContext.serialize(serializableBufferedImage.serialize()); - } - - /** - * Converts a {@link SerializableBufferedImage} object to its JSON representation. - * - * @param serializableBufferedImage The {@link SerializableBufferedImage} object to be converted. - * @return A JSON string representing the serialized {@link SerializableBufferedImage} object. - * @throws IllegalArgumentException If the provided {@link SerializableBufferedImage} object is null. - */ - public static String toJson(SerializableBufferedImage serializableBufferedImage) { - if (serializableBufferedImage == null) - throw new IllegalArgumentException("SerializableBufferedImage cannot be null"); - return serializableBufferedImage.toJson(); - } - - /** - * Converts a JSON string to a {@link SerializableBufferedImage} object. - * - * @param json The JSON string representing the serialized {@link SerializableBufferedImage} object. - * @return A {@link SerializableBufferedImage} object deserialized from the provided JSON string. - * @throws IllegalArgumentException If the provided JSON string is null or empty. - */ - public static SerializableBufferedImage fromJson(String json) { - if (json == null) throw new IllegalArgumentException("Json cannot be null"); - if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); - return SerializableBufferedImage.fromJson(json); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java b/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java deleted file mode 100644 index 51b08d97..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinPartsTypeAdapter.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; - -import com.georgev22.skinoverlay.handler.skin.SkinParts; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import com.google.gson.*; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Type; - -/** - * Gson TypeAdapter for serializing and deserializing {@link SkinParts} objects. - *

- * This adapter is responsible for converting {@link SkinParts} objects to and from JSON format. - * It handles the serialization of the buffered image and the skin name properties. - */ -public class SkinPartsTypeAdapter implements JsonSerializer, JsonDeserializer { - - /** - * Serializes a {@link SkinParts} object to a JSON representation. - * - * @param src The {@link SkinParts} object to be serialized. - * @param typeOfSrc The type of the source object. - * @param context The serialization context. - * @return A {@link JsonElement} representing the serialized {@link SkinParts} object. - */ - @Override - public JsonElement serialize(@NotNull SkinParts src, Type typeOfSrc, @NotNull JsonSerializationContext context) { - JsonObject jsonObject = new JsonObject(); - jsonObject.add("bufferedImage", context.serialize(src.getFullSkin())); - jsonObject.addProperty("skinName", src.getSkinName()); - return jsonObject; - } - - /** - * Deserializes a JSON representation into a {@link SkinParts} object. - * - * @param json The {@link JsonElement} containing the JSON representation. - * @param typeOfT The target type to deserialize into. - * @param context The deserialization context. - * @return A deserialized {@link SkinParts} object. - */ - @Override - public SkinParts deserialize(@NotNull JsonElement json, Type typeOfT, @NotNull JsonDeserializationContext context) { - JsonObject jsonObject = json.getAsJsonObject(); - - SerializableBufferedImage bufferedImage = context.deserialize(jsonObject.get("bufferedImage"), SerializableBufferedImage.class); - String skinName = jsonObject.get("skinName").getAsString(); - - return new SkinParts(bufferedImage, skinName); - } - - /** - * Converts a {@link SkinParts} object to its JSON representation. - * - * @param skinParts The {@link SkinParts} object to be converted. - * @return A JSON string representing the serialized {@link SkinParts} object. - * @throws IllegalArgumentException If the provided {@link SkinParts} object is null. - */ - public static String toJson(@NotNull SkinParts skinParts) throws IllegalArgumentException { - //noinspection ConstantValue - if (skinParts == null) throw new IllegalArgumentException("SkinParts cannot be null"); - return skinParts.toJson(); - } - - /** - * Converts a JSON string to a {@link SkinParts} object. - * - * @param json The JSON string representing the serialized {@link SkinParts} object. - * @return A {@link SkinParts} object deserialized from the provided JSON string. - * @throws IllegalArgumentException If the provided JSON string is null or empty. - */ - public static SkinParts fromJson(@NotNull String json) { - //noinspection ConstantValue - if (json == null) throw new IllegalArgumentException("Json cannot be null"); - if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); - return SkinParts.fromJson(json); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java b/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java deleted file mode 100644 index a71dab7f..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/SkinTypeAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; - -import com.georgev22.skinoverlay.handler.SProperty; -import com.georgev22.skinoverlay.handler.skin.SkinParts; -import com.georgev22.skinoverlay.storage.data.Skin; -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 { - - /** - * Serializes a {@link Skin} object to a JSON representation. - * - * @param skin The {@link Skin} 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 Skin} object. - */ - @Override - public JsonElement serialize(@NotNull Skin skin, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { - 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()); - - return jsonObject; - } - - /** - * Deserializes a JSON representation into a {@link Skin} 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 Skin} object. - */ - @Override - public Skin deserialize(@NotNull JsonElement jsonElement, Type type, @NotNull JsonDeserializationContext jsonDeserializationContext) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - - 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); - - return skin; - } - - /** - * Converts a JSON string to a {@link Skin} object. - * - * @param json The JSON string representing the serialized {@link Skin} object. - * @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); - } - - /** - * Converts a {@link Skin} object to its JSON representation. - * - * @param skin The {@link Skin} object to be converted. - * @return A JSON string representing the serialized {@link Skin} object. - * @throws IllegalArgumentException If the provided {@link Skin} object is null. - */ - public static String toJson(Skin skin) { - if (skin == null) throw new IllegalArgumentException("Skin cannot be null"); - return skin.toJson(); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java b/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java deleted file mode 100644 index 94e7b2c5..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/storage/gsonAdapters/UserTypeAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.georgev22.skinoverlay.storage.gsonAdapters; - -import com.georgev22.skinoverlay.storage.data.Skin; -import com.georgev22.skinoverlay.storage.data.User; -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 User} objects. - *

- * This adapter is responsible for converting {@link User} 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 { - - /** - * Serializes a {@link User} object to a JSON representation. - * - * @param user The {@link User} 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. - */ - @Override - public JsonElement serialize(@NotNull User 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())); - - return jsonObject; - } - - /** - * Deserializes a JSON representation into a {@link User} 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. - */ - @Override - public User 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); - user.setDefaultSkin(defaultSkin); - user.setSkin(skin); - - return user; - } - - /** - * Converts a {@link User} 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. - */ - public static String toJson(@NotNull User user) throws IllegalArgumentException { - //noinspection ConstantValue - if (user == null) { - throw new IllegalArgumentException("User cannot be null"); - } - return user.toJson(); - } - - /** - * Converts a JSON string to a {@link User} object. - * - * @param json The JSON string representing the serialized {@link User} object. - * @return A {@link User} 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) { - //noinspection ConstantValue - if (json == null) { - throw new IllegalArgumentException("Json cannot be null"); - } - if (json.isEmpty()) { - throw new IllegalArgumentException("Json cannot be empty"); - } - return User.fromJson(json); - } -} 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/Locale.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java deleted file mode 100644 index d00fadef..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.georgev22.skinoverlay.utilities; - -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -public enum Locale { - ENGLISH("en"), - FRENCH("fr"), - GERMAN("de"), - ITALIAN("it"), - SPANISH("es"), - PORTUGUESE("pt"), - DUTCH("nl"), - NORWEGIAN("no"), - SWEDISH("sv"), - DANISH("da"), - FINNISH("fi"), - POLISH("pl"), - CZECH("cs"), - SLOVAK("sk"), - HUNGARIAN("hu"), - ROMANIAN("ro"), - BULGARIAN("bg"), - GREEK("el"), - RUSSIAN("ru"), - TURKISH("tr"), - - CHINESE("zh"), - JAPANESE("ja"), - KOREAN("ko"), - - ENGLISH_US("en", "US"), - ENGLISH_GR("en", "GR"), - SPANISH_ES("es", "ES"), - SPANISH_MX("es", "MX"), - PORTUGUESE_BR("pt", "BR"), - FRENCH_CA("fr", "CA"); - - private final String languageCode; - private final String countryCode; - private final java.util.Locale locale; - private static final Map localeMap; - - static { - localeMap = new HashMap<>(); - Arrays.stream(Locale.values()).forEach(l -> localeMap.put(l.countryCode != null ? l.languageCode.toLowerCase() + "_" + l.countryCode : l.languageCode.toLowerCase(), l)); - } - - Locale(String languageCode) { - this.locale = new java.util.Locale(languageCode); - this.languageCode = languageCode; - this.countryCode = null; - } - - Locale(String languageCode, String countryCode) { - this.locale = new java.util.Locale(languageCode, countryCode); - this.languageCode = languageCode; - this.countryCode = countryCode; - } - - public java.util.Locale getLocale() { - return locale; - } - - public String getStringLocale() { - return countryCode != null ? languageCode + "_" + countryCode : languageCode; - } - - public static Locale fromString(@NotNull String s) { - return localeMap.containsKey(s) ? localeMap.get(s) : s.contains("_") ? localeMap.get(s.split("_")[0]) : ENGLISH_US; - } -} 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/SerializableBufferedImage.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java deleted file mode 100644 index 02abc65b..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java +++ /dev/null @@ -1,152 +0,0 @@ -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; -import org.jetbrains.annotations.Unmodifiable; - -import java.awt.image.BufferedImage; -import java.util.Base64; -import java.util.Map; -import java.util.Objects; - -/** - * Represents a {@link BufferedImage} that can be serialized and deserialized. - */ -@SuppressWarnings("ClassCanBeRecord") -public final class SerializableBufferedImage implements ConfigurationSerializable { - private final BufferedImage bufferedImage; - - public SerializableBufferedImage(BufferedImage bufferedImage) { - this.bufferedImage = bufferedImage; - } - - /** - * Serializes the {@link BufferedImage} to a YAML-compatible map for serialization. - * - * @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(); - int[] pixels = new int[width * height]; - bufferedImage.getRGB(0, 0, width, height, pixels, 0, width); - String encodedPixels = Base64.getEncoder().encodeToString(toByteArray(pixels)); - return Map.of( - "width", width, - "height", height, - "pixels", encodedPixels - ); - } - - /** - * Deserializes the YAML-compatible map to a {@link SerializableBufferedImage}. - * - * @param map The YAML-compatible map containing width, height, and pixel information. - * @return The deserialized {@link SerializableBufferedImage}. - */ - @Contract(pure = true) - public static @NotNull SerializableBufferedImage deserialize(@NotNull Map map) { - int width = (int) map.get("width"); - int height = (int) map.get("height"); - String encodedPixels = (String) map.get("pixels"); - int[] pixels = fromByteArray(Base64.getDecoder().decode(encodedPixels)); - - BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - bufferedImage.setRGB(0, 0, width, height, pixels, 0, width); - - return new SerializableBufferedImage(bufferedImage); - } - - @Contract(pure = true) - public static byte @NotNull [] toByteArray(int @NotNull [] pixels) { - byte[] result = new byte[pixels.length * 4]; - for (int i = 0, j = 0; i < pixels.length; i++, j += 4) { - result[j] = (byte) ((pixels[i] >> 24) & 0xFF); - result[j + 1] = (byte) ((pixels[i] >> 16) & 0xFF); - result[j + 2] = (byte) ((pixels[i] >> 8) & 0xFF); - result[j + 3] = (byte) (pixels[i] & 0xFF); - } - return result; - } - - @Contract(pure = true) - public static int @NotNull [] fromByteArray(byte @NotNull [] bytes) { - int[] result = new int[bytes.length / 4]; - for (int i = 0, j = 0; i < result.length; i++, j += 4) { - result[i] = ((bytes[j] & 0xFF) << 24) | - ((bytes[j + 1] & 0xFF) << 16) | - ((bytes[j + 2] & 0xFF) << 8) | - (bytes[j + 3] & 0xFF); - } - return result; - } - - /** - * Checks if this {@link SerializableBufferedImage} is equal to another object. - * - * @param obj The object to compare. - * @return {@code true} if the objects are equal, {@code false} otherwise. - */ - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (SerializableBufferedImage) obj; - return Objects.equals(this.bufferedImage, that.bufferedImage); - } - - /** - * Computes the hash code of this {@link SerializableBufferedImage}. - * - * @return The computed hash code. - */ - @Override - public int hashCode() { - return Objects.hash(bufferedImage); - } - - /** - * Returns a string representation of this {@link SerializableBufferedImage}. - * - * @return A string representation containing the buffered image. - */ - @Override - public @NotNull String toString() { - return "SerializableBufferedImage[" + - "bufferedImage=" + bufferedImage + ']'; - } - - /** - * Serializes the SerializableBufferedImage 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 SerializableBufferedImage object. - * - * @param json The JSON string to deserialize. - * @return The deserialized SerializableBufferedImage object. - */ - public static SerializableBufferedImage fromJson(String json) { - return SkinOverlay.getInstance().getGson().fromJson(json, SerializableBufferedImage.class); - } - - /** - * Returns the buffered image. - * - * @return The buffered image - */ - public BufferedImage getBufferedImage() { - return bufferedImage; - } - - -} 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/FileManager.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java deleted file mode 100644 index 3c29b74d..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.georgev22.skinoverlay.utilities.config; - -import com.georgev22.library.yaml.configmanager.CFG; -import com.georgev22.skinoverlay.SkinOverlay; - -public final class FileManager { - private static FileManager instance; - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - private CFG config; - - public static FileManager getInstance() { - return instance == null ? (instance = new FileManager()) : instance; - } - - private FileManager() { - } - - public void loadFiles() throws Exception { - this.config = new CFG("config", this.skinOverlay.getDataFolder(), true, true, skinOverlay.getLogger(), skinOverlay.getClass()); - } - - public CFG getConfig() { - return this.config; - } - -} - 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/config/OptionsUtil.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java deleted file mode 100644 index 1c2587a5..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.georgev22.skinoverlay.utilities.config; - -import com.georgev22.library.utilities.Color; -import com.georgev22.skinoverlay.SkinOverlay; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -public enum OptionsUtil { - DEBUG("debug", false, Optional.empty()), - - PROXY("proxy", false, Optional.of("bungeecord")), - - SECRET("secret", "SECRET HERE", Optional.empty()), - - MINESKIN_API_KEY("mineskin api key", "none", Optional.empty()), - - 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()), - - DISCORD("discord", false, Optional.empty()), - - UPDATER("updater.enabled", true, Optional.empty()), - - DEFAULT_SKIN_UUID("default skin uuid", "8667ba71-b85a-4004-af54-457a9734eed7", Optional.empty()), - - SKIN_HOOK("skin hook", "SkinsRestorer", Optional.empty()), - LOCALE("locale", "en_US", Optional.empty()), - ; - private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); - private final String pathName; - private final Object value; - private final Optional[] oldPaths; - - @SafeVarargs - @Contract(pure = true) - OptionsUtil(final String pathName, final Object value, Optional... oldPaths) { - this.pathName = pathName; - this.value = value; - this.oldPaths = oldPaths; - } - - public boolean getBooleanValue() { - return mainPlugin.getConfig().getBoolean(getPath(), Boolean.parseBoolean(String.valueOf(getDefaultValue()))); - } - - public Object getObjectValue() { - return mainPlugin.getConfig().get(getPath(), getDefaultValue()); - } - - public String getStringValue() { - return mainPlugin.getConfig().getString(getPath(), String.valueOf(getDefaultValue())); - } - - public @NotNull Long getLongValue() { - return mainPlugin.getConfig().getLong(getPath(), Long.parseLong(String.valueOf(getDefaultValue()))); - } - - public @NotNull Integer getIntValue() { - return mainPlugin.getConfig().getInt(getPath(), Integer.parseInt(String.valueOf(getDefaultValue()))); - } - - public @NotNull Double getDoubleValue() { - return mainPlugin.getConfig().getDouble(getPath(), Double.parseDouble(String.valueOf(getDefaultValue()))); - } - - public @NotNull List getStringList() { - return mainPlugin.getConfig().getStringList(getPath()); - } - - /** - * Converts and return a String List of color codes to a List of Color classes that represent the colors. - * - * @return a List of Color classes that represent the colors. - */ - public @NotNull List getColors() { - return getStringList().stream().map(Color::from).collect(Collectors.toList()); - } - - public boolean getBooleanValue(String arg) { - return mainPlugin.getConfig().getBoolean(String.format(getPath(), arg), Boolean.parseBoolean(String.valueOf(getDefaultValue()))); - } - - public Object getObjectValue(String arg) { - return mainPlugin.getConfig().get(getPath(), getDefaultValue()); - } - - public String getStringValue(String arg) { - return mainPlugin.getConfig().getString(getPath(), String.valueOf(getDefaultValue())); - } - - public @NotNull Long getLongValue(String arg) { - return mainPlugin.getConfig().getLong(getPath(), Long.parseLong(String.valueOf(getDefaultValue()))); - } - - public @NotNull Integer getIntValue(String arg) { - return mainPlugin.getConfig().getInt(getPath(), Integer.parseInt(String.valueOf(getDefaultValue()))); - } - - public @NotNull Double getDoubleValue(String arg) { - return mainPlugin.getConfig().getDouble(getPath(), Double.parseDouble(String.valueOf(getDefaultValue()))); - } - - public @NotNull List getStringList(String arg) { - return mainPlugin.getConfig().getStringList(getPath()); - } - - /** - * Converts and return a String List of color codes to a List of Color classes that represent the colors. - * - * @return a List of Color classes that represent the colors. - */ - public @NotNull List getColors(String arg) { - return getStringList().stream().map(Color::from).collect(Collectors.toList()); - } - - /** - * Returns the path. - * - * @return the path. - */ - public @NotNull String getPath() { - if (mainPlugin.getConfig().get("Options." + getDefaultPath()) == null) { - for (Optional path : getOldPaths()) { - if (path.isPresent()) { - if (mainPlugin.getConfig().get("Options." + path.get()) != null) { - return "Options." + path.get(); - } - } - } - } - return "Options." + getDefaultPath(); - } - - /** - * Returns the default path. - * - * @return the default path. - */ - @Contract(pure = true) - public @NotNull String getDefaultPath() { - return this.pathName; - } - - /** - * Returns the old path if it exists. - * - * @return the old path if it exists. - */ - public Optional[] getOldPaths() { - return oldPaths; - } - - /** - * Returns the default value if the path have no value. - * - * @return the default value if the path have no value. - */ - public Object getDefaultValue() { - return value; - } - - public Optional getOptionalStringValue() { - return Optional.ofNullable(getStringValue()); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java deleted file mode 100644 index 8c1b7f6d..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java +++ /dev/null @@ -1,140 +0,0 @@ -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 org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -public enum OverlayOptionsUtil { - PARTS_OVERLAY_JACKET("overlay.jacket", false, Optional.empty()), - - PARTS_OVERLAY_LEFT_SLEEVE("overlay.left_sleeve", false, Optional.empty()), - - PARTS_OVERLAY_RIGHT_SLEEVE("overlay.right_sleeve", false, Optional.empty()), - - PARTS_OVERLAY_LEFT_PANTS("overlay.left_pants", false, Optional.empty()), - - PARTS_OVERLAY_RIGHT_PANTS("overlay.right_pants", false, Optional.empty()), - - PARTS_OVERLAY_HAT("overlay.hat", false, Optional.empty()), - - // PLAYER PARTS - - PARTS_PLAYER_JACKET("player.jacket", false, Optional.empty()), - - PARTS_PLAYER_LEFT_SLEEVE("player.left_sleeve", false, Optional.empty()), - - PARTS_PLAYER_RIGHT_SLEEVE("player.right_sleeve", false, Optional.empty()), - - PARTS_PLAYER_LEFT_PANTS("player.left_pants", false, Optional.empty()), - - PARTS_PLAYER_RIGHT_PANTS("player.right_pants", false, Optional.empty()), - - PARTS_PLAYER_HAT("player.hat", false, Optional.empty()), - ; - - private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); - private final String pathName; - private final Object value; - private final Optional[] oldPaths; - - @SafeVarargs - @Contract(pure = true) - OverlayOptionsUtil(final String pathName, final Object value, Optional... oldPaths) { - this.pathName = pathName; - this.value = value; - this.oldPaths = oldPaths; - } - - public boolean getBooleanValue(FileConfiguration fileConfiguration) { - return fileConfiguration.getBoolean(getPath(fileConfiguration), Boolean.parseBoolean(String.valueOf(getDefaultValue(fileConfiguration)))); - } - - public Object getObjectValue(FileConfiguration fileConfiguration) { - return fileConfiguration.get(getPath(fileConfiguration), getDefaultValue(fileConfiguration)); - } - - public String getStringValue(FileConfiguration fileConfiguration) { - return fileConfiguration.getString(getPath(fileConfiguration), String.valueOf(getDefaultValue(fileConfiguration))); - } - - public @NotNull Long getLongValue(FileConfiguration fileConfiguration) { - return fileConfiguration.getLong(getPath(fileConfiguration), Long.parseLong(String.valueOf(getDefaultValue(fileConfiguration)))); - } - - public @NotNull Integer getIntValue(FileConfiguration fileConfiguration) { - return fileConfiguration.getInt(getPath(fileConfiguration), Integer.parseInt(String.valueOf(getDefaultValue(fileConfiguration)))); - } - - public @NotNull Double getDoubleValue(FileConfiguration fileConfiguration) { - return fileConfiguration.getDouble(getPath(fileConfiguration), Double.parseDouble(String.valueOf(getDefaultValue(fileConfiguration)))); - } - - public @NotNull List getStringList(FileConfiguration fileConfiguration) { - return fileConfiguration.getStringList(getPath(fileConfiguration)); - } - - /** - * Converts and return a String List of color codes to a List of Color classes that represent the colors. - * - * @return a List of Color classes that represent the colors. - */ - public @NotNull List getColors(FileConfiguration fileConfiguration) { - return getStringList(fileConfiguration).stream().map(Color::from).collect(Collectors.toList()); - } - - /** - * Returns the path. - * - * @return the path. - */ - public @NotNull String getPath(FileConfiguration fileConfiguration) { - if (fileConfiguration.get("Options." + getDefaultPath(fileConfiguration)) == null) { - for (Optional path : getOldPaths(fileConfiguration)) { - if (path.isPresent()) { - if (fileConfiguration.get("Options." + path.get()) != null) { - return "Options." + path.get(); - } - } - } - } - return "Options." + getDefaultPath(fileConfiguration); - } - - /** - * Returns the default path. - * - * @return the default path. - */ - @Contract(pure = true) - public @NotNull String getDefaultPath(FileConfiguration fileConfiguration) { - return this.pathName; - } - - /** - * Returns the old path if it exists. - * - * @return the old path if it exists. - */ - public Optional[] getOldPaths(FileConfiguration fileConfiguration) { - return oldPaths; - } - - /** - * Returns the default value if the path have no value. - * - * @return the default value if the path have no value. - */ - public Object getDefaultValue(FileConfiguration fileConfiguration) { - return value; - } - - public Optional getOptionalStringValue(FileConfiguration fileConfiguration) { - return Optional.ofNullable(getStringValue(fileConfiguration)); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java deleted file mode 100644 index bffca909..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java +++ /dev/null @@ -1,64 +0,0 @@ -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.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.IOException; -import java.util.logging.Level; - -public class SkinConfigurationFile { - - private final String name; - private FileConfiguration fileConfiguration; - private File file; - private boolean setup = false; - - public SkinConfigurationFile(final String name) { - this.name = name; - - } - - public File getFile() { - return file; - } - - public void setupFile() { - - if (setup) { - throw new RuntimeException( - "The void file with the name: " + this.name + " can't be setup twice!\n" + - "If you believe this is an issue, contact GeorgeV22."); - } - - final SkinOverlay plugin = SkinOverlay.getInstance(); - - if (!plugin.getDataFolder().exists()) { - plugin.getDataFolder().mkdirs(); - } - - this.file = new File(plugin.getDataFolder(), "skins-cfg" + File.separator + this.name + ".yml"); - - if (!this.file.exists()) { - try { - this.file.createNewFile(); - } catch (final IOException e) { - plugin.getLogger().log(Level.SEVERE, "Error while creating a new file:", e); - } - } - this.reloadFile(); - setup = true; - } - - public void reloadFile() { - this.fileConfiguration = YamlConfiguration.loadConfiguration(this.file); - } - - @NotNull - public FileConfiguration getFileConfiguration() { - return this.fileConfiguration; - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java deleted file mode 100644 index 3c3d8482..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java +++ /dev/null @@ -1,154 +0,0 @@ -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.storage.data.Skin; -import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.imageio.ImageIO; -import java.io.File; -import java.io.IOException; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.logging.Level; - -public class SkinFileCache { - private final ObjectMap skinConfigurationFiles = new HashObjectMap<>(); - - private final ObjectMap skinImages = new HashObjectMap<>(); - - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - - @NotNull - public ObjectMap getSkinConfigurationFiles() { - return skinConfigurationFiles; - } - - public ObjectMap getSkinImages() { - return skinImages; - } - - public void cache() { - this.skinOverlay.getLogger().info("Attempting to load skins and its configuration files in memory."); - this.skinConfigurationFiles.clear(); - this.skinImages.clear(); - - final File folder = new File(skinOverlay.getDataFolder(), "skins-cfg"); - if (!folder.exists()) { - if (folder.mkdirs()) { - this.skinOverlay.getLogger().log(Level.INFO, "skins-cfg folder was successfully created."); - } - this.copyFilesToFolder(false); - this.skinOverlay.getLogger().info("skins-cfg folder didn't exist, creating one and copy the default skins configuration files for it."); - } - - if (!this.skinOverlay.getSkinsDataFolder().exists()) { - if (this.skinOverlay.getSkinsDataFolder().mkdirs()) { - this.skinOverlay.getLogger().log(Level.INFO, "skins data folder was successfully created!!"); - } - this.copyFilesToFolder(true); - this.skinOverlay.getLogger().info("skins folder didn't exist, creating one and copy the default skins for it."); - } - - for (File file : Objects.requireNonNull(this.skinOverlay.getSkinsDataFolder().listFiles((dir, name) -> name.endsWith(".png")))) { - final String skinName = file.getName().substring(0, file.getName().length() - 4); - - if (skinName.isEmpty()) { - this.skinOverlay.getLogger().info("Invalid skin file name: " + skinName); - continue; - } - - try { - SerializableBufferedImage serializableBufferedImage = new SerializableBufferedImage(ImageIO.read(file)); - this.skinImages.put(skinName, serializableBufferedImage); - this.skinOverlay.getLogger().info("Skin image file: " + skinName + " successfully loaded in memory."); - } catch (IOException e) { - this.skinOverlay.getLogger().log(Level.WARNING, "Cannot load skin image file: " + skinName, e); - } - } - - for (File file : Objects.requireNonNull(folder.listFiles((dir, name) -> name.endsWith(".yml")))) { - final String skinName = file.getName().substring(0, file.getName().length() - 4); - - if (skinName.isEmpty()) { - this.skinOverlay.getLogger().info("Invalid skin configuration file name: " + skinName); - continue; - } - - final SkinConfigurationFile skinFile = new SkinConfigurationFile(skinName); - skinFile.setupFile(); - this.skinConfigurationFiles.put(skinName, skinFile); - this.skinOverlay.getLogger().info("Skin configuration file: " + skinName + " successfully loaded in memory."); - } - - } - - private void copyFilesToFolder(boolean b) { - String[] resources = new String[]{"alley", "frog_pijama", "bubbo_transparent", "fire_demon", "flame", "glare", "hoodie", "migrator", "pirate", "smoking", "policeman", "mustache"}; - if (b) { - for (String resource : resources) { - if (new File(this.skinOverlay.getSkinsDataFolder(), resource + ".png").exists()) continue; - try { - Utils.saveResource("skins/" + resource + ".png", false, this.skinOverlay.getDataFolder(), this.getClass()); - } catch (Exception e) { - this.skinOverlay.getLogger().log(Level.WARNING, "Cannot save default skins: ", e.getCause()); - } - } - } else { - for (String resource : resources) { - if (new File(this.skinOverlay.getDataFolder(), "skins-cfg/" + resource + ".yml").exists()) continue; - try { - Utils.saveResource("skins-cfg/" + resource + ".yml", false, this.skinOverlay.getDataFolder(), this.skinOverlay.getClass()); - } catch (Exception e) { - this.skinOverlay.getLogger().log(Level.WARNING, "Cannot save default skin configuration files: ", e.getCause()); - } - } - } - } - - /* - * Returns a cached or attempts to cache a possible SkinFile object from - * a specific key. - */ - @Nullable - public SkinConfigurationFile getCacheSkinConfig(final String name) { - SkinConfigurationFile cache = this.skinConfigurationFiles.get(name); - if (cache != null) { - return cache; - } - for (Entry entry : this.skinConfigurationFiles.entrySet()) { - if (entry.getKey().equalsIgnoreCase(name)) { - return entry.getValue(); - } - } - return null; - } - - @Nullable - public SkinConfigurationFile getCacheSkinConfig(final @NotNull Skin skin) { - return this.getCacheSkinConfig(skin.skinName()); - } - - @Nullable - public SerializableBufferedImage getSkinImage(final String skinName) { - SerializableBufferedImage cache = this.skinImages.get(skinName); - if (cache != null) { - return cache; - } - for (Entry entry : this.skinImages.entrySet()) { - if (entry.getKey().equalsIgnoreCase(skinName)) { - return entry.getValue(); - } - } - return null; - } - - @Nullable - public SerializableBufferedImage getSkinImage(final @NotNull Skin skin) { - return this.getSkinImage(skin.skinName()); - } -} diff --git a/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java b/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java deleted file mode 100644 index 6740b627..00000000 --- a/core/src/main/java/com/georgev22/skinoverlay/utilities/interfaces/ImageSupplier.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.georgev22.skinoverlay.utilities.interfaces; - -import java.awt.image.BufferedImage; -import java.io.IOException; - -/** - * A functional interface that supplies an image. - */ -@FunctionalInterface -public interface ImageSupplier { - /** - * Gets an image. - * - * @return the image supplied by this function - * @throws IOException if an I/O error occurs while getting the image - */ - BufferedImage get() throws IOException; -} 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 deleted file mode 100644 index afc1facb..00000000 --- a/gradle.properties +++ /dev/null @@ -1,28 +0,0 @@ -group = com.georgev22 -artifact = skinoverlay -packageName = com.georgev22.skinoverlay -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 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/publish.gradle b/gradle/publish.gradle deleted file mode 100644 index eebbb37b..00000000 --- a/gradle/publish.gradle +++ /dev/null @@ -1,103 +0,0 @@ -apply plugin: 'maven-publish' - -static def getBranch() { - def process = 'git branch --show-current'.execute() - process.waitFor() - return process.text.trim() -} - -static def getHash() { - def process = 'git rev-parse HEAD'.execute() - process.waitFor() - return process.text.trim() -} - -java { - withJavadocJar() - withSourcesJar() -} - -jar { - manifest { - attributes( - 'Build-Jdk': "${System.properties['java.vendor']} ${System.properties['java.vm.version']}", - 'Created-By': "Gradle ${gradle.gradleVersion}", - 'Git-Branch': getBranch(), - 'Git-Hash': getHash() - ) - } -} - -publishing { - publications { - shadow(MavenPublication) { publication -> - project.shadow.component(publication) - artifactId = project.getName().toLowerCase() - groupId = ((String) project.getGroup()).toLowerCase() - artifact sourcesJar - artifact javadocJar - } - } - repositories { - def isGitHub = System.getenv("GITHUB_REPOSITORY") ? true : false - if (isGitHub) { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/${project.findProperty("GITHUB_REPOSITORY") ?: System.getenv("GITHUB_REPOSITORY")}") - credentials { - username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") - } - } - maven { - name = "GeorgeV22" - - url = uri("https://repo.georgev22.com/releases") - - def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : - System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null - def mavenPassword = System.getenv("MAVEN_PASSWORD") ? System.getenv("MAVEN_PASSWORD") : - System.getProperty("MAVEN_PASSWORD") ? System.getProperty("MAVEN_PASSWORD") : null - - if (mavenUsername != null && mavenPassword != null) { - credentials { - username = mavenUsername - password = mavenPassword - } - authentication { - basic(BasicAuthentication) - } - } - } - } - if (!isGitHub) { - maven { - name = "GeorgeV22" - - url = uri("https://repo.georgev22.com/snapshots") - - def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : - System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null - def mavenPassword = System.getenv("MAVEN_PASSWORD") ? System.getenv("MAVEN_PASSWORD") : - System.getProperty("MAVEN_PASSWORD") ? System.getProperty("MAVEN_PASSWORD") : null - - if (mavenUsername != null && mavenPassword != null) { - credentials { - username = mavenUsername - password = mavenPassword - } - authentication { - basic(BasicAuthentication) - } - } - } - } - } -} - -javadoc { - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption('html5', true) - options.addStringOption('Xdoclint:none', '-quiet') - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ccebba7710deaf9f98673a68957ea02138b60d0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61608 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37aef8d3..00000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 79a61d42..00000000 --- a/gradlew +++ /dev/null @@ -1,244 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/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/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=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 -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 6689b85b..00000000 --- a/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -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 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_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.java b/multiver/mc_1_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.java deleted file mode 100644 index c8804d0e..00000000 --- a/multiver/mc_1_17_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_17_R1.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_17_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; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_17_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.dimensionType(), - 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 @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); - } - - @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/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_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.java b/multiver/mc_1_18_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.java deleted file mode 100644 index a6a09749..00000000 --- a/multiver/mc_1_18_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_18_R1.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_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; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_18_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.dimensionType(), - 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_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_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java b/multiver/mc_1_20_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java deleted file mode 100644 index 1675f728..00000000 --- a/multiver/mc_1_20_R1/src/main/java/com/georgev22/skinoverlay/handler/handlers/SkinHandler_1_20_R1.java +++ /dev/null @@ -1,126 +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.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_20_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; - -import static com.georgev22.skinoverlay.handler.handlers.SkinHandler_Unsupported.wrapper; - -public final class SkinHandler_1_20_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.serverLevel(); - 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(), - entityPlayer.getPortalCooldown() - ); - - 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_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/src/main/resources/bungee.yml b/src/main/resources/bungee.yml deleted file mode 100644 index 9c3898f1..00000000 --- a/src/main/resources/bungee.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: ${pluginName} -version: ${version} -main: ${bungeeMain} -author: ${author} -description: SkinOverlay -softDepends: - - SkinsRestorer \ 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/messages_en_US.yml b/src/main/resources/messages_en_US.yml deleted file mode 100644 index ca21fe49..00000000 --- a/src/main/resources/messages_en_US.yml +++ /dev/null @@ -1,16 +0,0 @@ -Messages: - No Permission: '&c&l(!)&c You do not have the correct permissions to do this!' - 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 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)' diff --git a/src/main/resources/messages_tr.yml b/src/main/resources/messages_tr.yml deleted file mode 100644 index b2fd5c54..00000000 --- a/src/main/resources/messages_tr.yml +++ /dev/null @@ -1,16 +0,0 @@ -Messages: - No Permission: '&c&l(!)&c Bunu yapmak için gerekli yetkilere sahip değilsin!' - Only Player Command: '&c&l(!)&c Bu komutu sadece oyuncular kullanabilir!' - Offline Player: '&c&l(!)&c %player% adlı oyuncu çevrimdışı!' - Overlay Applied: '&a&l(!)&a %url% kaplaması uygulandı!' - Overlay Reset: '&a&l(!)&a Varsayılan cilt uygulandı (%player%)!' - Overlay Not Found: '&c&l(!)&c %overlay% kaplaması bulunamadı!' - Insufficient arguments: '&c&l(!)&c Yetersiz argüman(lar)! (%command%)' -Commands: - Descriptions: - SkinOverlay: - help: 'Yardım sayfasını gösterir' - overlay: 'Eklentinin dosyalarından belli bir kaplama kuşan' - clear: 'Cilt kaplamasını kaldırır' - url: 'Bir URL''den belli bir kaplama kuşan' - reload: 'Eklentinin yapılandırma dosyalarını yenile (bazı ayarların yenilenmesi için sunucunun yeniden başlatılması gerekir)' 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/src/main/resources/skins-cfg/alley.yml b/src/main/resources/skins-cfg/alley.yml deleted file mode 100644 index f52f3d9b..00000000 --- a/src/main/resources/skins-cfg/alley.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true \ No newline at end of file diff --git a/src/main/resources/skins-cfg/bubbo_transparent.yml b/src/main/resources/skins-cfg/bubbo_transparent.yml deleted file mode 100644 index 541f7c74..00000000 --- a/src/main/resources/skins-cfg/bubbo_transparent.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/fire_demon.yml b/src/main/resources/skins-cfg/fire_demon.yml deleted file mode 100644 index bf99dbde..00000000 --- a/src/main/resources/skins-cfg/fire_demon.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/flame.yml b/src/main/resources/skins-cfg/flame.yml deleted file mode 100644 index bf99dbde..00000000 --- a/src/main/resources/skins-cfg/flame.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/frog_pijama.yml b/src/main/resources/skins-cfg/frog_pijama.yml deleted file mode 100644 index 9b558ef8..00000000 --- a/src/main/resources/skins-cfg/frog_pijama.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/glare.yml b/src/main/resources/skins-cfg/glare.yml deleted file mode 100644 index d22dabd7..00000000 --- a/src/main/resources/skins-cfg/glare.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: true - left_pants: true \ No newline at end of file diff --git a/src/main/resources/skins-cfg/hoodie.yml b/src/main/resources/skins-cfg/hoodie.yml deleted file mode 100644 index bf99dbde..00000000 --- a/src/main/resources/skins-cfg/hoodie.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/migrator.yml b/src/main/resources/skins-cfg/migrator.yml deleted file mode 100644 index ebf552c0..00000000 --- a/src/main/resources/skins-cfg/migrator.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: true - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/mustache.yml b/src/main/resources/skins-cfg/mustache.yml deleted file mode 100644 index 55e95363..00000000 --- a/src/main/resources/skins-cfg/mustache.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true \ No newline at end of file diff --git a/src/main/resources/skins-cfg/pirate.yml b/src/main/resources/skins-cfg/pirate.yml deleted file mode 100644 index bf99dbde..00000000 --- a/src/main/resources/skins-cfg/pirate.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: true - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: false - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/policeman.yml b/src/main/resources/skins-cfg/policeman.yml deleted file mode 100644 index 5bac9999..00000000 --- a/src/main/resources/skins-cfg/policeman.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: true - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins-cfg/smoking.yml b/src/main/resources/skins-cfg/smoking.yml deleted file mode 100644 index 5bac9999..00000000 --- a/src/main/resources/skins-cfg/smoking.yml +++ /dev/null @@ -1,15 +0,0 @@ -Options: - overlay: - hat: false - right_sleeve: true - left_sleeve: true - jacket: true - right_pants: true - left_pants: true - player: - hat: true - right_sleeve: false - left_sleeve: false - jacket: false - right_pants: false - left_pants: false \ No newline at end of file diff --git a/src/main/resources/skins/alley.png b/src/main/resources/skins/alley.png deleted file mode 100644 index c4394702bef9c583adc3c228ce4137459fbaf3f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2030 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 diff --git a/src/main/resources/skins/flame.png b/src/main/resources/skins/flame.png deleted file mode 100644 index 0b2d417905f474b243623ed21848a40d845ab37e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmV-^1cCdBP)Px(I7vi7RCr$PTd|JRFc3`|=nxguG#m{D4Rp6O@C6|F7QP4sRM$bnUIz_GG#o;r zhKd%NpttBPbF;Qzys?vLFIVlZW6zs6&-QqnYrC%Nn#5n*wr#3iwW;w{|JwFj1+X@N zgCOAS(W*OowGJvp5quB?q@1`A1Ux@_&^>``10%bqC z$HOUtQwmT{3~7h+=+EhuOMtSyCBU=nJ97S)24L;@?Uc@20+d6!?Lh_5@UNf$lf>^o zE;l!>|88OnVXk5IeC^27{+0kMc@GYbgMivoA+((mS1s~)6aa}|z4q^CDgfW{c?=Rr zzEvUYVw(kq(hA&o%DER)0V0oQK7PJbO$Cg;SAqSG&!v^nYYiyPx|;tCidA7P3_H6( zNCl8z&FI^N3Th(;yXK@0Tzv?QJ+qSus^hOrEGpLzhT`)?ECl$74rN&NS8^SyFx7ui zfMY<``?&R>-13&j&d^9UlI_Dq9q`vf|?4z*rD^kKPXvpLU^(WV1#k3^FVM#OLv3DS-L4mA`k`6ZGWEn{z&vIt}7R3OHf} z*g7G}L4Z6S6@ZPTV8}+?MmR)(ZLdYZ*T?raMNmtCcgDBj`^}f{l+Wh_79zlwrD9Wp zgw-P8!)mqTh&=%kH-96}yX$p10yv+z)z~RA1_883lI=xpF#=L_sVppu>Pio*BuOrU ziU4jgmGJi(Rymm(WDo(p&{cXUm7&6 zvY8-j5}`|zK~o?lHI?P5JiNs0u!o}1`vW{M#jr>P#5(%QYv{Q#37`^rE(m3U`@E#J z%>;ef%g(=$>}}3ef0GKZSI+ij1e+TBRC$c+a@g;)2 P zBmvftCBPnaZJmHAXNc|6O2}F8LT{G5%|<_~9J0gs6qGG%2~`NdjU6 zT4KBu`$#hvXszcZ0b}0|OF-iNg3|p*e?OqK1710*AhQ$jMFH+#W^x%E8iA?%%j@x4 zi_=`jT?m*Px&qe(jx7RhQZvi)>eg0{~du)hpLpa{f8}Sr7zY```ii@MCXcFP=yM1U$a> zd)IeVkdtOWR{%CvZnhSBbnI>A^rP5Plz~}+coM7rsE}8*a7#utRptuVq zhPeH$x_Ek66?P}s&a17ucyd@ZpWAr=$23EwIS=128o*MramM*aO9#cR_K~zacmVd4 zTz;pnCID%a0CaUwzEGJw18m_*|E`OtkC4WZe8jZ#XmLHXy|m-}wBbn_hXSR#UH&f> zW?t~=+z)OAmnC>-Srr13WylW=JC-KLkQ+_)xZm~shhCOxn}^PvE)F{!PP{x*~Ftt1D`3768@-%?Y#`qS==oT`Icz0C0s?DrYbNXqlq6%ap*(09Zv@(FA;nixje8 z=WH_*B~B7z0vZD(Um}H6%F~+@jP(H;0NDhr6DczaWGCo2ugp>4qCVMM6!0ZNv|suO zEfI3{m+}BR1EsTs6AVg(q*NFKY67E=FBLYotQ63f3cLDCsjx3X1*O7Le7r30)w98| z1Caq3yN0#(Wd>*njlDfHVC)*!)|VOJs|RQmU=7`QwNvI<$d?%~FD(sa+{}QXXlh<^ zW(Ld)*`bU(a|Yacu-+WCu{>>_<>S3pt~M`Ppx6?6))xwVF95Kt)#diM4Z!Z>m(5wA z23+XE**^m?EppO#m(B#ZU;<*_ACQ2O&iJ+OV=qM5@^zKd3aNQAnu$aVjKQN;Iecrn-GZMn_? X8TA}}$n(*s00000NkvXXu0mjfrXkB@ diff --git a/src/main/resources/skins/glare.png b/src/main/resources/skins/glare.png deleted file mode 100644 index be2a77e53ea38c8c124bfe19d0e3612117289485..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1253 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 ViRVt5C!+uW002ovPDHLkV1gsRQ)~bL diff --git a/src/main/resources/skins/migrator.png b/src/main/resources/skins/migrator.png deleted file mode 100644 index 0543ea69aeea8f1b2fbb4ea311a4cc5ea7ccdea3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2551 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 N002ovPDHLkV1gnwvU&gj diff --git a/src/main/resources/skins/mustache.png b/src/main/resources/skins/mustache.png deleted file mode 100644 index d6301757acac83d0a2276869bc544d15d1958527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 575 zcmV-F0>J%=P)0TmRnND;*%K~WG(jfG7Ri4lz2Y&I_;iOX(+1Y5zzB48of z_$vGd+6uM`f}jW@HvR!$jfC}%5fUWMGJ8MH%(={*0cSX?=d7j?$mI)WWG3WWj4k=x zFSO7@C*urjx@Anw&4u}QeBR+u^){$_{lBl_Xip?99UQH~jgvcws5ze z(GtRk!U0nbD14>#c$J?Or>p#~*o;P|!Kr%s(sdlKV?AT$golLvxolDYJ&(GHCz)TE zm)a+@ABzYxgzyok$SPS11f}LBv&O!WAKxr%;?d=AP{I^Pnv8e@q86oVQff-{q&y}5WQT}{0HFOgtN<< z7T<;QE$DqPG*kPLTr!=h6uh27M+~}dpl!LD)$ckd_YmEXC%T5ceHfJe-j#1l&1fw` z_i!ly000SaNLh0L01FZT01FZU(%pXi0001XNkl| zJjOWZ+v63sIzm~x8J=M&|6N|bJsm#R&h>W&00000000000002klL?(0ALrrd`8)ss N002ovPDHLkV1g$-{@DNk diff --git a/src/main/resources/skins/pirate.png b/src/main/resources/skins/pirate.png deleted file mode 100644 index a9ead907a29f3d1575baa9fdadb0ef0a3513d789..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2643 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 aLH0i^9hGj3pxFoj0000()); - 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(); - } else { - onDisable(); - } - 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); - } - } - - @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; - } - 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; - } - - @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; - } -} \ 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 deleted file mode 100644 index 9b3eb467..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java +++ /dev/null @@ -1,65 +0,0 @@ -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.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 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 - ) - ); - } - - @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 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)); - } - } - } -} - 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); - } -} From 2ec6cf855e7e29e9c157fff5e984a0a978dfe417 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 18:41:13 +0300 Subject: [PATCH 02/79] feat: Recode of the entire plugin BREAKING CHANGE: Old configs and save data may not be compatible with the new version --- .gitignore | 263 +- build-info/build.gradle.kts | 41 + .../skinoverlay/BuildParameters.java.peb | 14 + buildSrc/build.gradle.kts | 7 + .../buildlogic.java-conventions.gradle.kts | 46 + bukkit/build.gradle.kts | 71 + .../skinoverlay/SkinOverlayBukkit.java | 140 + .../appliers/LegacySkinApplier.java | 272 ++ .../command/BukkitCommandIssuer.java | 68 + .../command/BukkitCommandManager.java | 36 + .../command/BukkitPluginCommandWrapper.java | 34 + .../listeners/bukkit/PlayerListeners.java | 36 + .../skinoverlay/player/BukkitSPlayer.java | 26 + .../BukkitLegacyGameProfileProvider.java | 64 + .../providers/BukkitPlayerProvider.java | 85 + .../providers/PaperGameProfileProvider.java | 42 + .../scheduler/MinecraftBukkitScheduler.java | 303 +++ .../scheduler/MinecraftFoliaScheduler.java | 356 +++ .../utilities/BukkitMinecraftUtils.java | 903 +++++++ bukkit/versions/mc1_17_R1/build.gradle.kts | 18 + .../appliers/SkinApplier_1_17_R1.java | 100 + .../GameProfileProvider_1_17_R1.java | 44 + bukkit/versions/mc1_18_R1/build.gradle.kts | 18 + .../appliers/SkinApplier_1_18_R1.java | 100 + .../GameProfileProvider_1_18_R1.java | 44 + bukkit/versions/mc1_18_R2/build.gradle.kts | 18 + .../appliers/SkinApplier_1_18_R2.java | 103 + .../GameProfileProvider_1_18_R2.java | 44 + bukkit/versions/mc1_19_R1/build.gradle.kts | 18 + .../appliers/SkinApplier_1_19_R1.java | 104 + .../GameProfileProvider_1_19_R1.java | 44 + bukkit/versions/mc1_19_R2/build.gradle.kts | 18 + .../appliers/SkinApplier_1_19_R2.java | 103 + .../GameProfileProvider_1_19_R2.java | 44 + bukkit/versions/mc1_19_R3/build.gradle.kts | 18 + .../appliers/SkinApplier_1_19_R3.java | 103 + .../GameProfileProvider_1_19_R3.java | 44 + bukkit/versions/mc1_20_R1/build.gradle.kts | 18 + .../appliers/SkinApplier_1_20_R1.java | 104 + .../GameProfileProvider_1_20_R1.java | 44 + bukkit/versions/mc1_20_R2/build.gradle.kts | 18 + .../appliers/SkinApplier_1_20_R2.java | 90 + .../GameProfileProvider_1_20_R2.java | 44 + bukkit/versions/mc1_20_R3/build.gradle.kts | 18 + .../appliers/SkinApplier_1_20_R3.java | 91 + .../GameProfileProvider_1_20_R3.java | 44 + bukkit/versions/mc1_20_R4/build.gradle.kts | 19 + .../appliers/SkinApplier_1_20_R4.java | 91 + .../GameProfileProvider_1_20_R4.java | 44 + bukkit/versions/mc1_21_R1/build.gradle.kts | 19 + .../appliers/SkinApplier_1_21_R1.java | 90 + .../GameProfileProvider_1_21_R1.java | 44 + bukkit/versions/mc1_21_R2/build.gradle.kts | 19 + .../appliers/SkinApplier_1_21_R2.java | 90 + .../GameProfileProvider_1_21_R2.java | 44 + bukkit/versions/mc1_21_R3/build.gradle.kts | 19 + .../appliers/SkinApplier_1_21_R3.java | 90 + .../GameProfileProvider_1_21_R3.java | 44 + bukkit/versions/mc1_21_R4/build.gradle.kts | 19 + .../appliers/SkinApplier_1_21_R4.java | 90 + .../GameProfileProvider_1_21_R4.java | 44 + bukkit/versions/mc1_21_R5/build.gradle.kts | 19 + .../appliers/SkinApplier_1_21_R5.java | 90 + .../GameProfileProvider_1_21_R5.java | 44 + common/build.gradle.kts | 80 + .../georgev22/skinoverlay/SkinOverlay.java | 428 ++++ .../skinoverlay/appliers/NoopSkinApplier.java | 18 + .../skinoverlay/appliers/SkinApplier.java | 61 + .../skinoverlay/command/BaseCommand.java | 144 ++ .../skinoverlay/command/CommandContext.java | 11 + .../skinoverlay/command/CommandIssuer.java | 38 + .../skinoverlay/command/CommandManager.java | 73 + .../skinoverlay/command/CompletionEngine.java | 45 + .../skinoverlay/command/ExecutionType.java | 6 + .../command/annotation/Argument.java | 14 + .../command/annotation/CommandAlias.java | 12 + .../command/annotation/CommandCompletion.java | 12 + .../command/annotation/Description.java | 12 + .../command/annotation/Execution.java | 14 + .../command/annotation/Permission.java | 12 + .../command/annotation/PlayerOnly.java | 11 + .../command/annotation/Subcommand.java | 12 + .../skinoverlay/command/annotation/Usage.java | 12 + .../commands/SkinOverlayBaseCommand.java | 41 + .../command/commands/SkinOverlayMain.java | 31 + .../command/commands/sub/ClearSubCommand.java | 80 + .../command/commands/sub/InfoSubCommand.java | 39 + .../commands/sub/ReloadSubCommand.java | 31 + .../command/commands/sub/WearSubCommand.java | 88 + .../commands/sub/WearUrlSubCommand.java | 120 + .../command/resolvers/ArgumentResolver.java | 11 + .../PipeSeparatedArgumentResolver.java | 49 + .../command/resolvers/PlayersResolver.java | 18 + .../command/resolvers/RangeResolver.java | 26 + .../skinoverlay/event/Cancellable.java | 21 + .../georgev22/skinoverlay/event/Event.java | 7 + .../georgev22/skinoverlay/event/EventBus.java | 126 + .../skinoverlay/event/EventHandler.java | 17 + .../skinoverlay/event/HandlerPriority.java | 38 + .../events/player/PlayerDataLoadEvent.java | 18 + .../events/player/PlayerDataSaveEvent.java | 19 + .../event/events/player/SPlayerJoinEvent.java | 19 + .../events/player/SPlayerLeaveEvent.java | 19 + .../exceptions/NotFoundException.java | 34 + .../exceptions/PairDocumentException.java | 36 + .../exceptions/ReflectionException.java | 35 + .../skinoverlay/exceptions/SkinException.java | 11 + .../georgev22/skinoverlay/hooks/SkinHook.java | 25 + .../skinoverlay/hooks/SkinHookNoop.java | 16 + .../skinoverlay/hooks/SkinsRestorerHook.java | 61 + .../listeners/PlayerListeners.java | 95 + .../skinoverlay/maps/ConcurrentObjectMap.java | 504 ++++ .../skinoverlay/maps/HashObjectMap.java | 504 ++++ .../skinoverlay/maps/LinkedObjectMap.java | 504 ++++ .../georgev22/skinoverlay/maps/ObjectMap.java | 466 ++++ .../skinoverlay/maps/ObservableObjectMap.java | 217 ++ .../com/georgev22/skinoverlay/maps/Pair.java | 113 + .../skinoverlay/maps/PairDocument.java | 291 +++ .../skinoverlay/maps/TreeObjectMap.java | 513 ++++ .../maps/UnmodifiableObjectMap.java | 705 +++++ .../skinoverlay/message/RedisManager.java | 102 + .../georgev22/skinoverlay/player/SPlayer.java | 25 + .../providers/GameProfileProvider.java | 28 + .../skinoverlay/providers/PlayerProvider.java | 46 + .../skinoverlay/providers/SkinProvider.java | 525 ++++ .../registry/EntityManagerRegistry.java | 81 + .../scheduler/MinecraftScheduler.java | 327 +++ .../scheduler/SchedulerRunnable.java | 167 ++ .../skinoverlay/scheduler/SchedulerTask.java | 13 + .../skinoverlay/skin/SGameProfile.java | 55 + .../georgev22/skinoverlay/skin/SProperty.java | 5 + .../skinoverlay/storage/EntityManager.java | 187 ++ .../skinoverlay/storage/data/Entity.java | 70 + .../skinoverlay/storage/data/PlayerData.java | 145 ++ .../skinoverlay/storage/data/Skin.java | 161 ++ .../storage/gson/PartTypeAdapter.java | 87 + .../storage/gson/PlayerDataTypeAdapter.java | 94 + .../storage/gson/SPropertyTypeAdapter.java | 78 + .../SerializableBufferedImageTypeAdapter.java | 82 + .../storage/gson/SkinPartsTypeAdapter.java | 78 + .../storage/gson/SkinTypeAdapter.java | 81 + .../storage/manager/PlayerEntityManager.java | 150 ++ .../storage/manager/SkinEntityManager.java | 151 ++ .../manager/gson/PlayerFileManager.java | 312 +++ .../storage/manager/gson/SkinFileManager.java | 225 ++ .../skinoverlay/utilities/CustomData.java | 135 + .../skinoverlay/utilities/ImageSupplier.java | 18 + .../skinoverlay/utilities/Locale.java | 75 + .../skinoverlay/utilities/MessageBuilder.java | 443 ++++ .../utilities/SerializableBufferedImage.java | 149 ++ .../skinoverlay/utilities/Utils.java | 2269 +++++++++++++++++ .../skinoverlay/utilities/color/Color.java | 188 ++ .../utilities/color/ColorCalculations.java | 32 + .../utilities/color/MinecraftColor.java | 71 + .../skinoverlay/utilities/config/CFG.java | 212 ++ .../utilities/config/FileManager.java | 26 + .../utilities/config/MessagesUtil.java | 218 ++ .../utilities/config/OptionsUtil.java | 172 ++ .../utilities/config/OverlayOptionsUtil.java | 140 + .../config/SkinConfigurationFile.java | 63 + .../utilities/config/SkinFileCache.java | 154 ++ .../utilities/skin/MinecraftSkinRenderer.java | 67 + .../skinoverlay/utilities/skin/Part.java | 90 + .../skinoverlay/utilities/skin/Section.java | 180 ++ .../skinoverlay/utilities/skin/SkinParts.java | 332 +++ common/src/main/resources/bungee.yml | 7 + common/src/main/resources/config.yml | 34 + common/src/main/resources/messages_en_US.yml | 11 + common/src/main/resources/messages_tr.yml | 16 + common/src/main/resources/plugin.yml | 11 + common/src/main/resources/skins-cfg/alley.yml | 15 + .../resources/skins-cfg/bubbo_transparent.yml | 15 + .../main/resources/skins-cfg/fire_demon.yml | 15 + common/src/main/resources/skins-cfg/flame.yml | 15 + .../main/resources/skins-cfg/frog_pijama.yml | 15 + common/src/main/resources/skins-cfg/glare.yml | 15 + .../src/main/resources/skins-cfg/hoodie.yml | 15 + .../src/main/resources/skins-cfg/migrator.yml | 15 + .../src/main/resources/skins-cfg/mustache.yml | 15 + .../src/main/resources/skins-cfg/pirate.yml | 15 + .../main/resources/skins-cfg/policeman.yml | 15 + .../src/main/resources/skins-cfg/smoking.yml | 15 + common/src/main/resources/skins/alley.png | Bin 0 -> 2030 bytes .../resources/skins/bubbo_transparent.png | Bin 0 -> 720 bytes .../src/main/resources/skins/fire_demon.png | Bin 0 -> 3049 bytes common/src/main/resources/skins/flame.png | Bin 0 -> 1150 bytes .../src/main/resources/skins/frog_pijama.png | Bin 0 -> 1001 bytes common/src/main/resources/skins/glare.png | Bin 0 -> 1253 bytes common/src/main/resources/skins/hoodie.png | Bin 0 -> 1831 bytes common/src/main/resources/skins/migrator.png | Bin 0 -> 2551 bytes common/src/main/resources/skins/mustache.png | Bin 0 -> 575 bytes common/src/main/resources/skins/pirate.png | Bin 0 -> 2643 bytes common/src/main/resources/skins/policeman.png | Bin 0 -> 1792 bytes common/src/main/resources/skins/smoking.png | Bin 0 -> 900 bytes gradle.properties | 9 + gradle/libs.versions.toml | 51 + gradle/publish.gradle | 103 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 ++ gradlew.bat | 89 + settings.gradle.kts | 29 + 202 files changed, 20009 insertions(+), 225 deletions(-) create mode 100644 build-info/build.gradle.kts create mode 100644 build-info/src/main/java-templates/com/georgev22/skinoverlay/BuildParameters.java.peb create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts create mode 100644 bukkit/build.gradle.kts create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/player/BukkitSPlayer.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftBukkitScheduler.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftFoliaScheduler.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java create mode 100644 bukkit/versions/mc1_17_R1/build.gradle.kts create mode 100644 bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java create mode 100644 bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java create mode 100644 bukkit/versions/mc1_18_R1/build.gradle.kts create mode 100644 bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java create mode 100644 bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java create mode 100644 bukkit/versions/mc1_18_R2/build.gradle.kts create mode 100644 bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R2.java create mode 100644 bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java create mode 100644 bukkit/versions/mc1_19_R1/build.gradle.kts create mode 100644 bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R1.java create mode 100644 bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java create mode 100644 bukkit/versions/mc1_19_R2/build.gradle.kts create mode 100644 bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R2.java create mode 100644 bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java create mode 100644 bukkit/versions/mc1_19_R3/build.gradle.kts create mode 100644 bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R3.java create mode 100644 bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java create mode 100644 bukkit/versions/mc1_20_R1/build.gradle.kts create mode 100644 bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java create mode 100644 bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java create mode 100644 bukkit/versions/mc1_20_R2/build.gradle.kts create mode 100644 bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R2.java create mode 100644 bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java create mode 100644 bukkit/versions/mc1_20_R3/build.gradle.kts create mode 100644 bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R3.java create mode 100644 bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java create mode 100644 bukkit/versions/mc1_20_R4/build.gradle.kts create mode 100644 bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R4.java create mode 100644 bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java create mode 100644 bukkit/versions/mc1_21_R1/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R1.java create mode 100644 bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java create mode 100644 bukkit/versions/mc1_21_R2/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java create mode 100644 bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java create mode 100644 bukkit/versions/mc1_21_R3/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java create mode 100644 bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java create mode 100644 bukkit/versions/mc1_21_R4/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java create mode 100644 bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R4.java create mode 100644 bukkit/versions/mc1_21_R5/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R5.java create mode 100644 bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java create mode 100644 common/build.gradle.kts create mode 100644 common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/Cancellable.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/Event.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/EventBus.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/EventHandler.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/HandlerPriority.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataLoadEvent.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/events/player/PlayerDataSaveEvent.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerJoinEvent.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/event/events/player/SPlayerLeaveEvent.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/exceptions/NotFoundException.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/exceptions/PairDocumentException.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/exceptions/ReflectionException.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/exceptions/SkinException.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHook.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/hooks/SkinHookNoop.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/ConcurrentObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/HashObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/LinkedObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/ObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/ObservableObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/Pair.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/UnmodifiableObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/player/SPlayer.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/scheduler/MinecraftScheduler.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerRunnable.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/scheduler/SchedulerTask.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/data/Entity.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/data/PlayerData.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/PlayerFileManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/SkinFileManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/color/Color.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/color/ColorCalculations.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/color/MinecraftColor.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/skin/MinecraftSkinRenderer.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Section.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java create mode 100644 common/src/main/resources/bungee.yml create mode 100644 common/src/main/resources/config.yml create mode 100644 common/src/main/resources/messages_en_US.yml create mode 100644 common/src/main/resources/messages_tr.yml create mode 100644 common/src/main/resources/plugin.yml create mode 100644 common/src/main/resources/skins-cfg/alley.yml create mode 100644 common/src/main/resources/skins-cfg/bubbo_transparent.yml create mode 100644 common/src/main/resources/skins-cfg/fire_demon.yml create mode 100644 common/src/main/resources/skins-cfg/flame.yml create mode 100644 common/src/main/resources/skins-cfg/frog_pijama.yml create mode 100644 common/src/main/resources/skins-cfg/glare.yml create mode 100644 common/src/main/resources/skins-cfg/hoodie.yml create mode 100644 common/src/main/resources/skins-cfg/migrator.yml create mode 100644 common/src/main/resources/skins-cfg/mustache.yml create mode 100644 common/src/main/resources/skins-cfg/pirate.yml create mode 100644 common/src/main/resources/skins-cfg/policeman.yml create mode 100644 common/src/main/resources/skins-cfg/smoking.yml create mode 100644 common/src/main/resources/skins/alley.png create mode 100644 common/src/main/resources/skins/bubbo_transparent.png create mode 100644 common/src/main/resources/skins/fire_demon.png create mode 100644 common/src/main/resources/skins/flame.png create mode 100644 common/src/main/resources/skins/frog_pijama.png create mode 100644 common/src/main/resources/skins/glare.png create mode 100644 common/src/main/resources/skins/hoodie.png create mode 100644 common/src/main/resources/skins/migrator.png create mode 100644 common/src/main/resources/skins/mustache.png create mode 100644 common/src/main/resources/skins/pirate.png create mode 100644 common/src/main/resources/skins/policeman.png create mode 100644 common/src/main/resources/skins/smoking.png create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/publish.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore index 789aed76..b63da455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,229 +1,42 @@ -# 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/ - -# 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 +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ *.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/build-info/build.gradle.kts b/build-info/build.gradle.kts new file mode 100644 index 00000000..f3fc60df --- /dev/null +++ b/build-info/build.gradle.kts @@ -0,0 +1,41 @@ +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("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..b12d1026 --- /dev/null +++ b/build-info/src/main/java-templates/com/georgev22/skinoverlay/BuildParameters.java.peb @@ -0,0 +1,14 @@ +package com.georgev22.skinoverlay; + +public class BuildParameters { + public static final String VERSION = "{{ version }}"; + public static final String PLUGIN_NAME = "{{ pluginName }}"; + 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/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.kts b/bukkit/build.gradle.kts new file mode 100644 index 00000000..98c95f5f --- /dev/null +++ b/bukkit/build.gradle.kts @@ -0,0 +1,71 @@ +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.adventure.api) + compileOnly(libs.placeholder.api) + compileOnly(libs.auth.lib.legacy) + + implementation(libs.bstats.bukkit) + implementation(libs.adventure.platform.bukkit) + + implementation(project(":common")) + 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")) +} + +configurations.configureEach { + resolutionStrategy { + force(libs.adventure.platform.bukkit) + 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("shadow") + relocate("org.mineskin", "${project.property("packageName")}.mineskin") + relocate("com.google.gson", "${project.property("packageName")}.gson") + relocate("com.google.errorprone", "${project.property("packageName")}.gson.errorprone") + relocate("com.zaxxer", "${project.property("packageName")}.zaxxer") + relocate("org.bstats", "${project.property("packageName")}.bstats") + relocate("org.bspfsystems.yamlconfiguration", "${project.property("packageName")}.yaml") + relocate("org.yaml.snakeyaml", "${project.property("packageName")}.yaml") + relocate("org.intellij.lang", "${project.property("packageName")}.jetbrains") + relocate("org.jetbrains", "${project.property("packageName")}.jetbrains") + relocate("net.kyori", "${project.property("packageName")}.kyori") +} + +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 new file mode 100644 index 00000000..8bc81f3c --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -0,0 +1,140 @@ +package com.georgev22.skinoverlay; + +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.providers.*; +import com.georgev22.skinoverlay.scheduler.MinecraftBukkitScheduler; +import com.georgev22.skinoverlay.scheduler.MinecraftFoliaScheduler; +import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; + +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.V1_20_R1; +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion; + +public class SkinOverlayBukkit extends JavaPlugin { + + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + + @Override + public void onLoad() { + this.skinOverlay.setPlugin(this); + this.skinOverlay.setLogger(this.getLogger()); + this.skinOverlay.setDataFolder(this.getDataFolder()); + this.skinOverlay.setCommandManager(new BukkitCommandManager(this.getLogger(), 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()); + } + default -> { + this.setEnabled(false); + throw new RuntimeException("SkinOverlay does not support " + getCurrentVersion()); + } + } + } + + if (Bukkit.getPluginManager().isPluginEnabled("SkinsRestorer")) { + this.skinOverlay.setSkinHook(new SkinsRestorerHook()); + } else { + this.skinOverlay.setSkinHook(new SkinHookNoop()); + } + this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode()); + this.skinOverlay.setProxy(false); + if (BukkitMinecraftUtils.isFolia()) { + this.skinOverlay.setScheduler( + new MinecraftFoliaScheduler() + ); + } else { + this.skinOverlay.setScheduler( + new MinecraftBukkitScheduler() + ); + } + this.skinOverlay.onLoad(); + } + + @Override + public void onEnable() { + this.skinOverlay.setAudienceProvider(BukkitAudiences.create(this)); + + BukkitMinecraftUtils.registerListeners( + this, + new PlayerListeners() + ); + + // call onEnable + skinOverlay.onEnable(); + } + + @Override + public void onDisable() { + skinOverlay.onDisable(); + } +} diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java b/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java new file mode 100644 index 00000000..bce0e1f2 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java @@ -0,0 +1,272 @@ +package com.georgev22.skinoverlay.appliers; + +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 org.bukkit.Location; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +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 { + + private final Class playOutRespawn; + private final Class playOutPlayerInfo; + private final Class playOutPosition; + private final Class packet; + private final Class playOutHeldItemSlot; + private final Method getHandleMethod; + private Enum removePlayerEnum; + private Enum addPlayerEnum; + + public LegacySkinApplier() { + try { + packet = getNMSClass("Packet", "net.minecraft.network.protocol.Packet"); + playOutHeldItemSlot = getNMSClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot"); + playOutPosition = getNMSClass("PacketPlayOutPosition", "net.minecraft.network.protocol.game.PacketPlayOutPosition"); + playOutPlayerInfo = getNMSClass("PacketPlayOutPlayerInfo", "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo"); + playOutRespawn = getNMSClass("PacketPlayOutRespawn", "net.minecraft.network.protocol.game.PacketPlayOutRespawn"); + + try { + removePlayerEnum = getEnum(playOutPlayerInfo, "EnumPlayerInfoAction", "REMOVE_PLAYER"); + addPlayerEnum = getEnum(playOutPlayerInfo, "EnumPlayerInfoAction", "ADD_PLAYER"); + } catch (Exception e1) { + try { + Class enumPlayerInfoActionClass = Class.forName("net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo$EnumPlayerInfoAction"); + + removePlayerEnum = getEnum(enumPlayerInfoActionClass, 4); + addPlayerEnum = getEnum(enumPlayerInfoActionClass, 0); + } catch (Exception e2) { + try { + removePlayerEnum = getEnum(playOutPlayerInfo, "Action", "REMOVE_PLAYER"); + addPlayerEnum = getEnum(playOutPlayerInfo, "Action", "ADD_PLAYER"); + } catch (Exception e3) { + try { + Class enumPlayerInfoAction = getNMSClass("EnumPlayerInfoAction", null); + + removePlayerEnum = getEnum(enumPlayerInfoAction, "REMOVE_PLAYER"); + addPlayerEnum = getEnum(enumPlayerInfoAction, "ADD_PLAYER"); + } catch (Exception e4) { + //ignore + } + } + } + } + + getHandleMethod = getOBCClass("entity.CraftPlayer").getDeclaredMethod("getHandle"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + 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 bukkitPlayer = player.getPlayer(); + + final Object entityPlayer = getHandleMethod.invoke(player); + Object removePlayer; + Object addPlayer; + try { + removePlayer = invokeConstructor(playOutPlayerInfo, removePlayerEnum, ImmutableList.of(entityPlayer)); + addPlayer = invokeConstructor(playOutPlayerInfo, addPlayerEnum, ImmutableList.of(entityPlayer)); + } catch (ReflectionException e) { + int ping = (int) fetchField(entityPlayer.getClass(), entityPlayer, "ping"); + removePlayer = invokeConstructor(playOutPlayerInfo, bukkitPlayer.getPlayerListName(), false, 9999); + addPlayer = invokeConstructor(playOutPlayerInfo, bukkitPlayer.getPlayerListName(), true, ping); + } + + Object world; + try { + world = fetchMethodAndInvoke(entityPlayer.getClass(), "getWorld", entityPlayer, new Object[0], new Class[0]); + } catch (Exception ignore) { + world = fetchMethodAndInvoke(entityPlayer.getClass(), "getWorldServer", entityPlayer, new Object[0], new Class[0]); + } + Object difficulty; + try { + difficulty = fetchMethodAndInvoke(world.getClass(), "getDifficulty", world, new Object[0], new Class[0]); + } catch (Exception e) { + try { + difficulty = fetchField(world.getClass(), world, "difficulty"); + } catch (NoSuchFieldException ignore) { + skinOverlay.getLogger().info(world.getClass().getSimpleName() + " does not have getDifficulty method or difficulty field!!"); + difficulty = null; + } + + } + + Object worldData = null; + try { + worldData = fetchMethodAndInvoke(world.getClass(), "getWorldData", world, new Object[0], new Class[0]); + } catch (Exception ignored) { + try { + worldData = fetchField(world.getClass(), world, "worldData"); + } catch (Exception ignored2) { + skinOverlay.getLogger().info(world.getClass().getSimpleName() + " does not have getWorldData method or worldData field!!"); + } + } + + Object worldType = null; + try { + worldType = fetchMethodAndInvoke(Objects.requireNonNull(worldData).getClass(), "getType", worldData, new Object[0], new Class[0]); + } catch (Exception ignored) { + try { + worldType = fetchMethodAndInvoke(Objects.requireNonNull(worldData).getClass(), "getGameType", worldData, new Object[0], new Class[0]); + } catch (Exception ignored2) { + } + } + + Object playerIntManager = getFieldByType(entityPlayer, "PlayerInteractManager"); + Enum enumGamemode = (Enum) fetchMethodAndInvoke(playerIntManager.getClass(), "getGameMode", playerIntManager, new Object[0], new Class[0]); + + //noinspection UnstableApiUsage + int dimension = bukkitPlayer.getWorld().getEnvironment().getId(); + + Object respawn; + try { + respawn = invokeConstructor(playOutRespawn, dimension, difficulty, worldType, enumGamemode); + } catch (Exception ignored) { + Object worldObject = getFieldByType(entityPlayer, "World"); + Object dimensionManager = null; + try { + dimensionManager = getFieldByType(worldObject, "DimensionManager"); + } catch (ReflectionException e) { + try { + Class dimensionManagerClass = getNMSClass("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager"); + + for (Method m : dimensionManagerClass.getDeclaredMethods()) { + if (m.getReturnType() == dimensionManagerClass && m.getParameterCount() == 1 && m.getParameterTypes()[0] == Integer.TYPE) { + m.setAccessible(true); + dimensionManager = m.invoke(null, dimension); + } + } + } catch (InvocationTargetException | IllegalAccessException | ClassNotFoundException e2) { + throw new RuntimeException(e2); + } + } + + if (dimensionManager == null) { + throw new ReflectionException("Could not get DimensionManager from " + worldObject.getClass().getSimpleName()); + } + + try { + respawn = invokeConstructor(playOutRespawn, dimensionManager, difficulty, worldType, enumGamemode); + } catch (Exception ignored2) { + try { + respawn = invokeConstructor(playOutRespawn, dimensionManager, worldType, enumGamemode); + } catch (Exception ignored3) { + 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) { + Object dimensionKey = fetchMethodAndInvoke(worldObject.getClass(), "getDimensionKey", worldObject, new Object[0], new Class[0]); + boolean debug = (boolean) fetchMethodAndInvoke(worldObject.getClass(), "isDebugWorld", worldObject, new Object[0], new Class[0]); + boolean flat = (boolean) fetchMethodAndInvoke(worldObject.getClass(), "isFlatWorld", worldObject, new Object[0], new Class[0]); + List gameModeList = getFieldByTypeList(playerIntManager, "EnumGamemode"); + + Enum enumGamemodePrevious = null; + for (Object obj : gameModeList) { + if (obj != enumGamemode) + enumGamemodePrevious = (Enum) obj; + } + + try { + Object typeKey = fetchMethodAndInvoke(worldObject.getClass(), "getTypeKey", worldObject, new Object[0], new Class[0]); + + respawn = invokeConstructor(playOutRespawn, typeKey, dimensionKey, seedEncrypted, enumGamemode, enumGamemodePrevious, debug, flat, true); + } catch (Exception ignored6) { + respawn = invokeConstructor(playOutRespawn, dimensionManager, dimensionKey, seedEncrypted, enumGamemode, enumGamemodePrevious, debug, flat, true); + } + } + } + } + } + + Location l = bukkitPlayer.getLocation(); + Object pos; + try { + pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), new HashSet>(), 0); + } catch (Exception e1) { + try { + pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), new HashSet>()); + } catch (Exception e3) { + pos = invokeConstructor(playOutPosition, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), false); + } + } + + Object slot = invokeConstructor(playOutHeldItemSlot, bukkitPlayer.getInventory().getHeldItemSlot()); + Object playerConnection = getFieldByType(entityPlayer, "PlayerConnection"); + + sendPacket(playerConnection, removePlayer); + sendPacket(playerConnection, addPlayer); + + sendPacket(playerConnection, respawn); + + fetchMethodAndInvoke(entityPlayer.getClass(), "updateAbilities", entityPlayer, new Object[0], new Class[0]); + + sendPacket(playerConnection, pos); + sendPacket(playerConnection, slot); + + fetchMethodAndInvoke(player.getClass(), "updateScaledHealth", player, new Object[0], new Class[0]); + bukkitPlayer.updateInventory(); + fetchMethodAndInvoke(entityPlayer.getClass(), "triggerHealthUpdate", entityPlayer, new Object[0], new Class[0]); + + if (player.isOp()) { + this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), () -> { + bukkitPlayer.setOp(false); + bukkitPlayer.setOp(true); + }); + } + return true; + } catch (ReflectionException | InvocationTargetException | IllegalAccessException | NoSuchMethodException | + NoSuchFieldException e) { + throw new RuntimeException(e); + } + }, runnable -> this.skinOverlay.getScheduler().runTask(this.skinOverlay.getPlugin(), runnable)); + } + + +} 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..b2f59c45 --- /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("VoidChestConsole"); + + } + + @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..4f1f7b5a --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java @@ -0,0 +1,36 @@ +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.Logger; + +public class BukkitCommandManager extends CommandManager { + + private final SkinOverlayBukkit plugin; + + public BukkitCommandManager(Logger logger, SkinOverlayBukkit plugin) { + super(logger); + this.plugin = plugin; + } + + @Override + protected void registerCommand0(@NotNull BaseCommand command) throws ReflectiveOperationException { + 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); + } + } +} 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..5a2112ac --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java @@ -0,0 +1,34 @@ +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); + baseCommand.execute(commandIssuer, args); + 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/PlayerListeners.java b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java new file mode 100644 index 00000000..3317697b --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PlayerListeners.java @@ -0,0 +1,36 @@ +package com.georgev22.skinoverlay.listeners.bukkit; + +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 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; + +public class PlayerListeners implements Listener { + + private final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + + @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)); + } + } + + @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/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..dad19c0c --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -0,0 +1,903 @@ +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)), + 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/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/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java new file mode 100644 index 00000000..4b74ae03 --- /dev/null +++ b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java @@ -0,0 +1,100 @@ +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_17_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_17_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.dimensionType(), + 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_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/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java new file mode 100644 index 00000000..6c3449c6 --- /dev/null +++ b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java @@ -0,0 +1,100 @@ +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_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_18_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.dimensionType(), + 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_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/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java new file mode 100644 index 00000000..c6b8659a --- /dev/null +++ b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_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.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_20_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_20_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(); + 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(), + entityPlayer.getPortalCooldown() + ); + + 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_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/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 00000000..0561c65e --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,80 @@ +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) + + implementation(libs.hikari) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + implementation(libs.gson) + // ADVENTURE + implementation(libs.adventure.api) + implementation(libs.adventure.platform.api) + implementation(libs.adventure.text.minimessage) + implementation(libs.adventure.text.serializer.legacy) + implementation(libs.adventure.text.serializer.gson) { + exclude(group = "com.google.code.gson", module = "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.api) + force(libs.adventure.platform.api) + force(libs.adventure.text.serializer.legacy) + force(libs.adventure.text.serializer.gson) + 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" + } +} 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..c48ebb16 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -0,0 +1,428 @@ +package com.georgev22.skinoverlay; + +import com.georgev22.skinoverlay.appliers.SkinApplier; +import com.georgev22.skinoverlay.command.CommandManager; +import com.georgev22.skinoverlay.command.commands.SkinOverlayMain; +import com.georgev22.skinoverlay.command.resolvers.PlayersResolver; +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.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.Locale; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +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.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; + + /** + * 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 { + MessagesUtil.repairPaths(Locale.fromString(OptionsUtil.LOCALE.getStringValue())); + } 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() { + this.commandManager.addResolver("overlays", (commandIssuer, args) -> + skinFileCache.getSkinConfigurationFiles().keySet()); + this.commandManager.addResolver("players", new PlayersResolver()); + 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() { + return 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; + } +} 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..03ab51aa --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java @@ -0,0 +1,18 @@ +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 void applySkin(@NotNull SPlayer player) { + + } + + @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..e224d9d9 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java @@ -0,0 +1,61 @@ +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 com.georgev22.skinoverlay.utilities.config.OptionsUtil; +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); + + // Should we save the skin in the database? + if (!skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } + + 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 abstract void applySkin(@NotNull final SPlayer 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..6d8e6931 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java @@ -0,0 +1,144 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.annotation.*; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public abstract class BaseCommand { + private String[] aliases; + private String[] subCommandAliases; + private String description; + private String usage; + private final boolean playerOnly; + private String permission; + private ExecutionType executionType; + private final List> preprocessors = new ArrayList<>(); + private final List> postprocessors = new ArrayList<>(); + private final Map subcommands = new HashMap<>(); + + public BaseCommand() { + Class clazz = getClass(); + if (clazz.isAnnotationPresent(CommandAlias.class)) { + aliases = clazz.getAnnotation(CommandAlias.class).value(); + } + if (clazz.isAnnotationPresent(Subcommand.class)) { + subCommandAliases = clazz.getAnnotation(Subcommand.class).value(); + } + if (clazz.isAnnotationPresent(Description.class)) { + description = clazz.getAnnotation(Description.class).value(); + } + if (clazz.isAnnotationPresent(Usage.class)) { + usage = clazz.getAnnotation(Usage.class).value(); + } + playerOnly = clazz.isAnnotationPresent(PlayerOnly.class); + if (clazz.isAnnotationPresent(Permission.class)) { + permission = clazz.getAnnotation(Permission.class).value(); + } + if (clazz.isAnnotationPresent(Execution.class)) { + executionType = clazz.getAnnotation(Execution.class).value(); + } + } + + public void execute(CommandIssuer commandIssuer, String @NotNull [] args) { + SkinOverlay.getInstance().getCommandManager().getGlobalPreprocessors().forEach(pre -> pre.accept(commandIssuer)); + preprocessors.forEach(pre -> pre.accept(commandIssuer)); + + if (args.length > 0) { + BaseCommand subcommand = subcommands.get(args[0].toLowerCase()); + if (subcommand != null) { + subcommand.execute(commandIssuer, Arrays.copyOfRange(args, 1, args.length)); + return; + } + } + + if (playerOnly && !commandIssuer.isPlayer()) { + MessagesUtil.ONLY_PLAYER_COMMAND.msg(commandIssuer); + return; + } + + if (permission != null && !permission.isEmpty() && !commandIssuer.hasPermission(permission)) { + MessagesUtil.NO_PERMISSION.msg(commandIssuer); + return; + } + + CommandContext context = new CommandContext(); + if (executionType == ExecutionType.ASYNC) { + SkinOverlay.getInstance().getScheduler().runAsyncTask( + SkinOverlay.getInstance().getPlugin(), + () -> { + handle(commandIssuer, args, context); + SkinOverlay.getInstance().getCommandManager().getGlobalPostprocessors().forEach(post -> post.accept(commandIssuer, context)); + postprocessors.forEach(post -> post.accept(commandIssuer, context)); + } + ); + } else { + handle(commandIssuer, args, context); + SkinOverlay.getInstance().getCommandManager().getGlobalPostprocessors().forEach(post -> post.accept(commandIssuer, context)); + postprocessors.forEach(post -> post.accept(commandIssuer, context)); + } + } + + public Collection tabComplete(CommandIssuer commandIssuer, String @NotNull [] args) { + if (args.length > 1) { + BaseCommand subcommand = subcommands.get(args[0].toLowerCase()); + if (subcommand != null) { + if (commandIssuer.hasPermission(subcommand.getPermission())) + return subcommand.tabComplete(commandIssuer, Arrays.copyOfRange(args, 1, args.length)); + else + return Collections.emptyList(); + } + } + + return CompletionEngine.resolveCompletions(getClass(), commandIssuer, args); + } + + public void addSubcommand(@NotNull BaseCommand subcommand) { + for (String alias : subcommand.getAliases(true)) { + subcommands.put(alias.toLowerCase(), subcommand); + } + } + + public void addPreprocessor(Consumer preprocessor) { + if (preprocessors.contains(preprocessor)) return; + preprocessors.add(preprocessor); + } + + public void addPostprocessor(BiConsumer postprocessor) { + if (postprocessors.contains(postprocessor)) return; + postprocessors.add(postprocessor); + } + + public String[] getAliases(boolean isSubcommand) { + if (isSubcommand) { + return subCommandAliases; + } + return aliases; + } + + public String getUsage() { + return usage; + } + + public String getPermission() { + return permission; + } + + public String getDescription() { + return description; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public boolean isPlayerOnly() { + return playerOnly; + } + + protected abstract void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context); +} \ 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..151aae59 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java @@ -0,0 +1,11 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.utilities.CustomData; + +public class CommandContext { + private final CustomData customData = new CustomData(); + + public CustomData getData() { + return customData; + } +} \ No newline at end of file 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..346d3f35 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java @@ -0,0 +1,38 @@ +package com.georgev22.skinoverlay.command; + +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public interface CommandIssuer { + + boolean isPlayer(); + + void sendMessage(@NotNull String message); + + default void sendMessage(String @NotNull ... messages) { + for (String message : messages) { + sendMessage(message); + } + } + + void sendMessage(@NotNull Component component); + + default void sendMessage(Component @NotNull ... components) { + for (Component component : components) { + sendMessage(component); + } + } + + @NotNull T getIssuer(); + + boolean hasPermission(String permission); + + boolean isOp(); + + UUID getUniqueId(); + + 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..21f05a49 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java @@ -0,0 +1,73 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.command.annotation.CommandAlias; +import com.georgev22.skinoverlay.command.annotation.Subcommand; +import com.georgev22.skinoverlay.command.resolvers.ArgumentResolver; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class CommandManager { + + private final List> globalPreprocessors = new ArrayList<>(); + private final List> globalPostprocessors = new ArrayList<>(); + private final Logger logger; + private final Map resolvers = new HashMap<>(); + + public CommandManager(Logger logger) { + this.logger = logger; + } + + 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); + } + } + } catch (Exception e) { + this.logger.log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e); + } + } + + protected abstract void registerCommand0(@NotNull BaseCommand command) throws ReflectiveOperationException; + + public void addGlobalPreprocessor(Consumer preprocessor) { + globalPreprocessors.add(preprocessor); + } + + public void addGlobalPostprocessor(BiConsumer postprocessor) { + globalPostprocessors.add(postprocessor); + } + + public List> getGlobalPostprocessors() { + return globalPostprocessors; + } + + public List> getGlobalPreprocessors() { + return globalPreprocessors; + } + + public ArgumentResolver getResolver(@NotNull String key) { + return resolvers.get(key.toLowerCase()); + } + + public void addResolver(@NotNull String key, ArgumentResolver resolver) { + resolvers.put(key.toLowerCase(), resolver); + } +} \ No newline at end of file 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..e5762274 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java @@ -0,0 +1,45 @@ +package com.georgev22.skinoverlay.command; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.annotation.CommandCompletion; +import com.georgev22.skinoverlay.command.resolvers.ArgumentResolver; + +import java.util.ArrayList; +import java.util.Collection; + +public class CompletionEngine { + + public static Collection resolveCompletions(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]; + Collection completions = new ArrayList<>(); + + for (String option : segment.split("\\|")) { + if (option.startsWith("@")) { + String resolverKey = option.substring(1); + String[] resolverParts = resolverKey.split(":", 2); + String resolverName = resolverParts[0]; + String[] resolverArgs = resolverParts.length > 1 ? new String[]{resolverParts[1]} : new String[0]; + + ArgumentResolver resolver = SkinOverlay.getInstance().getCommandManager().getResolver(resolverName); + if (resolver != null) { + completions.addAll(resolver.resolve(commandIssuer, resolverArgs)); + } + } else { + completions.add(option); + } + } + + return completions; + } +} 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..7183a8be --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java @@ -0,0 +1,6 @@ +package com.georgev22.skinoverlay.command; + +public enum ExecutionType { + SYNC, + ASYNC +} \ No newline at end of file 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..ae7fb2d3 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java @@ -0,0 +1,14 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Argument { + String name(); + + String description() 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..61ca7059 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java @@ -0,0 +1,12 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CommandAlias { + String[] value(); +} \ No newline at end of file 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..30aa4d7c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java @@ -0,0 +1,12 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CommandCompletion { + String value(); +} 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..20a54ed7 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java @@ -0,0 +1,12 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Description { + String value() default ""; +} \ No newline at end of file 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..6ff53da3 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java @@ -0,0 +1,14 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Execution { + ExecutionType value() default ExecutionType.SYNC; +} \ No newline at end of file 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..09a25338 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java @@ -0,0 +1,12 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Permission { + String value() default ""; +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java new file mode 100644 index 00000000..a0acf79c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java @@ -0,0 +1,11 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PlayerOnly { +} \ No newline at end of file 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..aa4227d7 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java @@ -0,0 +1,12 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Subcommand { + String[] value(); +} \ No newline at end of file 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..0d20c6d2 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java @@ -0,0 +1,12 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Usage { + String value() default ""; +} \ No newline at end of file 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..3236e1fc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java @@ -0,0 +1,41 @@ +package com.georgev22.skinoverlay.command.commands; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.BaseCommand; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +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); + } + } + + protected @NotNull Optional getPlayerObject(@NotNull CommandIssuer issuer, String name) { + Optional target; + if (issuer.hasPermission("skinoverlay.wear.overlay.others")) { + target = mainPlugin.getPlayerProvider().isOnline(name) + ? Optional.ofNullable(mainPlugin.getPlayerProvider().getSPlayer(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; + } +} 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..1ff73dd5 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java @@ -0,0 +1,31 @@ +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.ClearSubCommand; +import com.georgev22.skinoverlay.command.commands.sub.ReloadSubCommand; +import com.georgev22.skinoverlay.command.commands.sub.WearSubCommand; +import com.georgev22.skinoverlay.command.commands.sub.WearUrlSubCommand; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +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()); + } + + @Override + protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + MessagesUtil.HELP_FORMAT.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..8ade45bb --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java @@ -0,0 +1,80 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.CommandCompletion; +import com.georgev22.skinoverlay.command.annotation.Description; +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.maps.HashObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +@Subcommand({"clear", "reset"}) +@CommandCompletion("@players") +@Description("Removes the skin overlay") +@Permission("skinoverlay.wear.clear") +public class ClearSubCommand extends SkinOverlayBaseCommand { + + @Override + protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + if (args.length == 0) { + if (!commandIssuer.isPlayer()) { + MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(commandIssuer, + new HashObjectMap().append("%command%", "clear "), 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); + MessagesUtil.RESET.msg( + commandIssuer, + new HashObjectMap().append("%player%", player.getName()), + true + ); + + }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); + } else { + Optional optionalPlayerObject = getPlayerObject(commandIssuer, args[0]); + if (optionalPlayerObject.isEmpty()) { + MessagesUtil.OFFLINE_PLAYER.msg(commandIssuer, new HashObjectMap().append("%player%", args[0]), true); + return; + } + SkinParts skinParts = new SkinParts(null, "default"); + mainPlugin.getSkinProvider().retrieveOrGenerateSkin( + optionalPlayerObject.get(), + skinParts) + .thenAcceptAsync(skinOptional -> { + if (skinOptional.isEmpty()) { + return; + } + Skin skin = skinOptional.get(); + mainPlugin.getSkinApplier().setSkin(optionalPlayerObject.get(), skin); + MessagesUtil.RESET.msg( + commandIssuer, + new HashObjectMap().append("%player%", optionalPlayerObject.get().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..ece5be44 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java @@ -0,0 +1,39 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.BuildParameters; +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; +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") +@Permission("skinoverlay.info") +public class InfoSubCommand extends SkinOverlayBaseCommand { + + @Override + protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + 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..9fd97ee6 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java @@ -0,0 +1,31 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; +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.utilities.Locale; +import com.georgev22.skinoverlay.utilities.MessageBuilder; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; + +@Subcommand("reload") +@Permission("skinoverlay.reload") +public class ReloadSubCommand extends SkinOverlayBaseCommand { + @Override + protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + //TODO RELOAD SKIN HANDLER + mainPlugin.getFileManager().getConfig().reloadFile(); + try { + MessagesUtil.repairPaths(Locale.fromString(OptionsUtil.LOCALE.getStringValue())); + MessagesUtil.getMessagesCFG().reloadFile(); + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error loading the language file: ", e); + } + commandIssuer.sendMessage(MessageBuilder.builder().appendMiniMessage("&a&l(!)&a Plugin reloaded!").build()); + } +} 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..9a1d963a --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java @@ -0,0 +1,88 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.CommandCompletion; +import com.georgev22.skinoverlay.command.annotation.Description; +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.maps.HashObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +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.Optional; +import java.util.logging.Level; + +@Subcommand({"wear", "overlay"}) +@Permission("skinoverlay.wear.overlay") +@Description("Wear an overlay on a player's skin") +@CommandCompletion("@overlays @players") +public class WearSubCommand extends SkinOverlayBaseCommand { + + @Override + protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + if (args.length < 1) { + MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(commandIssuer, new HashObjectMap() + .append("%command%", "wear "), true); + return; + } + + var overlay = args[0]; + SPlayer target; + + if (args.length > 1) { + Optional optionalSPlayer = getPlayerObject(commandIssuer, args[1]); + if (optionalSPlayer.isEmpty()) return; + target = optionalSPlayer.get(); + } else if (!(commandIssuer.isPlayer())) { + MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(commandIssuer, + new HashObjectMap().append("%command%", "wear skin "), true); + return; + } else { + target = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); + } + SkinParts skinParts; + try { + File overlayFile = new File(mainPlugin.getSkinsDataFolder(), overlay + ".png"); + if (!overlayFile.exists()) { + MessagesUtil.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; + } + mainPlugin.getSkinProvider() + .retrieveOrGenerateSkin( + target, + skinParts + ).thenAcceptAsync(optionalSkin -> { + if (optionalSkin.isEmpty()) { + mainPlugin.getLogger().info("Skin is null"); + return; + } + Skin skin = optionalSkin.get(); + mainPlugin.getSkinApplier() + .setSkin(target, skin); + MessagesUtil.DONE.msg( + commandIssuer, + new HashObjectMap() + .append("%player%", target.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..8b0ddd3b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java @@ -0,0 +1,120 @@ +package com.georgev22.skinoverlay.command.commands.sub; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandContext; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.annotation.CommandCompletion; +import com.georgev22.skinoverlay.command.annotation.Description; +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.maps.HashObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +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.Optional; +import java.util.logging.Level; + +@Subcommand("url") +@Permission("skinoverlay.wear.url") +@Description("Wear an overlay on a player's skin") +@CommandCompletion(" false|true|@players false|true false|true false|true false|true false|true false|true @players") +public class WearUrlSubCommand extends SkinOverlayBaseCommand { + + @Override + protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + if (args.length < 1) { + sendInsufficientArguments(commandIssuer); + return; + } + + try { + URL url = new URL(args[0]); + Optional targetPlayer = resolveTargetPlayer(commandIssuer, args); + if (targetPlayer.isEmpty()) { + return; + } + + byte[] imageBytes = downloadImageBytes(url); + if (imageBytes == null) { + MessagesUtil.INVALID_URL.msg(commandIssuer, new HashObjectMap().append("%url%", url.toString()), true); + return; + } + + SkinParts skinParts = createSkinPartsFromBytes(imageBytes, url); + applySkinToPlayer(commandIssuer, targetPlayer.get(), skinParts); + + } catch (Exception e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error executing WearUrlSubCommand:", e); + MessagesUtil.ERROR.msg(commandIssuer, new HashObjectMap().append("%error%", e.getMessage()), true); + } + } + + private void sendInsufficientArguments(CommandIssuer issuer) { + MessagesUtil.INSUFFICIENT_ARGUMENTS.msg( + issuer, + new HashObjectMap().append("%command%", "url "), + true + ); + } + + private Optional resolveTargetPlayer(CommandIssuer issuer, String @NotNull [] args) { + if (args.length > 1) { + return getPlayerObject(issuer, args[1]); + } else if (issuer.isPlayer()) { + return Optional.of(mainPlugin.getPlayerProvider().getSPlayer(issuer.getUniqueId())); + } else { + sendInsufficientArguments(issuer); + return Optional.empty(); + } + } + + 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()) { + MessagesUtil.ERROR.msg(issuer, new HashObjectMap().append("%error%", "Failed to retrieve or generate skin."), true); + return; + } + + Skin skin = optionalSkin.get(); + mainPlugin.getSkinApplier().setSkin(player, skin); + + MessagesUtil.DONE.msg( + issuer, + new HashObjectMap() + .append("%player%", player.getName()) + .append("%url%", skin.skinURL()), + true + ); + }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); + } +} 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..82b43da9 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java @@ -0,0 +1,11 @@ +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 { + Collection resolve(@NotNull CommandIssuer commandIssuer, @NotNull String... args); +} 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..3558cac4 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java @@ -0,0 +1,49 @@ +package com.georgev22.skinoverlay.command.resolvers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.command.CommandManager; +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("\\|"); + CommandManager manager = SkinOverlay.getInstance().getCommandManager(); + + 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 = manager.getResolver(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; + } +} 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..f60817e7 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java @@ -0,0 +1,18 @@ +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... args) { + return SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().stream() + .map(SPlayer::getName) + .collect(Collectors.toList()); + } +} 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..cff55a9f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java @@ -0,0 +1,26 @@ +package com.georgev22.skinoverlay.command.resolvers; + +import com.georgev22.skinoverlay.command.CommandIssuer; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class RangeResolver implements ArgumentResolver { + @Override + public List resolve(@NotNull CommandIssuer commandIssuer, String @NotNull ... args) { + if (args.length == 0 || !args[0].matches("\\d+-\\d+")) { + return List.of(); + } + + String[] range = args[0].split("-"); + int min = Integer.parseInt(range[0]); + int max = Integer.parseInt(range[1]); + + List list = new ArrayList<>(); + for (int i = min; i <= max; i++) { + list.add(String.valueOf(i)); + } + return list; + } +} 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/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/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java new file mode 100644 index 00000000..d0229b6b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java @@ -0,0 +1,61 @@ +package com.georgev22.skinoverlay.hooks; + +import com.georgev22.skinoverlay.SkinOverlay; +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.util.Optional; + +/** + * SkinsRestorerHook class implements the SkinHook interface for handling skin property + * data for players using the SkinsRestorer plugin. + */ +@ApiStatus.Internal +@ApiStatus.NonExtendable +public class SkinsRestorerHook implements SkinHook { + /** + * SkinsRestorerAPI object for accessing the SkinsRestorer API. + */ + private final SkinsRestorer skinsRestorerAPI; + + /** + * SkinOverlay object for accessing the SkinOverlay plugin. + */ + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + + /** + * Constructor for SkinsRestorerHook class. + * Initializes the SkinsRestorerAPI object. + */ + public SkinsRestorerHook() { + skinsRestorerAPI = SkinsRestorerProvider.get(); + } + + /** + * Retrieves the skin property data for the given player using the SkinsRestorer plugin. + * 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 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 + */ + @Override + @Nullable + public SProperty getProperty(@NotNull SPlayer player) { + Optional property; + try { + 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..5eda68dc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -0,0 +1,95 @@ +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(); + Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getManager(PlayerData.class); + if (optionalPlayerDataEntityManager.isEmpty()) { + return; + } + + Optional> optionalSkinEntityManager = EntityManagerRegistry.getManager(Skin.class); + if (optionalSkinEntityManager.isEmpty()) { + return; + } + + EntityManager playerDataEntityManager = optionalPlayerDataEntityManager.get(); + + EntityManager skinEntityManager = optionalSkinEntityManager.get(); + + Optional optionalPlayerData = playerDataEntityManager.findById(player.getUniqueId()); + if (optionalPlayerData.isEmpty()) { + optionalPlayerData = playerDataEntityManager.create(player.getUniqueId().toString(), ignore -> { + }); + if (optionalPlayerData.isEmpty()) { + return; + } + } + SGameProfile gameProfile = player.getGameProfile(); + SProperty property = gameProfile.getProperty("textures"); + if (property == null) { + try { + property = mainPlugin.getSkinProvider().getJavaSkin(player); + } catch (IOException e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error loading Skin:", e); + return; + } + } + UUID uuid = Utils.generateUUID("default" + player.getUniqueId().toString()); + Skin defaultSkin = optionalSkinEntityManager.get().findById(uuid).orElse(new Skin(uuid)); + defaultSkin.setProperty(property); + + PlayerData playerData = optionalPlayerData.get(); + playerData.setDefaultSkin(defaultSkin); + if (playerData.getCurrentSkin() != null && !playerData.getCurrentSkin().equals(defaultSkin)) { + this.mainPlugin.getSkinApplier().setSkin(player, playerData.getCurrentSkin()); + } + + if (!this.mainPlugin.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } + skinEntityManager.save(defaultSkin); + playerDataEntityManager.save(playerData); + } + + public void onPlayerLeave(@NotNull SPlayerLeaveEvent event) { + SPlayer player = event.getPlayer(); + 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(); + if (!this.mainPlugin.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } + playerDataEntityManager.save(playerData); + } + +} 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/RedisManager.java b/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java new file mode 100644 index 00000000..d20c67e0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java @@ -0,0 +1,102 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.skin.SProperty; +import org.jetbrains.annotations.NotNull; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPubSub; + +import java.util.UUID; +import java.util.logging.Level; + +public class RedisManager { + 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 volatile boolean running = true; + + 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); + } + } + + public void publishSkinProperty(String channel, @NotNull UUID playerUUID, String value, String signature) { + String message = playerUUID + "|" + value + "|" + signature; + jedis.publish(channel, message); + } + + public void subscribeSkinProperty(String channel, SkinPropertyHandler handler) { + 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("\\|", 3); + if (parts.length != 3) { + System.err.println("Invalid skin property message: " + message); + return; + } + UUID uuid = UUID.fromString(parts[0]); + String value = parts[1]; + String signature = parts[2]; + SProperty property = new SProperty(value, signature); + +// if (scheduler != null) { +// scheduler.runTask(() -> handler.handle(uuid, property)); +// } else { +// handler.handle(uuid, property); +// } + skinOverlay.getScheduler().runTask(skinOverlay.getPlugin(), () -> handler.handle(uuid, property)); + } catch (Exception e) { + skinOverlay.getLogger().log(Level.SEVERE, "Invalid skin property message: " + message, e); + } + } + }; + + subJedis.subscribe(jedisPubSub, channel); + } 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; + } + } + } + }, "RedisSubscriberThread"); + + subscriberThread.setDaemon(true); + subscriberThread.start(); + } + + /** + * Stop subscriber thread and close resources. + */ + public void close() { + running = false; + if (subscriberThread != null) { + subscriberThread.interrupt(); + } + jedis.close(); + } + + public interface SkinPropertyHandler { + void handle(UUID uuid, SProperty property); + } +} 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/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/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java new file mode 100644 index 00000000..f44a5f9f --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -0,0 +1,525 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.exceptions.SkinException; +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.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.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.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.*; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; + +public class SkinProvider { + + protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + protected final MineSkinClient mineskinClient; + + 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(); + } + + 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 { + 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 Optional.empty(); + } + + FileConfiguration fileConfiguration = skinConfigurationFile.getFileConfiguration(); + + BufferedImage currentSkin = this.getSkinImage(this.getProfileBytes(player, this.skinOverlay.getSkinHook().getProperty(player))); + + SkinParts currentSkinParts = new SkinParts(new SerializableBufferedImage(currentSkin), "currentSkin"); + currentSkinParts.createParts(); + + List overlaySkinParts = new ArrayList<>(); + for (Part part : skinParts.getParts().values()) { + if (part.name().startsWith("Jacket")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_JACKET.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Hat")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_HAT.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Left_Sleeve")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_LEFT_SLEEVE.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Right_Sleeve")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_RIGHT_SLEEVE.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Left_Pants")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_LEFT_PANTS.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Right_Pants")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_OVERLAY_RIGHT_PANTS.getBooleanValue(fileConfiguration)) { + continue; + } + } + overlaySkinParts.add(part); + } + + List newSkinParts = new ArrayList<>(); + for (Part part : currentSkinParts.getParts().values()) { + if (part.name().startsWith("Jacket")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_JACKET.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Hat")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_HAT.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Left_Sleeve")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_LEFT_SLEEVE.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Right_Sleeve")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_RIGHT_SLEEVE.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Left_Pants")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_LEFT_PANTS.getBooleanValue(fileConfiguration)) { + continue; + } + } + if (part.name().startsWith("Right_Pants")) { + if (!part.isEmpty() & !OverlayOptionsUtil.PARTS_PLAYER_RIGHT_PANTS.getBooleanValue(fileConfiguration)) { + continue; + } + } + newSkinParts.add(part); + } + + MinecraftSkinRenderer defaultSkinRenderer = new MinecraftSkinRenderer(newSkinParts.toArray(Part[]::new)); + defaultSkinRenderer.createFullSkinImage(); + + MinecraftSkinRenderer overlaySkinRenderer = new MinecraftSkinRenderer(overlaySkinParts.toArray(Part[]::new)); + overlaySkinRenderer.createFullSkinImage(); + + BufferedImage skinToBeGenerated = new BufferedImage(currentSkin.getWidth(), currentSkin.getHeight(), 2); + Graphics2D canvas = skinToBeGenerated.createGraphics(); + + canvas.drawImage(defaultSkinRenderer.getFullSkinImage().getBufferedImage(), 0, 0, null); + canvas.drawImage(overlaySkinRenderer.getFullSkinImage().getBufferedImage(), 0, 0, null); + canvas.dispose(); + + 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); + } + skinManager.save(skin); + return Optional.of(skin); + } catch (IOException | RuntimeException exception) { + skinOverlay.getLogger().log(Level.SEVERE, "Error generating or retrieving the skin:", exception); + return Optional.empty(); + } + } + }); + } + + /** + * 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 Bedrock {@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[] 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 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[] getJavaProfileBytes(@NotNull final SPlayer player, @Nullable SProperty property) throws IOException { + return property != null ? + new ByteArrayInputStream(this.createJsonFromProperty(player, property) + .getAsJsonObject().toString().getBytes()).readAllBytes() : + new Request() + .openConnection( + String.format( + "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false", + (skinOverlay.isOnlineMode() ? + player.getUniqueId() : + getUUID(player.getName())) + .toString().replaceAll("-", "")) + ) + .getRequest() + .finalizeRequest() + .getBytes(); + } + + /** + * Retrieves a skin image from the provided SPlayer. + * + * @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 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 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 SPlayer player) throws IOException, ExecutionException, InterruptedException { + return player.isBedrock() ? this.getXUIDSkin(this.getXUID(player)) : this.getJavaSkin(player); + } + + /** + * Retrieves a skin image from the provided SProperty. + * + * @param sProperty The SProperty containing the skin information. + * @return The BufferedImage representing the player's skin. + * @throws IOException If an I/O error occurs while fetching the image. + */ + public BufferedImage getSkinImage(final @NotNull SProperty sProperty) throws IOException { + 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. + * + * @param profileBytes The profile bytes containing skin information. + * @return The BufferedImage representing the player's skin. + * @throws IOException If an I/O error occurs while fetching the image. + */ + public BufferedImage getSkinImage(final byte @NotNull [] profileBytes) throws IOException { + JsonElement json = JsonParser.parseString(new String(profileBytes)); + JsonArray properties = json.getAsJsonObject().getAsJsonArray("properties"); + + JsonElement textures = findTexturesProperty(properties); + if (textures == null) { + skinOverlay.getLogger().log(Level.SEVERE, "Property object 'textures' not found", new SkinException("Property object 'textures' not found")); + return null; + } + + String base64Texture = textures.getAsJsonObject().get("value").getAsString(); + String decodedTexture = new String(Base64.getDecoder().decode(base64Texture)); + + JsonElement textureJson = JsonParser.parseString(decodedTexture); + String skinUrl = textureJson.getAsJsonObject() + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url") + .getAsString(); + + return ImageIO.read(new URL(skinUrl)); + } + + /** + * Find the 'textures' property within a JsonArray of properties. + * + * @param properties The JsonArray of properties to search for 'textures'. + * @return The JsonElement representing the 'textures' property, or null if not found. + */ + public JsonElement findTexturesProperty(@NotNull JsonArray properties) { + for (JsonElement property : properties) { + if (property.isJsonObject() && "textures".equals(property.getAsJsonObject().get("name").getAsString())) { + return property; + } + } + return null; + } + + /** + * This method checks if a Minecraft username is a premium account. + * A premium account is one that has paid for the game. + * + * @param username The Minecraft username to check. + * @return True if the username is a premium account, false otherwise. + */ + public boolean isUsernamePremium(String username) { + try { + URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + username); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + + int status = con.getResponseCode(); + if (status == 200) { + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuilder content = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(content.toString(), JsonObject.class); + return jsonObject != null && jsonObject.has("id") && jsonObject.has("name"); + } else { + con.disconnect(); + return false; + } + } catch (Exception e) { + skinOverlay.getLogger().log(Level.SEVERE, "Unable to check if username " + username + " is a premium account.", e); + 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/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/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..e8e1b779 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java @@ -0,0 +1,161 @@ +package com.georgev22.skinoverlay.storage.data; + +import com.georgev22.skinoverlay.skin.SProperty; +import com.georgev22.skinoverlay.utilities.CustomData; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import com.google.gson.JsonParser; +import org.jetbrains.annotations.Nullable; + +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(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java new file mode 100644 index 00000000..3a157b55 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PartTypeAdapter.java @@ -0,0 +1,87 @@ +package com.georgev22.skinoverlay.storage.gson; + +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.Part; +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; + +/** + * Gson TypeAdapter for serializing and deserializing {@link Part} objects. + *

+ * This adapter is responsible for converting {@link Part} objects to and from JSON format. + * It handles the serialization and deserialization of the part's name, image, position, dimensions, and emptiness status. + */ +public class PartTypeAdapter implements JsonSerializer, JsonDeserializer { + + /** + * Deserializes the JSON element to a Part object. + * + * @param jsonElement The JSON element containing part information. + * @param typeOfT The type of the Object to deserialize to. + * @param jsonDeserializationContext The context for deserialization. + * @return The deserialized Part object. + * @throws JsonParseException If there is an issue during the deserialization process. + */ + @Override + public Part deserialize(@NotNull JsonElement jsonElement, Type typeOfT, @NotNull JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + String name = jsonObject.get("name").getAsString(); + SerializableBufferedImage image = jsonDeserializationContext.deserialize(jsonObject.get("bufferedImage"), SerializableBufferedImage.class); + int x = jsonObject.get("x").getAsInt(); + int y = jsonObject.get("y").getAsInt(); + int width = jsonObject.get("width").getAsInt(); + int height = jsonObject.get("height").getAsInt(); + boolean isEmpty = jsonObject.get("isEmpty").getAsBoolean(); + + return new Part(name, image, x, y, width, height, isEmpty); + } + + /** + * Serializes the Part object to a JSON element. + * + * @param src The Part object to serialize. + * @param typeOfSrc The actual type (fully genericized version) of the source object. + * @param jsonSerializationContext The context for serialization. + * @return The serialized JSON element. + */ + @Override + public JsonElement serialize(@NotNull Part src, Type typeOfSrc, @NotNull JsonSerializationContext jsonSerializationContext) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("name", src.name()); + jsonObject.add("bufferedImage", jsonSerializationContext.serialize(src.image())); + jsonObject.addProperty("x", src.x()); + jsonObject.addProperty("y", src.y()); + jsonObject.addProperty("width", src.width()); + jsonObject.addProperty("height", src.height()); + jsonObject.addProperty("isEmpty", src.isEmpty()); + return jsonObject; + } + + /** + * Converts a {@link Part} object to its JSON representation. + * + * @param part The {@link Part} object to be converted. + * @return A JSON string representing the serialized {@link Part} object. + * @throws IllegalArgumentException If the provided {@link Part} object is null. + */ + public static String toJson(Part part) { + if (part == null) throw new IllegalArgumentException("Part cannot be null"); + return part.toJson(); + } + + /** + * Converts a JSON string to a {@link Part} object. + * + * @param json The JSON string representing the serialized {@link Part} object. + * @return A {@link Part} object deserialized from the provided JSON string. + * @throws IllegalArgumentException If the provided JSON string is null or empty. + */ + public static Part fromJson(String json) { + if (json == null) throw new IllegalArgumentException("Json cannot be null"); + if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); + return Part.fromJson(json); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java new file mode 100644 index 00000000..b2ce191b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java @@ -0,0 +1,94 @@ +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.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; +import java.util.UUID; + +/** + * Gson TypeAdapter for serializing and deserializing {@link PlayerData} objects. + *

+ * 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 PlayerDataTypeAdapter implements JsonSerializer, JsonDeserializer { + + /** + * Serializes a {@link PlayerData} object to a JSON representation. + * + * @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 PlayerData} object. + */ + @Override + 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.getDefaultSkin())); + jsonObject.add("skin", jsonSerializationContext.serialize(user.getCurrentSkin())); + + return jsonObject; + } + + /** + * 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 PlayerData} object. + */ + @Override + 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); + + PlayerData user = new PlayerData(entityId); + user.setDefaultSkin(defaultSkin); + user.setCurrentSkin(skin); + + return user; + } + + /** + * Converts a {@link PlayerData} object to its JSON representation. + * + * @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 PlayerData user) throws IllegalArgumentException { + //noinspection ConstantValue + if (user == null) { + throw new IllegalArgumentException("PlayerData cannot be null"); + } + return SkinOverlay.getInstance().getGson().toJson(user); + } + + /** + * Converts a JSON string to a {@link PlayerData} object. + * + * @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 PlayerData fromJson(@NotNull String entityId, @NotNull String json) { + //noinspection ConstantValue + 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, PlayerData.class); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java new file mode 100644 index 00000000..a9b87de9 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java @@ -0,0 +1,78 @@ +package com.georgev22.skinoverlay.storage.gson; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.skin.SProperty; +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; + +/** + * Gson TypeAdapter for serializing and deserializing {@link SProperty} objects. + *

+ * This adapter is responsible for converting {@link SProperty} objects to and from JSON format. + * It handles the serialization and deserialization of the property's name, value, and signature. + */ +public class SPropertyTypeAdapter implements + JsonSerializer, + JsonDeserializer { + + /** + * Deserializes the JSON element to an {@code SProperty}. + * + * @param jsonElement The JSON element containing name, value, and signature information. + * @param typeOfT The type of the Object to deserialize to. + * @param jsonDeserializationContext The context for deserialization. + * @return The deserialized {@code SProperty}. + */ + @Override + public @NotNull SProperty deserialize(@NotNull JsonElement jsonElement, Type typeOfT, JsonDeserializationContext jsonDeserializationContext) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + String value = jsonObject.get("value").getAsString(); + String signature = jsonObject.get("signature").getAsString(); + + return new SProperty(value, signature); + } + + /** + * Serializes the {@code SProperty} to a JSON element. + * + * @param src The {@code SProperty} to serialize. + * @param typeOfSrc The actual type (fully genericized version) of the source object. + * @param jsonSerializationContext The context for serialization. + * @return The serialized JSON element. + */ + @Override + public @NotNull JsonElement serialize(@NotNull SProperty src, Type typeOfSrc, JsonSerializationContext jsonSerializationContext) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("value", src.value()); + jsonObject.addProperty("signature", src.signature()); + return jsonObject; + } + + /** + * Converts a JSON string to an {@code SProperty} object. + * + * @param json The JSON string representing the serialized {@link SProperty} object. + * @return An {@link SProperty} object deserialized from the provided JSON string. + * @throws IllegalArgumentException If the provided JSON string is null or empty. + */ + 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 SkinOverlay.getInstance().getGson().fromJson(json, SProperty.class); + } + + /** + * Converts an {@code SProperty} object to its JSON representation. + * + * @param sProperty The {@code SProperty} object to be converted. + * @return A JSON string representing the serialized {@code SProperty} object. + * @throws IllegalArgumentException If the provided {@code SProperty} object is null. + */ + public static String toJson(SProperty sProperty) { + if (sProperty == null) throw new IllegalArgumentException("SProperty cannot be null"); + return SkinOverlay.getInstance().getGson().toJson(sProperty); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java new file mode 100644 index 00000000..694f7ae3 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SerializableBufferedImageTypeAdapter.java @@ -0,0 +1,82 @@ +package com.georgev22.skinoverlay.storage.gson; + +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.awt.image.BufferedImage; +import java.lang.reflect.Type; +import java.util.Base64; + +/** + * Gson TypeAdapter for serializing and deserializing {@link SerializableBufferedImage} objects. + *

+ * This adapter is responsible for converting {@link SerializableBufferedImage} objects to and from JSON format. + * It handles the serialization and deserialization of the image's width, height, and pixel information. + */ +public class SerializableBufferedImageTypeAdapter implements + JsonSerializer, + JsonDeserializer { + + /** + * Deserializes the JSON element to a {@link SerializableBufferedImage}. + * + * @param jsonElement The JSON element containing width, height, and pixel information. + * @param type The type to deserialize. + * @param jsonDeserializationContext The context for deserialization. + * @return The deserialized {@link SerializableBufferedImage}. + */ + @Override + public @NotNull SerializableBufferedImage deserialize(@NotNull JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + int width = jsonObject.get("width").getAsInt(); + int height = jsonObject.get("height").getAsInt(); + String encodedPixels = jsonObject.get("pixels").getAsString(); + int[] pixels = SerializableBufferedImage.fromByteArray(Base64.getDecoder().decode(encodedPixels)); + + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + bufferedImage.setRGB(0, 0, width, height, pixels, 0, width); + + return new SerializableBufferedImage(bufferedImage); + } + + /** + * Serializes the {@link SerializableBufferedImage} to a JSON element. + * + * @param serializableBufferedImage The {@link SerializableBufferedImage} to serialize. + * @param type The type to serialize. + * @param jsonSerializationContext The context for serialization. + * @return The serialized JSON element. + */ + @Override + public JsonElement serialize(@NotNull SerializableBufferedImage serializableBufferedImage, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { + return jsonSerializationContext.serialize(serializableBufferedImage.serialize()); + } + + /** + * Converts a {@link SerializableBufferedImage} object to its JSON representation. + * + * @param serializableBufferedImage The {@link SerializableBufferedImage} object to be converted. + * @return A JSON string representing the serialized {@link SerializableBufferedImage} object. + * @throws IllegalArgumentException If the provided {@link SerializableBufferedImage} object is null. + */ + public static String toJson(SerializableBufferedImage serializableBufferedImage) { + if (serializableBufferedImage == null) + throw new IllegalArgumentException("SerializableBufferedImage cannot be null"); + return serializableBufferedImage.toJson(); + } + + /** + * Converts a JSON string to a {@link SerializableBufferedImage} object. + * + * @param json The JSON string representing the serialized {@link SerializableBufferedImage} object. + * @return A {@link SerializableBufferedImage} object deserialized from the provided JSON string. + * @throws IllegalArgumentException If the provided JSON string is null or empty. + */ + public static SerializableBufferedImage fromJson(String json) { + if (json == null) throw new IllegalArgumentException("Json cannot be null"); + if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); + return SerializableBufferedImage.fromJson(json); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java new file mode 100644 index 00000000..05c56b1b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinPartsTypeAdapter.java @@ -0,0 +1,78 @@ +package com.georgev22.skinoverlay.storage.gson; + +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; + +/** + * Gson TypeAdapter for serializing and deserializing {@link SkinParts} objects. + *

+ * This adapter is responsible for converting {@link SkinParts} objects to and from JSON format. + * It handles the serialization of the buffered image and the skin name properties. + */ +public class SkinPartsTypeAdapter implements JsonSerializer, JsonDeserializer { + + /** + * Serializes a {@link SkinParts} object to a JSON representation. + * + * @param src The {@link SkinParts} object to be serialized. + * @param typeOfSrc The type of the source object. + * @param context The serialization context. + * @return A {@link JsonElement} representing the serialized {@link SkinParts} object. + */ + @Override + public JsonElement serialize(@NotNull SkinParts src, Type typeOfSrc, @NotNull JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("bufferedImage", context.serialize(src.getFullSkin())); + jsonObject.addProperty("skinName", src.getSkinName()); + return jsonObject; + } + + /** + * Deserializes a JSON representation into a {@link SkinParts} object. + * + * @param json The {@link JsonElement} containing the JSON representation. + * @param typeOfT The target type to deserialize into. + * @param context The deserialization context. + * @return A deserialized {@link SkinParts} object. + */ + @Override + public SkinParts deserialize(@NotNull JsonElement json, Type typeOfT, @NotNull JsonDeserializationContext context) { + JsonObject jsonObject = json.getAsJsonObject(); + + SerializableBufferedImage bufferedImage = context.deserialize(jsonObject.get("bufferedImage"), SerializableBufferedImage.class); + String skinName = jsonObject.get("skinName").getAsString(); + + return new SkinParts(bufferedImage, skinName); + } + + /** + * Converts a {@link SkinParts} object to its JSON representation. + * + * @param skinParts The {@link SkinParts} object to be converted. + * @return A JSON string representing the serialized {@link SkinParts} object. + * @throws IllegalArgumentException If the provided {@link SkinParts} object is null. + */ + public static String toJson(@NotNull SkinParts skinParts) throws IllegalArgumentException { + //noinspection ConstantValue + if (skinParts == null) throw new IllegalArgumentException("SkinParts cannot be null"); + return skinParts.toJson(); + } + + /** + * Converts a JSON string to a {@link SkinParts} object. + * + * @param json The JSON string representing the serialized {@link SkinParts} object. + * @return A {@link SkinParts} object deserialized from the provided JSON string. + * @throws IllegalArgumentException If the provided JSON string is null or empty. + */ + public static SkinParts fromJson(@NotNull String json) { + //noinspection ConstantValue + if (json == null) throw new IllegalArgumentException("Json cannot be null"); + if (json.isEmpty()) throw new IllegalArgumentException("Json cannot be empty"); + return SkinParts.fromJson(json); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java new file mode 100644 index 00000000..94ff1c4a --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java @@ -0,0 +1,81 @@ +package com.georgev22.skinoverlay.storage.gson; + +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; + +public class SkinTypeAdapter implements JsonSerializer, JsonDeserializer { + + /** + * Serializes a {@link Skin} object to a JSON representation. + * + * @param skin The {@link Skin} 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 Skin} object. + */ + @Override + public JsonElement serialize(@NotNull Skin skin, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { + JsonObject jsonObject = new JsonObject(); + + jsonObject.addProperty("entity_id", skin.getId().toString()); + jsonObject.add("property", jsonSerializationContext.serialize(skin.getProperty())); + jsonObject.add("skinParts", jsonSerializationContext.serialize(skin.getSkinParts())); + + return jsonObject; + } + + /** + * Deserializes a JSON representation into a {@link Skin} 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 Skin} object. + */ + @Override + public Skin deserialize(@NotNull JsonElement jsonElement, Type type, @NotNull JsonDeserializationContext jsonDeserializationContext) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + 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); + + Skin skin = new Skin(entityId); + skin.setProperty(property); + skin.setSkinParts(skinParts); + + return skin; + } + + /** + * Converts a JSON string to a {@link Skin} object. + * + * @param json The JSON string representing the serialized {@link Skin} object. + * @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 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); + } + + /** + * Converts a {@link Skin} object to its JSON representation. + * + * @param skin The {@link Skin} object to be converted. + * @return A JSON string representing the serialized {@link Skin} object. + * @throws IllegalArgumentException If the provided {@link Skin} object is null. + */ + public static String toJson(Skin skin) { + if (skin == null) throw new IllegalArgumentException("Skin cannot be null"); + 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..046beb99 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java @@ -0,0 +1,150 @@ +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) { + 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..abfec928 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java @@ -0,0 +1,151 @@ +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); + 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) { + 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/CustomData.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java new file mode 100644 index 00000000..e951c146 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java @@ -0,0 +1,135 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.maps.ObjectMap; +import org.jetbrains.annotations.Nullable; + +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 { + + private final 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 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); + } + + /** + * 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. + * + * @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 { + //noinspection unchecked + return (T) customData.get(key); + } 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 and casts a custom data object associated with the specified key. + * + * @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); + 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; + } + } + + /** + * Adds a custom data entry to 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 add(String key, Object value) { + customData.append(key, value); + } + + /** + * 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(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java new file mode 100644 index 00000000..3cfe62f6 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/ImageSupplier.java @@ -0,0 +1,18 @@ +package com.georgev22.skinoverlay.utilities; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * A functional interface that supplies an image. + */ +@FunctionalInterface +public interface ImageSupplier { + /** + * Gets an image. + * + * @return the image supplied by this function + * @throws IOException if an I/O error occurs while getting the image + */ + BufferedImage get() throws IOException; +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java new file mode 100644 index 00000000..d00fadef --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Locale.java @@ -0,0 +1,75 @@ +package com.georgev22.skinoverlay.utilities; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public enum Locale { + ENGLISH("en"), + FRENCH("fr"), + GERMAN("de"), + ITALIAN("it"), + SPANISH("es"), + PORTUGUESE("pt"), + DUTCH("nl"), + NORWEGIAN("no"), + SWEDISH("sv"), + DANISH("da"), + FINNISH("fi"), + POLISH("pl"), + CZECH("cs"), + SLOVAK("sk"), + HUNGARIAN("hu"), + ROMANIAN("ro"), + BULGARIAN("bg"), + GREEK("el"), + RUSSIAN("ru"), + TURKISH("tr"), + + CHINESE("zh"), + JAPANESE("ja"), + KOREAN("ko"), + + ENGLISH_US("en", "US"), + ENGLISH_GR("en", "GR"), + SPANISH_ES("es", "ES"), + SPANISH_MX("es", "MX"), + PORTUGUESE_BR("pt", "BR"), + FRENCH_CA("fr", "CA"); + + private final String languageCode; + private final String countryCode; + private final java.util.Locale locale; + private static final Map localeMap; + + static { + localeMap = new HashMap<>(); + Arrays.stream(Locale.values()).forEach(l -> localeMap.put(l.countryCode != null ? l.languageCode.toLowerCase() + "_" + l.countryCode : l.languageCode.toLowerCase(), l)); + } + + Locale(String languageCode) { + this.locale = new java.util.Locale(languageCode); + this.languageCode = languageCode; + this.countryCode = null; + } + + Locale(String languageCode, String countryCode) { + this.locale = new java.util.Locale(languageCode, countryCode); + this.languageCode = languageCode; + this.countryCode = countryCode; + } + + public java.util.Locale getLocale() { + return locale; + } + + public String getStringLocale() { + return countryCode != null ? languageCode + "_" + countryCode : languageCode; + } + + public static Locale fromString(@NotNull String s) { + return localeMap.containsKey(s) ? localeMap.get(s) : s.contains("_") ? localeMap.get(s.split("_")[0]) : ENGLISH_US; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java new file mode 100644 index 00000000..82d68e17 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java @@ -0,0 +1,443 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.maps.Pair; +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 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; + +/** + * 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);
+ * }
+ */ +public class MessageBuilder { + + /** + * Color map for mapping legacy colors to MiniMessage colors. + *

+ * Map key: Legacy color code
+ * Map value:
+ * - Pair key: MiniMessage color (e.g., {@literal })
+ * - Pair value: Indicates if the color needs to be closed (e.g., {@literal
}) + */ + @NotNull + public static final ObjectMap> COLOR_MAP = new HashObjectMap>() + .append("0", Pair.create("black", true)) + .append("1", Pair.create("dark_blue", true)) + .append("2", Pair.create("dark_green", true)) + .append("3", Pair.create("dark_aqua", true)) + .append("4", Pair.create("dark_red", true)) + .append("5", Pair.create("dark_purple", true)) + .append("6", Pair.create("gold", true)) + .append("7", Pair.create("gray", true)) + .append("8", Pair.create("dark_gray", true)) + .append("9", Pair.create("blue", true)) + .append("a", Pair.create("green", true)) + .append("b", Pair.create("aqua", true)) + .append("c", Pair.create("red", true)) + .append("d", Pair.create("light_purple", true)) + .append("e", Pair.create("yellow", true)) + .append("f", Pair.create("white", true)) + .append("l", Pair.create("bold", false)) + .append("m", Pair.create("strikethrough", false)) + .append("n", Pair.create("underline", false)) + .append("o", Pair.create("italic", false)) + .append("r", Pair.create("reset", true)) + .append("k", Pair.create("obfuscated", false)); + private final ArrayList currentDecorations; + private TextComponent.Builder componentBuilder; + private TextColor currentColor = NamedTextColor.WHITE; + private ClickEvent currentClickEvent; + private HoverEvent currentHoverEvent; + + /** + * 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(); + } + + /** + * Translates legacy color codes (e.g., {@literal &}colorCode) to MiniMessage colors (e.g., {@literal }). + * + * @param message The message to translate. + * @return The translated message with MiniMessage formatting. + */ + public static String translateLegacyColors(String message) { + boolean colorMode = false; + boolean tagNeedToClose = false; + String tagToClose = ""; + + StringBuilder newText = new StringBuilder(); + + for (char ch : message.toCharArray()) { + if (ch == '&' && colorMode) { + newText.append('&'); + continue; + } + + if (ch == '&') { + colorMode = true; + continue; + } + + if (colorMode && COLOR_MAP.containsKey(String.valueOf(ch))) { + if (tagNeedToClose) { + tagNeedToClose = false; + newText.append(""); + } + + newText.append("<").append(COLOR_MAP.get(String.valueOf(ch)).key()).append(">"); + + if (!COLOR_MAP.get(String.valueOf(ch)).value()) { + tagNeedToClose = true; + tagToClose = COLOR_MAP.get(String.valueOf(ch)).key(); + } + + colorMode = false; + continue; + } else if (colorMode) { + colorMode = false; + newText.append('&'); + } + + newText.append(ch); + } + + if (tagNeedToClose) { + newText.append(""); + } + + message = newText.toString(); + return message; + } + + /** + * 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) { + 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 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)); + 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, @Nullable TagResolver tagResolver) { + if (tagResolver != null) { + return MiniMessage.miniMessage().deserialize(translateLegacyColors(message), tagResolver); + } + return MiniMessage.miniMessage().deserialize(translateLegacyColors(message)); + } + + /** + * 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) { + 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) { + 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(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(); + } + + /** + * 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; + } + + /** + * 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(); + return this.resetStyles(); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java new file mode 100644 index 00000000..a3c11e19 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/SerializableBufferedImage.java @@ -0,0 +1,149 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.SkinOverlay; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.awt.image.BufferedImage; +import java.util.Base64; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a {@link BufferedImage} that can be serialized and deserialized. + */ +@SuppressWarnings("ClassCanBeRecord") +public final class SerializableBufferedImage { + private final BufferedImage bufferedImage; + + public SerializableBufferedImage(BufferedImage bufferedImage) { + this.bufferedImage = bufferedImage; + } + + /** + * Serializes the {@link BufferedImage} to a YAML-compatible map for serialization. + * + * @return A YAML-compatible map containing width, height, and pixel information. + */ + public @NotNull @Unmodifiable Map serialize() { + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + int[] pixels = new int[width * height]; + bufferedImage.getRGB(0, 0, width, height, pixels, 0, width); + String encodedPixels = Base64.getEncoder().encodeToString(toByteArray(pixels)); + return Map.of( + "width", width, + "height", height, + "pixels", encodedPixels + ); + } + + /** + * Deserializes the YAML-compatible map to a {@link SerializableBufferedImage}. + * + * @param map The YAML-compatible map containing width, height, and pixel information. + * @return The deserialized {@link SerializableBufferedImage}. + */ + @Contract(pure = true) + public static @NotNull SerializableBufferedImage deserialize(@NotNull Map map) { + int width = (int) map.get("width"); + int height = (int) map.get("height"); + String encodedPixels = (String) map.get("pixels"); + int[] pixels = fromByteArray(Base64.getDecoder().decode(encodedPixels)); + + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + bufferedImage.setRGB(0, 0, width, height, pixels, 0, width); + + return new SerializableBufferedImage(bufferedImage); + } + + @Contract(pure = true) + public static byte @NotNull [] toByteArray(int @NotNull [] pixels) { + byte[] result = new byte[pixels.length * 4]; + for (int i = 0, j = 0; i < pixels.length; i++, j += 4) { + result[j] = (byte) ((pixels[i] >> 24) & 0xFF); + result[j + 1] = (byte) ((pixels[i] >> 16) & 0xFF); + result[j + 2] = (byte) ((pixels[i] >> 8) & 0xFF); + result[j + 3] = (byte) (pixels[i] & 0xFF); + } + return result; + } + + @Contract(pure = true) + public static int @NotNull [] fromByteArray(byte @NotNull [] bytes) { + int[] result = new int[bytes.length / 4]; + for (int i = 0, j = 0; i < result.length; i++, j += 4) { + result[i] = ((bytes[j] & 0xFF) << 24) | + ((bytes[j + 1] & 0xFF) << 16) | + ((bytes[j + 2] & 0xFF) << 8) | + (bytes[j + 3] & 0xFF); + } + return result; + } + + /** + * Checks if this {@link SerializableBufferedImage} is equal to another object. + * + * @param obj The object to compare. + * @return {@code true} if the objects are equal, {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (SerializableBufferedImage) obj; + return Objects.equals(this.bufferedImage, that.bufferedImage); + } + + /** + * Computes the hash code of this {@link SerializableBufferedImage}. + * + * @return The computed hash code. + */ + @Override + public int hashCode() { + return Objects.hash(bufferedImage); + } + + /** + * Returns a string representation of this {@link SerializableBufferedImage}. + * + * @return A string representation containing the buffered image. + */ + @Override + public @NotNull String toString() { + return "SerializableBufferedImage[" + + "bufferedImage=" + bufferedImage + ']'; + } + + /** + * Serializes the SerializableBufferedImage 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 SerializableBufferedImage object. + * + * @param json The JSON string to deserialize. + * @return The deserialized SerializableBufferedImage object. + */ + public static SerializableBufferedImage fromJson(String json) { + return SkinOverlay.getInstance().getGson().fromJson(json, SerializableBufferedImage.class); + } + + /** + * Returns the buffered image. + * + * @return The buffered image + */ + public BufferedImage getBufferedImage() { + return bufferedImage; + } + + +} \ 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..b5fd7d86 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java @@ -0,0 +1,2269 @@ +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.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.MessageDigest; +import java.security.NoSuchAlgorithmException; +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..2f60f570 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java @@ -0,0 +1,212 @@ +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 + ) throws Exception { + 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() throws Exception { + 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) { + e.printStackTrace(); + } + if (saveResource) { + saveResource(this.fileName, this.dataFolder, this.clazz); + } + } + + this.reloadFile(); + } + + private void saveResource(@NotNull String resourcePath, File dataFolder, Class clazz) throws Exception { + if (resourcePath.equals("")) { + throw new Exception("ResourcePath cannot be null or empty"); + } + + 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/common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java new file mode 100644 index 00000000..449889a5 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/FileManager.java @@ -0,0 +1,26 @@ +package com.georgev22.skinoverlay.utilities.config; + +import com.georgev22.skinoverlay.SkinOverlay; + +public final class FileManager { + private static FileManager instance; + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + private CFG config; + + public static FileManager getInstance() { + return instance == null ? (instance = new FileManager()) : instance; + } + + private FileManager() { + } + + public void loadFiles() throws Exception { + this.config = new CFG("config", this.skinOverlay.getDataFolder(), true, true, skinOverlay.getLogger(), skinOverlay.getClass()); + } + + public CFG getConfig() { + return this.config; + } + +} + diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java new file mode 100644 index 00000000..a9e42417 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java @@ -0,0 +1,218 @@ +package com.georgev22.skinoverlay.utilities.config; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.command.CommandIssuer; +import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.utilities.Locale; +import com.georgev22.skinoverlay.utilities.MessageBuilder; +import com.georgev22.skinoverlay.utilities.Utils; +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.HashMap; +import java.util.Map; +import java.util.Objects; + +import static com.georgev22.skinoverlay.utilities.Utils.placeHolder; + +public enum MessagesUtil { + HELP_FORMAT("Messages.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).", + "" + ), + 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%)"), + INVALID_URL("Messages.Invalid URL", "&c&l(!)&c Invalid URL or failed to download image: %url%"), + ERROR("Messages.Error", "&c&l(!)&c An error occurred: %error%"), + FAILED_TO_RETRIEVE_OR_GENERATE_SKIN("Messages.Failed To Retrieve Or Generate Skin", "&c&l(!)&c Failed to retrieve or generate skin for %player%!"), + ; + private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + private static CFG messagesCFG; + private static Locale locale; + private final String path; + private String[] messages; + + MessagesUtil(String path, String... messages) { + this.messages = messages; + this.path = path; + } + + public static void repairPaths(Locale locale) throws Exception { + if (messagesCFG == null | !Objects.equals(MessagesUtil.locale, locale)) { + setLocale(locale); + setMessagesCFG(new CFG("messages_" + locale.getStringLocale(), mainPlugin.getDataFolder(), true, true, mainPlugin.getLogger(), mainPlugin.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(); + } + } + + public static Locale getLocale() { + return locale; + } + + public static void setLocale(Locale locale) { + MessagesUtil.locale = locale; + } + + public static CFG getMessagesCFG() { + return messagesCFG; + } + + public static void setMessagesCFG(CFG messagesCFG) { + MessagesUtil.messagesCFG = messagesCFG; + } + + 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())); + } + } + + private boolean isMultiLined() { + return this.messages.length > 1; + } + + 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 sender) { + this.msg(sender, new HashObjectMap<>(), false, MessageType.CHAT); + } + + public void msg(@NotNull CommandIssuer sender, MessageType messageType) { + this.msg(sender, new HashObjectMap<>(), false, messageType); + } + + public void msg(@NotNull CommandIssuer sender, Map map, boolean ignoreCase) { + this.msg(sender, map, ignoreCase, MessageType.CHAT); + } + + public void msg(CommandIssuer sender, Map map, boolean ignoreCase, @NotNull MessageType messageType) { + MessageBuilder messageBuilder = new MessageBuilder(); + TagResolver tagResolver = StandardTags.defaults(); // ALL TAG RESOLVERS + Audience audience = mainPlugin.getAudienceProvider().player(sender.getUniqueId()); + switch (messageType) { + case ACTIONBAR -> audience.sendActionBar( + messageBuilder + .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) + .buildAndReset() + ); + case TITLE -> audience.showTitle(Title.title( + messageBuilder + .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) + .buildAndReset(), + messages.length > 1 + ? messageBuilder + .appendMiniMessage(placeHolder(messages[1], map, ignoreCase), tagResolver) + .buildAndReset() + : Component.text(""), + Title.DEFAULT_TIMES + )); + case CHAT -> { + if (this.isMultiLined()) { + Arrays.stream(messages).forEach(message -> audience.sendMessage( + messageBuilder + .appendMiniMessage(placeHolder(message, map, ignoreCase), tagResolver) + .buildAndReset() + )); + } else { + audience.sendMessage( + messageBuilder + .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) + .buildAndReset() + ); + } + } + } + } + + public void msgConsole() { + msgConsole(new HashMap<>(), false); + } + + public void msgConsole(Map map, boolean ignoreCase) { + MessageBuilder messageBuilder = new MessageBuilder(); + TagResolver tagResolver = StandardTags.defaults(); // ALL TAG RESOLVERS + Audience audience = mainPlugin.getAudienceProvider().console(); + if (this.isMultiLined()) { + Arrays.stream(messages).forEach(message -> audience.sendMessage( + messageBuilder + .appendMiniMessage(placeHolder(message, map, ignoreCase), tagResolver) + .buildAndReset() + )); + } else { + audience.sendMessage( + messageBuilder + .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) + .buildAndReset() + ); + } + } + + public void msgAll() { + mainPlugin.getPlayerProvider().getOnlinePlayers() + .forEach(this::msg); + } + + public void msgAll(Map map, boolean ignoreCase) { + mainPlugin.getPlayerProvider().getOnlinePlayers() + .forEach(player -> this.msg(player, map, ignoreCase)); + } + + public enum MessageType { + CHAT, + ACTIONBAR, + TITLE, + } + +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java new file mode 100644 index 00000000..6167d451 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java @@ -0,0 +1,172 @@ +package com.georgev22.skinoverlay.utilities.config; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.utilities.color.Color; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public enum OptionsUtil { + DEBUG("debug", false, Optional.empty()), + + PROXY("proxy", false, Optional.of("bungeecord")), + + SECRET("secret", "SECRET HERE", Optional.empty()), + + MINESKIN_API_KEY("mineskin api key", "none", Optional.empty()), + + COMMAND_SKINOVERLAY("commands.skinoverlay", true, Optional.empty()), + + EXPERIMENTAL_FEATURES("experimental features", false, Optional.empty()), + + METRICS("metrics", true, Optional.empty()), + + DISCORD("discord", false, Optional.empty()), + + UPDATER("updater.enabled", true, Optional.empty()), + + DEFAULT_SKIN_UUID("default skin uuid", "8667ba71-b85a-4004-af54-457a9734eed7", Optional.empty()), + + SKIN_HOOK("skin hook", "SkinsRestorer", Optional.empty()), + LOCALE("locale", "en_US", Optional.empty()), + SAVE_INTERVAL("save interval", 20, Optional.empty()),; + private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + private final String pathName; + private final Object value; + private final Optional[] oldPaths; + + @SafeVarargs + @Contract(pure = true) + OptionsUtil(final String pathName, final Object value, Optional... oldPaths) { + this.pathName = pathName; + this.value = value; + this.oldPaths = oldPaths; + } + + public boolean getBooleanValue() { + return mainPlugin.getConfig().getBoolean(getPath(), Boolean.parseBoolean(String.valueOf(getDefaultValue()))); + } + + public Object getObjectValue() { + return mainPlugin.getConfig().get(getPath(), getDefaultValue()); + } + + public String getStringValue() { + return mainPlugin.getConfig().getString(getPath(), String.valueOf(getDefaultValue())); + } + + public @NotNull Long getLongValue() { + return mainPlugin.getConfig().getLong(getPath(), Long.parseLong(String.valueOf(getDefaultValue()))); + } + + public @NotNull Integer getIntValue() { + return mainPlugin.getConfig().getInt(getPath(), Integer.parseInt(String.valueOf(getDefaultValue()))); + } + + public @NotNull Double getDoubleValue() { + return mainPlugin.getConfig().getDouble(getPath(), Double.parseDouble(String.valueOf(getDefaultValue()))); + } + + public @NotNull List getStringList() { + return mainPlugin.getConfig().getStringList(getPath()); + } + + /** + * Converts and return a String List of color codes to a List of Color classes that represent the colors. + * + * @return a List of Color classes that represent the colors. + */ + public @NotNull List getColors() { + return getStringList().stream().map(Color::from).collect(Collectors.toList()); + } + + public boolean getBooleanValue(String arg) { + return mainPlugin.getConfig().getBoolean(String.format(getPath(), arg), Boolean.parseBoolean(String.valueOf(getDefaultValue()))); + } + + public Object getObjectValue(String arg) { + return mainPlugin.getConfig().get(getPath(), getDefaultValue()); + } + + public String getStringValue(String arg) { + return mainPlugin.getConfig().getString(getPath(), String.valueOf(getDefaultValue())); + } + + public @NotNull Long getLongValue(String arg) { + return mainPlugin.getConfig().getLong(getPath(), Long.parseLong(String.valueOf(getDefaultValue()))); + } + + public @NotNull Integer getIntValue(String arg) { + return mainPlugin.getConfig().getInt(getPath(), Integer.parseInt(String.valueOf(getDefaultValue()))); + } + + public @NotNull Double getDoubleValue(String arg) { + return mainPlugin.getConfig().getDouble(getPath(), Double.parseDouble(String.valueOf(getDefaultValue()))); + } + + public @NotNull List getStringList(String arg) { + return mainPlugin.getConfig().getStringList(getPath()); + } + + /** + * Converts and return a String List of color codes to a List of Color classes that represent the colors. + * + * @return a List of Color classes that represent the colors. + */ + public @NotNull List getColors(String arg) { + return getStringList().stream().map(Color::from).collect(Collectors.toList()); + } + + /** + * Returns the path. + * + * @return the path. + */ + public @NotNull String getPath() { + if (mainPlugin.getConfig().get("Options." + getDefaultPath()) == null) { + for (Optional path : getOldPaths()) { + if (path.isPresent()) { + if (mainPlugin.getConfig().get("Options." + path.get()) != null) { + return "Options." + path.get(); + } + } + } + } + return "Options." + getDefaultPath(); + } + + /** + * Returns the default path. + * + * @return the default path. + */ + @Contract(pure = true) + public @NotNull String getDefaultPath() { + return this.pathName; + } + + /** + * Returns the old path if it exists. + * + * @return the old path if it exists. + */ + public Optional[] getOldPaths() { + return oldPaths; + } + + /** + * Returns the default value if the path have no value. + * + * @return the default value if the path have no value. + */ + public Object getDefaultValue() { + return value; + } + + public Optional getOptionalStringValue() { + return Optional.ofNullable(getStringValue()); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java new file mode 100644 index 00000000..87f86b43 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java @@ -0,0 +1,140 @@ +package com.georgev22.skinoverlay.utilities.config; + +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; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public enum OverlayOptionsUtil { + PARTS_OVERLAY_JACKET("overlay.jacket", false, Optional.empty()), + + PARTS_OVERLAY_LEFT_SLEEVE("overlay.left_sleeve", false, Optional.empty()), + + PARTS_OVERLAY_RIGHT_SLEEVE("overlay.right_sleeve", false, Optional.empty()), + + PARTS_OVERLAY_LEFT_PANTS("overlay.left_pants", false, Optional.empty()), + + PARTS_OVERLAY_RIGHT_PANTS("overlay.right_pants", false, Optional.empty()), + + PARTS_OVERLAY_HAT("overlay.hat", false, Optional.empty()), + + // PLAYER PARTS + + PARTS_PLAYER_JACKET("player.jacket", false, Optional.empty()), + + PARTS_PLAYER_LEFT_SLEEVE("player.left_sleeve", false, Optional.empty()), + + PARTS_PLAYER_RIGHT_SLEEVE("player.right_sleeve", false, Optional.empty()), + + PARTS_PLAYER_LEFT_PANTS("player.left_pants", false, Optional.empty()), + + PARTS_PLAYER_RIGHT_PANTS("player.right_pants", false, Optional.empty()), + + PARTS_PLAYER_HAT("player.hat", false, Optional.empty()), + ; + + private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + private final String pathName; + private final Object value; + private final Optional[] oldPaths; + + @SafeVarargs + @Contract(pure = true) + OverlayOptionsUtil(final String pathName, final Object value, Optional... oldPaths) { + this.pathName = pathName; + this.value = value; + this.oldPaths = oldPaths; + } + + public boolean getBooleanValue(FileConfiguration fileConfiguration) { + return fileConfiguration.getBoolean(getPath(fileConfiguration), Boolean.parseBoolean(String.valueOf(getDefaultValue(fileConfiguration)))); + } + + public Object getObjectValue(FileConfiguration fileConfiguration) { + return fileConfiguration.get(getPath(fileConfiguration), getDefaultValue(fileConfiguration)); + } + + public String getStringValue(FileConfiguration fileConfiguration) { + return fileConfiguration.getString(getPath(fileConfiguration), String.valueOf(getDefaultValue(fileConfiguration))); + } + + public @NotNull Long getLongValue(FileConfiguration fileConfiguration) { + return fileConfiguration.getLong(getPath(fileConfiguration), Long.parseLong(String.valueOf(getDefaultValue(fileConfiguration)))); + } + + public @NotNull Integer getIntValue(FileConfiguration fileConfiguration) { + return fileConfiguration.getInt(getPath(fileConfiguration), Integer.parseInt(String.valueOf(getDefaultValue(fileConfiguration)))); + } + + public @NotNull Double getDoubleValue(FileConfiguration fileConfiguration) { + return fileConfiguration.getDouble(getPath(fileConfiguration), Double.parseDouble(String.valueOf(getDefaultValue(fileConfiguration)))); + } + + public @NotNull List getStringList(FileConfiguration fileConfiguration) { + return fileConfiguration.getStringList(getPath(fileConfiguration)); + } + + /** + * Converts and return a String List of color codes to a List of Color classes that represent the colors. + * + * @return a List of Color classes that represent the colors. + */ + public @NotNull List getColors(FileConfiguration fileConfiguration) { + return getStringList(fileConfiguration).stream().map(Color::from).collect(Collectors.toList()); + } + + /** + * Returns the path. + * + * @return the path. + */ + public @NotNull String getPath(FileConfiguration fileConfiguration) { + if (fileConfiguration.get("Options." + getDefaultPath(fileConfiguration)) == null) { + for (Optional path : getOldPaths(fileConfiguration)) { + if (path.isPresent()) { + if (fileConfiguration.get("Options." + path.get()) != null) { + return "Options." + path.get(); + } + } + } + } + return "Options." + getDefaultPath(fileConfiguration); + } + + /** + * Returns the default path. + * + * @return the default path. + */ + @Contract(pure = true) + public @NotNull String getDefaultPath(FileConfiguration fileConfiguration) { + return this.pathName; + } + + /** + * Returns the old path if it exists. + * + * @return the old path if it exists. + */ + public Optional[] getOldPaths(FileConfiguration fileConfiguration) { + return oldPaths; + } + + /** + * Returns the default value if the path have no value. + * + * @return the default value if the path have no value. + */ + public Object getDefaultValue(FileConfiguration fileConfiguration) { + return value; + } + + public Optional getOptionalStringValue(FileConfiguration fileConfiguration) { + return Optional.ofNullable(getStringValue(fileConfiguration)); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java new file mode 100644 index 00000000..7391476c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinConfigurationFile.java @@ -0,0 +1,63 @@ +package com.georgev22.skinoverlay.utilities.config; + +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; +import java.io.IOException; +import java.util.logging.Level; + +public class SkinConfigurationFile { + + private final String name; + private FileConfiguration fileConfiguration; + private File file; + private boolean setup = false; + + public SkinConfigurationFile(final String name) { + this.name = name; + + } + + public File getFile() { + return file; + } + + public void setupFile() { + + if (setup) { + throw new RuntimeException( + "The void file with the name: " + this.name + " can't be setup twice!\n" + + "If you believe this is an issue, contact GeorgeV22."); + } + + final SkinOverlay plugin = SkinOverlay.getInstance(); + + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + this.file = new File(plugin.getDataFolder(), "skins-cfg" + File.separator + this.name + ".yml"); + + if (!this.file.exists()) { + try { + this.file.createNewFile(); + } catch (final IOException e) { + plugin.getLogger().log(Level.SEVERE, "Error while creating a new file:", e); + } + } + this.reloadFile(); + setup = true; + } + + public void reloadFile() { + this.fileConfiguration = YamlConfiguration.loadConfiguration(this.file); + } + + @NotNull + public FileConfiguration getFileConfiguration() { + return this.fileConfiguration; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java new file mode 100644 index 00000000..16769e49 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java @@ -0,0 +1,154 @@ +package com.georgev22.skinoverlay.utilities.config; + +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; + +import javax.imageio.ImageIO; +import java.io.File; +import java.io.IOException; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.logging.Level; + +public class SkinFileCache { + private final ObjectMap skinConfigurationFiles = new HashObjectMap<>(); + + private final ObjectMap skinImages = new HashObjectMap<>(); + + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + + @NotNull + public ObjectMap getSkinConfigurationFiles() { + return skinConfigurationFiles; + } + + public ObjectMap getSkinImages() { + return skinImages; + } + + public void cache() { + this.skinOverlay.getLogger().info("Attempting to load skins and its configuration files in memory."); + this.skinConfigurationFiles.clear(); + this.skinImages.clear(); + + final File folder = new File(skinOverlay.getDataFolder(), "skins-cfg"); + if (!folder.exists()) { + if (folder.mkdirs()) { + this.skinOverlay.getLogger().log(Level.INFO, "skins-cfg folder was successfully created."); + } + this.copyFilesToFolder(false); + this.skinOverlay.getLogger().info("skins-cfg folder didn't exist, creating one and copy the default skins configuration files for it."); + } + + if (!this.skinOverlay.getSkinsDataFolder().exists()) { + if (this.skinOverlay.getSkinsDataFolder().mkdirs()) { + this.skinOverlay.getLogger().log(Level.INFO, "skins data folder was successfully created!!"); + } + this.copyFilesToFolder(true); + this.skinOverlay.getLogger().info("skins folder didn't exist, creating one and copy the default skins for it."); + } + + for (File file : Objects.requireNonNull(this.skinOverlay.getSkinsDataFolder().listFiles((dir, name) -> name.endsWith(".png")))) { + final String skinName = file.getName().substring(0, file.getName().length() - 4); + + if (skinName.isEmpty()) { + this.skinOverlay.getLogger().info("Invalid skin file name: " + skinName); + continue; + } + + try { + SerializableBufferedImage serializableBufferedImage = new SerializableBufferedImage(ImageIO.read(file)); + this.skinImages.put(skinName, serializableBufferedImage); + this.skinOverlay.getLogger().info("Skin image file: " + skinName + " successfully loaded in memory."); + } catch (IOException e) { + this.skinOverlay.getLogger().log(Level.WARNING, "Cannot load skin image file: " + skinName, e); + } + } + + for (File file : Objects.requireNonNull(folder.listFiles((dir, name) -> name.endsWith(".yml")))) { + final String skinName = file.getName().substring(0, file.getName().length() - 4); + + if (skinName.isEmpty()) { + this.skinOverlay.getLogger().info("Invalid skin configuration file name: " + skinName); + continue; + } + + final SkinConfigurationFile skinFile = new SkinConfigurationFile(skinName); + skinFile.setupFile(); + this.skinConfigurationFiles.put(skinName, skinFile); + this.skinOverlay.getLogger().info("Skin configuration file: " + skinName + " successfully loaded in memory."); + } + + } + + private void copyFilesToFolder(boolean b) { + String[] resources = new String[]{"alley", "frog_pijama", "bubbo_transparent", "fire_demon", "flame", "glare", "hoodie", "migrator", "pirate", "smoking", "policeman", "mustache"}; + if (b) { + for (String resource : resources) { + if (new File(this.skinOverlay.getSkinsDataFolder(), resource + ".png").exists()) continue; + try { + Utils.saveResource("skins/" + resource + ".png", false, this.skinOverlay.getDataFolder(), this.getClass()); + } catch (Exception e) { + this.skinOverlay.getLogger().log(Level.WARNING, "Cannot save default skins: ", e.getCause()); + } + } + } else { + for (String resource : resources) { + if (new File(this.skinOverlay.getDataFolder(), "skins-cfg/" + resource + ".yml").exists()) continue; + try { + Utils.saveResource("skins-cfg/" + resource + ".yml", false, this.skinOverlay.getDataFolder(), this.skinOverlay.getClass()); + } catch (Exception e) { + this.skinOverlay.getLogger().log(Level.WARNING, "Cannot save default skin configuration files: ", e.getCause()); + } + } + } + } + + /* + * Returns a cached or attempts to cache a possible SkinFile object from + * a specific key. + */ + @Nullable + public SkinConfigurationFile getCacheSkinConfig(final String name) { + SkinConfigurationFile cache = this.skinConfigurationFiles.get(name); + if (cache != null) { + return cache; + } + for (Entry entry : this.skinConfigurationFiles.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + return null; + } + + @Nullable + public SkinConfigurationFile getCacheSkinConfig(final @NotNull Skin skin) { + return this.getCacheSkinConfig(skin.getSkinParts().getSkinName()); + } + + @Nullable + public SerializableBufferedImage getSkinImage(final String skinName) { + SerializableBufferedImage cache = this.skinImages.get(skinName); + if (cache != null) { + return cache; + } + for (Entry entry : this.skinImages.entrySet()) { + if (entry.getKey().equalsIgnoreCase(skinName)) { + return entry.getValue(); + } + } + return null; + } + + @Nullable + public SerializableBufferedImage getSkinImage(final @NotNull Skin skin) { + 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/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java new file mode 100644 index 00000000..64bdf65e --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/Part.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.utilities.skin; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a part of a player's skin. + */ +public record Part(String name, SerializableBufferedImage image, int x, int y, int width, int height, + boolean isEmpty) { + + /** + * Returns a string representation of the Part object. + * + * @return A string representation of the Part object. + */ + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "Part{" + + "name='" + name + '\'' + + ", image=" + image + + ", x=" + x + + ", y=" + y + + ", width=" + width + + ", height=" + height + + ", isEmpty=" + isEmpty + + '}'; + } + + /** + * Serializes the Part object to a map for YAML serialization. + * + * @return A map containing the serialized data. + */ + public @NotNull Map serialize() { + Map map = new HashMap<>(); + map.put("name", name); + map.put("bufferedImage", image); + map.put("x", x); + map.put("y", y); + map.put("width", width); + map.put("height", height); + map.put("isEmpty", isEmpty); + return map; + } + + /** + * Deserializes the YAML-compatible map to a Part object. + * + * @param map The YAML-compatible map containing part information. + * @return The deserialized Part object. + */ + @Contract("_ -> new") + public static @NotNull Part deserialize(@NotNull Map map) { + return new Part( + (String) map.get("name"), + (SerializableBufferedImage) map.get("bufferedImage"), + (int) map.get("x"), + (int) map.get("y"), + (int) map.get("width"), + (int) map.get("height"), + (boolean) map.get("isEmpty") + ); + } + + /** + * Serializes the Part 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 Part object. + * + * @param json The JSON string to deserialize. + * @return The deserialized Part object. + */ + public static Part fromJson(String json) { + return SkinOverlay.getInstance().getGson().fromJson(json, Part.class); + } +} \ No newline at end of file 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/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java new file mode 100644 index 00000000..f54b6832 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java @@ -0,0 +1,332 @@ +package com.georgev22.skinoverlay.utilities.skin; + +import com.georgev22.skinoverlay.SkinOverlay; +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 org.jetbrains.annotations.NotNull; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.URL; +import java.util.Map; + +/** + * Represents different sections and parts of a player's skin. + */ +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(); + + 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); + } + } catch (IOException e) { + steveSkin = null; + throw new RuntimeException(e); + } + steveSkin = new SerializableBufferedImage(ImageIO.read(new ByteArrayInputStream(output.toByteArray()))); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private SerializableBufferedImage fullSkin; + + private String skinName; + + private final ObjectMap parts; + + /** + * Default constructor for SkinParts using the Steve skin. + */ + public SkinParts() { + this(steveSkin, "Steve"); + } + + /** + * Constructor for SkinParts with a specified full skin and skin name. + * + * @param fullSkin The full skin image. + * @param skinName The name of the skin. + */ + public SkinParts(SerializableBufferedImage fullSkin, String skinName) { + this.parts = new HashObjectMap<>(); + if (fullSkin != null) { + this.fullSkin = fullSkin; + } + if (skinName != null) { + this.skinName = skinName; + } + createParts(); + } + + /** + * Creates different parts based on the full skin image. + */ + public void createParts() { + if (fullSkin == null) { + return; + } + if (isOldSkin()) { + SerializableBufferedImage fullSkin = this.fullSkin; + this.fullSkin = convertSkin(fullSkin); + } + parts.put("Head_Top", createPart(Section.SectionType.Head_Top.getSection())); + parts.put("Head_Bottom", createPart(Section.SectionType.Head_Bottom.getSection())); + parts.put("Head_Right", createPart(Section.SectionType.Head_Right.getSection())); + parts.put("Head_Front", createPart(Section.SectionType.Head_Front.getSection())); + parts.put("Head_Left", createPart(Section.SectionType.Head_Left.getSection())); + parts.put("Head_Back", createPart(Section.SectionType.Head_Back.getSection())); + parts.put("Hat_Top", createPart(Section.SectionType.Hat_Top.getSection())); + parts.put("Hat_Bottom", createPart(Section.SectionType.Hat_Bottom.getSection())); + parts.put("Hat_Right", createPart(Section.SectionType.Hat_Right.getSection())); + parts.put("Hat_Front", createPart(Section.SectionType.Hat_Front.getSection())); + parts.put("Hat_Left", createPart(Section.SectionType.Hat_Left.getSection())); + parts.put("Hat_Back", createPart(Section.SectionType.Hat_Back.getSection())); + parts.put("Right_Leg_Top", createPart(Section.SectionType.Right_Leg_Top.getSection())); + parts.put("Right_Leg_Bottom", createPart(Section.SectionType.Right_Leg_Bottom.getSection())); + parts.put("Right_Leg_Right", createPart(Section.SectionType.Right_Leg_Right.getSection())); + parts.put("Right_Leg_Front", createPart(Section.SectionType.Right_Leg_Front.getSection())); + parts.put("Right_Leg_Left", createPart(Section.SectionType.Right_Leg_Left.getSection())); + parts.put("Right_Leg_Back", createPart(Section.SectionType.Right_Leg_Back.getSection())); + parts.put("Torso_Top", createPart(Section.SectionType.Torso_Top.getSection())); + parts.put("Torso_Bottom", createPart(Section.SectionType.Torso_Bottom.getSection())); + parts.put("Torso_Right", createPart(Section.SectionType.Torso_Right.getSection())); + parts.put("Torso_Front", createPart(Section.SectionType.Torso_Front.getSection())); + parts.put("Torso_Left", createPart(Section.SectionType.Torso_Left.getSection())); + parts.put("Torso_Back", createPart(Section.SectionType.Torso_Back.getSection())); + parts.put("Right_Arm_Top", createPart(Section.SectionType.Right_Arm_Top.getSection())); + parts.put("Right_Arm_Bottom", createPart(Section.SectionType.Right_Arm_Bottom.getSection())); + parts.put("Right_Arm_Right", createPart(Section.SectionType.Right_Arm_Right.getSection())); + parts.put("Right_Arm_Front", createPart(Section.SectionType.Right_Arm_Front.getSection())); + parts.put("Right_Arm_Left", createPart(Section.SectionType.Right_Arm_Left.getSection())); + parts.put("Right_Arm_Back", createPart(Section.SectionType.Right_Arm_Back.getSection())); + parts.put("Left_Leg_Top", createPart(Section.SectionType.Left_Leg_Top.getSection())); + parts.put("Left_Leg_Bottom", createPart(Section.SectionType.Left_Leg_Bottom.getSection())); + parts.put("Left_Leg_Right", createPart(Section.SectionType.Left_Leg_Right.getSection())); + parts.put("Left_Leg_Front", createPart(Section.SectionType.Left_Leg_Front.getSection())); + parts.put("Left_Leg_Left", createPart(Section.SectionType.Left_Leg_Left.getSection())); + parts.put("Left_Leg_Back", createPart(Section.SectionType.Left_Leg_Back.getSection())); + parts.put("Left_Arm_Top", createPart(Section.SectionType.Left_Arm_Top.getSection())); + parts.put("Left_Arm_Bottom", createPart(Section.SectionType.Left_Arm_Bottom.getSection())); + parts.put("Left_Arm_Right", createPart(Section.SectionType.Left_Arm_Right.getSection())); + parts.put("Left_Arm_Front", createPart(Section.SectionType.Left_Arm_Front.getSection())); + parts.put("Left_Arm_Left", createPart(Section.SectionType.Left_Arm_Left.getSection())); + parts.put("Left_Arm_Back", createPart(Section.SectionType.Left_Arm_Back.getSection())); + parts.put("Right_Pants_Leg_Top", createPart(Section.SectionType.Right_Pants_Leg_Top.getSection())); + parts.put("Right_Pants_Leg_Bottom", createPart(Section.SectionType.Right_Pants_Leg_Bottom.getSection())); + parts.put("Right_Pants_Leg_Right", createPart(Section.SectionType.Right_Pants_Leg_Right.getSection())); + parts.put("Right_Pants_Leg_Front", createPart(Section.SectionType.Right_Pants_Leg_Front.getSection())); + parts.put("Right_Pants_Leg_Left", createPart(Section.SectionType.Right_Pants_Leg_Left.getSection())); + parts.put("Right_Pants_Leg_Back", createPart(Section.SectionType.Right_Pants_Leg_Back.getSection())); + parts.put("Jacket_Top", createPart(Section.SectionType.Jacket_Top.getSection())); + parts.put("Jacket_Bottom", createPart(Section.SectionType.Jacket_Bottom.getSection())); + parts.put("Jacket_Right", createPart(Section.SectionType.Jacket_Right.getSection())); + parts.put("Jacket_Front", createPart(Section.SectionType.Jacket_Front.getSection())); + parts.put("Jacket_Left", createPart(Section.SectionType.Jacket_Left.getSection())); + parts.put("Jacket_Back", createPart(Section.SectionType.Jacket_Back.getSection())); + parts.put("Right_Sleeve_Top", createPart(Section.SectionType.Right_Sleeve_Top.getSection())); + parts.put("Right_Sleeve_Bottom", createPart(Section.SectionType.Right_Sleeve_Bottom.getSection())); + parts.put("Right_Sleeve_Right", createPart(Section.SectionType.Right_Sleeve_Right.getSection())); + parts.put("Right_Sleeve_Front", createPart(Section.SectionType.Right_Sleeve_Front.getSection())); + parts.put("Right_Sleeve_Left", createPart(Section.SectionType.Right_Sleeve_Left.getSection())); + parts.put("Right_Sleeve_Back", createPart(Section.SectionType.Right_Sleeve_Back.getSection())); + parts.put("Left_Pants_Leg_Top", createPart(Section.SectionType.Left_Pants_Leg_Top.getSection())); + parts.put("Left_Pants_Leg_Bottom", createPart(Section.SectionType.Left_Pants_Leg_Bottom.getSection())); + parts.put("Left_Pants_Leg_Right", createPart(Section.SectionType.Left_Pants_Leg_Right.getSection())); + parts.put("Left_Pants_Leg_Front", createPart(Section.SectionType.Left_Pants_Leg_Front.getSection())); + parts.put("Left_Pants_Leg_Left", createPart(Section.SectionType.Left_Pants_Leg_Left.getSection())); + parts.put("Left_Pants_Leg_Back", createPart(Section.SectionType.Left_Pants_Leg_Back.getSection())); + parts.put("Left_Sleeve_Top", createPart(Section.SectionType.Left_Sleeve_Top.getSection())); + parts.put("Left_Sleeve_Bottom", createPart(Section.SectionType.Left_Sleeve_Bottom.getSection())); + parts.put("Left_Sleeve_Right", createPart(Section.SectionType.Left_Sleeve_Right.getSection())); + parts.put("Left_Sleeve_Front", createPart(Section.SectionType.Left_Sleeve_Front.getSection())); + parts.put("Left_Sleeve_Left", createPart(Section.SectionType.Left_Sleeve_Left.getSection())); + parts.put("Left_Sleeve_Back", createPart(Section.SectionType.Left_Sleeve_Back.getSection())); + } + + /** + * Creates a part image based on the specified section. + * + * @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.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); + + boolean isAreaTransparent = true; + for (int pixel : pixelData) { + int alpha = (pixel >> 24) & 0xFF; + if (alpha > 0) { + isAreaTransparent = false; + break; + } + } + + String partName = section.getClass().getSimpleName(); + return new Part(partName, new SerializableBufferedImage(partImage), x, y, width, height, isAreaTransparent); + } + + /** + * Retrieves a specific part by name. + * + * @param partName The name of the part. + * @return The Part object corresponding to the specified name. + */ + public Part getPart(String partName) { + return parts.get(partName); + } + + /** + * Saves images of all parts to the specified output directory. + * + * @param outputDirectory The directory to save the part images. + */ + public void savePartImages(File outputDirectory) { + for (Part part : parts.values()) { + String fileName = part.name() + ".png"; + try { + ImageIO.write(part.image().getBufferedImage(), "png", new File(outputDirectory, fileName)); + } catch (IOException e) { + System.out.println("Failed to save part image: " + fileName); + System.out.println(e.getMessage()); + } + } + } + + /** + * Checks if the skin is an old format (64x32). + * + * @return True if the skin is in the old format, false otherwise. + */ + public boolean isOldSkin() { + BufferedImage image = this.fullSkin.getBufferedImage(); + 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). + * + * @param original The original skin image in old format. + * @return The converted skin image in new format. + */ + public SerializableBufferedImage convertSkin(@NotNull SerializableBufferedImage original) { + BufferedImage orig = original.getBufferedImage(); + BufferedImage newImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); + + // Copy top half of the original image + newImage.getGraphics().drawImage(orig, 0, 0, 64, 32, null); + + // Copy left leg from the original image + BufferedImage leftLeg = orig.getSubimage(0, 16, 16, 16); + newImage.getGraphics().drawImage(leftLeg, 16, 48, null); + + // Copy right leg from the original image + BufferedImage rightLeg = orig.getSubimage(40, 16, 16, 16); + newImage.getGraphics().drawImage(rightLeg, 32, 48, null); + + 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. + */ + public @NotNull Map serialize() { + return Map.of("bufferedImage", this.fullSkin, "skinName", this.skinName); + } + + /** + * Deserializes the YAML-compatible map to a SkinParts object. + * + * @param map The YAML-compatible map containing skin information. + * @return The deserialized SkinParts object. + */ + public static @NotNull SkinParts deserialize(@NotNull Map map) { + return new SkinParts((SerializableBufferedImage) map.get("bufferedImage"), (String) map.get("skinName")); + } + + + /** + * Serializes the SkinParts 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 SkinParts object. + * + * @param json The JSON string to deserialize. + * @return The deserialized SkinParts object. + */ + public static SkinParts fromJson(String json) { + return SkinOverlay.getInstance().getGson().fromJson(json, SkinParts.class); + } + + /** + * Returns a string representation of the SkinParts object. + * + * @return A string representation of the SkinParts object. + */ + @Override + public String toString() { + return "SkinParts{" + + "fullSkin=" + fullSkin + + ", parts=" + parts + + '}'; + } +} \ No newline at end of file diff --git a/common/src/main/resources/bungee.yml b/common/src/main/resources/bungee.yml new file mode 100644 index 00000000..9c3898f1 --- /dev/null +++ b/common/src/main/resources/bungee.yml @@ -0,0 +1,7 @@ +name: ${pluginName} +version: ${version} +main: ${bungeeMain} +author: ${author} +description: SkinOverlay +softDepends: + - SkinsRestorer \ No newline at end of file diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml new file mode 100644 index 00000000..6e8f5ba2 --- /dev/null +++ b/common/src/main/resources/config.yml @@ -0,0 +1,34 @@ +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. \ No newline at end of file diff --git a/common/src/main/resources/messages_en_US.yml b/common/src/main/resources/messages_en_US.yml new file mode 100644 index 00000000..e3e4cd20 --- /dev/null +++ b/common/src/main/resources/messages_en_US.yml @@ -0,0 +1,11 @@ +Messages: + No Permission: '&c&l(!)&c You do not have the correct permissions to do this!' + 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 Not Found: '&c&l(!)&c Overlay %overlay% not found!' + Insufficient arguments: '&c&l(!)&c Insufficient arguments (%command%)' + 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/common/src/main/resources/messages_tr.yml b/common/src/main/resources/messages_tr.yml new file mode 100644 index 00000000..b2fd5c54 --- /dev/null +++ b/common/src/main/resources/messages_tr.yml @@ -0,0 +1,16 @@ +Messages: + No Permission: '&c&l(!)&c Bunu yapmak için gerekli yetkilere sahip değilsin!' + Only Player Command: '&c&l(!)&c Bu komutu sadece oyuncular kullanabilir!' + Offline Player: '&c&l(!)&c %player% adlı oyuncu çevrimdışı!' + Overlay Applied: '&a&l(!)&a %url% kaplaması uygulandı!' + Overlay Reset: '&a&l(!)&a Varsayılan cilt uygulandı (%player%)!' + Overlay Not Found: '&c&l(!)&c %overlay% kaplaması bulunamadı!' + Insufficient arguments: '&c&l(!)&c Yetersiz argüman(lar)! (%command%)' +Commands: + Descriptions: + SkinOverlay: + help: 'Yardım sayfasını gösterir' + overlay: 'Eklentinin dosyalarından belli bir kaplama kuşan' + clear: 'Cilt kaplamasını kaldırır' + url: 'Bir URL''den belli bir kaplama kuşan' + reload: 'Eklentinin yapılandırma dosyalarını yenile (bazı ayarların yenilenmesi için sunucunun yeniden başlatılması gerekir)' 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/common/src/main/resources/skins-cfg/alley.yml b/common/src/main/resources/skins-cfg/alley.yml new file mode 100644 index 00000000..f52f3d9b --- /dev/null +++ b/common/src/main/resources/skins-cfg/alley.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: false + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/bubbo_transparent.yml b/common/src/main/resources/skins-cfg/bubbo_transparent.yml new file mode 100644 index 00000000..541f7c74 --- /dev/null +++ b/common/src/main/resources/skins-cfg/bubbo_transparent.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: false + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/fire_demon.yml b/common/src/main/resources/skins-cfg/fire_demon.yml new file mode 100644 index 00000000..bf99dbde --- /dev/null +++ b/common/src/main/resources/skins-cfg/fire_demon.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/flame.yml b/common/src/main/resources/skins-cfg/flame.yml new file mode 100644 index 00000000..bf99dbde --- /dev/null +++ b/common/src/main/resources/skins-cfg/flame.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/frog_pijama.yml b/common/src/main/resources/skins-cfg/frog_pijama.yml new file mode 100644 index 00000000..9b558ef8 --- /dev/null +++ b/common/src/main/resources/skins-cfg/frog_pijama.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/glare.yml b/common/src/main/resources/skins-cfg/glare.yml new file mode 100644 index 00000000..d22dabd7 --- /dev/null +++ b/common/src/main/resources/skins-cfg/glare.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: true + left_pants: true \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/hoodie.yml b/common/src/main/resources/skins-cfg/hoodie.yml new file mode 100644 index 00000000..bf99dbde --- /dev/null +++ b/common/src/main/resources/skins-cfg/hoodie.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/migrator.yml b/common/src/main/resources/skins-cfg/migrator.yml new file mode 100644 index 00000000..ebf552c0 --- /dev/null +++ b/common/src/main/resources/skins-cfg/migrator.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: true + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/mustache.yml b/common/src/main/resources/skins-cfg/mustache.yml new file mode 100644 index 00000000..55e95363 --- /dev/null +++ b/common/src/main/resources/skins-cfg/mustache.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/pirate.yml b/common/src/main/resources/skins-cfg/pirate.yml new file mode 100644 index 00000000..bf99dbde --- /dev/null +++ b/common/src/main/resources/skins-cfg/pirate.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: true + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: false + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/policeman.yml b/common/src/main/resources/skins-cfg/policeman.yml new file mode 100644 index 00000000..5bac9999 --- /dev/null +++ b/common/src/main/resources/skins-cfg/policeman.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: false + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: true + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/smoking.yml b/common/src/main/resources/skins-cfg/smoking.yml new file mode 100644 index 00000000..5bac9999 --- /dev/null +++ b/common/src/main/resources/skins-cfg/smoking.yml @@ -0,0 +1,15 @@ +Options: + overlay: + hat: false + right_sleeve: true + left_sleeve: true + jacket: true + right_pants: true + left_pants: true + player: + hat: true + right_sleeve: false + left_sleeve: false + jacket: false + right_pants: false + left_pants: false \ No newline at end of file diff --git a/common/src/main/resources/skins/alley.png b/common/src/main/resources/skins/alley.png new file mode 100644 index 0000000000000000000000000000000000000000..c4394702bef9c583adc3c228ce4137459fbaf3f9 GIT binary patch literal 2030 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 literal 0 HcmV?d00001 diff --git a/common/src/main/resources/skins/flame.png b/common/src/main/resources/skins/flame.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2d417905f474b243623ed21848a40d845ab37e GIT binary patch literal 1150 zcmV-^1cCdBP)Px(I7vi7RCr$PTd|JRFc3`|=nxguG#m{D4Rp6O@C6|F7QP4sRM$bnUIz_GG#o;r zhKd%NpttBPbF;Qzys?vLFIVlZW6zs6&-QqnYrC%Nn#5n*wr#3iwW;w{|JwFj1+X@N zgCOAS(W*OowGJvp5quB?q@1`A1Ux@_&^>``10%bqC z$HOUtQwmT{3~7h+=+EhuOMtSyCBU=nJ97S)24L;@?Uc@20+d6!?Lh_5@UNf$lf>^o zE;l!>|88OnVXk5IeC^27{+0kMc@GYbgMivoA+((mS1s~)6aa}|z4q^CDgfW{c?=Rr zzEvUYVw(kq(hA&o%DER)0V0oQK7PJbO$Cg;SAqSG&!v^nYYiyPx|;tCidA7P3_H6( zNCl8z&FI^N3Th(;yXK@0Tzv?QJ+qSus^hOrEGpLzhT`)?ECl$74rN&NS8^SyFx7ui zfMY<``?&R>-13&j&d^9UlI_Dq9q`vf|?4z*rD^kKPXvpLU^(WV1#k3^FVM#OLv3DS-L4mA`k`6ZGWEn{z&vIt}7R3OHf} z*g7G}L4Z6S6@ZPTV8}+?MmR)(ZLdYZ*T?raMNmtCcgDBj`^}f{l+Wh_79zlwrD9Wp zgw-P8!)mqTh&=%kH-96}yX$p10yv+z)z~RA1_883lI=xpF#=L_sVppu>Pio*BuOrU ziU4jgmGJi(Rymm(WDo(p&{cXUm7&6 zvY8-j5}`|zK~o?lHI?P5JiNs0u!o}1`vW{M#jr>P#5(%QYv{Q#37`^rE(m3U`@E#J z%>;ef%g(=$>}}3ef0GKZSI+ij1e+TBRC$c+a@g;)2 P zBmvftCBPnaZJmHAXNc|6O2}F8LT{G5%|<_~9J0gs6qGG%2~`NdjU6 zT4KBu`$#hvXszcZ0b}0|OF-iNg3|p*e?OqK1710*AhQ$jMFH+#W^x%E8iA?%%j@x4 zi_=`jT?m*Px&qe(jx7RhQZvi)>eg0{~du)hpLpa{f8}Sr7zY```ii@MCXcFP=yM1U$a> zd)IeVkdtOWR{%CvZnhSBbnI>A^rP5Plz~}+coM7rsE}8*a7#utRptuVq zhPeH$x_Ek66?P}s&a17ucyd@ZpWAr=$23EwIS=128o*MramM*aO9#cR_K~zacmVd4 zTz;pnCID%a0CaUwzEGJw18m_*|E`OtkC4WZe8jZ#XmLHXy|m-}wBbn_hXSR#UH&f> zW?t~=+z)OAmnC>-Srr13WylW=JC-KLkQ+_)xZm~shhCOxn}^PvE)F{!PP{x*~Ftt1D`3768@-%?Y#`qS==oT`Icz0C0s?DrYbNXqlq6%ap*(09Zv@(FA;nixje8 z=WH_*B~B7z0vZD(Um}H6%F~+@jP(H;0NDhr6DczaWGCo2ugp>4qCVMM6!0ZNv|suO zEfI3{m+}BR1EsTs6AVg(q*NFKY67E=FBLYotQ63f3cLDCsjx3X1*O7Le7r30)w98| z1Caq3yN0#(Wd>*njlDfHVC)*!)|VOJs|RQmU=7`QwNvI<$d?%~FD(sa+{}QXXlh<^ zW(Ld)*`bU(a|Yacu-+WCu{>>_<>S3pt~M`Ppx6?6))xwVF95Kt)#diM4Z!Z>m(5wA z23+XE**^m?EppO#m(B#ZU;<*_ACQ2O&iJ+OV=qM5@^zKd3aNQAnu$aVjKQN;Iecrn-GZMn_? X8TA}}$n(*s00000NkvXXu0mjfrXkB@ literal 0 HcmV?d00001 diff --git a/common/src/main/resources/skins/glare.png b/common/src/main/resources/skins/glare.png new file mode 100644 index 0000000000000000000000000000000000000000..be2a77e53ea38c8c124bfe19d0e3612117289485 GIT binary patch literal 1253 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 ViRVt5C!+uW002ovPDHLkV1gsRQ)~bL literal 0 HcmV?d00001 diff --git a/common/src/main/resources/skins/migrator.png b/common/src/main/resources/skins/migrator.png new file mode 100644 index 0000000000000000000000000000000000000000..0543ea69aeea8f1b2fbb4ea311a4cc5ea7ccdea3 GIT binary patch literal 2551 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 N002ovPDHLkV1gnwvU&gj literal 0 HcmV?d00001 diff --git a/common/src/main/resources/skins/mustache.png b/common/src/main/resources/skins/mustache.png new file mode 100644 index 0000000000000000000000000000000000000000..d6301757acac83d0a2276869bc544d15d1958527 GIT binary patch literal 575 zcmV-F0>J%=P)0TmRnND;*%K~WG(jfG7Ri4lz2Y&I_;iOX(+1Y5zzB48of z_$vGd+6uM`f}jW@HvR!$jfC}%5fUWMGJ8MH%(={*0cSX?=d7j?$mI)WWG3WWj4k=x zFSO7@C*urjx@Anw&4u}QeBR+u^){$_{lBl_Xip?99UQH~jgvcws5ze z(GtRk!U0nbD14>#c$J?Or>p#~*o;P|!Kr%s(sdlKV?AT$golLvxolDYJ&(GHCz)TE zm)a+@ABzYxgzyok$SPS11f}LBv&O!WAKxr%;?d=AP{I^Pnv8e@q86oVQff-{q&y}5WQT}{0HFOgtN<< z7T<;QE$DqPG*kPLTr!=h6uh27M+~}dpl!LD)$ckd_YmEXC%T5ceHfJe-j#1l&1fw` z_i!ly000SaNLh0L01FZT01FZU(%pXi0001XNkl| zJjOWZ+v63sIzm~x8J=M&|6N|bJsm#R&h>W&00000000000002klL?(0ALrrd`8)ss N002ovPDHLkV1g$-{@DNk literal 0 HcmV?d00001 diff --git a/common/src/main/resources/skins/pirate.png b/common/src/main/resources/skins/pirate.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ead907a29f3d1575baa9fdadb0ef0a3513d789 GIT binary patch literal 2643 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 aLH0i^9hGj3pxFoj0000 + project.shadow.component(publication) + artifactId = project.getName().toLowerCase() + groupId = ((String) project.getGroup()).toLowerCase() + artifact sourcesJar + artifact javadocJar + } + } + repositories { + def isGitHub = System.getenv("GITHUB_REPOSITORY") ? true : false + if (isGitHub) { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/${project.findProperty("GITHUB_REPOSITORY") ?: System.getenv("GITHUB_REPOSITORY")}") + credentials { + username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + } + maven { + name = "GeorgeV22" + + url = uri("https://repo.georgev22.com/releases") + + def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : + System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null + def mavenPassword = System.getenv("MAVEN_PASSWORD") ? System.getenv("MAVEN_PASSWORD") : + System.getProperty("MAVEN_PASSWORD") ? System.getProperty("MAVEN_PASSWORD") : null + + if (mavenUsername != null && mavenPassword != null) { + credentials { + username = mavenUsername + password = mavenPassword + } + authentication { + basic(BasicAuthentication) + } + } + } + } + if (!isGitHub) { + maven { + name = "GeorgeV22" + + url = uri("https://repo.georgev22.com/snapshots") + + def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : + System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null + def mavenPassword = System.getenv("MAVEN_PASSWORD") ? System.getenv("MAVEN_PASSWORD") : + System.getProperty("MAVEN_PASSWORD") ? System.getProperty("MAVEN_PASSWORD") : null + + if (mavenUsername != null && mavenPassword != null) { + credentials { + username = mavenUsername + password = mavenPassword + } + authentication { + basic(BasicAuthentication) + } + } + } + } + } +} + +javadoc { + if (JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + options.addStringOption('Xdoclint:none', '-quiet') + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +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=. +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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..62e4e85c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,29 @@ +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") \ No newline at end of file From f62384394da4acf0fbcdeb1c1c16fd0592517bd2 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 22:47:32 +0300 Subject: [PATCH 03/79] build: add plugin ID to build parameters Introduce `pluginId` property in build.gradle.kts derived from existing `pluginName` (lowercased). Add corresponding PLUGIN_ID constant to BuildParameters.java template for generated build info. --- build-info/build.gradle.kts | 1 + .../com/georgev22/skinoverlay/BuildParameters.java.peb | 1 + 2 files changed, 2 insertions(+) diff --git a/build-info/build.gradle.kts b/build-info/build.gradle.kts index f3fc60df..b8c22fed 100644 --- a/build-info/build.gradle.kts +++ b/build-info/build.gradle.kts @@ -12,6 +12,7 @@ sourceSets { main { blossom { javaSources { + property("pluginId", project.property("pluginName").toString().toLowerCase()) property("pluginName", project.property("pluginName").toString()) property("version", version.toString()) property("description", rootProject.description) 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 index b12d1026..da31b09e 100644 --- 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 @@ -3,6 +3,7 @@ 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 }}"; From a64939330c35bbe5aa2cc3ef9fe912c57d2a504a Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 22:49:41 +0300 Subject: [PATCH 04/79] feat: add Redis and PluginMessage support for skin synchronization - Introduce MessageManager interface for skin data messaging - Implement RedisManager using Redis pub/sub for cross-server sync - Add PluginMessageListener for Bukkit inter-server communication - Extend configuration with connection type and Redis settings - Add Base64 serialization methods to Skin class - Integrate messaging system in Bukkit initialization - Update SkinApplier to support new applySkin method with Skin param - Add new configuration options in OptionsUtil and config.yml --- .../skinoverlay/SkinOverlayBukkit.java | 25 +++++++++++ .../bukkit/PluginMessageListenerImpl.java | 44 +++++++++++++++++++ .../georgev22/skinoverlay/SkinOverlay.java | 20 +++++++++ .../skinoverlay/appliers/SkinApplier.java | 15 ++++++- .../skinoverlay/message/MessageManager.java | 15 +++++++ .../skinoverlay/message/RedisManager.java | 40 +++++++---------- .../skinoverlay/storage/data/Skin.java | 13 ++++++ .../utilities/config/OptionsUtil.java | 7 ++- common/src/main/resources/config.yml | 13 +++++- 9 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 8bc81f3c..6680f5c8 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -5,10 +5,14 @@ import com.georgev22.skinoverlay.hooks.SkinHookNoop; import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; import com.georgev22.skinoverlay.listeners.bukkit.PlayerListeners; +import com.georgev22.skinoverlay.listeners.bukkit.PluginMessageListenerImpl; +import com.georgev22.skinoverlay.message.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 net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; @@ -118,6 +122,27 @@ public void onLoad() { ); } this.skinOverlay.onLoad(); + + if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { + new PluginMessageListenerImpl((uuid, skin) -> { + SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); + if (player != null) { + skinOverlay.getSkinApplier().setSkin(player, skin); + } + }); + } else { + this.skinOverlay.setMessageManager(new RedisManager( + OptionsUtil.REDIS_HOST.getStringValue(), + OptionsUtil.REDIS_PORT.getIntValue(), + OptionsUtil.REDIS_PASSWORD.getStringValue() + )); + skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> { + SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); + if (player != null) { + skinOverlay.getSkinApplier().setSkin(player, skin); + } + }); + } } @Override 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..02922433 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java @@ -0,0 +1,44 @@ +package com.georgev22.skinoverlay.listeners.bukkit; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.message.SkinPropertyHandler; +import com.georgev22.skinoverlay.storage.data.Skin; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class PluginMessageListenerImpl implements PluginMessageListener { + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + private final SkinPropertyHandler handler; + + public PluginMessageListenerImpl(SkinPropertyHandler handler) { + this.handler = handler; + Bukkit.getMessenger().registerIncomingPluginChannel(skinOverlay.getPlugin(), "skinoverlay:skinupdate", this); + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] message) { + if (!channel.equals("skinoverlay:skinupdate")) return; + + String data = new String(message, StandardCharsets.UTF_8); + String[] parts = data.split("\\|", 2); + if (parts.length != 2) { + skinOverlay.getLogger().severe("Invalid skin property message: " + data); + return; + } + + try { + UUID uuid = UUID.fromString(parts[0]); + String base64Skin = parts[1]; + Skin skin = Skin.fromBase64(base64Skin); + + Bukkit.getScheduler().runTask(skinOverlay.getPlugin(), () -> handler.handle(uuid, skin)); + } catch (Exception e) { + skinOverlay.getLogger().log(java.util.logging.Level.SEVERE, "Error handling skin property message: " + data, e); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index c48ebb16..0d6c34e1 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -10,6 +10,7 @@ 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.MessageManager; import com.georgev22.skinoverlay.providers.GameProfileProvider; import com.georgev22.skinoverlay.providers.PlayerProvider; import com.georgev22.skinoverlay.providers.SkinProvider; @@ -72,6 +73,7 @@ public static SkinOverlay getInstance() { private boolean isOnlineMode; private boolean isProxy; private File dataFolder; + private MessageManager messageManager; /** * Loads the plugin components. Should be called during plugin load phase. @@ -425,4 +427,22 @@ public SkinFileCache getSkinFileCache() { 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/SkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java index e224d9d9..2c0f6fe4 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java @@ -27,7 +27,7 @@ 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); + applySkin(player, skin); // Should we save the skin in the database? if (!skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { @@ -54,7 +54,18 @@ public void setSkin(@NotNull SPlayer player, @NotNull Skin skin) { * * @param player Player's {@link SPlayer} object. */ - protected abstract void applySkin(@NotNull final SPlayer player); + 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/message/MessageManager.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java new file mode 100644 index 00000000..a94897ab --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java @@ -0,0 +1,15 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.storage.data.Skin; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public interface MessageManager { + + void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin); + + void subscribeSkinProperty(@NotNull SkinPropertyHandler handler); + + void close(); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java b/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java index d20c67e0..b0180eaf 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.message; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.skin.SProperty; +import com.georgev22.skinoverlay.storage.data.Skin; import org.jetbrains.annotations.NotNull; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; @@ -9,7 +9,7 @@ import java.util.UUID; import java.util.logging.Level; -public class RedisManager { +public class RedisManager implements MessageManager { private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); private final String host; private final int port; @@ -28,12 +28,14 @@ public RedisManager(String host, int port, String password) { } } - public void publishSkinProperty(String channel, @NotNull UUID playerUUID, String value, String signature) { - String message = playerUUID + "|" + value + "|" + signature; - jedis.publish(channel, message); + @Override + public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { + String message = playerUUID + "|" + skin.toBase64(); + jedis.publish("skinoverlay:skinupdate", message); } - public void subscribeSkinProperty(String channel, SkinPropertyHandler handler) { + @Override + public void subscribeSkinProperty(@NotNull SkinPropertyHandler handler) { subscriberThread = new Thread(() -> { while (running && !Thread.currentThread().isInterrupted()) { try (Jedis subJedis = new Jedis(host, port)) { @@ -45,29 +47,23 @@ public void subscribeSkinProperty(String channel, SkinPropertyHandler handler) { @Override public void onMessage(String ch, String message) { try { - String[] parts = message.split("\\|", 3); - if (parts.length != 3) { + 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 value = parts[1]; - String signature = parts[2]; - SProperty property = new SProperty(value, signature); + String base64Skin = parts[1]; + Skin skin = Skin.fromBase64(base64Skin); -// if (scheduler != null) { -// scheduler.runTask(() -> handler.handle(uuid, property)); -// } else { -// handler.handle(uuid, property); -// } - skinOverlay.getScheduler().runTask(skinOverlay.getPlugin(), () -> handler.handle(uuid, property)); + skinOverlay.getScheduler().runTask(skinOverlay.getPlugin(), () -> handler.handle(uuid, skin)); } catch (Exception e) { skinOverlay.getLogger().log(Level.SEVERE, "Invalid skin property message: " + message, e); } } }; - subJedis.subscribe(jedisPubSub, channel); + subJedis.subscribe(jedisPubSub, "skinoverlay:skinupdate"); } catch (Exception e) { skinOverlay.getLogger().log(Level.SEVERE, "Redis subscribe connection lost, retrying in 5 seconds...", e); try { @@ -85,9 +81,7 @@ public void onMessage(String ch, String message) { subscriberThread.start(); } - /** - * Stop subscriber thread and close resources. - */ + @Override public void close() { running = false; if (subscriberThread != null) { @@ -95,8 +89,4 @@ public void close() { } jedis.close(); } - - public interface SkinPropertyHandler { - void handle(UUID uuid, SProperty property); - } } 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 index e8e1b779..fb4ef972 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java @@ -1,11 +1,14 @@ 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; @@ -158,4 +161,14 @@ public boolean equals(Object obj) { 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/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java index 6167d451..a53be8d4 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java @@ -32,7 +32,12 @@ public enum OptionsUtil { SKIN_HOOK("skin hook", "SkinsRestorer", Optional.empty()), LOCALE("locale", "en_US", Optional.empty()), - SAVE_INTERVAL("save interval", 20, 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; private final Object value; diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 6e8f5ba2..7cd07588 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -31,4 +31,15 @@ Options: # 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. \ No newline at end of file + 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 From 345924c6c4e105ffb2129e4c2686a07cdfaec1c3 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 22:50:00 +0300 Subject: [PATCH 05/79] feat: Velocity support --- .../message/SkinPropertyHandler.java | 9 + settings.gradle.kts | 3 +- velocity/build.gradle.kts | 42 ++ .../skinoverlay/SkinOverlayVelocity.java | 90 +++++ .../skinoverlay/VelocityAudienceProvider.java | 99 +++++ .../appliers/VelocitySkinApplier.java | 23 ++ .../command/VelocityCommandIssuer.java | 65 ++++ .../command/VelocityCommandManager.java | 27 ++ .../command/VelocityPluginCommandWrapper.java | 45 +++ .../message/VelocityPluginMessageManager.java | 48 +++ .../skinoverlay/player/VelocitySPlayer.java | 24 ++ .../VelocityGameProfileProvider.java | 43 +++ .../providers/VelocityPlayerProvider.java | 84 ++++ .../scheduler/VelocityMinecraftScheduler.java | 364 ++++++++++++++++++ 14 files changed, 965 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java create mode 100644 velocity/build.gradle.kts create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandManager.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/player/VelocitySPlayer.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/scheduler/VelocityMinecraftScheduler.java diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java b/common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java new file mode 100644 index 00000000..010012d0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java @@ -0,0 +1,9 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.storage.data.Skin; + +import java.util.UUID; + +public interface SkinPropertyHandler { + void handle(UUID uuid, Skin skin); +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 62e4e85c..382464bd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,4 +26,5 @@ 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") \ No newline at end of file +include("bukkit:versions:mc1_21_R5") +include("velocity") \ No newline at end of file diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts new file mode 100644 index 00000000..df83af5b --- /dev/null +++ b/velocity/build.gradle.kts @@ -0,0 +1,42 @@ +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")) + implementation(libs.bstats.velocity) + implementation(libs.adventure.platform.api) +} + +configurations.configureEach { + +} + +tasks.shadowJar { + archiveBaseName.set("skinoverlay") + archiveClassifier.set("shadow") + relocate("org.mineskin", "${project.property("packageName")}.mineskin") + relocate("com.google.gson", "${project.property("packageName")}.gson") + relocate("com.google.errorprone", "${project.property("packageName")}.gson.errorprone") + relocate("com.zaxxer", "${project.property("packageName")}.zaxxer") + relocate("org.bstats", "${project.property("packageName")}.bstats") + relocate("org.bspfsystems.yamlconfiguration", "${project.property("packageName")}.yaml") + relocate("org.yaml.snakeyaml", "${project.property("packageName")}.yaml") + relocate("org.intellij.lang", "${project.property("packageName")}.jetbrains") + relocate("org.jetbrains", "${project.property("packageName")}.jetbrains") + relocate("net.kyori", "${project.property("packageName")}.kyori") +} + +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 new file mode 100644 index 00000000..60545043 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay; + +import com.georgev22.skinoverlay.appliers.VelocitySkinApplier; +import com.georgev22.skinoverlay.hooks.SkinHookNoop; +import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; +import com.georgev22.skinoverlay.message.RedisManager; +import com.georgev22.skinoverlay.message.VelocityPluginMessageManager; +import com.georgev22.skinoverlay.providers.VelocityGameProfileProvider; +import com.georgev22.skinoverlay.providers.VelocityPlayerProvider; +import com.georgev22.skinoverlay.scheduler.VelocityMinecraftScheduler; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Dependency; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.nio.file.Path; +import java.util.logging.Logger; + +@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; + + @Inject + public SkinOverlayVelocity(ProxyServer server, Logger logger, @DataDirectory @NotNull Path dataDirectory) { + this.server = server; + this.skinOverlay.setLogger(logger); + this.skinOverlay.setProxy(true); + File dataDirectoryFile = dataDirectory.toFile(); + if (!dataDirectoryFile.exists()) { + if (dataDirectoryFile.mkdirs()) { + logger.info("Folder " + dataDirectory + " has been created!"); + } else { + logger.warning("Failed to create folder " + dataDirectory); + } + } + this.skinOverlay.setDataFolder(dataDirectoryFile); + this.skinOverlay.setPlugin(this); + this.skinOverlay.setScheduler(new VelocityMinecraftScheduler<>(server)); + this.skinOverlay.onLoad(); + + if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { + this.skinOverlay.setMessageManager(new VelocityPluginMessageManager(server)); + } else { + this.skinOverlay.setMessageManager(new RedisManager( + OptionsUtil.REDIS_HOST.getStringValue(), + OptionsUtil.REDIS_PORT.getIntValue(), + OptionsUtil.REDIS_PASSWORD.getStringValue() + )); + } + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + if (server.getPluginManager().getPlugin("skinsrestorer").isPresent()) { + this.skinOverlay.setSkinHook(new SkinsRestorerHook()); + } else { + this.skinOverlay.setSkinHook(new SkinHookNoop()); + } + this.skinOverlay.setSkinApplier(new VelocitySkinApplier()); + this.skinOverlay.setGameProfileProvider(new VelocityGameProfileProvider()); + this.skinOverlay.setPlayerProvider(new VelocityPlayerProvider(server)); + this.skinOverlay.setAudienceProvider(new VelocityAudienceProvider(server)); + this.skinOverlay.setOnlineMode(server.getConfiguration().isOnlineMode()); + + this.skinOverlay.onEnable(); + } + + @Subscribe + public void onProxyShutdown(ProxyShutdownEvent event) { + this.skinOverlay.onDisable(); + } +} 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..5dc5fa15 --- /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 ProxyServer server) { + this.server = server; + server.getEventManager().register(this, 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..c2b55d98 --- /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("VoidChestConsole"); + } + + @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..0d3e22eb --- /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(); + + try { + baseCommand.execute(new VelocityCommandIssuer(source), args); + } 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/message/VelocityPluginMessageManager.java b/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java new file mode 100644 index 00000000..a80fb4d0 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java @@ -0,0 +1,48 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.storage.data.Skin; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; + +public class VelocityPluginMessageManager implements MessageManager { + private final ProxyServer proxyServer; + private final MinecraftChannelIdentifier channelIdentifier; + + public VelocityPluginMessageManager(@NotNull ProxyServer proxyServer) { + this.proxyServer = proxyServer; + this.channelIdentifier = MinecraftChannelIdentifier.from("skinoverlay:skinupdate"); + proxyServer.getChannelRegistrar().register(channelIdentifier); + } + + @Override + public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { + String message = playerUUID + "|" + skin.toBase64(); + byte[] data = message.getBytes(StandardCharsets.UTF_8); + + Optional playerOptional = proxyServer.getAllPlayers().stream().findAny(); + if (playerOptional.isPresent()) { + Player player = playerOptional.get(); + player.sendPluginMessage(channelIdentifier, data); + } else { + proxyServer.getConsoleCommandSource().sendMessage( + Component.text("[SkinOverlay] No players online to send plugin message.") + ); + } + } + + @Override + public void subscribeSkinProperty(@NotNull SkinPropertyHandler handler) { + } + + @Override + public void close() { + proxyServer.getChannelRegistrar().unregister(channelIdentifier); + } +} 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 From c888d103b3afec27fa5538bd5c62fec38a33aac0 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 23:03:48 +0300 Subject: [PATCH 06/79] refactor(command): remove logger dependency from command managers Removed the logger parameter from CommandManager and its subclass. Instead, using SkinOverlay's logger instance directly for logging. This simplifies the constructor and centralizes logger access. - Removed logger parameter from BukkitCommandManager - Updated SkinOverlayBukkit to instantiate without logger - CommandManager now uses SkinOverlay's logger instance --- .../com/georgev22/skinoverlay/SkinOverlayBukkit.java | 2 +- .../skinoverlay/command/BukkitCommandManager.java | 4 +--- .../georgev22/skinoverlay/command/CommandManager.java | 10 +++------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 6680f5c8..cf6e411f 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -29,7 +29,7 @@ public void onLoad() { this.skinOverlay.setPlugin(this); this.skinOverlay.setLogger(this.getLogger()); this.skinOverlay.setDataFolder(this.getDataFolder()); - this.skinOverlay.setCommandManager(new BukkitCommandManager(this.getLogger(), this)); + this.skinOverlay.setCommandManager(new BukkitCommandManager(this)); this.skinOverlay.setPlayerProvider(new BukkitPlayerProvider()); if (BukkitMinecraftUtils.isPaper() && getCurrentVersion().isAboveOrEqual(V1_20_R1)) { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java index 4f1f7b5a..0b37c40e 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java @@ -6,14 +6,12 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; -import java.util.logging.Logger; public class BukkitCommandManager extends CommandManager { private final SkinOverlayBukkit plugin; - public BukkitCommandManager(Logger logger, SkinOverlayBukkit plugin) { - super(logger); + public BukkitCommandManager(SkinOverlayBukkit plugin) { this.plugin = plugin; } diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java index 21f05a49..fbf02fea 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java @@ -1,5 +1,6 @@ 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.resolvers.ArgumentResolver; @@ -12,19 +13,14 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; -import java.util.logging.Logger; public abstract class CommandManager { + protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); private final List> globalPreprocessors = new ArrayList<>(); private final List> globalPostprocessors = new ArrayList<>(); - private final Logger logger; private final Map resolvers = new HashMap<>(); - public CommandManager(Logger logger) { - this.logger = logger; - } - public void registerCommand(@NotNull BaseCommand command) { try { registerCommand0(command); @@ -41,7 +37,7 @@ public void registerCommand(@NotNull BaseCommand command) { } } } catch (Exception e) { - this.logger.log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e); + this.skinOverlay.getLogger().log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e); } } From f9b59c5afc5c43b63d49dc1f8b5b71bcdecfe224 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 23:04:53 +0300 Subject: [PATCH 07/79] feat(velocity): add VelocityCommandManager initialization Add VelocityCommandManager to SkinOverlayVelocity plugin setup. This integrates the command manager during plugin enable phase. --- .../java/com/georgev22/skinoverlay/SkinOverlayVelocity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index 60545043..a87694de 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -1,6 +1,7 @@ package com.georgev22.skinoverlay; 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.message.RedisManager; @@ -54,6 +55,7 @@ public SkinOverlayVelocity(ProxyServer server, Logger logger, @DataDirectory @No 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")) { From 2996a136fad4f9e99d0e560636700350f0ed650c Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 7 Jul 2025 23:36:40 +0300 Subject: [PATCH 08/79] fix(velocity): pass plugin instance to audience provider Previously, the VelocityAudienceProvider registered events with itself as the plugin parameter, which was incorrect. Now the main plugin instance is passed to the VelocityAudienceProvider constructor. This ensures proper event registration by using the actual plugin instance. --- .../java/com/georgev22/skinoverlay/SkinOverlayVelocity.java | 2 +- .../com/georgev22/skinoverlay/VelocityAudienceProvider.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index a87694de..ae02b994 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -79,7 +79,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { this.skinOverlay.setSkinApplier(new VelocitySkinApplier()); this.skinOverlay.setGameProfileProvider(new VelocityGameProfileProvider()); this.skinOverlay.setPlayerProvider(new VelocityPlayerProvider(server)); - this.skinOverlay.setAudienceProvider(new VelocityAudienceProvider(server)); + this.skinOverlay.setAudienceProvider(new VelocityAudienceProvider(this, server)); this.skinOverlay.setOnlineMode(server.getConfiguration().isOnlineMode()); this.skinOverlay.onEnable(); diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java index 5dc5fa15..12bbdaee 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java @@ -22,9 +22,9 @@ public class VelocityAudienceProvider implements AudienceProvider { private final ProxyServer server; private final Set players = ConcurrentHashMap.newKeySet(); - public VelocityAudienceProvider(@NotNull ProxyServer server) { + public VelocityAudienceProvider(@NotNull Object plugin, @NotNull ProxyServer server) { this.server = server; - server.getEventManager().register(this, this); + server.getEventManager().register(plugin, this); players.addAll(server.getAllPlayers()); } From 576eb75cff1be409392345c3b897e5b53378b24e Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 8 Jul 2025 18:23:46 +0300 Subject: [PATCH 09/79] build: optimize dependency management and shading configurations - Exclude slf4j-api from common and platform modules to prevent conflicts - Adjust Adventure dependencies: move API to compileOnly, add implementations - Force specific Adventure library versions in resolution strategies - Update shadowJar tasks with module-specific classifiers and relocations - Add relocations for json, pool2, and jedis dependencies - Migrate to Kotlin's lowercase() in build-info plugin configuration --- build-info/build.gradle.kts | 2 +- bukkit/build.gradle.kts | 14 +++++++++++--- common/build.gradle.kts | 22 +++++++++++----------- velocity/build.gradle.kts | 12 ++++++++---- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/build-info/build.gradle.kts b/build-info/build.gradle.kts index b8c22fed..cdd1a1df 100644 --- a/build-info/build.gradle.kts +++ b/build-info/build.gradle.kts @@ -12,7 +12,7 @@ sourceSets { main { blossom { javaSources { - property("pluginId", project.property("pluginName").toString().toLowerCase()) + property("pluginId", project.property("pluginName").toString().lowercase()) property("pluginName", project.property("pluginName").toString()) property("version", version.toString()) property("description", rootProject.description) diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 98c95f5f..fb47a4a0 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -11,14 +11,17 @@ repositories { dependencies { compileOnly(libs.folia.api) - compileOnly(libs.adventure.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(project(":common")) + 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")) @@ -39,6 +42,8 @@ dependencies { 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")) @@ -53,7 +58,7 @@ configurations.configureEach { tasks.shadowJar { archiveBaseName.set("skinoverlay") - archiveClassifier.set("shadow") + archiveClassifier.set("bukkit") relocate("org.mineskin", "${project.property("packageName")}.mineskin") relocate("com.google.gson", "${project.property("packageName")}.gson") relocate("com.google.errorprone", "${project.property("packageName")}.gson.errorprone") @@ -63,7 +68,10 @@ tasks.shadowJar { relocate("org.yaml.snakeyaml", "${project.property("packageName")}.yaml") relocate("org.intellij.lang", "${project.property("packageName")}.jetbrains") relocate("org.jetbrains", "${project.property("packageName")}.jetbrains") + relocate("org.json", "${project.property("packageName")}.json") + relocate("org.apache.commons.pool2", "${project.property("packageName")}.pool2") relocate("net.kyori", "${project.property("packageName")}.kyori") + relocate("redis.clients", "${project.property("packageName")}.jedis") } tasks.named("publish") { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 0561c65e..62286b3d 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -17,20 +17,18 @@ 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) + implementation(libs.hikari) { exclude(group = "org.slf4j", module = "slf4j-api") } implementation(libs.gson) - // ADVENTURE - implementation(libs.adventure.api) - implementation(libs.adventure.platform.api) - implementation(libs.adventure.text.minimessage) - implementation(libs.adventure.text.serializer.legacy) - implementation(libs.adventure.text.serializer.gson) { - exclude(group = "com.google.code.gson", module = "gson") - } // MINESKIN CLIENT implementation(libs.mineskinclient.client) { @@ -54,10 +52,7 @@ dependencies { configurations.configureEach { resolutionStrategy { - force(libs.adventure.api) - force(libs.adventure.platform.api) force(libs.adventure.text.serializer.legacy) - force(libs.adventure.text.serializer.gson) force(libs.adventure.text.minimessage) force(libs.jetbrains.annotations) } @@ -78,3 +73,8 @@ tasks.processResources { filteringCharset = "UTF-8" } } + +tasks.shadowJar { + archiveBaseName.set("skinoverlay") + archiveClassifier.set("common") +} diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index df83af5b..7fd7c81d 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -13,9 +13,11 @@ dependencies { compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") - implementation(project(":common")) + implementation(project(":common")) { + exclude(group = "org.slf4j", module = "slf4j-api") + } implementation(libs.bstats.velocity) - implementation(libs.adventure.platform.api) + compileOnly(libs.adventure.platform.api) } configurations.configureEach { @@ -24,7 +26,7 @@ configurations.configureEach { tasks.shadowJar { archiveBaseName.set("skinoverlay") - archiveClassifier.set("shadow") + archiveClassifier.set("velocity") relocate("org.mineskin", "${project.property("packageName")}.mineskin") relocate("com.google.gson", "${project.property("packageName")}.gson") relocate("com.google.errorprone", "${project.property("packageName")}.gson.errorprone") @@ -34,7 +36,9 @@ tasks.shadowJar { relocate("org.yaml.snakeyaml", "${project.property("packageName")}.yaml") relocate("org.intellij.lang", "${project.property("packageName")}.jetbrains") relocate("org.jetbrains", "${project.property("packageName")}.jetbrains") - relocate("net.kyori", "${project.property("packageName")}.kyori") + relocate("org.json", "${project.property("packageName")}.json") + relocate("org.apache.commons.pool2", "${project.property("packageName")}.pool2") + relocate("redis.clients", "${project.property("packageName")}.jedis") } tasks.named("publish") { From 9dffca35a088c4b460354a880a7ca8ebf71f19a1 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 8 Jul 2025 18:43:26 +0300 Subject: [PATCH 10/79] fix(completion): handle empty args array in tab completion Fix ArrayIndexOutOfBoundsException when tab completing with no arguments. Previously, accessing parts[-1] when args was empty caused an exception. Now uses parts[0] when args array is empty to avoid index errors. --- .../com/georgev22/skinoverlay/command/CompletionEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java index e5762274..f1ca18a5 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java @@ -21,7 +21,7 @@ public static Collection resolveCompletions(Class clazz, CommandIssue return new ArrayList<>(); } - String segment = parts[args.length - 1]; + String segment = args.length == 0 ? parts[0] : parts[args.length - 1]; Collection completions = new ArrayList<>(); for (String option : segment.split("\\|")) { From 2f35d09962416ae0c759087cb37d21541c44f944 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 8 Jul 2025 18:44:49 +0300 Subject: [PATCH 11/79] feat(utilities): refactor message handling and add MessageParser Refactor MessageBuilder to use new MessageParser for MiniMessage conversion - Remove legacy color translation method and COLOR_MAP - Add placeholder support throughout MessageBuilder - Implement methods for legacy string serialization (buildLegacyString) - Remove Audience-based send method to decouple building/sending - Add MessageParser with robust legacy/MiniMessage hybrid parsing - Fix legacy color handling within quoted MiniMessage tag arguments - Add comprehensive placeholder replacement functionality --- .../skinoverlay/utilities/MessageBuilder.java | 218 +++++++++--------- .../skinoverlay/utilities/MessageParser.java | 90 ++++++++ 2 files changed, 193 insertions(+), 115 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/MessageParser.java diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java index 82d68e17..355f0235 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java @@ -2,8 +2,6 @@ import com.georgev22.skinoverlay.maps.HashObjectMap; import com.georgev22.skinoverlay.maps.ObjectMap; -import com.georgev22.skinoverlay.maps.Pair; -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; @@ -13,9 +11,9 @@ 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; @@ -46,7 +44,7 @@ * .clickEvent(ClickEvent.Action.RUN_COMMAND, "/help") * .hoverEvent("Click to run /help") * .append(" for more info.") - * .send(player); + * .build(); * * // Using MiniMessage for more complex formatting * messageBuilder @@ -54,48 +52,17 @@ * .appendMiniMessage(" Click here for more info.") * .clickEvent(ClickEvent.Action.RUN_COMMAND, "/help") * .hoverEvent("Click to run /help") - * .send(player); + * .build(); * } */ +@SuppressWarnings({"UnusedReturnValue", "unused"}) public class MessageBuilder { - - /** - * Color map for mapping legacy colors to MiniMessage colors. - *

- * Map key: Legacy color code
- * Map value:
- * - Pair key: MiniMessage color (e.g., {@literal })
- * - Pair value: Indicates if the color needs to be closed (e.g., {@literal
}) - */ - @NotNull - public static final ObjectMap> COLOR_MAP = new HashObjectMap>() - .append("0", Pair.create("black", true)) - .append("1", Pair.create("dark_blue", true)) - .append("2", Pair.create("dark_green", true)) - .append("3", Pair.create("dark_aqua", true)) - .append("4", Pair.create("dark_red", true)) - .append("5", Pair.create("dark_purple", true)) - .append("6", Pair.create("gold", true)) - .append("7", Pair.create("gray", true)) - .append("8", Pair.create("dark_gray", true)) - .append("9", Pair.create("blue", true)) - .append("a", Pair.create("green", true)) - .append("b", Pair.create("aqua", true)) - .append("c", Pair.create("red", true)) - .append("d", Pair.create("light_purple", true)) - .append("e", Pair.create("yellow", true)) - .append("f", Pair.create("white", true)) - .append("l", Pair.create("bold", false)) - .append("m", Pair.create("strikethrough", false)) - .append("n", Pair.create("underline", false)) - .append("o", Pair.create("italic", false)) - .append("r", Pair.create("reset", true)) - .append("k", Pair.create("obfuscated", false)); 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. @@ -115,61 +82,6 @@ public MessageBuilder() { return new MessageBuilder(); } - /** - * Translates legacy color codes (e.g., {@literal &}colorCode) to MiniMessage colors (e.g., {@literal }). - * - * @param message The message to translate. - * @return The translated message with MiniMessage formatting. - */ - public static String translateLegacyColors(String message) { - boolean colorMode = false; - boolean tagNeedToClose = false; - String tagToClose = ""; - - StringBuilder newText = new StringBuilder(); - - for (char ch : message.toCharArray()) { - if (ch == '&' && colorMode) { - newText.append('&'); - continue; - } - - if (ch == '&') { - colorMode = true; - continue; - } - - if (colorMode && COLOR_MAP.containsKey(String.valueOf(ch))) { - if (tagNeedToClose) { - tagNeedToClose = false; - newText.append(""); - } - - newText.append("<").append(COLOR_MAP.get(String.valueOf(ch)).key()).append(">"); - - if (!COLOR_MAP.get(String.valueOf(ch)).value()) { - tagNeedToClose = true; - tagToClose = COLOR_MAP.get(String.valueOf(ch)).key(); - } - - colorMode = false; - continue; - } else if (colorMode) { - colorMode = false; - newText.append('&'); - } - - newText.append(ch); - } - - if (tagNeedToClose) { - newText.append(""); - } - - message = newText.toString(); - return message; - } - /** * Appends plain text to the message and applies the current styles. * @@ -186,6 +98,7 @@ public static String translateLegacyColors(String message) { * @return The MessageBuilder instance for method chaining. */ public MessageBuilder append(String text) { + text = replacePlaceholders(text); TextComponent.Builder textComponent = Component.text() .content(text) .color(currentColor); @@ -206,6 +119,17 @@ public MessageBuilder append(String text) { 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:

@@ -238,7 +162,7 @@ public MessageBuilder appendMiniMessage(String miniMessage) { * @return The MessageBuilder instance for method chaining. */ public MessageBuilder appendMiniMessage(String miniMessage, TagResolver tagResolver) { - componentBuilder.append(miniMessage(miniMessage, tagResolver)); + componentBuilder.append(miniMessage(miniMessage, tagResolver)).decoration(TextDecoration.ITALIC, false); return this; } @@ -249,11 +173,9 @@ public MessageBuilder appendMiniMessage(String miniMessage, TagResolver tagResol * @param tagResolver The TagResolver to use, can be null if not needed. * @return The converted Component. */ - private @NotNull Component miniMessage(String message, @Nullable TagResolver tagResolver) { - if (tagResolver != null) { - return MiniMessage.miniMessage().deserialize(translateLegacyColors(message), tagResolver); - } - return MiniMessage.miniMessage().deserialize(translateLegacyColors(message)); + private @NotNull Component miniMessage(String message, TagResolver tagResolver) { + message = replacePlaceholders(message); + return MessageParser.miniMessage(message, tagResolver); } /** @@ -327,6 +249,7 @@ public MessageBuilder removeDecorate(TextDecoration... decorations) { * @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; } @@ -345,42 +268,45 @@ public MessageBuilder clickEvent(ClickEvent.Action action, String value) { * @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. + * Builds the component for further use. * *

Example usage:

*
{@code
-     * messageBuilder
-     *     .append("Hello, ")
-     *     .append(player.getName())
-     *     .append("!")
-     *     .send(player);
+     * Component component = messageBuilder
+     *     .append("Hello, World!")
+     *     .build();
      * }
* - * @param audience The player to send the message to. + * @return The constructed Component. */ - public void send(Audience audience) { - audience.sendMessage(componentBuilder.build()); + public Component build() { + return componentBuilder.build(); } /** - * Builds the component for further use. + * Serializes the built component into a string using Legacy format. * *

Example usage:

*
{@code
-     * Component component = messageBuilder
+     * String messageString = messageBuilder
      *     .append("Hello, World!")
-     *     .build();
+     *     .buildLegacyString();
      * }
* - * @return The constructed Component. + * @return The serialized Legacy string. */ - public Component build() { - return componentBuilder.build(); + public String buildLegacyString() { + return LegacyComponentSerializer.builder() + .hexColors() + .useUnusualXRepeatedCharacterHexFormat() + .build() + .serialize(build()); } /** @@ -401,6 +327,24 @@ public Component buildAndReset() { 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. * @@ -438,6 +382,50 @@ public MessageBuilder resetStyles() { public MessageBuilder reset() { this.componentBuilder = Component.text(); + this.placeholders.clear(); return this.resetStyles(); } -} \ No newline at end of file + + /** + * 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; + } + + /** + * 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); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageParser.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageParser.java new file mode 100644 index 00000000..898449f3 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageParser.java @@ -0,0 +1,90 @@ +package com.georgev22.skinoverlay.utilities; + +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); + } +} From 1381f515fd828813a4c9127c9879cd93b69bcab2 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 8 Jul 2025 18:48:06 +0300 Subject: [PATCH 12/79] fix: prevent saving entities in non-proxy mode when proxy enabled Centralize proxy save condition in entity managers and remove redundant checks. Added proxy condition to saveEntityWithRetry in SkinEntityManager and PlayerEntityManager. Removed duplicate checks from SkinApplier and PlayerListeners since the condition is now handled at the save level. Changes: - Added proxy check in saveEntityWithRetry for both managers - Removed proxy condition from SkinApplier.applySkin - Removed proxy condition from PlayerListeners.playerJoinEvent - Removed unused import in SkinApplier --- .../com/georgev22/skinoverlay/appliers/SkinApplier.java | 6 ------ .../georgev22/skinoverlay/listeners/PlayerListeners.java | 3 --- .../skinoverlay/storage/manager/PlayerEntityManager.java | 3 +++ .../skinoverlay/storage/manager/SkinEntityManager.java | 4 ++++ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java index 2c0f6fe4..6698ce04 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java @@ -7,7 +7,6 @@ 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.config.OptionsUtil; import org.jetbrains.annotations.NotNull; import java.util.Optional; @@ -29,11 +28,6 @@ public void setSkin(@NotNull SPlayer player, @NotNull Skin skin) { skinOverlay.getGameProfileProvider().applyUpdatedGameProfile(player); applySkin(player, skin); - // Should we save the skin in the database? - if (!skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } - Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getManager(PlayerData.class); if (optionalPlayerDataEntityManager.isEmpty()) { return; diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 5eda68dc..23d5f91f 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -67,9 +67,6 @@ public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { this.mainPlugin.getSkinApplier().setSkin(player, playerData.getCurrentSkin()); } - if (!this.mainPlugin.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } skinEntityManager.save(defaultSkin); playerDataEntityManager.save(playerData); } 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 index 046beb99..9c20c13b 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java @@ -109,6 +109,9 @@ public void shutdown(Consumer consumer) { } protected void saveEntityWithRetry(PlayerData entity) { + if (!SkinOverlay.getInstance().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } int maxRetries = 3; int retryCount = 0; boolean success = false; 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 index abfec928..d8478047 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java @@ -86,6 +86,7 @@ public String getSimpleName() { 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); } @@ -107,6 +108,9 @@ protected void batchSave() { } protected void saveEntityWithRetry(Skin entity) { + if (!SkinOverlay.getInstance().isProxy() && OptionsUtil.PROXY.getBooleanValue()) { + return; + } int maxRetries = 3; int retryCount = 0; boolean success = false; From d77f772a828c7d02c88bc1a54102fb37660e6c4c Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 8 Jul 2025 18:50:38 +0300 Subject: [PATCH 13/79] feat: conditionally initialize message handling based on proxy setting Wrap PluginMessage and Redis initialization inside PROXY config check. Only initialize message handling when proxy mode is enabled. Prevents unnecessary setup in non-proxy environments. --- .../skinoverlay/SkinOverlayBukkit.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index cf6e411f..21da64de 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -123,25 +123,27 @@ public void onLoad() { } this.skinOverlay.onLoad(); - if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { - new PluginMessageListenerImpl((uuid, skin) -> { - SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); - if (player != null) { - skinOverlay.getSkinApplier().setSkin(player, skin); - } - }); - } else { - this.skinOverlay.setMessageManager(new RedisManager( - OptionsUtil.REDIS_HOST.getStringValue(), - OptionsUtil.REDIS_PORT.getIntValue(), - OptionsUtil.REDIS_PASSWORD.getStringValue() - )); - skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> { - SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); - if (player != null) { - skinOverlay.getSkinApplier().setSkin(player, skin); - } - }); + if (OptionsUtil.PROXY.getBooleanValue()) { + if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { + new PluginMessageListenerImpl((uuid, skin) -> { + SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); + if (player != null) { + skinOverlay.getSkinApplier().setSkin(player, skin); + } + }); + } else { + this.skinOverlay.setMessageManager(new RedisManager( + OptionsUtil.REDIS_HOST.getStringValue(), + OptionsUtil.REDIS_PORT.getIntValue(), + OptionsUtil.REDIS_PASSWORD.getStringValue() + )); + skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> { + SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); + if (player != null) { + skinOverlay.getSkinApplier().setSkin(player, skin); + } + }); + } } } From 9deaaf1c25dfa24f9d47d8bf07004fd32dd25736 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 14 Jul 2025 09:00:40 +0300 Subject: [PATCH 14/79] feat(messaging): recode messaging system with encryption and channels - Add MessageManagerNoop implementation for disabled messaging - Introduce AES encryption/decryption utilities in Utils class - Create MessageData record for structured message handling - Refactor RedisManager to handle multiple channels and callbacks - Rewrite VelocityPluginMessageManager with channel separation - Implement new PluginMessageListenerImpl with encryption support - Update Bukkit/Velocity main classes to use new message system - Add player join publishing/subscribing to messaging system - Introduce channel constants and message encryption handling - Refactor MessageManager into abstract class with utilities --- .../skinoverlay/SkinOverlayBukkit.java | 43 +++---- .../bukkit/PluginMessageListenerImpl.java | 86 ++++++++++--- .../skinoverlay/message/MessageData.java | 10 ++ .../skinoverlay/message/MessageManager.java | 120 +++++++++++++++++- .../message/MessageManagerNoop.java | 35 +++++ .../skinoverlay/message/RedisManager.java | 78 +++++++++++- .../message/SkinPropertyHandler.java | 9 -- .../skinoverlay/utilities/Utils.java | 79 ++++++++++++ .../skinoverlay/SkinOverlayVelocity.java | 50 ++++++-- .../message/VelocityPluginMessageManager.java | 101 +++++++++++++-- 10 files changed, 526 insertions(+), 85 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 21da64de..c0f7838e 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -6,6 +6,7 @@ import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; import com.georgev22.skinoverlay.listeners.bukkit.PlayerListeners; import com.georgev22.skinoverlay.listeners.bukkit.PluginMessageListenerImpl; +import com.georgev22.skinoverlay.message.MessageManagerNoop; import com.georgev22.skinoverlay.message.RedisManager; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.providers.*; @@ -122,29 +123,6 @@ public void onLoad() { ); } this.skinOverlay.onLoad(); - - if (OptionsUtil.PROXY.getBooleanValue()) { - if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { - new PluginMessageListenerImpl((uuid, skin) -> { - SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); - if (player != null) { - skinOverlay.getSkinApplier().setSkin(player, skin); - } - }); - } else { - this.skinOverlay.setMessageManager(new RedisManager( - OptionsUtil.REDIS_HOST.getStringValue(), - OptionsUtil.REDIS_PORT.getIntValue(), - OptionsUtil.REDIS_PASSWORD.getStringValue() - )); - skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> { - SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); - if (player != null) { - skinOverlay.getSkinApplier().setSkin(player, skin); - } - }); - } - } } @Override @@ -156,6 +134,25 @@ public void onEnable() { new PlayerListeners() ); + 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()); + } + skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> { + SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); + if (player != null) { + skinOverlay.getSkinApplier().setSkin(player, skin); + } + }); + } // call onEnable skinOverlay.onEnable(); } 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 index 02922433..12dfc125 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java @@ -1,44 +1,94 @@ package com.georgev22.skinoverlay.listeners.bukkit; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.message.SkinPropertyHandler; +import com.georgev22.skinoverlay.message.MessageData; +import com.georgev22.skinoverlay.message.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.nio.charset.StandardCharsets; +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 implements PluginMessageListener { - private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - private final SkinPropertyHandler handler; +public class PluginMessageListenerImpl extends MessageManager implements PluginMessageListener { + private BiConsumer handler = (uuid, skin) -> { + }; - public PluginMessageListenerImpl(SkinPropertyHandler handler) { - this.handler = handler; - Bukkit.getMessenger().registerIncomingPluginChannel(skinOverlay.getPlugin(), "skinoverlay:skinupdate", this); + 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("skinoverlay:skinupdate")) return; + if (!channel.equals(CHANNEL_TO_BACKEND)) return; - String data = new String(message, StandardCharsets.UTF_8); - String[] parts = data.split("\\|", 2); - if (parts.length != 2) { - skinOverlay.getLogger().severe("Invalid skin property message: " + data); + @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[0]); - String base64Skin = parts[1]; + UUID uuid = UUID.fromString(parts.get(0)); + String base64Skin = parts.get(1); Skin skin = Skin.fromBase64(base64Skin); - Bukkit.getScheduler().runTask(skinOverlay.getPlugin(), () -> handler.handle(uuid, skin)); + this.mainPlugin.getScheduler().runTask(mainPlugin.getPlugin(), () -> handler.accept(uuid, skin)); } catch (Exception e) { - skinOverlay.getLogger().log(java.util.logging.Level.SEVERE, "Error handling skin property message: " + data, 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/common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java new file mode 100644 index 00000000..ad38f689 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java @@ -0,0 +1,10 @@ +package com.georgev22.skinoverlay.message; + +/** + * 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/message/MessageManager.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java index a94897ab..6bfeeccc 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java @@ -1,15 +1,127 @@ package com.georgev22.skinoverlay.message; +import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import org.jetbrains.annotations.NotNull; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.*; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Objects; import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; -public interface MessageManager { +import static com.georgev22.skinoverlay.utilities.Utils.decrypt; +import static com.georgev22.skinoverlay.utilities.Utils.encrypt; - void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin); +public abstract class MessageManager { - void subscribeSkinProperty(@NotNull SkinPropertyHandler handler); + protected final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + protected static final String CHANNEL_TO_BACKEND = "skinoverlay:tobackend"; + protected static final String CHANNEL_FROM_BACKEND = "skinoverlay:frombackend"; + + public abstract void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin); + + public abstract void subscribeSkinProperty(@NotNull BiConsumer handler); + + public abstract void publishPlayerJoin(@NotNull UUID playerUUID); + + public abstract void subscribePlayerJoin(Consumer handler); + + public abstract void close(); + + /** + * 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 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 = encrypt(data, OptionsUtil.SECRET.getStringValue()); + dataOut.writeUTF(Objects.requireNonNull(encryptedData)); + } + dataOut.flush(); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | + InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException("Failed to write data output", e); + } + return byteOut; + } + + /** + * 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(); + } + + /** + * Reads a byte array into its sub-channel and decrypted data entries. + * + * @param byteArray the byte array to read + * @return a MessageData object containing the sub-channel and decrypted data entries + * @throws IOException if an I/O error occurs + */ + public @NotNull MessageData readByteArray(byte @NotNull [] byteArray) throws IOException { + ByteArrayInputStream byteIn = new ByteArrayInputStream(byteArray); + DataInputStream dataIn = new DataInputStream(byteIn); + String subChannel = dataIn.readUTF(); + + java.util.List dataList = new java.util.ArrayList<>(); + while (dataIn.available() > 0) { + String encryptedData = dataIn.readUTF(); + try { + String decryptedData = decrypt(encryptedData, OptionsUtil.SECRET.getStringValue()); + dataList.add(decryptedData); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | + InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException("Failed to decrypt data", e); + } + } + + return new MessageData(subChannel, dataList.toArray(new String[0])); + } + + /** + * Reads a DataInputStream into its sub-channel and decrypted data entries. + * + * @param dataIn the DataInputStream to read + * @return a MessageData object containing the sub-channel and decrypted data entries + * @throws IOException if an I/O error occurs + */ + public @NotNull MessageData readDataInput(@NotNull DataInputStream dataIn) throws IOException { + String subChannel = dataIn.readUTF(); + + java.util.List dataList = new java.util.ArrayList<>(); + while (dataIn.available() > 0) { + String encryptedData = dataIn.readUTF(); + try { + String decryptedData = decrypt(encryptedData, OptionsUtil.SECRET.getStringValue()); + dataList.add(decryptedData); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | + InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException("Failed to decrypt data", e); + } + } + + return new MessageData(subChannel, dataList.toArray(new String[0])); + } - void close(); } diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java new file mode 100644 index 00000000..d5a7d10b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java @@ -0,0 +1,35 @@ +package com.georgev22.skinoverlay.message; + +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/message/RedisManager.java b/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java index b0180eaf..44d28868 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java @@ -7,17 +7,26 @@ 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 implements MessageManager { +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; @@ -31,11 +40,16 @@ public RedisManager(String host, int port, String password) { @Override public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { String message = playerUUID + "|" + skin.toBase64(); - jedis.publish("skinoverlay:skinupdate", message); + jedis.publish(CHANNEL_TO_BACKEND, message); } @Override - public void subscribeSkinProperty(@NotNull SkinPropertyHandler handler) { + 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)) { @@ -56,14 +70,17 @@ public void onMessage(String ch, String message) { String base64Skin = parts[1]; Skin skin = Skin.fromBase64(base64Skin); - skinOverlay.getScheduler().runTask(skinOverlay.getPlugin(), () -> handler.handle(uuid, skin)); + 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, "skinoverlay:skinupdate"); + subJedis.subscribe(jedisPubSub, CHANNEL_TO_BACKEND); } catch (Exception e) { skinOverlay.getLogger().log(Level.SEVERE, "Redis subscribe connection lost, retrying in 5 seconds...", e); try { @@ -75,18 +92,67 @@ public void onMessage(String ch, String message) { } } } - }, "RedisSubscriberThread"); + }, "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/message/SkinPropertyHandler.java b/common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java deleted file mode 100644 index 010012d0..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/message/SkinPropertyHandler.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.georgev22.skinoverlay.message; - -import com.georgev22.skinoverlay.storage.data.Skin; - -import java.util.UUID; - -public interface SkinPropertyHandler { - void handle(UUID uuid, Skin skin); -} \ 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 index b5fd7d86..b010fb61 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java @@ -11,6 +11,9 @@ 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; @@ -24,8 +27,12 @@ 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; @@ -1159,6 +1166,78 @@ public static boolean hasClass(String className) { } } + /** + * Decrypts a Base64-encoded string that was encrypted using AES encryption with a salt prepended. + * + * @param encryptedText the Base64-encoded string to decrypt (including the prepended 16-byte salt) + * @param secret the secret password used for key derivation + * @return the decrypted plaintext string + * @throws NoSuchAlgorithmException if the PBKDF2WithHmacSHA256 algorithm is not available + * @throws InvalidKeySpecException if the key specification is invalid + * @throws NoSuchPaddingException if the padding mechanism is not available + * @throws InvalidKeyException if the key is invalid + * @throws IllegalBlockSizeException if the encrypted data length is incorrect + * @throws BadPaddingException if the padding of the data is incorrect + */ + @Contract("_, _ -> new") + public static @NotNull String decrypt(String encryptedText, @NotNull String secret) throws + NoSuchAlgorithmException, + InvalidKeySpecException, + NoSuchPaddingException, + InvalidKeyException, + IllegalBlockSizeException, + BadPaddingException { + byte[] salt = Arrays.copyOfRange(Base64.getDecoder().decode(encryptedText), 0, 16); + KeySpec spec = new PBEKeySpec(secret.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); + } + + + /** + * Encrypts a string using AES encryption with a randomly generated salt and returns the result as a Base64-encoded string. + * The resulting string includes the salt prepended to the encrypted data. + * + * @param plaintext the plaintext string to encrypt + * @param secret the secret password used for key derivation + * @return the Base64-encoded string containing the salt and encrypted data + * @throws NoSuchAlgorithmException if the PBKDF2WithHmacSHA256 algorithm is not available + * @throws InvalidKeySpecException if the key specification is invalid + * @throws NoSuchPaddingException if the padding mechanism is not available + * @throws InvalidKeyException if the key is invalid + * @throws IllegalBlockSizeException if the data size is invalid + * @throws BadPaddingException if the data is improperly padded + */ + public static @NotNull String encrypt(@NotNull String plaintext, @NotNull String secret) throws + NoSuchAlgorithmException, + InvalidKeySpecException, + NoSuchPaddingException, + InvalidKeyException, + IllegalBlockSizeException, + BadPaddingException { + + byte[] salt = new byte[16]; + SecureRandom random = new SecureRandom(); + random.nextBytes(salt); + KeySpec spec = new PBEKeySpec(secret.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); + } + public static final class Assertions { /** diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index ae02b994..02ae1612 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -4,11 +4,15 @@ import com.georgev22.skinoverlay.command.VelocityCommandManager; import com.georgev22.skinoverlay.hooks.SkinHookNoop; import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; +import com.georgev22.skinoverlay.message.MessageManagerNoop; import com.georgev22.skinoverlay.message.RedisManager; import com.georgev22.skinoverlay.message.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.config.OptionsUtil; import com.google.inject.Inject; import com.velocitypowered.api.event.Subscribe; @@ -18,12 +22,15 @@ import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; +import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import java.io.File; import java.nio.file.Path; import java.util.logging.Logger; +import static com.georgev22.skinoverlay.message.VelocityPluginMessageManager.inChannelIdentifier; + @Plugin( id = BuildParameters.PLUGIN_ID, name = BuildParameters.PLUGIN_NAME, @@ -57,16 +64,8 @@ public SkinOverlayVelocity(ProxyServer server, Logger logger, @DataDirectory @No this.skinOverlay.setScheduler(new VelocityMinecraftScheduler<>(server)); this.skinOverlay.setCommandManager(new VelocityCommandManager(server)); this.skinOverlay.onLoad(); - - if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { - this.skinOverlay.setMessageManager(new VelocityPluginMessageManager(server)); - } else { - this.skinOverlay.setMessageManager(new RedisManager( - OptionsUtil.REDIS_HOST.getStringValue(), - OptionsUtil.REDIS_PORT.getIntValue(), - OptionsUtil.REDIS_PASSWORD.getStringValue() - )); - } + if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) + this.server.getChannelRegistrar().register(inChannelIdentifier); } @Subscribe @@ -81,7 +80,36 @@ public void onProxyInitialization(ProxyInitializeEvent event) { 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.server.sendMessage(Component.text("[SkinOverlay] 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()); + } + this.skinOverlay.getMessageManager().subscribePlayerJoin(uuid -> { + this.server.sendMessage(Component.text("[SkinOverlay] Player " + uuid + " has joined the 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.skinOverlay.onEnable(); } diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java b/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java index a80fb4d0..3927b64b 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java @@ -1,48 +1,121 @@ package com.georgev22.skinoverlay.message; import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteStreams; +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 net.kyori.adventure.text.Component; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import java.nio.charset.StandardCharsets; 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 implements MessageManager { +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 final MinecraftChannelIdentifier channelIdentifier; + private Consumer playerJoinHandler = uuid -> { + }; public VelocityPluginMessageManager(@NotNull ProxyServer proxyServer) { this.proxyServer = proxyServer; - this.channelIdentifier = MinecraftChannelIdentifier.from("skinoverlay:skinupdate"); - proxyServer.getChannelRegistrar().register(channelIdentifier); } @Override public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { String message = playerUUID + "|" + skin.toBase64(); - byte[] data = message.getBytes(StandardCharsets.UTF_8); + byte[] data = toByteArray("skinupdate", message); - Optional playerOptional = proxyServer.getAllPlayers().stream().findAny(); + 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(); - player.sendPluginMessage(channelIdentifier, data); + 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 { - proxyServer.getConsoleCommandSource().sendMessage( - Component.text("[SkinOverlay] No players online to send plugin message.") - ); + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.mainPlugin.getLogger().info("Player " + playerUUID + " is not online."); + } } } @Override - public void subscribeSkinProperty(@NotNull SkinPropertyHandler handler) { + 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(channelIdentifier); + 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; + } + + //event.setResult(PluginMessageEvent.ForwardResult.handled()); + + + if (!(event.getSource() instanceof ServerConnection serverConnection)) { + return; + } + Player player = serverConnection.getPlayer(); + + ByteArrayDataInput in = ByteStreams.newDataInput(event.getData()); + String channel = in.readUTF(); + if (channel.isEmpty()) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing channel from plugin message"); + return; + } + if (!channel.equalsIgnoreCase("playerjoin")) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing channel from plugin message"); + return; + } + String message = in.readUTF(); + UUID uuid; + try { + uuid = UUID.fromString(message); + } catch (Exception e) { + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing UUID from plugin message: " + message, e); + return; + } + playerJoinHandler.accept(uuid); } } From 1723de0e92f63d367fdf066a2ee4b9c6852941d8 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 14 Jul 2025 09:05:09 +0300 Subject: [PATCH 15/79] refactor(common): Improve generics handling in getScheduler Update getScheduler method to use specific type parameters instead of wildcards for better type safety. Added unchecked cast with suppression comment where necessary. Also adjusted Javadoc alignment in setPlugin method for consistency. - Changed getScheduler return type to parameterized version - Added type parameters to method signature - Fixed Javadoc indentation in setPlugin --- .../main/java/com/georgev22/skinoverlay/SkinOverlay.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index 0d6c34e1..f6bf90c6 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -233,7 +233,7 @@ public T getPlugin() { * Sets the plugin instance. * * @param plugin the plugin instance - * @param plugin type + * @param plugin type */ public void setPlugin(T plugin) { this.plugin = plugin; @@ -244,8 +244,9 @@ public void setPlugin(T plugin) { * * @return the MinecraftScheduler */ - public MinecraftScheduler getScheduler() { - return scheduler; + public MinecraftScheduler getScheduler() { + //noinspection unchecked + return (MinecraftScheduler) scheduler; } /** From f1ffaf741acbd3301264c71d92969e09b0a9e831 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 14 Jul 2025 09:07:14 +0300 Subject: [PATCH 16/79] refactor(listener): simplify player join and leave listeners - Streamlined player join logic by combining conditionals and using orElseGet for skin creation - Replaced nested optionals with concise functional approach in player leave handler - Improved variable naming for better readability - Centralized entity manager retrieval and reduced code duplication - Optimized skin handling with early returns and null checks --- .../listeners/PlayerListeners.java | 59 +++++++------------ 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 23d5f91f..292673b0 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -11,7 +11,6 @@ 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; @@ -25,68 +24,54 @@ public class PlayerListeners { public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { SPlayer player = event.getPlayer(); - Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getManager(PlayerData.class); - if (optionalPlayerDataEntityManager.isEmpty()) { - return; - } - Optional> optionalSkinEntityManager = EntityManagerRegistry.getManager(Skin.class); - if (optionalSkinEntityManager.isEmpty()) { + Optional> playerDataManagerOpt = EntityManagerRegistry.getManager(PlayerData.class); + Optional> skinManagerOpt = EntityManagerRegistry.getManager(Skin.class); + + if (playerDataManagerOpt.isEmpty() || skinManagerOpt.isEmpty()) { return; } - EntityManager playerDataEntityManager = optionalPlayerDataEntityManager.get(); + EntityManager playerDataManager = playerDataManagerOpt.get(); + EntityManager skinManager = skinManagerOpt.get(); - EntityManager skinEntityManager = optionalSkinEntityManager.get(); + PlayerData playerData = playerDataManager.findById(player.getUniqueId()).orElse(null); - Optional optionalPlayerData = playerDataEntityManager.findById(player.getUniqueId()); - if (optionalPlayerData.isEmpty()) { - optionalPlayerData = playerDataEntityManager.create(player.getUniqueId().toString(), ignore -> { - }); - if (optionalPlayerData.isEmpty()) { - return; - } + 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) { - this.mainPlugin.getLogger().log(Level.SEVERE, "Error loading Skin:", e); + mainPlugin.getLogger().log(Level.SEVERE, "Error loading Skin:", e); return; } } - UUID uuid = Utils.generateUUID("default" + player.getUniqueId().toString()); - Skin defaultSkin = optionalSkinEntityManager.get().findById(uuid).orElse(new Skin(uuid)); + + UUID uuid = Utils.generateUUID("default" + player.getUniqueId()); + Skin defaultSkin = skinManager.findById(uuid).orElseGet(() -> new Skin(uuid)); defaultSkin.setProperty(property); - PlayerData playerData = optionalPlayerData.get(); playerData.setDefaultSkin(defaultSkin); + if (playerData.getCurrentSkin() != null && !playerData.getCurrentSkin().equals(defaultSkin)) { - this.mainPlugin.getSkinApplier().setSkin(player, playerData.getCurrentSkin()); + mainPlugin.getSkinApplier().setSkin(player, playerData.getCurrentSkin()); } - skinEntityManager.save(defaultSkin); - playerDataEntityManager.save(playerData); + skinManager.save(defaultSkin); + playerDataManager.save(playerData); } public void onPlayerLeave(@NotNull SPlayerLeaveEvent event) { SPlayer player = event.getPlayer(); - 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(); - if (!this.mainPlugin.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { - return; - } - playerDataEntityManager.save(playerData); + EntityManagerRegistry.getManager(PlayerData.class) + .ifPresent(entityManager -> entityManager.findById(player.getUniqueId()) + .ifPresent(entityManager::save)); } } From 07ae9a599559e6eb13b506cd1230a9a4f92527c3 Mon Sep 17 00:00:00 2001 From: "George V." Date: Mon, 14 Jul 2025 09:09:26 +0300 Subject: [PATCH 17/79] feat(player): delay publishPlayerJoin when PROXY enabled and not proxy Add delayed task for publishing player join event in non-proxy environments when PROXY option is enabled. This ensures the proxy receives the plugin message by delaying 20 ticks. Fixes unreliable message delivery when PROXY=true on non-proxy instances. --- .../skinoverlay/listeners/PlayerListeners.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 292673b0..8e6e5fb0 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -11,6 +11,7 @@ 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; @@ -25,6 +26,19 @@ public class PlayerListeners { 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); From 0c901ef9066b34dce5615c3d0bfb8503408380d9 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 15 Jul 2025 00:47:02 +0300 Subject: [PATCH 18/79] feat: recode encryption and message handling Introduce new `EncryptUtils` class for secure AES-GCM encryption with PBKDF2 key derivation and GZIP compression. Remove outdated ECB-mode encryption methods from `Utils`. Refactor message handling: - Replace `ByteStreams` with custom encryption-aware serialization - Implement `MessageEncryptionException` for error handling - Update `MessageManager` to use new encryption utilities - Modify Velocity message parsing to match new encrypted format - Ensure all plugin messages are encrypted and compressed - Improve error logging and data validation --- .../MessageEncryptionException.java | 10 ++ .../skinoverlay/message/MessageManager.java | 135 +++++++++------ .../skinoverlay/utilities/EncryptUtils.java | 156 ++++++++++++++++++ .../skinoverlay/utilities/Utils.java | 72 -------- .../message/VelocityPluginMessageManager.java | 30 ++-- 5 files changed, 265 insertions(+), 138 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/exceptions/MessageEncryptionException.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/EncryptUtils.java 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/message/MessageManager.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java index 6bfeeccc..5100cb88 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java @@ -1,47 +1,91 @@ package com.georgev22.skinoverlay.message; 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 javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.io.*; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; +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; -import static com.georgev22.skinoverlay.utilities.Utils.decrypt; -import static com.georgev22.skinoverlay.utilities.Utils.encrypt; - +/** + * 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 ByteArrayDataOutput object with the given sub-channel and data. + * 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 the sub-channel to write to the output. - * @param dataArray the data to write to the output. - * @return a new ByteArrayDataOutput object. + * @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) { @@ -50,78 +94,67 @@ public ByteArrayOutputStream byteArrayDataOutput(@NotNull String subChannel, Str try { dataOut.writeUTF(subChannel); for (String data : dataArray) { - String encryptedData = encrypt(data, OptionsUtil.SECRET.getStringValue()); + String encryptedData = EncryptUtils.encrypt(data, OptionsUtil.SECRET.getStringValue()); dataOut.writeUTF(Objects.requireNonNull(encryptedData)); } dataOut.flush(); - } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | - InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException("Failed to write data output", e); + } 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 data in the given channel and data array to a byte array. + * Converts the given channel and data entries to a byte array suitable for plugin message sending. * - * @param channel the channel to use. - * @param dataArray the data to convert. - * @return a byte array representation of the data. + * @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 a byte array into its sub-channel and decrypted data entries. + * Reads and parses a plugin message byte array into its sub-channel and decrypted data entries. * - * @param byteArray the byte array to read - * @return a MessageData object containing the sub-channel and decrypted data entries - * @throws IOException if an I/O error occurs + * @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 { - ByteArrayInputStream byteIn = new ByteArrayInputStream(byteArray); - DataInputStream dataIn = new DataInputStream(byteIn); - String subChannel = dataIn.readUTF(); - - java.util.List dataList = new java.util.ArrayList<>(); - while (dataIn.available() > 0) { - String encryptedData = dataIn.readUTF(); - try { - String decryptedData = decrypt(encryptedData, OptionsUtil.SECRET.getStringValue()); - dataList.add(decryptedData); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | - InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException("Failed to decrypt data", e); - } + try (ByteArrayInputStream byteIn = new ByteArrayInputStream(byteArray); + DataInputStream dataIn = new DataInputStream(byteIn)) { + return readDataInput(dataIn); } - - return new MessageData(subChannel, dataList.toArray(new String[0])); } /** - * Reads a DataInputStream into its sub-channel and decrypted data entries. + * Reads and parses a {@link DataInputStream} into its sub-channel and decrypted data entries. * - * @param dataIn the DataInputStream to read - * @return a MessageData object containing the sub-channel and decrypted data entries - * @throws IOException if an I/O error occurs + * @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<>(); - java.util.List dataList = new java.util.ArrayList<>(); while (dataIn.available() > 0) { String encryptedData = dataIn.readUTF(); try { - String decryptedData = decrypt(encryptedData, OptionsUtil.SECRET.getStringValue()); + String decryptedData = EncryptUtils.decrypt(encryptedData, OptionsUtil.SECRET.getStringValue()); dataList.add(decryptedData); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | - InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException("Failed to decrypt data", e); + } 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/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/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java index b010fb61..62a19be4 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java @@ -1166,78 +1166,6 @@ public static boolean hasClass(String className) { } } - /** - * Decrypts a Base64-encoded string that was encrypted using AES encryption with a salt prepended. - * - * @param encryptedText the Base64-encoded string to decrypt (including the prepended 16-byte salt) - * @param secret the secret password used for key derivation - * @return the decrypted plaintext string - * @throws NoSuchAlgorithmException if the PBKDF2WithHmacSHA256 algorithm is not available - * @throws InvalidKeySpecException if the key specification is invalid - * @throws NoSuchPaddingException if the padding mechanism is not available - * @throws InvalidKeyException if the key is invalid - * @throws IllegalBlockSizeException if the encrypted data length is incorrect - * @throws BadPaddingException if the padding of the data is incorrect - */ - @Contract("_, _ -> new") - public static @NotNull String decrypt(String encryptedText, @NotNull String secret) throws - NoSuchAlgorithmException, - InvalidKeySpecException, - NoSuchPaddingException, - InvalidKeyException, - IllegalBlockSizeException, - BadPaddingException { - byte[] salt = Arrays.copyOfRange(Base64.getDecoder().decode(encryptedText), 0, 16); - KeySpec spec = new PBEKeySpec(secret.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); - } - - - /** - * Encrypts a string using AES encryption with a randomly generated salt and returns the result as a Base64-encoded string. - * The resulting string includes the salt prepended to the encrypted data. - * - * @param plaintext the plaintext string to encrypt - * @param secret the secret password used for key derivation - * @return the Base64-encoded string containing the salt and encrypted data - * @throws NoSuchAlgorithmException if the PBKDF2WithHmacSHA256 algorithm is not available - * @throws InvalidKeySpecException if the key specification is invalid - * @throws NoSuchPaddingException if the padding mechanism is not available - * @throws InvalidKeyException if the key is invalid - * @throws IllegalBlockSizeException if the data size is invalid - * @throws BadPaddingException if the data is improperly padded - */ - public static @NotNull String encrypt(@NotNull String plaintext, @NotNull String secret) throws - NoSuchAlgorithmException, - InvalidKeySpecException, - NoSuchPaddingException, - InvalidKeyException, - IllegalBlockSizeException, - BadPaddingException { - - byte[] salt = new byte[16]; - SecureRandom random = new SecureRandom(); - random.nextBytes(salt); - KeySpec spec = new PBEKeySpec(secret.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); - } - public static final class Assertions { /** diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java b/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java index 3927b64b..f3fb6656 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java @@ -2,8 +2,6 @@ import com.georgev22.skinoverlay.storage.data.Skin; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteStreams; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.proxy.Player; @@ -13,6 +11,8 @@ 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; @@ -34,8 +34,7 @@ public VelocityPluginMessageManager(@NotNull ProxyServer proxyServer) { @Override public void publishSkinProperty(@NotNull UUID playerUUID, @NotNull Skin skin) { - String message = playerUUID + "|" + skin.toBase64(); - byte[] data = toByteArray("skinupdate", message); + 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); @@ -90,30 +89,31 @@ public void onPluginMessageFromPlayer(PluginMessageEvent event) { return; } - //event.setResult(PluginMessageEvent.ForwardResult.handled()); - - if (!(event.getSource() instanceof ServerConnection serverConnection)) { return; } - Player player = serverConnection.getPlayer(); - ByteArrayDataInput in = ByteStreams.newDataInput(event.getData()); - String channel = in.readUTF(); - if (channel.isEmpty()) { + 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 (!channel.equalsIgnoreCase("playerjoin")) { + if (!data.subChannel().equalsIgnoreCase("playerjoin")) { this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing channel from plugin message"); return; } - String message = in.readUTF(); + String stringUUID = data.dataEntries()[0]; UUID uuid; try { - uuid = UUID.fromString(message); + uuid = UUID.fromString(stringUUID); } catch (Exception e) { - this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing UUID from plugin message: " + message, e); + this.mainPlugin.getLogger().log(Level.SEVERE, "Error parsing UUID from plugin message: " + Arrays.toString(data.dataEntries()), e); return; } playerJoinHandler.accept(uuid); From 59bb275c0544b774f3bc309b15e2a0feea5595d7 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 15 Jul 2025 08:36:21 +0300 Subject: [PATCH 19/79] chore: update build process and CI configuration Simplify Gradle build commands by replacing per-version reobfJar tasks with single reobfJar task. Update jar output paths to include platform specific directories and filenames. Update GitHub Actions workflow: - Upgrade to JDK 21 and Node.js 20 - Use latest GitHub Action versions (v4) - Simplify build command to single task - Add Maven credential environment variables - Fix workflow trigger spacing Improve update-versions.sh script: - Add shebang and usage instructions - Require version argument parameter - Use more robust version replacement pattern --- .github/workflows/gradle.yml | 39 ++++++++++++++++++++---------------- README.md | 6 +++--- update-versions.sh | 13 +++++++++++- 3 files changed, 37 insertions(+), 21 deletions(-) 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/README.md b/README.md index ef93f48f..bcd4a5c3 100644 --- a/README.md +++ b/README.md @@ -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/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." From ce32b3477f7f69405a163485e5d11f0d1d76fd35 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 15 Jul 2025 06:05:47 +0000 Subject: [PATCH 20/79] chore(release): 8.0.0-beta.1 [skip ci] # [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 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ README.md | 4 ++-- gradle.properties | 4 ++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cdd5d7e..a2d2b867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +# [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 bcd4a5c3..8a7e0c07 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.1 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:7.1.0" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.1" } ``` diff --git a/gradle.properties b/gradle.properties index 3fa5e851..67fc41a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 1.0.0 -description = SkinOverlay \ No newline at end of file +version = 8.0.0-beta.1 +description = SkinOverlay From 82b112180486cca39592993afde599d7852c9af0 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 16 Jul 2025 06:06:44 +0300 Subject: [PATCH 21/79] feat(logging): add LoggerWrapper for unified logging integration Introduce LoggerWrapper to bridge java.util.Logger with Log4j/SLF4J. Modify SkinOverlayVelocity to use wrapper for consistent logging. Add log4j-api dependency in common module and update versions catalog. --- common/build.gradle.kts | 3 + .../skinoverlay/utilities/LoggerWrapper.java | 187 ++++++++++++++++++ gradle/libs.versions.toml | 2 + .../skinoverlay/SkinOverlayVelocity.java | 9 +- 4 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/LoggerWrapper.java diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 62286b3d..8e9e1d85 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -23,6 +23,9 @@ dependencies { 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") 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3542c082..730056e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ skinsrestorer = "15.7.3" yamlconfiguration = "3.0.2" jedis = "6.0.0" jetbrains-annotations = "26.0.2" +log4j-api = "3.0.0-beta2" [plugins] gradleup-shadow = { id = "com.gradleup.shadow", version.ref = "gradleup-shadow" } @@ -49,3 +50,4 @@ skinsrestorer-api = { module = "net.skinsrestorer:skinsrestorer-api", version.re 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" } \ 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 02ae1612..9551fe46 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -13,6 +13,7 @@ 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.google.inject.Inject; import com.velocitypowered.api.event.Subscribe; @@ -24,10 +25,10 @@ import com.velocitypowered.api.proxy.ProxyServer; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; import java.io.File; import java.nio.file.Path; -import java.util.logging.Logger; import static com.georgev22.skinoverlay.message.VelocityPluginMessageManager.inChannelIdentifier; @@ -49,14 +50,14 @@ public class SkinOverlayVelocity { @Inject public SkinOverlayVelocity(ProxyServer server, Logger logger, @DataDirectory @NotNull Path dataDirectory) { this.server = server; - this.skinOverlay.setLogger(logger); + this.skinOverlay.setLogger(new LoggerWrapper(logger)); this.skinOverlay.setProxy(true); File dataDirectoryFile = dataDirectory.toFile(); if (!dataDirectoryFile.exists()) { if (dataDirectoryFile.mkdirs()) { - logger.info("Folder " + dataDirectory + " has been created!"); + this.skinOverlay.getLogger().info("Folder " + dataDirectory + " has been created!"); } else { - logger.warning("Failed to create folder " + dataDirectory); + this.skinOverlay.getLogger().warning("Failed to create folder " + dataDirectory); } } this.skinOverlay.setDataFolder(dataDirectoryFile); From a626d793c6eb0161c0f2b8547486260d1f97b8a0 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 16 Jul 2025 06:09:14 +0300 Subject: [PATCH 22/79] feat(velocity): add player event listeners and debug logging - Added new PlayerListeners class for Velocity to handle player join/quit - Registered PlayerListeners to fire SPlayerJoin/SPlayerLeave events - Condition player join log messages on debug option to reduce noise --- .../skinoverlay/SkinOverlayVelocity.java | 10 ++++-- .../listeners/velocity/PlayerListeners.java | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index 9551fe46..c43c612a 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -4,6 +4,7 @@ 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.message.MessageManagerNoop; import com.georgev22.skinoverlay.message.RedisManager; import com.georgev22.skinoverlay.message.VelocityPluginMessageManager; @@ -23,7 +24,6 @@ import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; -import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -85,7 +85,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { VelocityPluginMessageManager velocityPluginMessageManager = new VelocityPluginMessageManager(server); this.skinOverlay.setMessageManager(velocityPluginMessageManager); this.server.getEventManager().register(this, velocityPluginMessageManager); - this.server.sendMessage(Component.text("[SkinOverlay] Plugin message connection type: " + OptionsUtil.CONNECTION_TYPE.getStringValue())); + 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(), @@ -96,7 +96,9 @@ public void onProxyInitialization(ProxyInitializeEvent event) { this.skinOverlay.setMessageManager(new MessageManagerNoop()); } this.skinOverlay.getMessageManager().subscribePlayerJoin(uuid -> { - this.server.sendMessage(Component.text("[SkinOverlay] Player " + uuid + " has joined the server.")); + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.skinOverlay.getLogger().info("Player " + uuid + " has joined the server."); + } this.skinOverlay.getScheduler().runAsyncTask(this.skinOverlay.getPlugin(), () -> { EntityManagerRegistry.getManager(PlayerData.class) .flatMap(entityManager -> entityManager.findById(uuid)) @@ -111,6 +113,8 @@ public void onProxyInitialization(ProxyInitializeEvent event) { }); }); }); + + this.server.getEventManager().register(this, new PlayerListeners()); this.skinOverlay.onEnable(); } 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 new file mode 100644 index 00000000..b78a38c2 --- /dev/null +++ b/velocity/src/main/java/com/georgev22/skinoverlay/listeners/velocity/PlayerListeners.java @@ -0,0 +1,31 @@ +package com.georgev22.skinoverlay.listeners.velocity; + +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.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.LoginEvent; + +public class PlayerListeners { + + private final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + + @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 onPlayerQuit(DisconnectEvent event) { + SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(event.getPlayer()); + if (player != null) { + mainPlugin.getEventBus().post(new SPlayerLeaveEvent(player)); + } + } + +} From 712e248e58516e99c449e2d18af8e4cca938c543 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 16 Jul 2025 06:18:44 +0300 Subject: [PATCH 23/79] build(velocity): add log4j-api to compileOnly dependencies --- velocity/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 7fd7c81d..97dfe8b8 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { } implementation(libs.bstats.velocity) compileOnly(libs.adventure.platform.api) + compileOnly(libs.log4j.api) } configurations.configureEach { From 4ae9cd62e22b184bf9c63f222ef2637b783f58eb Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 16 Jul 2025 06:20:44 +0300 Subject: [PATCH 24/79] fix(provider): handle default skin not found case Added fallback logic when default skin is missing. Now attempts to fetch and save the skin if it's the default one. Also fixed Javadoc formatting for parameter alignment in multiple methods. - Implemented new else-if branch for 'default' skin handling - Added skin creation and save flow when default skin missing - Corrected Javadoc indentation for parameter/exception alignment - Ensured consistent documentation formatting throughout file --- .../skinoverlay/providers/SkinProvider.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java index f44a5f9f..90a2bf8c 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -73,6 +73,22 @@ public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer 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 { + 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()); @@ -208,10 +224,10 @@ public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer /** * 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 + * @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. + * @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); @@ -220,10 +236,10 @@ public byte[] getProfileBytes(@NotNull final SPlayer player, @Nullable SProperty /** * Retrieves Bedrock {@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 + * @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. + * @throws IOException When an I/O exception to some sort has occurred. */ public byte[] getBedrockProfileBytes(@NotNull final SPlayer player, final SProperty property) throws IOException { return property != null ? @@ -234,10 +250,10 @@ public byte[] getBedrockProfileBytes(@NotNull final SPlayer player, final SPrope /** * Retrieves Java {@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 + * @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. + * @throws IOException When an I/O exception to some sort has occurred. */ public byte[] getJavaProfileBytes(@NotNull final SPlayer player, @Nullable SProperty property) throws IOException { return property != null ? @@ -398,7 +414,7 @@ public boolean isUsernamePremium(String username) { * * @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. + * @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) { @@ -477,8 +493,8 @@ public JsonObject createJsonForBedrock(@NotNull final SPlayer player) throws IOE /** * 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 + * @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) { From 386992a75ee6c6df7d2ebf7ec4d152073d878b02 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 16 Jul 2025 14:54:27 +0000 Subject: [PATCH 25/79] chore(release): 8.0.0-beta.2 [skip ci] # [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)) --- CHANGELOG.md | 13 +++++++++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d2b867..6f017ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [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) diff --git a/README.md b/README.md index 8a7e0c07..7769de7d 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 - 8.0.0-beta.1 + 8.0.0-beta.2 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.1" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.2" } ``` diff --git a/gradle.properties b/gradle.properties index 67fc41a4..47ed972d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.1 +version = 8.0.0-beta.2 description = SkinOverlay From 6942d021e1607a23bcb6c9e2a662a5fbf8cf5da0 Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 28 Nov 2025 13:38:13 +0200 Subject: [PATCH 26/79] feat: extend Minecraft 1.21 support range for Bukkit Add V1_21_R6 version to support Minecraft 1.21 subversions 9-10. Update V1_21_R5 range to cover subversions 6-8 instead of just 6. --- .../georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java index dad19c0c..b333f932 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -684,7 +684,8 @@ public enum MinecraftVersion { 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)), + V1_21_R5(new SubVersionRange("1.21", 6, 8)), + V1_21_R6(new SubVersionRange("1.21", 9, 10)), UNKNOWN(new SubVersionRange("UNKNOWN", 0, 0)), ; From c065c1f0e8a243788e6f6f970dcafcfed1c0de6b Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 28 Nov 2025 13:38:26 +0200 Subject: [PATCH 27/79] chore: update .gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b63da455..d4c6a6aa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,7 @@ build/ !**/src/test/**/build/ ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea/* *.iws *.iml *.ipr From 85f5fce25f26b8b9e94a43a07ec5b29ebaf700bf Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 28 Nov 2025 15:21:08 +0200 Subject: [PATCH 28/79] feat: add support for Minecraft 1.21.10-R0.1-SNAPSHOT - Add new module for Minecraft version 1.21.R6 - Implement SkinApplier and GameProfileProvider for 1.21.R6 - Add Reflect library dependency for reflection utilities - Update Bukkit build configuration to include new version module - Update common module dependencies with Reflect library --- bukkit/build.gradle.kts | 1 + .../skinoverlay/SkinOverlayBukkit.java | 4 + bukkit/versions/mc1_21_R6/build.gradle.kts | 20 ++++ .../appliers/SkinApplier_1_21_R6.java | 97 +++++++++++++++++++ .../GameProfileProvider_1_21_R6.java | 57 +++++++++++ common/build.gradle.kts | 1 + gradle/libs.versions.toml | 4 +- settings.gradle.kts | 1 + 8 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 bukkit/versions/mc1_21_R6/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R6.java create mode 100644 bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index fb47a4a0..79355ceb 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { 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 { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index c0f7838e..4385f230 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -99,6 +99,10 @@ public void onLoad() { 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.setEnabled(false); throw new RuntimeException("SkinOverlay does not support " + getCurrentVersion()); 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/common/build.gradle.kts b/common/build.gradle.kts index 8e9e1d85..99194097 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(libs.jedis) implementation(libs.jetbrains.annotations) + implementation(libs.reflect) } configurations.configureEach { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 730056e0..67ad8c67 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ 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" } @@ -50,4 +51,5 @@ skinsrestorer-api = { module = "net.skinsrestorer:skinsrestorer-api", version.re 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" } \ No newline at end of file +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/settings.gradle.kts b/settings.gradle.kts index 382464bd..a8c93935 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,4 +27,5 @@ 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 From 1dae36eb1861b590be88349904e0cbe29b079924 Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 28 Nov 2025 15:25:48 +0200 Subject: [PATCH 29/79] feat: add no-op game profile provider and handle unsupported versions Add GameProfileProviderNoop implementation for environments where game profile operations are not supported. Update Bukkit version handling to use no-op implementations instead of disabling the plugin when encountering unsupported versions. This improves compatibility and user experience by allowing the plugin to run with limited functionality rather than crashing. --- .../skinoverlay/SkinOverlayBukkit.java | 5 ++-- .../providers/GameProfileProviderNoop.java | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProviderNoop.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 4385f230..cc655479 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -104,8 +104,9 @@ public void onLoad() { this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R6()); } default -> { - this.setEnabled(false); - throw new RuntimeException("SkinOverlay does not support " + getCurrentVersion()); + this.skinOverlay.setSkinApplier(new NoopSkinApplier()); + this.skinOverlay.setGameProfileProvider(new GameProfileProviderNoop()); + this.getLogger().info("SkinOverlay does not support " + Bukkit.getBukkitVersion()); } } } 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) { + + } +} From b073d48ebd5f2ff70103d8f6b4b0e1f629cb5c6f Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 28 Nov 2025 15:51:47 +0200 Subject: [PATCH 30/79] feat: add SkinsRestorer integration and refactor skin applier - Implement GameProfileProvider_SkinsRestorer for SkinsRestorer support - Remove empty applySkin method from NoopSkinApplier - Set GameProfileProvider and SkinApplier when SkinsRestorer is enabled - Improve skin property handling and error logging --- .../skinoverlay/SkinOverlayBukkit.java | 2 + .../skinoverlay/appliers/NoopSkinApplier.java | 4 -- .../GameProfileProvider_SkinsRestorer.java | 54 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_SkinsRestorer.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index cc655479..18c09484 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -113,6 +113,8 @@ public void onLoad() { if (Bukkit.getPluginManager().isPluginEnabled("SkinsRestorer")) { this.skinOverlay.setSkinHook(new SkinsRestorerHook()); + this.skinOverlay.setGameProfileProvider(new GameProfileProvider_SkinsRestorer()); + this.skinOverlay.setSkinApplier(new NoopSkinApplier()); } else { this.skinOverlay.setSkinHook(new SkinHookNoop()); } diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java index 03ab51aa..0501492f 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java @@ -6,10 +6,6 @@ import java.util.concurrent.CompletableFuture; public class NoopSkinApplier extends SkinApplier { - @Override - protected void applySkin(@NotNull SPlayer player) { - - } @Override protected @NotNull CompletableFuture sendPackets(@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); + } +} From 4f43e10b996fd58ba5a2ef08969475b075af719c Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 05:44:49 +0200 Subject: [PATCH 31/79] refactor: rename message package to messaging Rename the 'message' package to 'messaging' across the codebase and update all import statements accordingly to maintain consistency. --- .../java/com/georgev22/skinoverlay/SkinOverlayBukkit.java | 4 ++-- .../listeners/bukkit/PluginMessageListenerImpl.java | 4 ++-- .../main/java/com/georgev22/skinoverlay/SkinOverlay.java | 2 +- .../skinoverlay/{message => messaging}/MessageData.java | 2 +- .../{message => messaging}/MessageManager.java | 2 +- .../{message => messaging}/MessageManagerNoop.java | 2 +- .../skinoverlay/{message => messaging}/RedisManager.java | 2 +- .../com/georgev22/skinoverlay/SkinOverlayVelocity.java | 8 ++++---- .../VelocityPluginMessageManager.java | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) rename common/src/main/java/com/georgev22/skinoverlay/{message => messaging}/MessageData.java (84%) rename common/src/main/java/com/georgev22/skinoverlay/{message => messaging}/MessageManager.java (99%) rename common/src/main/java/com/georgev22/skinoverlay/{message => messaging}/MessageManagerNoop.java (93%) rename common/src/main/java/com/georgev22/skinoverlay/{message => messaging}/RedisManager.java (99%) rename velocity/src/main/java/com/georgev22/skinoverlay/{message => messaging}/VelocityPluginMessageManager.java (99%) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 18c09484..5667b3d3 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -6,8 +6,8 @@ import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; import com.georgev22.skinoverlay.listeners.bukkit.PlayerListeners; import com.georgev22.skinoverlay.listeners.bukkit.PluginMessageListenerImpl; -import com.georgev22.skinoverlay.message.MessageManagerNoop; -import com.georgev22.skinoverlay.message.RedisManager; +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; 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 index 12dfc125..6cce722e 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/listeners/bukkit/PluginMessageListenerImpl.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.listeners.bukkit; -import com.georgev22.skinoverlay.message.MessageData; -import com.georgev22.skinoverlay.message.MessageManager; +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; diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index f6bf90c6..5ba5341d 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -10,7 +10,7 @@ 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.MessageManager; +import com.georgev22.skinoverlay.messaging.MessageManager; import com.georgev22.skinoverlay.providers.GameProfileProvider; import com.georgev22.skinoverlay.providers.PlayerProvider; import com.georgev22.skinoverlay.providers.SkinProvider; diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageData.java similarity index 84% rename from common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java rename to common/src/main/java/com/georgev22/skinoverlay/messaging/MessageData.java index ad38f689..c878d93d 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageData.java +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageData.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.message; +package com.georgev22.skinoverlay.messaging; /** * Represents parsed message data with sub-channel and data entries. diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManager.java similarity index 99% rename from common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java rename to common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManager.java index 5100cb88..08164f11 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManager.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.message; +package com.georgev22.skinoverlay.messaging; import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.exceptions.MessageEncryptionException; diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManagerNoop.java similarity index 93% rename from common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java rename to common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManagerNoop.java index d5a7d10b..33d7d92c 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageManagerNoop.java +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/MessageManagerNoop.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.message; +package com.georgev22.skinoverlay.messaging; import com.georgev22.skinoverlay.storage.data.Skin; import org.jetbrains.annotations.NotNull; diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java b/common/src/main/java/com/georgev22/skinoverlay/messaging/RedisManager.java similarity index 99% rename from common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java rename to common/src/main/java/com/georgev22/skinoverlay/messaging/RedisManager.java index 44d28868..df134d29 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/RedisManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/messaging/RedisManager.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.message; +package com.georgev22.skinoverlay.messaging; import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.storage.data.Skin; diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index c43c612a..b39d29b1 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -5,9 +5,9 @@ import com.georgev22.skinoverlay.hooks.SkinHookNoop; import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; import com.georgev22.skinoverlay.listeners.velocity.PlayerListeners; -import com.georgev22.skinoverlay.message.MessageManagerNoop; -import com.georgev22.skinoverlay.message.RedisManager; -import com.georgev22.skinoverlay.message.VelocityPluginMessageManager; +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; @@ -30,7 +30,7 @@ import java.io.File; import java.nio.file.Path; -import static com.georgev22.skinoverlay.message.VelocityPluginMessageManager.inChannelIdentifier; +import static com.georgev22.skinoverlay.messaging.VelocityPluginMessageManager.inChannelIdentifier; @Plugin( id = BuildParameters.PLUGIN_ID, diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java b/velocity/src/main/java/com/georgev22/skinoverlay/messaging/VelocityPluginMessageManager.java similarity index 99% rename from velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java rename to velocity/src/main/java/com/georgev22/skinoverlay/messaging/VelocityPluginMessageManager.java index f3fb6656..1b7cc208 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/message/VelocityPluginMessageManager.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/messaging/VelocityPluginMessageManager.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.message; +package com.georgev22.skinoverlay.messaging; import com.georgev22.skinoverlay.storage.data.Skin; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; From f7768af1de07742f9f330c6b8b2fc462c8aaf25d Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 16:49:06 +0200 Subject: [PATCH 32/79] feat(core)!: introduce new command architecture and remove legacy message utilities BREAKING CHANGE: - 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. --- .../command/BukkitCommandIssuer.java | 2 +- .../command/BukkitCommandManager.java | 27 +- .../command/BukkitPluginCommandWrapper.java | 3 +- .../georgev22/skinoverlay/SkinOverlay.java | 18 +- .../skinoverlay/command/BaseCommand.java | 384 ++++++++++++++---- .../skinoverlay/command/CommandContext.java | 17 +- .../skinoverlay/command/CommandIssuer.java | 60 ++- .../skinoverlay/command/CommandManager.java | 78 ++-- .../skinoverlay/command/CompletionEngine.java | 185 ++++++++- .../skinoverlay/command/ExecutionType.java | 16 +- .../skinoverlay/command/MethodCommand.java | 164 ++++++++ .../command/annotation/Argument.java | 67 ++- .../command/annotation/CommandAlias.java | 26 +- .../command/annotation/CommandCompletion.java | 39 ++ .../command/annotation/CommandTarget.java | 23 ++ .../command/annotation/Default.java | 43 ++ .../command/annotation/Description.java | 29 +- .../command/annotation/Execution.java | 27 +- .../command/annotation/Permission.java | 32 +- .../command/annotation/PlayerOnly.java | 11 - .../command/annotation/Subcommand.java | 35 +- .../skinoverlay/command/annotation/Usage.java | 32 +- .../commands/SkinOverlayBaseCommand.java | 20 - .../command/commands/SkinOverlayMain.java | 8 +- .../command/commands/sub/ClearSubCommand.java | 32 +- .../command/commands/sub/InfoSubCommand.java | 5 +- .../commands/sub/ReloadSubCommand.java | 22 +- .../command/commands/sub/WearSubCommand.java | 48 +-- .../commands/sub/WearUrlSubCommand.java | 65 ++- .../command/processors/PostProcessor.java | 14 + .../command/processors/PreProcessor.java | 14 + .../command/resolvers/ArgumentResolver.java | 13 + .../PipeSeparatedArgumentResolver.java | 23 +- .../command/resolvers/PlayersResolver.java | 10 +- .../command/resolvers/RangeResolver.java | 100 ++++- .../MessageBuilder.java | 79 +++- .../skinoverlay/message/MessageEntry.java | 219 ++++++++++ .../{utilities => message}/MessageParser.java | 2 +- .../skinoverlay/message/MessagesRegistry.java | 82 ++++ .../message/messages/CommandMessages.java | 67 +++ .../message/messages/CoreMessages.java | 51 +++ .../registry/AbstractRegistry.java | 69 ++++ .../registry/CommandTargetRegistry.java | 86 ++++ .../skinoverlay/registry/Registry.java | 84 ++++ .../skinoverlay/utilities/Copyable.java | 64 +++ .../skinoverlay/utilities/CustomData.java | 113 +++++- .../skinoverlay/utilities/DeepCloner.java | 278 +++++++++++++ .../skinoverlay/utilities/config/CFG.java | 13 +- .../utilities/config/MessagesUtil.java | 218 ---------- .../command/VelocityCommandIssuer.java | 2 +- .../command/VelocityPluginCommandWrapper.java | 4 +- 51 files changed, 2577 insertions(+), 546 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/processors/PostProcessor.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/command/processors/PreProcessor.java rename common/src/main/java/com/georgev22/skinoverlay/{utilities => message}/MessageBuilder.java (81%) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java rename common/src/main/java/com/georgev22/skinoverlay/{utilities => message}/MessageParser.java (98%) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/MessagesRegistry.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/messages/CommandMessages.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/messages/CoreMessages.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/Copyable.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java index b2f59c45..53b25c20 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java @@ -57,7 +57,7 @@ public UUID getUniqueId() { if (isPlayer()) { return ((Player) this.sender).getUniqueId(); } - return Utils.generateUUID("VoidChestConsole"); + return Utils.generateUUID("SkinOverlayConsole"); } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java index 0b37c40e..5e5874f9 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; +import java.util.logging.Level; public class BukkitCommandManager extends CommandManager { @@ -16,19 +17,23 @@ public BukkitCommandManager(SkinOverlayBukkit plugin) { } @Override - protected void registerCommand0(@NotNull BaseCommand command) throws ReflectiveOperationException { - CommandMap commandMap; + protected void registerCommand0(@NotNull BaseCommand command) { try { - commandMap = plugin.getServer().getCommandMap(); - } catch (Exception e) { - Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); - commandMapField.setAccessible(true); - commandMap = (CommandMap) commandMapField.get(Bukkit.getServer()); - } + 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); + 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 index 5a2112ac..aa6ac44b 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitPluginCommandWrapper.java @@ -22,7 +22,8 @@ public BukkitPluginCommandWrapper(@NotNull BaseCommand command, String alias) { @Override public boolean execute(@NotNull CommandSender sender, @NotNull String label, String @NotNull [] args) { CommandIssuer commandIssuer = new BukkitCommandIssuer(sender); - baseCommand.execute(commandIssuer, args); + CommandContext context = new CommandContext(); + baseCommand.execute(commandIssuer, args, context); return true; } diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index 5ba5341d..bbdd3d43 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -2,14 +2,18 @@ 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.command.resolvers.PlayersResolver; 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; @@ -23,11 +27,8 @@ 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.Locale; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; 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.skin.Part; import com.georgev22.skinoverlay.utilities.skin.SkinParts; @@ -99,11 +100,13 @@ public void onLoad() { this.skinFileCache.cache(); this.skinProvider = new SkinProvider(); try { - MessagesUtil.repairPaths(Locale.fromString(OptionsUtil.LOCALE.getStringValue())); + 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); @@ -118,9 +121,8 @@ public void onLoad() { * Enables the plugin components. Should be called during plugin enable phase. */ public void onEnable() { - this.commandManager.addResolver("overlays", (commandIssuer, args) -> + CompletionEngine.registerResolver("@overlays", (commandIssuer, args) -> skinFileCache.getSkinConfigurationFiles().keySet()); - this.commandManager.addResolver("players", new PlayersResolver()); this.commandManager.registerCommand(new SkinOverlayMain()); getLogger().info("Commands registered successfully!"); diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java index 6d8e6931..fe92de7d 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java @@ -2,143 +2,369 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.annotation.*; -import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +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.function.BiConsumer; -import java.util.function.Consumer; +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; - private String[] subCommandAliases; - private String description; - private String usage; - private final boolean playerOnly; - private String permission; + 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 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(); - if (clazz.isAnnotationPresent(CommandAlias.class)) { - aliases = clazz.getAnnotation(CommandAlias.class).value(); - } - if (clazz.isAnnotationPresent(Subcommand.class)) { - subCommandAliases = clazz.getAnnotation(Subcommand.class).value(); + + 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); + } } - if (clazz.isAnnotationPresent(Description.class)) { - description = clazz.getAnnotation(Description.class).value(); + } + + /** + * 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; } - if (clazz.isAnnotationPresent(Usage.class)) { - usage = clazz.getAnnotation(Usage.class).value(); + + // 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; } - playerOnly = clazz.isAnnotationPresent(PlayerOnly.class); - if (clazz.isAnnotationPresent(Permission.class)) { - permission = clazz.getAnnotation(Permission.class).value(); + + 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(); } - if (clazz.isAnnotationPresent(Execution.class)) { - executionType = clazz.getAnnotation(Execution.class).value(); + } + + /** + * 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; } - public void execute(CommandIssuer commandIssuer, String @NotNull [] args) { - SkinOverlay.getInstance().getCommandManager().getGlobalPreprocessors().forEach(pre -> pre.accept(commandIssuer)); - preprocessors.forEach(pre -> pre.accept(commandIssuer)); + /** + * 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)); + } - if (args.length > 0) { - BaseCommand subcommand = subcommands.get(args[0].toLowerCase()); - if (subcommand != null) { - subcommand.execute(commandIssuer, Arrays.copyOfRange(args, 1, args.length)); - return; + /** + * 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 (playerOnly && !commandIssuer.isPlayer()) { - MessagesUtil.ONLY_PLAYER_COMMAND.msg(commandIssuer); - return; + 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); } - if (permission != null && !permission.isEmpty() && !commandIssuer.hasPermission(permission)) { - MessagesUtil.NO_PERMISSION.msg(commandIssuer); - return; + 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); } - CommandContext context = new CommandContext(); - if (executionType == ExecutionType.ASYNC) { - SkinOverlay.getInstance().getScheduler().runAsyncTask( - SkinOverlay.getInstance().getPlugin(), - () -> { - handle(commandIssuer, args, context); - SkinOverlay.getInstance().getCommandManager().getGlobalPostprocessors().forEach(post -> post.accept(commandIssuer, context)); - postprocessors.forEach(post -> post.accept(commandIssuer, context)); - } - ); - } else { - handle(commandIssuer, args, context); - SkinOverlay.getInstance().getCommandManager().getGlobalPostprocessors().forEach(post -> post.accept(commandIssuer, context)); - postprocessors.forEach(post -> post.accept(commandIssuer, context)); + 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); } - public Collection tabComplete(CommandIssuer commandIssuer, String @NotNull [] args) { - if (args.length > 1) { - BaseCommand subcommand = subcommands.get(args[0].toLowerCase()); - if (subcommand != null) { - if (commandIssuer.hasPermission(subcommand.getPermission())) - return subcommand.tabComplete(commandIssuer, Arrays.copyOfRange(args, 1, args.length)); - else - return Collections.emptyList(); + /** + * 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 CompletionEngine.resolveCompletions(getClass(), commandIssuer, args); + return available; } - public void addSubcommand(@NotNull BaseCommand subcommand) { - for (String alias : subcommand.getAliases(true)) { - subcommands.put(alias.toLowerCase(), subcommand); + /** + * Adds a nested BaseCommand as a subcommand. + * + * @param command the subcommand to add + */ + public void addSubcommand(@NotNull BaseCommand command) { + if (command.subCommandAliases.length == 0 && !(command instanceof MethodCommand)) { + throw new IllegalArgumentException("Subcommand must define at least one alias"); + } + for (String alias : command.subCommandAliases) { + subcommands.put(alias.toLowerCase(), command); + } + if (command instanceof MethodCommand && ((MethodCommand) command).isDefault()) { + command.subcommands.put("__default__", command); } } - public void addPreprocessor(Consumer preprocessor) { - if (preprocessors.contains(preprocessor)) return; - preprocessors.add(preprocessor); + /** + * Adds a preprocessor for this command. + * + * @param preprocessor the preprocessor to add + */ + public void addPreprocessor(PreProcessor preprocessor) { + if (!preprocessors.contains(preprocessor)) preprocessors.add(preprocessor); } - public void addPostprocessor(BiConsumer postprocessor) { - if (postprocessors.contains(postprocessor)) return; - postprocessors.add(postprocessor); + /** + * Adds a postprocessor for this command. + * + * @param postprocessor the postprocessor to add + */ + public void addPostprocessor(PostProcessor postprocessor) { + if (!postprocessors.contains(postprocessor)) postprocessors.add(postprocessor); } - public String[] getAliases(boolean isSubcommand) { - if (isSubcommand) { - return subCommandAliases; - } - return aliases; + /** + * 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; } - public boolean isPlayerOnly() { - return playerOnly; + /** + * 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; } - protected abstract void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context); } \ 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 index 151aae59..da9ec2e9 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java @@ -2,10 +2,25 @@ 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; } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java index 346d3f35..0a0114ef 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java @@ -5,34 +5,92 @@ 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 index fbf02fea..e432ea0f 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java @@ -3,24 +3,35 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.annotation.CommandAlias; import com.georgev22.skinoverlay.command.annotation.Subcommand; -import com.georgev22.skinoverlay.command.resolvers.ArgumentResolver; +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.HashMap; import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; 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 { - protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - private final List> globalPreprocessors = new ArrayList<>(); - private final List> globalPostprocessors = new ArrayList<>(); - private final Map resolvers = new HashMap<>(); + 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); @@ -35,35 +46,54 @@ public void registerCommand(@NotNull BaseCommand command) { if (innerClass.isAnnotationPresent(CommandAlias.class)) { registerCommand0(subcommand); } + + registerCommand(subcommand); } } catch (Exception e) { - this.skinOverlay.getLogger().log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e); + plugin.getLogger().log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e); } } - protected abstract void registerCommand0(@NotNull BaseCommand command) throws ReflectiveOperationException; + /** + * Registers a single command. + * + * @param command the command to register + */ + protected abstract void registerCommand0(@NotNull BaseCommand command); - public void addGlobalPreprocessor(Consumer preprocessor) { + /** + * Adds a global preprocessor that runs before any command execution. + * + * @param preprocessor the preprocessor to add + */ + public void addGlobalPreprocessor(PreProcessor preprocessor) { globalPreprocessors.add(preprocessor); } - public void addGlobalPostprocessor(BiConsumer postprocessor) { + /** + * Adds a global postprocessor that runs after any command execution. + * + * @param postprocessor the postprocessor to add + */ + public void addGlobalPostprocessor(PostProcessor postprocessor) { globalPostprocessors.add(postprocessor); } - public List> getGlobalPostprocessors() { + /** + * Returns a list of all global postprocessors. + * + * @return the global postprocessors + */ + public List getGlobalPostprocessors() { return globalPostprocessors; } - public List> getGlobalPreprocessors() { + /** + * Returns a list of all global preprocessors. + * + * @return the global preprocessors + */ + public List getGlobalPreprocessors() { return globalPreprocessors; } - - public ArgumentResolver getResolver(@NotNull String key) { - return resolvers.get(key.toLowerCase()); - } - - public void addResolver(@NotNull String key, ArgumentResolver resolver) { - resolvers.put(key.toLowerCase(), resolver); - } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java index f1ca18a5..99f99852 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java @@ -1,15 +1,63 @@ package com.georgev22.skinoverlay.command; -import com.georgev22.skinoverlay.SkinOverlay; +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.util.ArrayList; -import java.util.Collection; +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 { - public static Collection resolveCompletions(Class clazz, CommandIssuer commandIssuer, String[] args) { + /** + * 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<>(); } @@ -21,17 +69,132 @@ public static Collection resolveCompletions(Class clazz, CommandIssue return new ArrayList<>(); } - String segment = args.length == 0 ? parts[0] : parts[args.length - 1]; + 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 resolverKey = option.substring(1); - String[] resolverParts = resolverKey.split(":", 2); + String[] resolverParts = option.split(":", 2); String resolverName = resolverParts[0]; String[] resolverArgs = resolverParts.length > 1 ? new String[]{resolverParts[1]} : new String[0]; - ArgumentResolver resolver = SkinOverlay.getInstance().getCommandManager().getResolver(resolverName); + ArgumentResolver resolver = resolve(resolverName); if (resolver != null) { completions.addAll(resolver.resolve(commandIssuer, resolverArgs)); } @@ -40,6 +203,10 @@ public static Collection resolveCompletions(Class clazz, CommandIssue } } - return completions; + 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 index 7183a8be..0ce6b160 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java @@ -1,6 +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 -} \ No newline at end of file +} 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 index ae7fb2d3..448c55a4 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java @@ -1,14 +1,67 @@ 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; +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) -@Target(ElementType.TYPE) +@Documented public @interface Argument { - String name(); - String description() default ""; + /** + * 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 index 61ca7059..961abaec 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java @@ -5,8 +5,32 @@ 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(); -} \ No newline at end of file +} 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 index 30aa4d7c..13c5addc 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java @@ -5,8 +5,47 @@ 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 index 20a54ed7..e571695d 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java @@ -5,8 +5,31 @@ 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) +@Target({ElementType.TYPE, ElementType.METHOD}) public @interface Description { - String value() default ""; -} \ No newline at end of file + + /** + * 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 index 6ff53da3..17a03aa8 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java @@ -7,8 +7,33 @@ 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; -} \ No newline at end of file +} 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 index 09a25338..2e6e71c7 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java @@ -5,8 +5,34 @@ 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) +@Target({ElementType.TYPE, ElementType.METHOD}) public @interface Permission { - String value() default ""; -} \ No newline at end of file + + /** + * 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/PlayerOnly.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java deleted file mode 100644 index a0acf79c..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/PlayerOnly.java +++ /dev/null @@ -1,11 +0,0 @@ -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; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface PlayerOnly { -} \ No newline at end of file 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 index aa4227d7..335bf345 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java @@ -5,8 +5,39 @@ 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) +@Target({ElementType.TYPE, ElementType.METHOD}) public @interface Subcommand { + + /** + * One or more aliases for the subcommand. + * + * @return an array of subcommand names + */ String[] value(); -} \ No newline at end of file +} 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 index 0d20c6d2..b40639b5 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java @@ -5,8 +5,34 @@ 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) +@Target({ElementType.TYPE, ElementType.METHOD}) public @interface Usage { - String value() default ""; -} \ No newline at end of file + + /** + * 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 index 3236e1fc..ff3c310b 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java @@ -2,10 +2,6 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.BaseCommand; -import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.player.SPlayer; -import com.georgev22.skinoverlay.utilities.config.MessagesUtil; import org.jetbrains.annotations.NotNull; import java.util.Optional; @@ -22,20 +18,4 @@ public void addSubcommand(@NotNull BaseCommand subcommand) { mainPlugin.getLogger().log(Level.SEVERE, "Failed to register subcommand " + subcommand.getClass().getName(), e); } } - - protected @NotNull Optional getPlayerObject(@NotNull CommandIssuer issuer, String name) { - Optional target; - if (issuer.hasPermission("skinoverlay.wear.overlay.others")) { - target = mainPlugin.getPlayerProvider().isOnline(name) - ? Optional.ofNullable(mainPlugin.getPlayerProvider().getSPlayer(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; - } } 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 index 1ff73dd5..c52dd612 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java @@ -7,7 +7,7 @@ import com.georgev22.skinoverlay.command.commands.sub.ReloadSubCommand; import com.georgev22.skinoverlay.command.commands.sub.WearSubCommand; import com.georgev22.skinoverlay.command.commands.sub.WearUrlSubCommand; -import com.georgev22.skinoverlay.utilities.config.MessagesUtil; +import com.georgev22.skinoverlay.message.messages.CommandMessages; import org.jetbrains.annotations.NotNull; @CommandAlias({"skinoverlay", "soverlay", "skino"}) @@ -24,8 +24,8 @@ public SkinOverlayMain() { this.addSubcommand(new ReloadSubCommand()); } - @Override - protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { - MessagesUtil.HELP_FORMAT.msg(commandIssuer); + @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 index 8ade45bb..0cefed3c 100644 --- 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 @@ -3,15 +3,12 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.command.annotation.CommandCompletion; -import com.georgev22.skinoverlay.command.annotation.Description; -import com.georgev22.skinoverlay.command.annotation.Permission; -import com.georgev22.skinoverlay.command.annotation.Subcommand; +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.config.MessagesUtil; import com.georgev22.skinoverlay.utilities.skin.SkinParts; import org.jetbrains.annotations.NotNull; @@ -23,12 +20,12 @@ @Permission("skinoverlay.wear.clear") public class ClearSubCommand extends SkinOverlayBaseCommand { - @Override - protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { - if (args.length == 0) { + @Default + protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "target", completion = "@players", optional = true) SPlayer target) { + if (target == null) { if (!commandIssuer.isPlayer()) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(commandIssuer, - new HashObjectMap().append("%command%", "clear "), true); + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, + new HashObjectMap().append("%command%", "clear ").append("%arg%", "target"), true); return; } SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); @@ -45,7 +42,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] a } Skin skin = optionalSkin.get(); mainPlugin.getSkinApplier().setSkin(player, skin); - MessagesUtil.RESET.msg( + CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, new HashObjectMap().append("%player%", player.getName()), true @@ -53,24 +50,23 @@ protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] a }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); } else { - Optional optionalPlayerObject = getPlayerObject(commandIssuer, args[0]); - if (optionalPlayerObject.isEmpty()) { - MessagesUtil.OFFLINE_PLAYER.msg(commandIssuer, new HashObjectMap().append("%player%", args[0]), true); + 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( - optionalPlayerObject.get(), + target, skinParts) .thenAcceptAsync(skinOptional -> { if (skinOptional.isEmpty()) { return; } Skin skin = skinOptional.get(); - mainPlugin.getSkinApplier().setSkin(optionalPlayerObject.get(), skin); - MessagesUtil.RESET.msg( + mainPlugin.getSkinApplier().setSkin(target, skin); + CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, - new HashObjectMap().append("%player%", optionalPlayerObject.get().getName()), + new HashObjectMap().append("%player%", target.getName()), true ); 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 index ece5be44..f3e57f03 100644 --- 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 @@ -3,6 +3,7 @@ import com.georgev22.skinoverlay.BuildParameters; import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; +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; @@ -12,8 +13,8 @@ @Permission("skinoverlay.info") public class InfoSubCommand extends SkinOverlayBaseCommand { - @Override - protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + @Default + protected void handle(@NotNull CommandIssuer commandIssuer) { String pluginName = BuildParameters.PLUGIN_NAME; String pluginVersion = BuildParameters.VERSION; String pluginAuthor = BuildParameters.AUTHOR; 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 index 9fd97ee6..a50986ec 100644 --- 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 @@ -1,14 +1,14 @@ package com.georgev22.skinoverlay.command.commands.sub; -import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; +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.utilities.Locale; -import com.georgev22.skinoverlay.utilities.MessageBuilder; -import com.georgev22.skinoverlay.utilities.config.MessagesUtil; -import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +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; @@ -16,16 +16,18 @@ @Subcommand("reload") @Permission("skinoverlay.reload") public class ReloadSubCommand extends SkinOverlayBaseCommand { - @Override - protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { + @Default + protected void handle(@NotNull CommandIssuer commandIssuer) { //TODO RELOAD SKIN HANDLER mainPlugin.getFileManager().getConfig().reloadFile(); try { - MessagesUtil.repairPaths(Locale.fromString(OptionsUtil.LOCALE.getStringValue())); - MessagesUtil.getMessagesCFG().reloadFile(); + MessagesRegistry.registerAll(new MessageEntry[][]{ + CommandMessages.values(), + CoreMessages.values(), + }); } catch (Exception e) { mainPlugin.getLogger().log(Level.SEVERE, "Error loading the language file: ", e); } - commandIssuer.sendMessage(MessageBuilder.builder().appendMiniMessage("&a&l(!)&a Plugin reloaded!").build()); + 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 index 9a1d963a..d973bd0b 100644 --- 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 @@ -1,25 +1,20 @@ package com.georgev22.skinoverlay.command.commands.sub; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.command.annotation.CommandCompletion; -import com.georgev22.skinoverlay.command.annotation.Description; -import com.georgev22.skinoverlay.command.annotation.Permission; -import com.georgev22.skinoverlay.command.annotation.Subcommand; +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.config.MessagesUtil; 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.Optional; import java.util.logging.Level; @Subcommand({"wear", "overlay"}) @@ -28,33 +23,29 @@ @CommandCompletion("@overlays @players") public class WearSubCommand extends SkinOverlayBaseCommand { - @Override - protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { - if (args.length < 1) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(commandIssuer, new HashObjectMap() - .append("%command%", "wear "), true); + @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; } - var overlay = args[0]; - SPlayer target; - - if (args.length > 1) { - Optional optionalSPlayer = getPlayerObject(commandIssuer, args[1]); - if (optionalSPlayer.isEmpty()) return; - target = optionalSPlayer.get(); - } else if (!(commandIssuer.isPlayer())) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg(commandIssuer, - new HashObjectMap().append("%command%", "wear skin "), true); - return; - } else { + 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()) { - MessagesUtil.OVERLAY_NOT_FOUND.msg(commandIssuer, new HashObjectMap().append("%overlay%", overlay), true); + CommandMessages.COMMAND_OVERLAY_NOT_FOUND.msg(commandIssuer, new HashObjectMap().append("%overlay%", overlay), true); return; } skinParts = new SkinParts(new SerializableBufferedImage(ImageIO.read(overlayFile)), overlay); @@ -62,6 +53,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] a mainPlugin.getLogger().log(Level.SEVERE, "Error while trying to load the skin: ", e); return; } + SPlayer finalTarget = target; mainPlugin.getSkinProvider() .retrieveOrGenerateSkin( target, @@ -73,11 +65,11 @@ protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] a } Skin skin = optionalSkin.get(); mainPlugin.getSkinApplier() - .setSkin(target, skin); - MessagesUtil.DONE.msg( + .setSkin(finalTarget, skin); + CommandMessages.COMMAND_OVERLAY_DONE.msg( commandIssuer, new HashObjectMap() - .append("%player%", target.getName()) + .append("%player%", finalTarget.getName()) .append("%url%", skin.skinURL()) .append("%name%", skin.getSkinParts().getSkinName()) .append("%skinParts%", skin.getSkinParts().toString()), 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 index 8b0ddd3b..950e6b7a 100644 --- 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 @@ -3,16 +3,13 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.command.annotation.CommandCompletion; -import com.georgev22.skinoverlay.command.annotation.Description; -import com.georgev22.skinoverlay.command.annotation.Permission; -import com.georgev22.skinoverlay.command.annotation.Subcommand; +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.config.MessagesUtil; import com.georgev22.skinoverlay.utilities.skin.SkinParts; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -28,54 +25,37 @@ @Subcommand("url") @Permission("skinoverlay.wear.url") @Description("Wear an overlay on a player's skin") -@CommandCompletion(" false|true|@players false|true false|true false|true false|true false|true false|true @players") +@CommandCompletion(" @players") public class WearUrlSubCommand extends SkinOverlayBaseCommand { - @Override - protected void handle(@NotNull CommandIssuer commandIssuer, String @NotNull [] args, @NotNull CommandContext context) { - if (args.length < 1) { - sendInsufficientArguments(commandIssuer); - return; - } + @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(args[0]); - Optional targetPlayer = resolveTargetPlayer(commandIssuer, args); - if (targetPlayer.isEmpty()) { - return; - } + URL url = new URL(urlStr); byte[] imageBytes = downloadImageBytes(url); if (imageBytes == null) { - MessagesUtil.INVALID_URL.msg(commandIssuer, new HashObjectMap().append("%url%", url.toString()), true); + 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, targetPlayer.get(), skinParts); + applySkinToPlayer(commandIssuer, target, skinParts); } catch (Exception e) { mainPlugin.getLogger().log(Level.SEVERE, "Error executing WearUrlSubCommand:", e); - MessagesUtil.ERROR.msg(commandIssuer, new HashObjectMap().append("%error%", e.getMessage()), true); - } - } - - private void sendInsufficientArguments(CommandIssuer issuer) { - MessagesUtil.INSUFFICIENT_ARGUMENTS.msg( - issuer, - new HashObjectMap().append("%command%", "url "), - true - ); - } - - private Optional resolveTargetPlayer(CommandIssuer issuer, String @NotNull [] args) { - if (args.length > 1) { - return getPlayerObject(issuer, args[1]); - } else if (issuer.isPlayer()) { - return Optional.of(mainPlugin.getPlayerProvider().getSPlayer(issuer.getUniqueId())); - } else { - sendInsufficientArguments(issuer); - return Optional.empty(); + CommandMessages.COMMAND_ERROR.msg(commandIssuer, new HashObjectMap().append("%error%", e.getMessage()), true); } } @@ -101,18 +81,19 @@ private Optional resolveTargetPlayer(CommandIssuer issuer, String @NotN private void applySkinToPlayer(CommandIssuer issuer, SPlayer player, SkinParts skinParts) { mainPlugin.getSkinProvider().retrieveOrGenerateSkin(player, skinParts).thenAcceptAsync(optionalSkin -> { if (optionalSkin.isEmpty()) { - MessagesUtil.ERROR.msg(issuer, new HashObjectMap().append("%error%", "Failed to retrieve or generate skin."), true); + 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); - MessagesUtil.DONE.msg( + CommandMessages.COMMAND_OVERLAY_DONE.msg( issuer, new HashObjectMap() .append("%player%", player.getName()) - .append("%url%", skin.skinURL()), + .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 index 82b43da9..cec7e8fd 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/ArgumentResolver.java @@ -7,5 +7,18 @@ @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 index 3558cac4..c9363a4f 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PipeSeparatedArgumentResolver.java @@ -1,8 +1,7 @@ package com.georgev22.skinoverlay.command.resolvers; -import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.command.CommandManager; +import com.georgev22.skinoverlay.command.CompletionEngine; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -19,7 +18,6 @@ private record ResolverWrapper(ArgumentResolver resolver, String[] args) { public PipeSeparatedArgumentResolver(@NotNull String pipeSeparated) { String[] tokens = pipeSeparated.split("\\|"); - CommandManager manager = SkinOverlay.getInstance().getCommandManager(); for (String token : tokens) { if (token.startsWith("@")) { @@ -28,7 +26,7 @@ public PipeSeparatedArgumentResolver(@NotNull String pipeSeparated) { String resolverKey = parts[0]; String[] resolverArgs = parts.length > 1 ? new String[]{parts[1]} : new String[0]; - ArgumentResolver dynamicResolver = manager.getResolver(resolverKey); + ArgumentResolver dynamicResolver = CompletionEngine.resolve(resolverKey); if (dynamicResolver != null) { resolvers.add(new ResolverWrapper(dynamicResolver, resolverArgs)); } @@ -46,4 +44,21 @@ public Collection resolve(@NotNull CommandIssuer commandIssuer, String.. } 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 index f60817e7..75661e11 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/PlayersResolver.java @@ -9,10 +9,18 @@ import java.util.stream.Collectors; public class PlayersResolver implements ArgumentResolver { + @Override - public List resolve(@NotNull CommandIssuer commandIssuer, String... args) { + 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 index cff55a9f..2918b8bb 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/resolvers/RangeResolver.java @@ -3,24 +3,106 @@ 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 || !args[0].matches("\\d+-\\d+")) { - return List.of(); + 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); } - String[] range = args[0].split("-"); - int min = Integer.parseInt(range[0]); - int max = Integer.parseInt(range[1]); + 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<>(); - List list = new ArrayList<>(); - for (int i = min; i <= max; i++) { - list.add(String.valueOf(i)); + 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 list; + + 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/utilities/MessageBuilder.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java similarity index 81% rename from common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java rename to common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java index 355f0235..004d8ded 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageBuilder.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java @@ -1,7 +1,10 @@ -package com.georgev22.skinoverlay.utilities; +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; @@ -14,10 +17,12 @@ 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 @@ -44,7 +49,7 @@ * .clickEvent(ClickEvent.Action.RUN_COMMAND, "/help") * .hoverEvent("Click to run /help") * .append(" for more info.") - * .build(); + * .send(player); * * // Using MiniMessage for more complex formatting * messageBuilder @@ -52,7 +57,7 @@ * .appendMiniMessage(" Click here for more info.") * .clickEvent(ClickEvent.Action.RUN_COMMAND, "/help") * .hoverEvent("Click to run /help") - * .build(); + * .send(player); * } */ @SuppressWarnings({"UnusedReturnValue", "unused"}) @@ -273,6 +278,24 @@ public MessageBuilder hoverEvent(String hoverText) { 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. * @@ -419,6 +442,52 @@ public MessageBuilder addPlaceholder(String key, String 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. * @@ -428,4 +497,8 @@ public MessageBuilder addPlaceholder(String key, String value) { 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/utilities/MessageParser.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageParser.java similarity index 98% rename from common/src/main/java/com/georgev22/skinoverlay/utilities/MessageParser.java rename to common/src/main/java/com/georgev22/skinoverlay/message/MessageParser.java index 898449f3..736d5bf4 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/MessageParser.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageParser.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.utilities; +package com.georgev22.skinoverlay.message; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; 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/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/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/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 index e951c146..c60900c9 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java @@ -2,8 +2,12 @@ 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; /** @@ -15,9 +19,9 @@ * 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 { +public final class CustomData implements Cloneable, Copyable { - private final ObjectMap customData; + private ObjectMap customData; public CustomData() { this.customData = ObjectMap.newConcurrentObjectMap(); @@ -35,6 +39,18 @@ public void set(ObjectMap customData) { 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. * @@ -45,6 +61,18 @@ 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. * @@ -59,6 +87,7 @@ public ObjectMap getCustomData() { * 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 @@ -70,17 +99,34 @@ public T get(String key) { return null; } try { + Object value = customData.get(key); //noinspection unchecked - return (T) customData.get(key); + return value instanceof Supplier ? ((Supplier) value).get() : (T) value; } catch (ClassCastException e) { - SkinOverlay.getInstance().getLogger() + 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 @@ -94,7 +140,12 @@ public T getAs(String key, Class type) { } try { Object value = customData.get(key); - return type.cast(value); + 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); @@ -103,13 +154,20 @@ public T getAs(String key, Class type) { } /** - * Adds a custom data entry to the internal data store. + * 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 custom data, must not be null - * @param value the value associated with the key, can be null + * @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 */ - public void add(String key, Object value) { - customData.append(key, value); + @NotNull + public T getOr(String key, T defaultValue, Class type) { + T value = getAs(key, type); + return value != null ? value : defaultValue; } /** @@ -132,4 +190,39 @@ public void remove(String key) { 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/config/CFG.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java index 2f60f570..be8ec729 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/CFG.java @@ -41,7 +41,7 @@ public CFG( final boolean replace, final Logger logger, final Class clazz - ) throws Exception { + ) { this.fileName = string + ".yml"; this.dataFolder = dataFolder; this.saveResource = saveResource; @@ -61,7 +61,7 @@ public static void reloadFiles() { * * @see #reloadFile() */ - public void setup() throws Exception { + public void setup() { if (!dataFolder.exists()) { if (dataFolder.mkdir()) { logger.info("Folder " + dataFolder.getName() + " has been created!"); @@ -76,7 +76,7 @@ public void setup() throws Exception { logger.info("File " + this.file.getName() + " has been created!"); } } catch (final IOException e) { - e.printStackTrace(); + this.logger.log(Level.SEVERE, "Could not create file " + this.file.getName(), e); } if (saveResource) { saveResource(this.fileName, this.dataFolder, this.clazz); @@ -86,9 +86,10 @@ public void setup() throws Exception { this.reloadFile(); } - private void saveResource(@NotNull String resourcePath, File dataFolder, Class clazz) throws Exception { - if (resourcePath.equals("")) { - throw new Exception("ResourcePath cannot be null or empty"); + 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('\\', '/'); diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java deleted file mode 100644 index a9e42417..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/MessagesUtil.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.georgev22.skinoverlay.utilities.config; - -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.utilities.Locale; -import com.georgev22.skinoverlay.utilities.MessageBuilder; -import com.georgev22.skinoverlay.utilities.Utils; -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.HashMap; -import java.util.Map; -import java.util.Objects; - -import static com.georgev22.skinoverlay.utilities.Utils.placeHolder; - -public enum MessagesUtil { - HELP_FORMAT("Messages.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).", - "" - ), - 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%)"), - INVALID_URL("Messages.Invalid URL", "&c&l(!)&c Invalid URL or failed to download image: %url%"), - ERROR("Messages.Error", "&c&l(!)&c An error occurred: %error%"), - FAILED_TO_RETRIEVE_OR_GENERATE_SKIN("Messages.Failed To Retrieve Or Generate Skin", "&c&l(!)&c Failed to retrieve or generate skin for %player%!"), - ; - private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); - private static CFG messagesCFG; - private static Locale locale; - private final String path; - private String[] messages; - - MessagesUtil(String path, String... messages) { - this.messages = messages; - this.path = path; - } - - public static void repairPaths(Locale locale) throws Exception { - if (messagesCFG == null | !Objects.equals(MessagesUtil.locale, locale)) { - setLocale(locale); - setMessagesCFG(new CFG("messages_" + locale.getStringLocale(), mainPlugin.getDataFolder(), true, true, mainPlugin.getLogger(), mainPlugin.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(); - } - } - - public static Locale getLocale() { - return locale; - } - - public static void setLocale(Locale locale) { - MessagesUtil.locale = locale; - } - - public static CFG getMessagesCFG() { - return messagesCFG; - } - - public static void setMessagesCFG(CFG messagesCFG) { - MessagesUtil.messagesCFG = messagesCFG; - } - - 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())); - } - } - - private boolean isMultiLined() { - return this.messages.length > 1; - } - - 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 sender) { - this.msg(sender, new HashObjectMap<>(), false, MessageType.CHAT); - } - - public void msg(@NotNull CommandIssuer sender, MessageType messageType) { - this.msg(sender, new HashObjectMap<>(), false, messageType); - } - - public void msg(@NotNull CommandIssuer sender, Map map, boolean ignoreCase) { - this.msg(sender, map, ignoreCase, MessageType.CHAT); - } - - public void msg(CommandIssuer sender, Map map, boolean ignoreCase, @NotNull MessageType messageType) { - MessageBuilder messageBuilder = new MessageBuilder(); - TagResolver tagResolver = StandardTags.defaults(); // ALL TAG RESOLVERS - Audience audience = mainPlugin.getAudienceProvider().player(sender.getUniqueId()); - switch (messageType) { - case ACTIONBAR -> audience.sendActionBar( - messageBuilder - .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) - .buildAndReset() - ); - case TITLE -> audience.showTitle(Title.title( - messageBuilder - .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) - .buildAndReset(), - messages.length > 1 - ? messageBuilder - .appendMiniMessage(placeHolder(messages[1], map, ignoreCase), tagResolver) - .buildAndReset() - : Component.text(""), - Title.DEFAULT_TIMES - )); - case CHAT -> { - if (this.isMultiLined()) { - Arrays.stream(messages).forEach(message -> audience.sendMessage( - messageBuilder - .appendMiniMessage(placeHolder(message, map, ignoreCase), tagResolver) - .buildAndReset() - )); - } else { - audience.sendMessage( - messageBuilder - .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) - .buildAndReset() - ); - } - } - } - } - - public void msgConsole() { - msgConsole(new HashMap<>(), false); - } - - public void msgConsole(Map map, boolean ignoreCase) { - MessageBuilder messageBuilder = new MessageBuilder(); - TagResolver tagResolver = StandardTags.defaults(); // ALL TAG RESOLVERS - Audience audience = mainPlugin.getAudienceProvider().console(); - if (this.isMultiLined()) { - Arrays.stream(messages).forEach(message -> audience.sendMessage( - messageBuilder - .appendMiniMessage(placeHolder(message, map, ignoreCase), tagResolver) - .buildAndReset() - )); - } else { - audience.sendMessage( - messageBuilder - .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), tagResolver) - .buildAndReset() - ); - } - } - - public void msgAll() { - mainPlugin.getPlayerProvider().getOnlinePlayers() - .forEach(this::msg); - } - - public void msgAll(Map map, boolean ignoreCase) { - mainPlugin.getPlayerProvider().getOnlinePlayers() - .forEach(player -> this.msg(player, map, ignoreCase)); - } - - public enum MessageType { - CHAT, - ACTIONBAR, - TITLE, - } - -} \ No newline at end of file diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java index c2b55d98..cb3ca10c 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java @@ -51,7 +51,7 @@ public UUID getUniqueId() { if (isPlayer()) { return ((Player) this.source).getUniqueId(); } - return Utils.generateUUID("VoidChestConsole"); + return Utils.generateUUID("SkinOverlayConsole"); } @Override diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java index 0d3e22eb..2e1f6367 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityPluginCommandWrapper.java @@ -23,9 +23,9 @@ public VelocityPluginCommandWrapper(BaseCommand baseCommand) { 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); + baseCommand.execute(new VelocityCommandIssuer(source), args, context); } catch (Exception e) { SkinOverlay.getInstance().getLogger().log(Level.SEVERE, "An error occurred while executing the command.", e); } From 22742f9745e8a4801244bd3ed8300f7d3f85a43a Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 17:10:45 +0200 Subject: [PATCH 33/79] refactor: simplify skin parts creation with loop Replace repetitive individual part creation calls with a loop over SectionType enum values. This reduces code duplication and improves maintainability. --- .../skinoverlay/utilities/skin/SkinParts.java | 79 ++----------------- 1 file changed, 5 insertions(+), 74 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java index f54b6832..19f4c14a 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java @@ -5,6 +5,7 @@ import com.georgev22.skinoverlay.maps.ObjectMap; import com.georgev22.skinoverlay.maps.UnmodifiableObjectMap; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; +import com.georgev22.skinoverlay.utilities.skin.Section.SectionType; import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; @@ -88,78 +89,9 @@ public void createParts() { SerializableBufferedImage fullSkin = this.fullSkin; this.fullSkin = convertSkin(fullSkin); } - parts.put("Head_Top", createPart(Section.SectionType.Head_Top.getSection())); - parts.put("Head_Bottom", createPart(Section.SectionType.Head_Bottom.getSection())); - parts.put("Head_Right", createPart(Section.SectionType.Head_Right.getSection())); - parts.put("Head_Front", createPart(Section.SectionType.Head_Front.getSection())); - parts.put("Head_Left", createPart(Section.SectionType.Head_Left.getSection())); - parts.put("Head_Back", createPart(Section.SectionType.Head_Back.getSection())); - parts.put("Hat_Top", createPart(Section.SectionType.Hat_Top.getSection())); - parts.put("Hat_Bottom", createPart(Section.SectionType.Hat_Bottom.getSection())); - parts.put("Hat_Right", createPart(Section.SectionType.Hat_Right.getSection())); - parts.put("Hat_Front", createPart(Section.SectionType.Hat_Front.getSection())); - parts.put("Hat_Left", createPart(Section.SectionType.Hat_Left.getSection())); - parts.put("Hat_Back", createPart(Section.SectionType.Hat_Back.getSection())); - parts.put("Right_Leg_Top", createPart(Section.SectionType.Right_Leg_Top.getSection())); - parts.put("Right_Leg_Bottom", createPart(Section.SectionType.Right_Leg_Bottom.getSection())); - parts.put("Right_Leg_Right", createPart(Section.SectionType.Right_Leg_Right.getSection())); - parts.put("Right_Leg_Front", createPart(Section.SectionType.Right_Leg_Front.getSection())); - parts.put("Right_Leg_Left", createPart(Section.SectionType.Right_Leg_Left.getSection())); - parts.put("Right_Leg_Back", createPart(Section.SectionType.Right_Leg_Back.getSection())); - parts.put("Torso_Top", createPart(Section.SectionType.Torso_Top.getSection())); - parts.put("Torso_Bottom", createPart(Section.SectionType.Torso_Bottom.getSection())); - parts.put("Torso_Right", createPart(Section.SectionType.Torso_Right.getSection())); - parts.put("Torso_Front", createPart(Section.SectionType.Torso_Front.getSection())); - parts.put("Torso_Left", createPart(Section.SectionType.Torso_Left.getSection())); - parts.put("Torso_Back", createPart(Section.SectionType.Torso_Back.getSection())); - parts.put("Right_Arm_Top", createPart(Section.SectionType.Right_Arm_Top.getSection())); - parts.put("Right_Arm_Bottom", createPart(Section.SectionType.Right_Arm_Bottom.getSection())); - parts.put("Right_Arm_Right", createPart(Section.SectionType.Right_Arm_Right.getSection())); - parts.put("Right_Arm_Front", createPart(Section.SectionType.Right_Arm_Front.getSection())); - parts.put("Right_Arm_Left", createPart(Section.SectionType.Right_Arm_Left.getSection())); - parts.put("Right_Arm_Back", createPart(Section.SectionType.Right_Arm_Back.getSection())); - parts.put("Left_Leg_Top", createPart(Section.SectionType.Left_Leg_Top.getSection())); - parts.put("Left_Leg_Bottom", createPart(Section.SectionType.Left_Leg_Bottom.getSection())); - parts.put("Left_Leg_Right", createPart(Section.SectionType.Left_Leg_Right.getSection())); - parts.put("Left_Leg_Front", createPart(Section.SectionType.Left_Leg_Front.getSection())); - parts.put("Left_Leg_Left", createPart(Section.SectionType.Left_Leg_Left.getSection())); - parts.put("Left_Leg_Back", createPart(Section.SectionType.Left_Leg_Back.getSection())); - parts.put("Left_Arm_Top", createPart(Section.SectionType.Left_Arm_Top.getSection())); - parts.put("Left_Arm_Bottom", createPart(Section.SectionType.Left_Arm_Bottom.getSection())); - parts.put("Left_Arm_Right", createPart(Section.SectionType.Left_Arm_Right.getSection())); - parts.put("Left_Arm_Front", createPart(Section.SectionType.Left_Arm_Front.getSection())); - parts.put("Left_Arm_Left", createPart(Section.SectionType.Left_Arm_Left.getSection())); - parts.put("Left_Arm_Back", createPart(Section.SectionType.Left_Arm_Back.getSection())); - parts.put("Right_Pants_Leg_Top", createPart(Section.SectionType.Right_Pants_Leg_Top.getSection())); - parts.put("Right_Pants_Leg_Bottom", createPart(Section.SectionType.Right_Pants_Leg_Bottom.getSection())); - parts.put("Right_Pants_Leg_Right", createPart(Section.SectionType.Right_Pants_Leg_Right.getSection())); - parts.put("Right_Pants_Leg_Front", createPart(Section.SectionType.Right_Pants_Leg_Front.getSection())); - parts.put("Right_Pants_Leg_Left", createPart(Section.SectionType.Right_Pants_Leg_Left.getSection())); - parts.put("Right_Pants_Leg_Back", createPart(Section.SectionType.Right_Pants_Leg_Back.getSection())); - parts.put("Jacket_Top", createPart(Section.SectionType.Jacket_Top.getSection())); - parts.put("Jacket_Bottom", createPart(Section.SectionType.Jacket_Bottom.getSection())); - parts.put("Jacket_Right", createPart(Section.SectionType.Jacket_Right.getSection())); - parts.put("Jacket_Front", createPart(Section.SectionType.Jacket_Front.getSection())); - parts.put("Jacket_Left", createPart(Section.SectionType.Jacket_Left.getSection())); - parts.put("Jacket_Back", createPart(Section.SectionType.Jacket_Back.getSection())); - parts.put("Right_Sleeve_Top", createPart(Section.SectionType.Right_Sleeve_Top.getSection())); - parts.put("Right_Sleeve_Bottom", createPart(Section.SectionType.Right_Sleeve_Bottom.getSection())); - parts.put("Right_Sleeve_Right", createPart(Section.SectionType.Right_Sleeve_Right.getSection())); - parts.put("Right_Sleeve_Front", createPart(Section.SectionType.Right_Sleeve_Front.getSection())); - parts.put("Right_Sleeve_Left", createPart(Section.SectionType.Right_Sleeve_Left.getSection())); - parts.put("Right_Sleeve_Back", createPart(Section.SectionType.Right_Sleeve_Back.getSection())); - parts.put("Left_Pants_Leg_Top", createPart(Section.SectionType.Left_Pants_Leg_Top.getSection())); - parts.put("Left_Pants_Leg_Bottom", createPart(Section.SectionType.Left_Pants_Leg_Bottom.getSection())); - parts.put("Left_Pants_Leg_Right", createPart(Section.SectionType.Left_Pants_Leg_Right.getSection())); - parts.put("Left_Pants_Leg_Front", createPart(Section.SectionType.Left_Pants_Leg_Front.getSection())); - parts.put("Left_Pants_Leg_Left", createPart(Section.SectionType.Left_Pants_Leg_Left.getSection())); - parts.put("Left_Pants_Leg_Back", createPart(Section.SectionType.Left_Pants_Leg_Back.getSection())); - parts.put("Left_Sleeve_Top", createPart(Section.SectionType.Left_Sleeve_Top.getSection())); - parts.put("Left_Sleeve_Bottom", createPart(Section.SectionType.Left_Sleeve_Bottom.getSection())); - parts.put("Left_Sleeve_Right", createPart(Section.SectionType.Left_Sleeve_Right.getSection())); - parts.put("Left_Sleeve_Front", createPart(Section.SectionType.Left_Sleeve_Front.getSection())); - parts.put("Left_Sleeve_Left", createPart(Section.SectionType.Left_Sleeve_Left.getSection())); - parts.put("Left_Sleeve_Back", createPart(Section.SectionType.Left_Sleeve_Back.getSection())); + for (SectionType sectionType : SectionType.values()) { + parts.put(sectionType.name(), createPart(sectionType.getSection(), sectionType.name())); + } } /** @@ -168,7 +100,7 @@ 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) { + private @NotNull Part createPart(@NotNull Section section, String partName) { int x = section.x1(); int y = section.y1(); int width = section.width(); @@ -186,7 +118,6 @@ public void createParts() { } } - String partName = section.getClass().getSimpleName(); return new Part(partName, new SerializableBufferedImage(partImage), x, y, width, height, isAreaTransparent); } From 7a4f8372629cd8252ab99fb05f9834f705b3c8d2 Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 17:47:35 +0200 Subject: [PATCH 34/79] feat: add command aliases for subcommands - Add CommandAlias annotation to all subcommands - Register subcommands with command manager when aliases are present - Improve subcommand registration logic in BaseCommand --- .../skinoverlay/command/BaseCommand.java | 16 +++++++++------- .../command/commands/sub/ClearSubCommand.java | 4 +--- .../command/commands/sub/InfoSubCommand.java | 2 ++ .../command/commands/sub/ReloadSubCommand.java | 2 ++ .../command/commands/sub/WearSubCommand.java | 1 + .../command/commands/sub/WearUrlSubCommand.java | 3 +-- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java index fe92de7d..ccdce4dc 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java @@ -257,18 +257,20 @@ public Collection tabComplete(CommandIssuer sender, String @NotNull [] a /** * Adds a nested BaseCommand as a subcommand. * - * @param command the subcommand to add + * @param subCommand the subcommand to add */ - public void addSubcommand(@NotNull BaseCommand command) { - if (command.subCommandAliases.length == 0 && !(command instanceof MethodCommand)) { + 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 : command.subCommandAliases) { - subcommands.put(alias.toLowerCase(), command); + for (String alias : subCommand.subCommandAliases) { + subcommands.put(alias.toLowerCase(), subCommand); } - if (command instanceof MethodCommand && ((MethodCommand) command).isDefault()) { - command.subcommands.put("__default__", command); + 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); } /** 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 index 0cefed3c..20556920 100644 --- 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 @@ -1,7 +1,6 @@ package com.georgev22.skinoverlay.command.commands.sub; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; @@ -12,9 +11,8 @@ import com.georgev22.skinoverlay.utilities.skin.SkinParts; import org.jetbrains.annotations.NotNull; -import java.util.Optional; - @Subcommand({"clear", "reset"}) +@CommandAlias("soclear") @CommandCompletion("@players") @Description("Removes the skin overlay") @Permission("skinoverlay.wear.clear") 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 index f3e57f03..5c833fc7 100644 --- 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 @@ -3,6 +3,7 @@ import com.georgev22.skinoverlay.BuildParameters; import com.georgev22.skinoverlay.command.CommandContext; 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; @@ -10,6 +11,7 @@ import org.jetbrains.annotations.NotNull; @Subcommand("info") +@CommandAlias("soinfo") @Permission("skinoverlay.info") public class InfoSubCommand extends SkinOverlayBaseCommand { 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 index a50986ec..26846995 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -14,6 +15,7 @@ import java.util.logging.Level; @Subcommand("reload") +@CommandAlias("soreload") @Permission("skinoverlay.reload") public class ReloadSubCommand extends SkinOverlayBaseCommand { @Default 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 index d973bd0b..9c636816 100644 --- 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 @@ -18,6 +18,7 @@ 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") 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 index 950e6b7a..a7784c09 100644 --- 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 @@ -1,7 +1,6 @@ package com.georgev22.skinoverlay.command.commands.sub; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; @@ -19,10 +18,10 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; -import java.util.Optional; import java.util.logging.Level; @Subcommand("url") +@CommandAlias("sowearurl") @Permission("skinoverlay.wear.url") @Description("Wear an overlay on a player's skin") @CommandCompletion(" @players") From 76320631a4f4e924b87ff9f968e73f244a75cc6b Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 17:53:19 +0200 Subject: [PATCH 35/79] feat: add reflect library and update package relocation paths - Add reflect library dependency to bukkit module - Move reflect dependency from common to bukkit module - Update all package relocation paths to use `.lib` subpackage - Add reflect library to bukkit shadow jar relocation --- bukkit/build.gradle.kts | 28 +++++++++++++++------------- common/build.gradle.kts | 1 - velocity/build.gradle.kts | 24 ++++++++++++------------ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 79355ceb..22d7e9d1 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { 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") @@ -60,19 +61,20 @@ configurations.configureEach { tasks.shadowJar { archiveBaseName.set("skinoverlay") archiveClassifier.set("bukkit") - relocate("org.mineskin", "${project.property("packageName")}.mineskin") - relocate("com.google.gson", "${project.property("packageName")}.gson") - relocate("com.google.errorprone", "${project.property("packageName")}.gson.errorprone") - relocate("com.zaxxer", "${project.property("packageName")}.zaxxer") - relocate("org.bstats", "${project.property("packageName")}.bstats") - relocate("org.bspfsystems.yamlconfiguration", "${project.property("packageName")}.yaml") - relocate("org.yaml.snakeyaml", "${project.property("packageName")}.yaml") - relocate("org.intellij.lang", "${project.property("packageName")}.jetbrains") - relocate("org.jetbrains", "${project.property("packageName")}.jetbrains") - relocate("org.json", "${project.property("packageName")}.json") - relocate("org.apache.commons.pool2", "${project.property("packageName")}.pool2") - relocate("net.kyori", "${project.property("packageName")}.kyori") - relocate("redis.clients", "${project.property("packageName")}.jedis") + 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") { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 99194097..8e9e1d85 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -51,7 +51,6 @@ dependencies { implementation(libs.jedis) implementation(libs.jetbrains.annotations) - implementation(libs.reflect) } configurations.configureEach { diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 97dfe8b8..a2fb89c4 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -28,18 +28,18 @@ configurations.configureEach { tasks.shadowJar { archiveBaseName.set("skinoverlay") archiveClassifier.set("velocity") - relocate("org.mineskin", "${project.property("packageName")}.mineskin") - relocate("com.google.gson", "${project.property("packageName")}.gson") - relocate("com.google.errorprone", "${project.property("packageName")}.gson.errorprone") - relocate("com.zaxxer", "${project.property("packageName")}.zaxxer") - relocate("org.bstats", "${project.property("packageName")}.bstats") - relocate("org.bspfsystems.yamlconfiguration", "${project.property("packageName")}.yaml") - relocate("org.yaml.snakeyaml", "${project.property("packageName")}.yaml") - relocate("org.intellij.lang", "${project.property("packageName")}.jetbrains") - relocate("org.jetbrains", "${project.property("packageName")}.jetbrains") - relocate("org.json", "${project.property("packageName")}.json") - relocate("org.apache.commons.pool2", "${project.property("packageName")}.pool2") - relocate("redis.clients", "${project.property("packageName")}.jedis") + 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") { From 08d292f191636102bb175b8df3f56a90f7b4d257 Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 17:55:55 +0200 Subject: [PATCH 36/79] chore: remove unused dependencies and plugins Remove paperlib, jvmdowngrader, and adventure-text-serializer-gson dependencies and plugins from gradle/libs.versions.toml to clean up the build configuration. --- gradle/libs.versions.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67ad8c67..ce6bedba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,11 @@ [versions] folia-api = "1.21.5-R0.1-SNAPSHOT" -paperlib = "1.0.8" 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" -jvmdowngrader = "1.2.2" hikari = "4.0.3" paperweight = "2.0.0-beta.17" gson = "2.13.1" @@ -22,18 +20,15 @@ reflect = "1.5.0" [plugins] gradleup-shadow = { id = "com.gradleup.shadow", version.ref = "gradleup-shadow" } -jvmdowngrader = { id = "xyz.wagyourtail.jvmdowngrader", version.ref = "jvmdowngrader" } paperweight-userdev = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" } blossom = { id = "net.kyori.blossom", version = "2.1.0" } [libraries] -paperlib = { module = "io.papermc:paperlib", version.ref = "paperlib" } 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-serializer-gson = { module = "net.kyori:adventure-text-serializer-gson", 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" } @@ -41,7 +36,6 @@ adventure-platform-bungeecord = { module = "net.kyori:adventure-platform-bungeec 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" } -jvmdowngrader = { module = "xyz.wagyourtail.jvmdowngrader:jvmdowngrader-java-api", version.ref = "jvmdowngrader" } 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" } From 26939eae5a8477f986f5a77963f58547169fa673 Mon Sep 17 00:00:00 2001 From: "George V." Date: Sun, 30 Nov 2025 17:57:02 +0200 Subject: [PATCH 37/79] feat: add info subcommand to main command Introduce new InfoSubCommand to the SkinOverlayMain command structure. This extends the available subcommands with additional functionality for retrieving skin overlay information. --- .../skinoverlay/command/commands/SkinOverlayMain.java | 6 ++---- .../skinoverlay/command/commands/sub/InfoSubCommand.java | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) 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 index c52dd612..382e18bd 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java @@ -3,10 +3,7 @@ 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.ClearSubCommand; -import com.georgev22.skinoverlay.command.commands.sub.ReloadSubCommand; -import com.georgev22.skinoverlay.command.commands.sub.WearSubCommand; -import com.georgev22.skinoverlay.command.commands.sub.WearUrlSubCommand; +import com.georgev22.skinoverlay.command.commands.sub.*; import com.georgev22.skinoverlay.message.messages.CommandMessages; import org.jetbrains.annotations.NotNull; @@ -22,6 +19,7 @@ public SkinOverlayMain() { this.addSubcommand(new ClearSubCommand()); this.addSubcommand(new WearUrlSubCommand()); this.addSubcommand(new ReloadSubCommand()); + this.addSubcommand(new InfoSubCommand()); } @Default 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 index 5c833fc7..59840c19 100644 --- 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 @@ -1,7 +1,6 @@ package com.georgev22.skinoverlay.command.commands.sub; import com.georgev22.skinoverlay.BuildParameters; -import com.georgev22.skinoverlay.command.CommandContext; import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.command.annotation.CommandAlias; import com.georgev22.skinoverlay.command.annotation.Default; From 7ed472e04190f4404283aea692796f555b87f61e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 30 Nov 2025 16:09:24 +0000 Subject: [PATCH 38/79] chore(release): 8.0.0-beta.3 [skip ci] # [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. --- CHANGELOG.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f017ff1..48c02363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +# [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) diff --git a/README.md b/README.md index 7769de7d..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 - 8.0.0-beta.2 + 8.0.0-beta.3 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.2" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.3" } ``` diff --git a/gradle.properties b/gradle.properties index 47ed972d..bc1974e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.2 +version = 8.0.0-beta.3 description = SkinOverlay From d6519c931d27534c406850d2c54b961a26a9b1ad Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 5 May 2026 07:08:19 +0300 Subject: [PATCH 39/79] feat: restructure data structures, storage, and task management Completely revamped the internal data structures, storage layer, and task management system for improved maintainability and clarity. - Replaced the `com.georgev22.skinoverlay.maps` package with a new `com.georgev22.skinoverlay.datastructures` package containing redesigned map and list implementations (e.g., `ConcurrentHashObjectMap`, `LinkedHashObjectMap`, `ObjectMaps` utility, `NullableArrayList`). - Consolidated entity management into a single `AbstractEntityManager` with a unified `FileEntityManager` for JSON storage, removing the previous per-entity managers (e.g., `PlayerFileManager`, `SkinFileManager`). - Introduced `ManagedEntity` record and `EntityFactory` interface for type-safe entity registration and creation. - Replaced custom `Pair` class with a new immutable `Pair` implementation in the `datastructures` package. - Added a centralized `ExecutorManager` and related types (`ExecutorType`, `ExecutorMetrics`, `NamedThreadFactory`) for managing thread pools. - Migrated `Entity` from an interface to an abstract class with lifecycle hooks (`postLoad`, `postSave`, etc.) and built-in custom data handling. - Updated `EntityManager` interface to use `CompletableFuture` for async loading and added `getLoadedEntities()` for cached entity views. - Replaced JetBrains annotations with JSpecify annotations. - Updated all dependent classes across Bukkit and Velocity modules to reflect the new package structure and API changes. BREAKING CHANGE: - Removed old `ConcurrentObjectMap`, `HashObjectMap`, `LinkedObjectMap`, `ObjectMap`, `ObservableObjectMap`, `Pair`, `TreeObjectMap`, `UnmodifiableObjectMap`, `PlayerEntityManager`, `SkinEntityManager`, `PlayerFileManager`, `SkinFileManager`, `PairDocument`. - `Entity.getId()` renamed to `Entity.getUniqueId()`. - `EntityManagerRegistry` is now a singleton via `getInstance()` and uses typed lookups via `getTyped(Class)`. - `EntityManager` load methods now return `CompletableFuture`. --- .../BukkitLegacyGameProfileProvider.java | 4 +- .../utilities/BukkitMinecraftUtils.java | 2 +- .../GameProfileProvider_1_17_R1.java | 4 +- .../GameProfileProvider_1_18_R1.java | 4 +- .../GameProfileProvider_1_18_R2.java | 4 +- .../GameProfileProvider_1_19_R1.java | 4 +- .../GameProfileProvider_1_19_R2.java | 4 +- .../GameProfileProvider_1_19_R3.java | 4 +- .../GameProfileProvider_1_20_R1.java | 4 +- .../GameProfileProvider_1_20_R2.java | 4 +- .../GameProfileProvider_1_20_R3.java | 4 +- .../GameProfileProvider_1_20_R4.java | 4 +- .../GameProfileProvider_1_21_R1.java | 4 +- .../GameProfileProvider_1_21_R2.java | 4 +- .../GameProfileProvider_1_21_R3.java | 4 +- .../GameProfileProvider_1_21_R4.java | 4 +- .../GameProfileProvider_1_21_R5.java | 4 +- .../GameProfileProvider_1_21_R6.java | 4 +- common/build.gradle.kts | 2 +- .../georgev22/skinoverlay/SkinOverlay.java | 63 +- .../skinoverlay/appliers/SkinApplier.java | 6 +- .../skinoverlay/command/BaseCommand.java | 2 +- .../skinoverlay/command/CompletionEngine.java | 6 +- .../skinoverlay/command/MethodCommand.java | 2 +- .../command/commands/sub/ClearSubCommand.java | 2 +- .../command/commands/sub/WearSubCommand.java | 2 +- .../commands/sub/WearUrlSubCommand.java | 2 +- .../skinoverlay/datastructures/Pair.java | 132 ++ .../PairDocument.java | 12 +- .../datastructures/lists/ListListener.java | 14 + .../lists/NullableArrayList.java | 156 +++ .../lists/UnmodifiableArrayList.java | 248 ++++ .../lists/UnmodifiableNullableArrayList.java | 228 ++++ .../maps/AbstractObjectMap.java | 1087 +++++++++++++++++ .../maps/AbstractObjectMultiMap.java | 132 ++ .../maps/ConcurrentHashObjectMap.java | 101 ++ .../datastructures/maps/EnumObjectMap.java | 55 + .../datastructures/maps/HashObjectMap.java | 138 +++ .../maps/LinkedHashObjectMap.java | 223 ++++ .../datastructures/maps/ObjectMap.java | 513 ++++++++ .../datastructures/maps/ObjectMaps.java | 173 +++ .../maps/ObservableObjectMap.java | 282 +++++ .../datastructures/maps/TreeObjectMap.java | 365 ++++++ .../maps/UnmodifiableObjectMap.java | 310 +++++ .../ObjectMapSerializerDeserializer.java | 39 + .../listeners/PlayerListeners.java | 6 +- .../skinoverlay/maps/ConcurrentObjectMap.java | 504 -------- .../skinoverlay/maps/HashObjectMap.java | 504 -------- .../skinoverlay/maps/LinkedObjectMap.java | 504 -------- .../georgev22/skinoverlay/maps/ObjectMap.java | 466 ------- .../skinoverlay/maps/ObservableObjectMap.java | 217 ---- .../com/georgev22/skinoverlay/maps/Pair.java | 113 -- .../skinoverlay/maps/TreeObjectMap.java | 513 -------- .../maps/UnmodifiableObjectMap.java | 705 ----------- .../skinoverlay/message/MessageBuilder.java | 4 +- .../skinoverlay/message/MessageEntry.java | 2 +- .../providers/GameProfileProvider.java | 48 +- .../skinoverlay/providers/PlayerProvider.java | 74 +- .../skinoverlay/providers/SkinProvider.java | 2 +- .../registry/AbstractRegistry.java | 8 +- .../registry/CommandTargetRegistry.java | 2 +- .../registry/EntityManagerRegistry.java | 141 ++- .../skinoverlay/registry/Registry.java | 2 +- .../skinoverlay/skin/SGameProfile.java | 4 +- .../skinoverlay/storage/EntityFactory.java | 34 + .../skinoverlay/storage/EntityManager.java | 88 +- .../skinoverlay/storage/ManagedEntity.java | 30 + .../skinoverlay/storage/data/Entity.java | 130 +- .../skinoverlay/storage/data/PlayerData.java | 38 +- .../skinoverlay/storage/data/Skin.java | 37 +- .../storage/gson/PlayerDataTypeAdapter.java | 2 +- .../storage/gson/SkinTypeAdapter.java | 2 +- .../manager/AbstractEntityManager.java | 261 ++++ .../storage/manager/PlayerEntityManager.java | 153 --- .../storage/manager/SkinEntityManager.java | 155 --- .../manager/gson/FileEntityManager.java | 228 ++++ .../manager/gson/PlayerFileManager.java | 312 ----- .../storage/manager/gson/SkinFileManager.java | 225 ---- .../skinoverlay/task/ExecutorManager.java | 265 ++++ .../skinoverlay/task/ExecutorMetrics.java | 8 + .../skinoverlay/task/ExecutorType.java | 26 + .../skinoverlay/task/NamedThreadFactory.java | 25 + .../skinoverlay/utilities/CustomData.java | 9 +- .../skinoverlay/utilities/DeepCloner.java | 6 +- .../skinoverlay/utilities/GsonUtils.java | 152 +++ .../skinoverlay/utilities/Utils.java | 19 +- .../utilities/config/SkinFileCache.java | 8 +- .../skinoverlay/utilities/skin/SkinParts.java | 6 +- gradle/libs.versions.toml | 2 + .../skinoverlay/SkinOverlayVelocity.java | 2 +- .../VelocityGameProfileProvider.java | 4 +- 91 files changed, 5759 insertions(+), 4652 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/Pair.java rename common/src/main/java/com/georgev22/skinoverlay/{maps => datastructures}/PairDocument.java (97%) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/ListListener.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/NullableArrayList.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableArrayList.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableNullableArrayList.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMultiMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ConcurrentHashObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/EnumObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/HashObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/LinkedHashObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMaps.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObservableObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/TreeObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/UnmodifiableObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/utilities/ObjectMapSerializerDeserializer.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/ConcurrentObjectMap.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/HashObjectMap.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/LinkedObjectMap.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/ObjectMap.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/ObservableObjectMap.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/Pair.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/maps/UnmodifiableObjectMap.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/EntityFactory.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/ManagedEntity.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/AbstractEntityManager.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/FileEntityManager.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/PlayerFileManager.java delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/SkinFileManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/task/ExecutorManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/task/ExecutorMetrics.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/task/ExecutorType.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/task/NamedThreadFactory.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/utilities/GsonUtils.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java index ffda4fb5..58b67f0e 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java index b333f932..8d2c61bb 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay.utilities; -import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.utilities.color.Color; import com.google.common.collect.Lists; import org.bukkit.*; 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 index 186130c3..c438eabb 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 760b4efb..94ea19e9 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 45c9f2ab..38805043 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 1634c68c..33d78ab3 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index f159f9ca..26b79873 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 8c1622b5..bd6e6d66 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 23ac11f6..c5122ad2 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 3391d554..038a1962 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 54482c5d..4a88d466 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 9d623e58..fe36740e 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 61bc64e8..429a4ecc 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 24a02256..530f1f4c 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index 6958b4e3..48570f5b 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index f9006ab0..f04e8a16 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index d9340575..f5facc3c 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; 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 index c7fb7153..5370a897 100644 --- 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 @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 8e9e1d85..f6cf0d6b 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { implementation(libs.jedis) - implementation(libs.jetbrains.annotations) + implementation(libs.jspecify) } configurations.configureEach { diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index bbdd3d43..4b733f0a 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -20,20 +20,16 @@ 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.ManagedEntity; +import com.georgev22.skinoverlay.storage.data.Entity; 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.storage.manager.AbstractEntityManager; +import com.georgev22.skinoverlay.storage.manager.gson.FileEntityManager; +import com.georgev22.skinoverlay.utilities.GsonUtils; 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; @@ -81,15 +77,7 @@ public static SkinOverlay getInstance() { */ 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.gson = GsonUtils.getGson(true); this.fileManager = FileManager.getInstance(); try { this.fileManager.loadFiles(); @@ -107,14 +95,21 @@ public void onLoad() { } 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); + EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); + File folder = new File(this.dataFolder, "save-data"); - SkinFileManager skinFileManager = new SkinFileManager(new File(getDataFolder(), "skindata")); - EntityManagerRegistry.registerManager(Skin.class, skinFileManager); + if (!folder.exists() && folder.mkdirs()) { + this.getLogger().info( + "Created " + folder.getPath() + " folder" + ); + } - playerFileManager.loadAll(); - skinFileManager.loadAll(); + for (ManagedEntity managedEntity : AbstractEntityManager.getManagedEntities()) { + if (entityManagerRegistry.get(managedEntity.type()).isEmpty()) { + registerEntityManager(entityManagerRegistry, managedEntity, + new FileEntityManager<>(managedEntity, new File(folder, managedEntity.key()))); + } + } } /** @@ -144,8 +139,7 @@ public void onDisable() { audienceProvider = null; } - EntityManagerRegistry.getManager(PlayerData.class).ifPresent(EntityManager::saveAll); - EntityManagerRegistry.getManager(Skin.class).ifPresent(EntityManager::saveAll); + EntityManagerRegistry.getInstance().entries().forEach((key, value) -> value.shutdown()); } /** @@ -246,9 +240,9 @@ public void setPlugin(T plugin) { * * @return the MinecraftScheduler */ - public MinecraftScheduler getScheduler() { + public MinecraftScheduler getScheduler() { //noinspection unchecked - return (MinecraftScheduler) scheduler; + return (MinecraftScheduler) scheduler; } /** @@ -448,4 +442,17 @@ public void setMessageManager(MessageManager messageManager) { public MessageManager getMessageManager() { return messageManager; } + + private void registerEntityManager(EntityManagerRegistry entityManagerRegistry, + ManagedEntity managedEntity, + EntityManager entityManager) { + try { + entityManagerRegistry.register(managedEntity.type(), entityManager); + this.getLogger().info( + "Registered " + managedEntity.type().getSimpleName() + " entity manager"); + } catch (Exception e) { + this.getLogger().log(Level.SEVERE, + "Failed to register " + managedEntity.type().getSimpleName() + " entity manager", e); + } + } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java index 6698ce04..60e7e1c6 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java +++ b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java @@ -20,7 +20,7 @@ public abstract class SkinApplier { * Apply the skin for the specified {@link SPlayer} * * @param player Player's {@link SPlayer} object. - * @param skin Skin + * @param skin Skin */ public void setSkin(@NotNull SPlayer player, @NotNull Skin skin) { SGameProfile gameProfile = skinOverlay.getGameProfileProvider().getGameProfile(player); @@ -28,7 +28,7 @@ public void setSkin(@NotNull SPlayer player, @NotNull Skin skin) { skinOverlay.getGameProfileProvider().applyUpdatedGameProfile(player); applySkin(player, skin); - Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getManager(PlayerData.class); + Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getInstance().getTyped(PlayerData.class); if (optionalPlayerDataEntityManager.isEmpty()) { return; } @@ -55,7 +55,7 @@ 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 + * @param skin Skin */ protected void applySkin(@NotNull final SPlayer player, @NotNull final Skin skin) { applySkin(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 index ccdce4dc..0728bb17 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java @@ -4,7 +4,7 @@ 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.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.registry.CommandTargetRegistry; import org.jetbrains.annotations.NotNull; diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java index 99f99852..cc8917dc 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java @@ -5,8 +5,8 @@ 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 com.georgev22.skinoverlay.datastructures.maps.ConcurrentHashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -38,7 +38,7 @@ public class CompletionEngine { /** * Holds all registered argument resolvers, mapped by lowercase key. */ - private static final ObjectMap resolvers = new ConcurrentObjectMap<>(); + private static final ObjectMap resolvers = new ConcurrentHashObjectMap<>(); static { registerDefaultResolvers(); diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java index a411e484..bea33545 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java @@ -2,7 +2,7 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.annotation.*; -import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import org.jetbrains.annotations.NotNull; 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 index 20556920..88bd249c 100644 --- 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 @@ -4,7 +4,7 @@ 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.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.storage.data.Skin; 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 index 9c636816..bc27adfc 100644 --- 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 @@ -4,7 +4,7 @@ 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.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.storage.data.Skin; 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 index a7784c09..0fad8f53 100644 --- 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 @@ -4,7 +4,7 @@ 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.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.storage.data.Skin; diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/Pair.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/Pair.java new file mode 100644 index 00000000..46693929 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/Pair.java @@ -0,0 +1,132 @@ +package com.georgev22.skinoverlay.datastructures; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; +import java.util.Optional; + +/** + * A lightweight immutable pair of two related values. + * + *

This class represents an ordered container holding two elements: + * a mandatory {@code first} value and an optional {@code second} value. + * It is designed for simple structured return values and small data groupings.

+ * + *

Equality, hashing and string representation are defined based on both contained values.

+ * + * @param the type of the first element (non-null) + * @param the type of the second element (nullable) + */ +public final class Pair implements Serializable { + + @Serial + private static final long serialVersionUID = 0L; + + private final F first; + private S second; + + /** + * Constructs a new {@code Pair} with the specified values. + * + * @param first the first (mandatory) element + * @param second the second (optional) element, may be {@code null} + */ + public Pair(@NonNull F first, @Nullable S second) { + this.first = first; + this.second = second; + } + + /** + * Indicates whether some other object is equal to this pair. + * + *

Two pairs are considered equal if both their {@code first} and {@code second} + * elements are equal.

+ * + * @param o the object to compare with + * @return {@code true} if both pairs contain equal values, otherwise {@code false} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Pair p)) return false; + return Objects.equals(first, p.first) && Objects.equals(second, p.second); + } + + /** + * Returns the hash code value for this pair. + * + *

The hash is derived from both elements.

+ * + * @return the hash code of this pair + */ + @Override + public int hashCode() { + return Objects.hash(first, second); + } + + /** + * Returns a human-readable string representation of this pair. + * + * @return a string representation of this pair + */ + @Contract(pure = true) + @Override + public @NonNull String toString() { + return "Pair[first=" + first + ", second=" + second + "]"; + } + + /** + * Creates a new {@code Pair} instance. + * + * @param first the first (mandatory) value + * @param second the second (optional) value + * @param the first element type + * @param the second element type + * @return a new pair containing the supplied values + */ + @Contract(value = "_, _ -> new", pure = true) + public static @NonNull Pair create(@NonNull F first, @Nullable S second) { + return new Pair<>(first, second); + } + + /** + * Returns the first element of this pair. + * + * @return the first element (never {@code null}) + */ + public @NonNull F first() { + return first; + } + + /** + * Returns the second element of this pair. + * + * @return the second element, or {@code null} if not present + */ + public @Nullable S second() { + return second; + } + + /** + * Returns the second element wrapped in an {@link Optional}. + * + * @return an {@code Optional} describing the second element, or empty if absent + */ + @Contract(pure = true) + public @NonNull Optional secondOptional() { + return Optional.ofNullable(second); + } + + /** + * Replaces the second element of this pair. + * + * @param second the new second value (maybe {@code null}) + */ + public void setSecond(@Nullable S second) { + this.second = second; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/PairDocument.java similarity index 97% rename from common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java rename to common/src/main/java/com/georgev22/skinoverlay/datastructures/PairDocument.java index 9ffb4088..c3e5e1ad 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/PairDocument.java +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/PairDocument.java @@ -1,9 +1,9 @@ -package com.georgev22.skinoverlay.maps; +package com.georgev22.skinoverlay.datastructures; import com.georgev22.skinoverlay.exceptions.PairDocumentException; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.io.Serial; import java.io.Serializable; @@ -251,8 +251,8 @@ public T get(final Object key, final T defaultValue) { */ public @Nullable T get(final Object key) { for (Pair pair : objectPairs) { - if (pair.key().equals(key)) { - return (T) pair.value(); + if (pair.first().equals(key)) { + return (T) pair.second(); } } return null; @@ -265,7 +265,7 @@ public T get(final Object key, final T defaultValue) { */ @Contract(pure = true) @Override - public @NotNull String toString() { + public @NonNull String toString() { return "PairDocument{" + "pairs=" + objectPairs + "}"; diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/ListListener.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/ListListener.java new file mode 100644 index 00000000..a18d6c00 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/ListListener.java @@ -0,0 +1,14 @@ +package com.georgev22.skinoverlay.datastructures.lists; + +/** + * Interface for listeners to be notified of changes in the list. + * + * @param the type of elements in the list + */ +public interface ListListener { + enum EventType { + ADD, REMOVE, SET + } + + void onListChanged(int index, E oldValue, E newValue, EventType eventType); +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/NullableArrayList.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/NullableArrayList.java new file mode 100644 index 00000000..92d596ba --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/NullableArrayList.java @@ -0,0 +1,156 @@ +package com.georgev22.skinoverlay.datastructures.lists; + +import org.jetbrains.annotations.Range; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * A custom ArrayList implementation that allows handling of potential IndexOutOfBoundsException by + * returning null values instead of throwing exceptions. + * + * @param the type of elements in this list + */ +public class NullableArrayList extends ArrayList { + + private final ArrayList> listeners = new ArrayList<>(); + + /** + * Constructs an empty list with an initial capacity of ten. + */ + public NullableArrayList() { + super(); + } + + /** + * Constructs an empty list with the specified initial capacity. + * + * @param initialCapacity the initial capacity of the list + * @throws IllegalArgumentException if the specified initial capacity + * is negative + */ + public NullableArrayList(@Range(from = 0, to = Integer.MAX_VALUE) int initialCapacity) { + super(initialCapacity); + } + + /** + * Constructs a list containing the elements of the specified collection in the order they are returned by the + * collection's iterator. + * + * @param c the collection whose elements are to be placed into this list + * @throws NullPointerException if the specified collection is null + */ + public NullableArrayList(Collection c) { + super(c); + } + + /** + * Returns the element at the specified position in this list or null if the index is out of bounds. + * + * @param index the index of the element to return + * @return the element at the specified position in this list or null if the index is out of bounds + */ + @Override + public E get(int index) { + try { + return super.get(index); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /** + * Replaces the element at the specified position in this list with the specified element. + * If the index is out of bounds, null elements will be added until the specified index is reached. + * + * @param index the index at which to set the element + * @param element the element to be set + * @return the element previously at the specified position, or null if the index is out of bounds + */ + @Override + public E set(int index, E element) { + try { + E previousElement = super.set(index, element); + notifyListeners(index, previousElement, element, ListListener.EventType.SET); + return previousElement; + } catch (IndexOutOfBoundsException e) { + for (int i = size(); i <= index; i++) { + super.add(null); + } + super.add(index, element); + notifyListeners(index, null, element, ListListener.EventType.SET); + return null; + } + } + + /** + * Inserts the specified element at the specified position in this list. + * If the index is out of bounds, null elements will be added until the specified index is reached. + * + * @param index the index at which to insert the element + * @param element the element to be inserted + */ + @Override + public void add(int index, E element) { + try { + super.add(index, element); + notifyListeners(index, null, element, ListListener.EventType.ADD); + } catch (IndexOutOfBoundsException e) { + for (int i = size(); i <= index; i++) { + super.add(null); + } + super.add(index, element); + notifyListeners(index, null, element, ListListener.EventType.ADD); + } + } + + /** + * Removes the element at the specified position in this list and sets it to null. + * If the index is out of bounds, returns null. + * + * @param index the index of the element to be removed and set to null + * @return the element previously at the specified position, or null if the index is out of bounds + */ + @Override + public E remove(int index) { + try { + E removedElement = this.set(index, null); + notifyListeners(index, removedElement, null, ListListener.EventType.REMOVE); + return removedElement; + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /** + * Register a listener to be notified of changes in the list. + * + * @param listener the listener to be registered + */ + public void addListener(ListListener listener) { + listeners.add(listener); + } + + /** + * Unregister a listener from receiving notifications of changes in the list. + * + * @param listener the listener to be unregistered + */ + public void removeListener(ListListener listener) { + listeners.remove(listener); + } + + /** + * Notify all registered listeners of a change in the list. + * + * @param index the index at which the change occurred + * @param oldValue the previous value at the index + * @param newValue the new value at the index + * @param eventType the type of event that occurred + */ + private void notifyListeners(int index, E oldValue, E newValue, ListListener.EventType eventType) { + for (ListListener listener : listeners) { + listener.onListChanged(index, oldValue, newValue, eventType); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableArrayList.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableArrayList.java new file mode 100644 index 00000000..3c14095e --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableArrayList.java @@ -0,0 +1,248 @@ +package com.georgev22.skinoverlay.datastructures.lists; + +import org.jspecify.annotations.NonNull; + +import java.util.*; +import java.util.function.Consumer; + +/** + * An unmodifiable implementation of the {@link List} interface. + * + * @param the type of elements in this list + */ +public class UnmodifiableArrayList implements List { + + private final List underlyingList; + + /** + * Constructs an unmodifiable list that is a copy of the specified collection. + * + * @param collection the collection to be copied into an unmodifiable list + */ + public UnmodifiableArrayList(Collection collection) { + this.underlyingList = new ArrayList<>(collection); + } + + @Override + public int size() { + return underlyingList.size(); + } + + @Override + public boolean isEmpty() { + return underlyingList.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return underlyingList.contains(o); + } + + @Override + public @NonNull Iterator iterator() { + return new Iterator() { + private final Iterator i = underlyingList.iterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public E next() { + return i.next(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void forEachRemaining(Consumer action) { + i.forEachRemaining(action); + } + }; + } + + @Override + public Object @NonNull [] toArray() { + return underlyingList.toArray(); + } + + @Override + public T @NonNull [] toArray(T @NonNull [] a) { + return underlyingList.toArray(a); + } + + @Override + public boolean add(E t) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public boolean containsAll(@NonNull Collection c) { + return new HashSet<>(underlyingList).containsAll(c); + } + + @Override + public boolean addAll(@NonNull Collection c) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public boolean addAll(int index, @NonNull Collection c) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public boolean removeAll(@NonNull Collection c) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public boolean retainAll(@NonNull Collection c) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + + @Override + public E get(int index) { + return underlyingList.get(index); + } + + @Override + public E set(int index, E element) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public void add(int index, E element) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public E remove(int index) { + throw new UnsupportedOperationException("Cannot modify the unmodifiable list"); + } + + @Override + public int indexOf(Object o) { + return underlyingList.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return underlyingList.lastIndexOf(o); + } + + @Override + public @NonNull ListIterator listIterator() { + return new ListIterator() { + private final ListIterator i + = underlyingList.listIterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public E next() { + return i.next(); + } + + public boolean hasPrevious() { + return i.hasPrevious(); + } + + public E previous() { + return i.previous(); + } + + public int nextIndex() { + return i.nextIndex(); + } + + public int previousIndex() { + return i.previousIndex(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void set(E e) { + throw new UnsupportedOperationException(); + } + + public void add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void forEachRemaining(Consumer action) { + i.forEachRemaining(action); + } + }; + } + + @Override + public @NonNull ListIterator listIterator(int index) { + return new ListIterator() { + private final ListIterator i + = underlyingList.listIterator(index); + + public boolean hasNext() { + return i.hasNext(); + } + + public E next() { + return i.next(); + } + + public boolean hasPrevious() { + return i.hasPrevious(); + } + + public E previous() { + return i.previous(); + } + + public int nextIndex() { + return i.nextIndex(); + } + + public int previousIndex() { + return i.previousIndex(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void set(E e) { + throw new UnsupportedOperationException(); + } + + public void add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void forEachRemaining(Consumer action) { + i.forEachRemaining(action); + } + }; + } + + @Override + public @NonNull List subList(int fromIndex, int toIndex) { + return new UnmodifiableArrayList<>(underlyingList.subList(fromIndex, toIndex)); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableNullableArrayList.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableNullableArrayList.java new file mode 100644 index 00000000..b85f87eb --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/lists/UnmodifiableNullableArrayList.java @@ -0,0 +1,228 @@ +package com.georgev22.skinoverlay.datastructures.lists; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * An unmodifiable version of the NullableArrayList class. + * + * @param the type of elements in this list + */ +public class UnmodifiableNullableArrayList extends NullableArrayList { + + /** + * Constructs an unmodifiable list backed by the specified list. + * + * @param list the list to be wrapped in an unmodifiable list + * @throws NullPointerException if the specified list is null + */ + public UnmodifiableNullableArrayList(NullableArrayList list) { + super(list); + } + + /** + * Throws UnsupportedOperationException. + * + * @param index the index at which to set the element + * @param element the element to be set + * @return the element previously at the specified position + * @throws UnsupportedOperationException always + */ + @Override + public E set(int index, E element) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param index the index at which to insert the element + * @param element the element to be inserted + * @throws UnsupportedOperationException always + */ + @Override + public void add(int index, E element) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param index the index of the element to be removed and set to null + * @return the element previously at the specified position + * @throws UnsupportedOperationException always + */ + @Override + public E remove(int index) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param listener the listener to be registered + * @throws UnsupportedOperationException always + */ + @Override + public void addListener(ListListener listener) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param listener the listener to be unregistered + * @throws UnsupportedOperationException always + */ + @Override + public void removeListener(ListListener listener) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param c the collection containing elements to be added to this list + * @return true if this list changed as a result of the call + * @throws UnsupportedOperationException always + */ + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param index the index at which to insert the first element from the specified collection + * @param c the collection containing elements to be added to this list + * @return true if this list changed as a result of the call + * @throws UnsupportedOperationException always + */ + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param c the collection containing elements to be removed from this list + * @return true if this list changed as a result of the call + * @throws UnsupportedOperationException always + */ + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param c the collection containing elements to be retained in this list + * @return true if this list changed as a result of the call + * @throws UnsupportedOperationException always + */ + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param operator the operator to apply to each element + * @throws UnsupportedOperationException always + */ + @Override + public void replaceAll(UnaryOperator operator) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @param filter a predicate which returns true for elements to be removed + * @return true if any elements were removed + * @throws UnsupportedOperationException always + */ + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Throws UnsupportedOperationException. + * + * @throws UnsupportedOperationException always + */ + @Override + public void clear() { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Returns an unmodifiable list iterator over the elements in this list. + * + * @return an unmodifiable list iterator + */ + @Override + public @NonNull ListIterator listIterator() { + return Collections.unmodifiableList(this).listIterator(); + } + + /** + * Returns an unmodifiable list iterator over the elements in this list, starting at the specified position. + * + * @param index the starting position of the iterator + * @return an unmodifiable list iterator + */ + @Override + public @NonNull ListIterator listIterator(int index) { + return Collections.unmodifiableList(this).listIterator(index); + } + + /** + * Returns an unmodifiable sublist of this list. + * + * @param fromIndex the starting index (inclusive) + * @param toIndex the ending index (exclusive) + * @return an unmodifiable sublist + */ + @Override + public @NonNull List subList(int fromIndex, int toIndex) { + return Collections.unmodifiableList(super.subList(fromIndex, toIndex)); + } + + /** + * Throws UnsupportedOperationException. + * + * @param action the action to be performed for each element + * @throws UnsupportedOperationException always + */ + @Override + public void forEach(Consumer action) { + throw new UnsupportedOperationException("Unmodifiable list"); + } + + /** + * Returns an unmodifiable view of the specified list. + * + * @param list the list to be wrapped in an unmodifiable list + * @param the type of elements in the list + * @return an unmodifiable view of the specified list + * @throws NullPointerException if the specified list is null + */ + @Contract("_ -> new") + public static @NonNull UnmodifiableNullableArrayList unmodifiableList(NullableArrayList list) { + return new UnmodifiableNullableArrayList<>(list); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMap.java new file mode 100644 index 00000000..fddc0469 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMap.java @@ -0,0 +1,1087 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import com.georgev22.skinoverlay.utilities.DeepCloner; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Abstract base class for {@link ObjectMap} implementations. + * Provides common typed getter implementations delegating to the underlying {@link Map}. + */ +@SuppressWarnings("JavadocReference") +public abstract class AbstractObjectMap implements ObjectMap { + + protected Map delegate; + + protected AbstractObjectMap(Map delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate cannot be null"); + } + + /** + * Attempts to retrieve a value associated with the specified key and cast it to the given class. + * If the value is null or cannot be cast to the specified class, null is returned. + * + * @param key the key whose associated value is to be returned + * @param clazz the class object of the type to cast the value to + * @param the type of the value to be returned + * @return the value associated with the key cast to the specified class, or null if the value is null or the cast fails + */ + public T castOrNull(Object key, Class clazz) { + //noinspection SuspiciousMethodCalls + Object val = delegate.get(key); + if (val == null) return null; + try { + return clazz.cast(val); + } catch (ClassCastException ignored) { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + /** + * {@inheritDoc} + */ + @Override + public V get(Object key) { + return delegate.get(key); + } + + /** + * {@inheritDoc} + */ + @Override + public V put(K key, V value) { + return delegate.put(key, value); + } + + /** + * {@inheritDoc} + */ + @Override + public V remove(Object key) { + return delegate.remove(key); + } + + /** + * {@inheritDoc} + */ + @Override + public void putAll(@NonNull Map m) { + delegate.putAll(m); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull Set keySet() { + return delegate.keySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull Collection values() { + return delegate.values(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull Set> entrySet() { + return delegate.entrySet(); + } + + /** + * Adds or replaces a single entry, returning this map for fluent chaining. + * + * @param key the key + * @param value the value + * @return this map + */ + @Override + public ObjectMap append(K key, V value) { + delegate.put(key, value); + return this; + } + + /** + * Adds or replaces all entries from the given map, returning this map for fluent chaining. + * + * @param map the map to append + * @return this map + */ + @Override + public ObjectMap append(Map map) { + delegate.putAll(map); + return this; + } + + /** + * Adds or replaces all entries from the given {@link ObjectMap}, returning this map for fluent chaining. + * + * @param map the map to append + * @return this map + */ + @Override + public ObjectMap append(ObjectMap map) { + delegate.putAll(map); + return this; + } + + /** + * Adds or replaces an entry if the given {@link Optional} contains a value. + * + * @param key the key + * @param value optional value (if empty, nothing is added) + * @return this map + */ + @Override + public ObjectMap append(K key, @NonNull Optional value) { + value.ifPresent(v -> delegate.put(key, v)); + return this; + } + + /** + * Adds or replaces an entry if the condition is true. + * + * @param key the key + * @param value the value + * @param ifTrue whether to insert + * @return this map + */ + @Override + public ObjectMap append(K key, V value, boolean ifTrue) { + if (ifTrue) delegate.put(key, value); + return this; + } + + /** + * Adds or replaces an entry with one of two possible values depending on the condition. + * + * @param key the key + * @param valueIfTrue value if condition is true + * @param valueIfFalse value if condition is false + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap append(K key, V valueIfTrue, V valueIfFalse, boolean ifTrue) { + if (ifTrue) delegate.put(key, valueIfTrue); + else delegate.put(key, valueIfFalse); + return this; + } + + /** + * Adds or replaces all entries from the given map if the condition is true. + * + * @param map the map + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap append(Map map, boolean ifTrue) { + if (ifTrue) delegate.putAll(map); + return this; + } + + /** + * Adds or replaces all entries from one of two maps depending on the condition. + * + * @param mapIfTrue map if condition is true + * @param mapIfFalse map if condition is false + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap append(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) delegate.putAll(mapIfTrue); + else delegate.putAll(mapIfFalse); + return this; + } + + /** + * Adds or replaces all entries from the given {@link ObjectMap} if the condition is true. + * + * @param map the map + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap append(ObjectMap map, boolean ifTrue) { + if (ifTrue) delegate.putAll(map); + return this; + } + + /** + * Adds or replaces all entries from one of two maps depending on the condition. + * + * @param mapIfTrue map if condition is true + * @param mapIfFalse map if condition is false + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap append(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + if (ifTrue) delegate.putAll(mapIfTrue); + else delegate.putAll(mapIfFalse); + return this; + } + + /** + * Removes an entry by key. + * + * @param key the key to remove + * @return this map + */ + @Override + public ObjectMap removeEntry(K key) { + delegate.remove(key); + return this; + } + + /** + * Removes all entries with keys present in the given map. + * + * @param map the map whose keys should be removed + * @return this map + */ + @Override + public ObjectMap removeEntries(Map map) { + delegate.keySet().removeAll(map.keySet()); + return this; + } + + /** + * Removes all entries with keys present in the given {@link ObjectMap}. + * + * @param map the map whose keys should be removed + * @return this map + */ + @Override + public ObjectMap removeEntries(ObjectMap map) { + delegate.keySet().removeAll(map.keySet()); + return this; + } + + /** + * Removes an entry if the condition is true. + * + * @param key the key to remove + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap removeEntry(K key, boolean ifTrue) { + if (ifTrue) delegate.remove(key); + return this; + } + + /** + * Removes all entries from the given map if the condition is true. + * + * @param map the map whose keys should be removed + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap removeEntries(Map map, boolean ifTrue) { + if (ifTrue) delegate.keySet().removeAll(map.keySet()); + return this; + } + + /** + * Removes all entries from the given {@link ObjectMap} if the condition is true. + * + * @param map the map whose keys should be removed + * @param ifTrue condition + * @return this map + */ + @Override + public ObjectMap removeEntries(ObjectMap map, boolean ifTrue) { + if (ifTrue) delegate.keySet().removeAll(map.keySet()); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Long getLong(Object key) { + return castOrNull(key, Long.class); + } + + /** + * {@inheritDoc} + */ + @Override + public Long getLong(Object key, long defaultValue) { + return Optional.ofNullable(getLong(key)).orElse(defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public Double getDouble(Object key) { + return castOrNull(key, Double.class); + } + + /** + * {@inheritDoc} + */ + @Override + public Double getDouble(Object key, double defaultValue) { + return Optional.ofNullable(getDouble(key)).orElse(defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public Date getDate(Object key) { + return castOrNull(key, Date.class); + } + + /** + * {@inheritDoc} + */ + @Override + public Date getDate(Object key, Date defaultValue) { + return Optional.ofNullable(getDate(key)).orElse(defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public List getList(Object key, Class clazz) { + //noinspection SuspiciousMethodCalls + Object val = delegate.get(key); + if (val == null) return null; + if (!(val instanceof List list)) { + throw new ClassCastException("Value is not a List for key: " + key); + } + for (Object obj : list) { + if (!clazz.isInstance(obj)) { + throw new ClassCastException( + "List element is not of type " + clazz.getName() + " for key: " + key + ); + } + } + //noinspection unchecked + return (List) list; + } + + /** + * {@inheritDoc} + */ + @Override + public List getList(Object key, Class clazz, List defaultValue) { + List list = getList(key, clazz); + return list != null ? list : defaultValue; + } + + /** + * {@inheritDoc} + */ + @Override + public Integer getInteger(Object key) { + return castOrNull(key, Integer.class); + } + + /** + * {@inheritDoc} + */ + @Override + public int getInteger(Object key, int defaultValue) { + return Optional.ofNullable(getInteger(key)).orElse(defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public String getString(Object key) { + return castOrNull(key, String.class); + } + + /** + * {@inheritDoc} + */ + @Override + public String getString(Object key, String defaultValue) { + return Optional.ofNullable(getString(key)).orElse(defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public Boolean getBoolean(Object key) { + return castOrNull(key, Boolean.class); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getBoolean(Object key, boolean defaultValue) { + return Optional.ofNullable(getBoolean(key)).orElse(defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public T get(Object key, Class clazz) { + return castOrNull(key, clazz); + } + + /** + * {@inheritDoc} + */ + @Override + public T get(Object key, T defaultValue) { + //noinspection SuspiciousMethodCalls + Object val = delegate.get(key); + //noinspection unchecked + return (T) (val != null ? val : defaultValue); + } + + + /** + * Returns the value to which the specified key is mapped, or + * {@code defaultValue} if this map contains no mapping for the key. + * + * @param key the key whose associated value is to be returned + * @param defaultValue the default mapping of the key + * @return the value to which the specified key is mapped, or + * {@code defaultValue} if this map contains no mapping for the key + * @throws ClassCastException if the key is of an inappropriate type for + * this map ({@linkplain Collection##optional-restrictions optional}) + * @throws NullPointerException if the specified key is null and this map + * does not permit null keys ({@linkplain Collection##optional-restrictions optional}) + * @implSpec The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public V getOrDefault(Object key, V defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + /** + * Performs the given action for each entry in this map until all entries + * have been processed or the action throws an exception. Unless + * otherwise specified by the implementing class, actions are performed in + * the order of entry set iteration (if an iteration order is specified.) + * Exceptions thrown by the action are relayed to the caller. + * + * @param action The action to be performed for each entry + * @throws NullPointerException if the specified action is null + * @throws ConcurrentModificationException if an entry is found to be + * removed during iteration + * @implSpec The default implementation is equivalent to, for this {@code map}: + *
 {@code
+     * for (Map.Entry entry : map.entrySet())
+     *     action.accept(entry.getKey(), entry.getValue());
+     * }
+ *

+ * The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public void forEach(BiConsumer action) { + delegate.forEach(action); + } + + /** + * Replaces each entry's value with the result of invoking the given + * function on that entry until all entries have been processed or the + * function throws an exception. Exceptions thrown by the function are + * relayed to the caller. + * + * @param function the function to apply to each entry + * @throws UnsupportedOperationException if the {@code set} operation + * is not supported by this map's entry set iterator. + * @throws ClassCastException if the class of a replacement value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws NullPointerException if the specified function is null, or if a + * replacement value is null and this map does not permit null values + * ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of a replacement value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ConcurrentModificationException if an entry is found to be + * removed during iteration + * @implSpec

The default implementation is equivalent to, for this {@code map}: + *

 {@code
+     * for (Map.Entry entry : map.entrySet())
+     *     entry.setValue(function.apply(entry.getKey(), entry.getValue()));
+     * }
+ * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public void replaceAll(BiFunction function) { + delegate.replaceAll(function); + } + + /** + * If the specified key is not already associated with a value (or is mapped + * to {@code null}) associates it with the given value and returns + * {@code null}, else returns the current value. + * + * @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. + * (A {@code null} return can also indicate that the map + * previously associated {@code null} with the key, + * if the implementation supports null values.) + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the key or value is of an inappropriate + * type for this map ({@linkplain Collection##optional-restrictions optional}) + * @throws NullPointerException if the specified key or value is null, + * and this map does not permit null keys or values + * ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @implSpec The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * V v = map.get(key);
+     * if (v == null)
+     *     v = map.put(key, value);
+     *
+     * return v;
+     * }
+ * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public @Nullable V putIfAbsent(K key, V value) { + return delegate.putIfAbsent(key, value); + } + + /** + * Removes the entry for the specified key only if it is currently + * mapped to the specified 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 + * @throws UnsupportedOperationException if the {@code remove} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the key or value is of an inappropriate + * type for this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws NullPointerException if the specified key or value is null, + * and this map does not permit null keys or values + * ({@linkplain Collection##optional-restrictions optional}) + * @implSpec The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
+     *     map.remove(key);
+     *     return true;
+     * } else
+     *     return false;
+     * }
+ * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public boolean remove(Object key, Object value) { + return delegate.remove(key, value); + } + + /** + * Replaces the entry for the specified key only if currently + * mapped to the specified 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 + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the class of a specified key or value + * prevents it from being stored in this map + * @throws NullPointerException if a specified key or newValue is null, + * and this map does not permit null keys or values + * @throws NullPointerException if oldValue is null and this map does not + * permit null values ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of a specified key + * or value prevents it from being stored in this map + * @implSpec The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * if (map.containsKey(key) && Objects.equals(map.get(key), oldValue)) {
+     *     map.put(key, newValue);
+     *     return true;
+     * } else
+     *     return false;
+     * }
+ *

+ * The default implementation does not throw NullPointerException + * for maps that do not support null values if oldValue is null unless + * newValue is also null. + * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public boolean replace(K key, V oldValue, V newValue) { + return delegate.replace(key, oldValue, newValue); + } + + /** + * Replaces the entry for the specified key only if it is + * 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. + * (A {@code null} return can also indicate that the map + * previously associated {@code null} with the key, + * if the implementation supports null values.) + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @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 + * @implSpec The default implementation is equivalent to, for this {@code map}: + * + *

 {@code
+     * if (map.containsKey(key)) {
+     *     return map.put(key, value);
+     * } else
+     *     return null;
+     * }
+ * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. + * @since 1.8 + */ + @Override + public @Nullable V replace(K key, V value) { + return delegate.replace(key, value); + } + + /** + * If the specified key is not already associated with a value (or is mapped + * to {@code null}), attempts to compute its value using the given mapping + * function and enters it into this map unless {@code null}. + * + *

If the mapping function returns {@code null}, no mapping is recorded. + * If the mapping function itself throws an (unchecked) exception, the + * exception is rethrown, and no mapping is recorded. The most + * common usage is to construct a new object serving as an initial + * mapped value or memoized result, as in: + * + *

 {@code
+     * map.computeIfAbsent(key, k -> new Value(f(k)));
+     * }
+ * + *

Or to implement a multi-value map, {@code Map>}, + * supporting multiple values per key: + * + *

 {@code
+     * map.computeIfAbsent(key, k -> new HashSet()).add(v);
+     * }
+ * + *

The mapping function should not modify this map during computation. + * + * @param key key with which the specified value is to be associated + * @param mappingFunction the mapping function to compute a value + * @return the current (existing or computed) value associated with + * the specified key, or null if the computed value is null + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the mappingFunction + * is null + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @implSpec The default implementation is equivalent to the following steps for this + * {@code map}, then returning the current value or {@code null} if now + * absent: + * + *

 {@code
+     * if (map.get(key) == null) {
+     *     V newValue = mappingFunction.apply(key);
+     *     if (newValue != null)
+     *         map.put(key, newValue);
+     * }
+     * }
+ * + *

The default implementation makes no guarantees about detecting if the + * mapping function modifies this map during computation and, if + * appropriate, reporting an error. Non-concurrent implementations should + * override this method and, on a best-effort basis, throw a + * {@code ConcurrentModificationException} if it is detected that the + * mapping function modifies this map during computation. Concurrent + * implementations should override this method and, on a best-effort basis, + * throw an {@code IllegalStateException} if it is detected that the + * mapping function modifies this map during computation and as a result + * computation would never complete. + * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. In particular, all implementations of + * subinterface {@link ConcurrentMap} must document + * whether the mapping function is applied once atomically only if the value + * is not present. + * @since 1.8 + */ + @Override + public V computeIfAbsent(K key, @NonNull Function mappingFunction) { + return delegate.computeIfAbsent(key, mappingFunction); + } + + /** + * If the value for the specified key is present and non-null, attempts to + * compute a new mapping given the key and its current mapped value. + * + *

If the remapping function returns {@code null}, the mapping is removed. + * If the remapping function itself throws an (unchecked) exception, the + * exception is rethrown, and the current mapping is left unchanged. + * + *

The remapping function should not modify this map during computation. + * + * @param key key with which the specified value is to be associated + * @param remappingFunction the remapping function to compute a value + * @return the new value associated with the specified key, or null if none + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the + * remappingFunction is null + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @implSpec The default implementation is equivalent to performing the following + * steps for this {@code map}, then returning the current value or + * {@code null} if now absent: + * + *

 {@code
+     * if (map.get(key) != null) {
+     *     V oldValue = map.get(key);
+     *     V newValue = remappingFunction.apply(key, oldValue);
+     *     if (newValue != null)
+     *         map.put(key, newValue);
+     *     else
+     *         map.remove(key);
+     * }
+     * }
+ * + *

The default implementation makes no guarantees about detecting if the + * remapping function modifies this map during computation and, if + * appropriate, reporting an error. Non-concurrent implementations should + * override this method and, on a best-effort basis, throw a + * {@code ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. Concurrent + * implementations should override this method and, on a best-effort basis, + * throw an {@code IllegalStateException} if it is detected that the + * remapping function modifies this map during computation and as a result + * computation would never complete. + * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. In particular, all implementations of + * subinterface {@link ConcurrentMap} must document + * whether the remapping function is applied once atomically only if the + * value is not present. + * @since 1.8 + */ + @Override + public V computeIfPresent(K key, @NonNull BiFunction remappingFunction) { + return delegate.computeIfPresent(key, remappingFunction); + } + + /** + * Attempts to compute a mapping for the specified key and its current + * mapped value (or {@code null} if there is no current mapping). For + * example, to either create or append a {@code String} msg to a value + * mapping: + * + *

 {@code
+     * map.compute(key, (k, v) -> (v == null) ? msg : v.concat(msg))}
+ * (Method {@link #merge merge()} is often simpler to use for such purposes.) + * + *

If the remapping function returns {@code null}, the mapping is removed + * (or remains absent if initially absent). If the remapping function + * itself throws an (unchecked) exception, the exception is rethrown, and + * the current mapping is left unchanged. + * + *

The remapping function should not modify this map during computation. + * + * @param key key with which the specified value is to be associated + * @param remappingFunction the remapping function to compute a value + * @return the new value associated with the specified key, or null if none + * @throws NullPointerException if the specified key is null and + * this map does not support null keys, or the + * remappingFunction is null + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @implSpec The default implementation is equivalent to performing the following + * steps for this {@code map}: + * + *

 {@code
+     * V oldValue = map.get(key);
+     * V newValue = remappingFunction.apply(key, oldValue);
+     * if (newValue != null) {
+     *     map.put(key, newValue);
+     * } else if (oldValue != null || map.containsKey(key)) {
+     *     map.remove(key);
+     * }
+     * return newValue;
+     * }
+ * + *

The default implementation makes no guarantees about detecting if the + * remapping function modifies this map during computation and, if + * appropriate, reporting an error. Non-concurrent implementations should + * override this method and, on a best-effort basis, throw a + * {@code ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. Concurrent + * implementations should override this method and, on a best-effort basis, + * throw an {@code IllegalStateException} if it is detected that the + * remapping function modifies this map during computation and as a result + * computation would never complete. + * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. In particular, all implementations of + * subinterface {@link ConcurrentMap} must document + * whether the remapping function is applied once atomically only if the + * value is not present. + * @since 1.8 + */ + @Override + public V compute(K key, @NonNull BiFunction remappingFunction) { + return delegate.compute(key, remappingFunction); + } + + /** + * If the specified key is not already associated with a value or is + * associated with null, associates it with the given non-null value. + * Otherwise, replaces the associated value with the results of the given + * remapping function, or removes if the result is {@code null}. This + * method may be of use when combining multiple mapped values for a key. + * For example, to either create or append a {@code String msg} to a + * value mapping: + * + *

 {@code
+     * map.merge(key, msg, String::concat)
+     * }
+ * + *

If the remapping function returns {@code null}, the mapping is removed. + * If the remapping function itself throws an (unchecked) exception, the + * exception is rethrown, and the current mapping is left unchanged. + * + *

The remapping function should not modify this map during computation. + * + * @param key key with which the resulting value is to be associated + * @param value the non-null value to be merged with the existing value + * associated with the key or, if no existing value or a null value + * is associated with the key, to be associated with the key + * @param remappingFunction the remapping function to recompute a value if + * present + * @return the new value associated with the specified key, or null if no + * value is associated with the key + * @throws UnsupportedOperationException if the {@code put} operation + * is not supported by this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws ClassCastException if the class of the specified key or value + * prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws IllegalArgumentException if some property of the specified key + * or value prevents it from being stored in this map + * ({@linkplain Collection##optional-restrictions optional}) + * @throws NullPointerException if the specified key is null and this map + * does not support null keys or the value or remappingFunction is + * null + * @implSpec The default implementation is equivalent to performing the following + * steps for this {@code map}, then returning the current value or + * {@code null} if absent: + * + *

 {@code
+     * V oldValue = map.get(key);
+     * V newValue = (oldValue == null) ? value :
+     *              remappingFunction.apply(oldValue, value);
+     * if (newValue == null)
+     *     map.remove(key);
+     * else
+     *     map.put(key, newValue);
+     * }
+ * + *

The default implementation makes no guarantees about detecting if the + * remapping function modifies this map during computation and, if + * appropriate, reporting an error. Non-concurrent implementations should + * override this method and, on a best-effort basis, throw a + * {@code ConcurrentModificationException} if it is detected that the + * remapping function modifies this map during computation. Concurrent + * implementations should override this method and, on a best-effort basis, + * throw an {@code IllegalStateException} if it is detected that the + * remapping function modifies this map during computation and as a result + * computation would never complete. + * + *

The default implementation makes no guarantees about synchronization + * or atomicity properties of this method. Any implementation providing + * atomicity guarantees must override this method and document its + * concurrency properties. In particular, all implementations of + * subinterface {@link ConcurrentMap} must document + * whether the remapping function is applied once atomically only if the + * value is not present. + * @since 1.8 + */ + @Override + public V merge(K key, @NonNull V value, @NonNull BiFunction remappingFunction) { + return delegate.merge(key, value, remappingFunction); + } + + private void putDelegate(K key, V value) { + delegate.put(key, value); + } + + private void putAllDelegate(Map m) { + delegate.putAll(m); + } + + /** + * {@inheritDoc} + */ + @Override + public abstract AbstractObjectMap newObjectMap(); + + /** + * {@inheritDoc} + */ + @Override + public @NonNull ObjectMap shallowCopy() { + AbstractObjectMap copy = newObjectMap(); + copy.putAllDelegate(this); + return copy; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public @NonNull ObjectMap deepCopy() { + AbstractObjectMap copy = newObjectMap(); + for (Entry entry : entrySet()) { + try { + copy.putDelegate( + (K) DeepCloner.cloneValue(entry.getKey()), + (V) DeepCloner.cloneValue(entry.getValue())); + } catch (UnsupportedOperationException e) { + break; // target map does not support modifications + } catch (Exception ignored) { + } + } + return copy; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMultiMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMultiMap.java new file mode 100644 index 00000000..3683c2c4 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/AbstractObjectMultiMap.java @@ -0,0 +1,132 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import org.jspecify.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Base class for MultiMaps, supporting multiple values per key. + *

+ * Subclasses must define the type of collection to store the values + * (e.g., List or Set) by implementing {@link #createEmptyCollection()}. + * + * @param key type + * @param value type + * @param the concrete subclass type for fluent API + */ +public abstract class AbstractObjectMultiMap> + extends AbstractObjectMap> { + + protected AbstractObjectMultiMap(Map> map) { + super(map); + } + + /** + * Subclasses must provide the type of collection used to store values. + * For example, a ListMultiMap would return new ArrayList<>(), a SetMultiMap would return new HashSet<>(). + * + * @return a new empty collection for values + */ + protected abstract Collection createEmptyCollection(); + + /** + * Returns {@code this} cast to the concrete subclass type. + */ + @SuppressWarnings("unchecked") + protected M self() { + return (M) this; + } + + /** + * Adds a value to the collection associated with the given key. + * + * @param key the key + * @param value the value to add + * @return this map for fluent chaining + */ + public M appendValue(K key, V value) { + delegate.computeIfAbsent(key, k -> createEmptyCollection()).add(value); + return self(); + } + + /** + * Adds multiple values to the collection associated with the given key. + * + * @param key the key + * @param values the values to add + * @return this map for fluent chaining + */ + public M appendValues(K key, Collection values) { + delegate.computeIfAbsent(key, k -> createEmptyCollection()).addAll(values); + return self(); + } + + /** + * Removes a value from the collection associated with the given key. + * If the collection becomes empty, the key is removed from the map. + * + * @param key the key + * @param value the value to remove + * @return true if the value was removed, false otherwise + */ + public boolean removeValue(K key, V value) { + Collection values = delegate.get(key); + if (values != null) { + boolean removed = values.remove(value); + if (values.isEmpty()) delegate.remove(key); + return removed; + } + return false; + } + + /** + * Retrieves a specific element from the collection associated with the given key. + * + *

If the key exists and the collection contains the element, the element is returned. + * If the key does not exist or the element is not present, {@code null} is returned.

+ * + * @param key the key whose associated collection to search + * @param element the element to find in the collection + * @return the element if found, or {@code null} if not found + */ + public @Nullable V getValue(K key, V element) { + Collection values = delegate.get(key); + if (values != null) { + for (V v : values) { + if (Objects.equals(v, element)) { + return v; + } + } + } + return null; + } + + /** + * Retrieves a specific element from the collection associated with the given key, + * wrapped in an {@link Optional}. + * + *

If the key exists and the collection contains the element, an {@link Optional} + * containing the element is returned. If the key does not exist or the element is not + * present, {@link Optional#empty()} is returned.

+ * + * @param key the key whose associated collection to search + * @param element the element to find in the collection + * @return an {@link Optional} containing the element if found, or {@link Optional#empty()} if not found + */ + public Optional getValueOptional(K key, V element) { + return Optional.ofNullable(getValue(key, element)); + } + + /** + * Retrieves the collection of values associated with the given key. + * + * @param key the key + * @return the collection of values, or an empty collection if none exist + */ + public Collection getValues(K key) { + return delegate.computeIfAbsent(key, k -> createEmptyCollection()); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ConcurrentHashObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ConcurrentHashObjectMap.java new file mode 100644 index 00000000..5f70db48 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ConcurrentHashObjectMap.java @@ -0,0 +1,101 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A thread-safe implementation of {@link ObjectMap} backed by a {@link ConcurrentHashMap}. + *

+ * This class supports multiple ways of construction: + *

    + *
  • Create a new empty backing map.
  • + *
  • Create a new backing map initialized with another map.
  • + *
  • Wrap (delegate to) an existing {@link ConcurrentHashMap} without copying.
  • + *
  • Create a new backing map with a specified initial capacity and/or load factor.
  • + *
+ * + * @param the type of keys + * @param the type of values + */ +public class ConcurrentHashObjectMap extends AbstractObjectMap implements ObjectMap { + + private int initialCapacity = -1; + private float loadFactor = -1; + + /** + * Creates an empty {@code ConcurrentHashObjectMap} with a new {@link ConcurrentHashMap}. + */ + public ConcurrentHashObjectMap() { + super(new ConcurrentHashMap<>()); + } + + /** + * Creates a new {@code ConcurrentHashObjectMap} by copying all mappings + * from the given {@link ObjectMap} into a new {@link ConcurrentHashMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public ConcurrentHashObjectMap(final ObjectMap map) { + super(new ConcurrentHashMap<>(map)); + } + + /** + * Creates a new {@code ConcurrentHashObjectMap} by copying all mappings + * from the given {@link Map} into a new {@link ConcurrentHashMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public ConcurrentHashObjectMap(final Map map) { + super(new ConcurrentHashMap<>(map)); + } + + /** + * Creates a {@code ConcurrentHashObjectMap} that directly delegates + * to the provided {@link ConcurrentHashMap} without copying. + *

+ * Changes in this instance will directly affect the given map. + * + * @param backingMap the {@link ConcurrentHashMap} to delegate to + */ + public ConcurrentHashObjectMap(final ConcurrentHashMap backingMap) { + super(backingMap); + } + + /** + * Constructs a new {@code ConcurrentHashObjectMap} with the specified initial capacity. + * + * @param initialCapacity the initial capacity of the map + */ + public ConcurrentHashObjectMap(final int initialCapacity) { + super(new ConcurrentHashMap<>(initialCapacity)); + this.initialCapacity = initialCapacity; + } + + /** + * Constructs a new {@code ConcurrentHashObjectMap} with the specified + * initial capacity and load factor. + * + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor threshold for resizing + */ + public ConcurrentHashObjectMap(final int initialCapacity, final float loadFactor) { + super(new ConcurrentHashMap<>(initialCapacity, loadFactor)); + this.initialCapacity = initialCapacity; + this.loadFactor = loadFactor; + } + + /** + * {@inheritDoc} + *

+ * Returns a new empty {@code ConcurrentHashObjectMap} of the same type. + * Capacity and load factor are preserved if they were explicitly set. + */ + @Override + public ConcurrentHashObjectMap newObjectMap() { + return (initialCapacity > 0) + ? (loadFactor > 0 + ? new ConcurrentHashObjectMap<>(initialCapacity, loadFactor) + : new ConcurrentHashObjectMap<>(initialCapacity)) + : new ConcurrentHashObjectMap<>(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/EnumObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/EnumObjectMap.java new file mode 100644 index 00000000..64d9e4d0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/EnumObjectMap.java @@ -0,0 +1,55 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import java.util.EnumMap; +import java.util.Map; + +public class EnumObjectMap, V> extends AbstractObjectMap implements ObjectMap { + + private final Class kClass; + + /** + * Creates an empty {@code EnumObjectMap} with a new {@link EnumMap}. + */ + public EnumObjectMap(Class kClass) { + super(new EnumMap<>(kClass)); + this.kClass = kClass; + } + + /** + * Creates a new {@code EnumObjectMap} by copying all mappings + * from the given {@link EnumMap} into a new {@link EnumMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public EnumObjectMap(Class kClass, EnumMap map) { + super(new EnumMap<>(map)); + this.kClass = kClass; + } + + /** + * Creates a new {@code EnumObjectMap} by copying all mappings + * from the given {@link ObjectMap} into a new {@link EnumMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public EnumObjectMap(Class kClass, final ObjectMap map) { + super(new EnumMap<>(map)); + this.kClass = kClass; + } + + /** + * Creates a new {@code EnumObjectMap} by copying all mappings + * from the given {@link Map} into a new {@link EnumMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public EnumObjectMap(Class kClass, final Map map) { + super(new EnumMap<>(map)); + this.kClass = kClass; + } + + @Override + public EnumObjectMap newObjectMap() { + return new EnumObjectMap<>(kClass); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/HashObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/HashObjectMap.java new file mode 100644 index 00000000..eff82661 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/HashObjectMap.java @@ -0,0 +1,138 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import java.util.HashMap; +import java.util.Map; + +/** + * A non-thread-safe implementation of {@link ObjectMap} backed by a {@link HashMap}. + *

+ * This class supports multiple construction modes: + *

    + *
  • Create a new empty backing {@link HashMap}.
  • + *
  • Create a new backing {@link HashMap} initialized with another map (copy constructor).
  • + *
  • Wrap (delegate to) an existing {@link HashMap} without copying.
  • + *
  • Create a new backing {@link HashMap} with a specified initial capacity and/or load factor.
  • + *
+ *

+ * Implements {@link Cloneable} for shallow cloning of the map. + * + * @param the type of keys + * @param the type of values + */ +public class HashObjectMap extends AbstractObjectMap implements ObjectMap, Cloneable { + + private int initialCapacity = -1; + private float loadFactor = -1; + + /** + * Creates an empty {@code HashObjectMap} with a new {@link HashMap}. + */ + public HashObjectMap() { + super(new HashMap<>()); + } + + /** + * Creates a new {@code HashObjectMap} by copying all mappings + * from the given {@link ObjectMap} into a new {@link HashMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public HashObjectMap(final ObjectMap map) { + super(new HashMap<>(map)); + } + + /** + * Creates a new {@code HashObjectMap} by copying all mappings + * from the given {@link Map} into a new {@link HashMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public HashObjectMap(final Map map) { + super(new HashMap<>(map)); + } + + /** + * Creates a {@code HashObjectMap} that directly delegates + * to the provided {@link HashMap} without copying. + *

+ * Changes in this instance will directly affect the given map. + * + * @param backingMap the {@link HashMap} to delegate to + */ + public HashObjectMap(final HashMap backingMap) { + super(backingMap); + } + + /** + * Constructs a new {@code HashObjectMap} with the specified initial capacity. + * + * @param initialCapacity the initial capacity of the map + */ + public HashObjectMap(final int initialCapacity) { + super(new HashMap<>(initialCapacity)); + this.initialCapacity = initialCapacity; + } + + /** + * Constructs a new {@code HashObjectMap} with the specified + * initial capacity and load factor. + * + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor threshold for resizing + */ + public HashObjectMap(final int initialCapacity, final float loadFactor) { + super(new HashMap<>(initialCapacity, loadFactor)); + this.initialCapacity = initialCapacity; + this.loadFactor = loadFactor; + } + + /** + * {@inheritDoc} + *

+ * Returns a new empty {@code HashObjectMap} of the same type. + * Capacity and load factor are preserved if they were explicitly set. + */ + @Override + public HashObjectMap newObjectMap() { + return (initialCapacity > 0) + ? (loadFactor > 0 + ? new HashObjectMap<>(initialCapacity, loadFactor) + : new HashObjectMap<>(initialCapacity)) + : new HashObjectMap<>(); + } + + /** + * Creates a shallow clone of this {@link HashObjectMap}. + *

+ * The returned {@code HashObjectMap} is a new instance with its own + * backing {@link HashMap}, so modifications to the clone's entries + * do not affect the original map and vice versa. + *

+ * Note: This is a shallow copy. The keys and values themselves + * are not cloned; if they are mutable objects, changes to them will + * be reflected in both the original and the clone. + *

+ * If a deep copy is required, use the {@link #deepCopy()} method + * provided by the {@link ObjectMap} + * interface (which extends {@link com.georgev22.skinoverlay.utilities.Copyable}). + * + * @return a shallow clone of this {@code HashObjectMap} + */ + @Override + public HashObjectMap clone() { + try { + @SuppressWarnings("unchecked") + HashObjectMap clone = (HashObjectMap) super.clone(); + if (this.delegate instanceof HashMap hashMap) { + //noinspection unchecked + clone.delegate = (HashMap) hashMap.clone(); + } else { + clone.delegate = new HashMap<>(this.delegate); + } + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); // should never happen + } + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/LinkedHashObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/LinkedHashObjectMap.java new file mode 100644 index 00000000..71549416 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/LinkedHashObjectMap.java @@ -0,0 +1,223 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import org.jspecify.annotations.NonNull; + +import java.util.*; + +/** + * An implementation of {@link ObjectMap} backed by a {@link LinkedHashMap}. + *

+ * This class preserves insertion order and supports the {@link SequencedMap} + * API introduced in Java 21. + *

+ * Construction options include: + *

    + *
  • Create a new empty backing {@link LinkedHashMap}.
  • + *
  • Create a new backing {@link LinkedHashMap} initialized with another map (copy constructor).
  • + *
  • Wrap (delegate to) an existing {@link LinkedHashMap} without copying.
  • + *
  • Create a new backing {@link LinkedHashMap} with a specified initial capacity and/or load factor.
  • + *
+ *

+ * Implements {@link Cloneable} for shallow cloning of the map. + * + * @param the type of keys + * @param the type of values + */ +public class LinkedHashObjectMap extends AbstractObjectMap + implements ObjectMap, SequencedMap, Cloneable { + + private int initialCapacity = -1; + private float loadFactor = -1; + + /** + * Creates an empty {@code LinkedHashObjectMap} with a new {@link LinkedHashMap}. + */ + public LinkedHashObjectMap() { + super(new LinkedHashMap<>()); + } + + /** + * Creates a new {@code LinkedHashObjectMap} by copying all mappings + * from the given {@link ObjectMap} into a new {@link LinkedHashMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public LinkedHashObjectMap(final ObjectMap map) { + super(new LinkedHashMap<>(map)); + } + + /** + * Creates a new {@code LinkedHashObjectMap} by copying all mappings + * from the given {@link Map} into a new {@link LinkedHashMap}. + * + * @param map the map whose mappings are to be placed in this map + */ + public LinkedHashObjectMap(final Map map) { + super(new LinkedHashMap<>(map)); + } + + /** + * Creates a {@code LinkedHashObjectMap} that directly delegates + * to the provided {@link LinkedHashMap} without copying. + *

+ * Changes in this instance will directly affect the given map. + * + * @param backingMap the {@link LinkedHashMap} to delegate to + */ + public LinkedHashObjectMap(final LinkedHashMap backingMap) { + super(backingMap); + } + + /** + * Constructs a new {@code LinkedHashObjectMap} with the specified initial capacity. + * + * @param initialCapacity the initial capacity of the map + */ + public LinkedHashObjectMap(final int initialCapacity) { + super(new LinkedHashMap<>(initialCapacity)); + this.initialCapacity = initialCapacity; + } + + /** + * Constructs a new {@code LinkedHashObjectMap} with the specified + * initial capacity and load factor. + * + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor threshold for resizing + */ + public LinkedHashObjectMap(final int initialCapacity, final float loadFactor) { + super(new LinkedHashMap<>(initialCapacity, loadFactor)); + this.initialCapacity = initialCapacity; + this.loadFactor = loadFactor; + } + + /** + * {@inheritDoc} + *

+ * Returns a new empty {@code LinkedHashObjectMap} of the same type. + * Capacity and load factor are preserved if they were explicitly set. + */ + @Override + public LinkedHashObjectMap newObjectMap() { + return (initialCapacity > 0) + ? (loadFactor > 0 + ? new LinkedHashObjectMap<>(initialCapacity, loadFactor) + : new LinkedHashObjectMap<>(initialCapacity)) + : new LinkedHashObjectMap<>(); + } + + /** + * {@inheritDoc} + */ + @Override + public SequencedMap reversed() { + return ((SequencedMap) delegate).reversed(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry firstEntry() { + return ((SequencedMap) delegate).firstEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry lastEntry() { + return ((SequencedMap) delegate).lastEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry pollFirstEntry() { + return ((SequencedMap) delegate).pollFirstEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry pollLastEntry() { + return ((SequencedMap) delegate).pollLastEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public V putFirst(K k, V v) { + return ((SequencedMap) delegate).putFirst(k, v); + } + + /** + * {@inheritDoc} + */ + @Override + public V putLast(K k, V v) { + return ((SequencedMap) delegate).putLast(k, v); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SequencedSet sequencedKeySet() { + return ((SequencedMap) delegate).sequencedKeySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SequencedCollection sequencedValues() { + return ((SequencedMap) delegate).sequencedValues(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SequencedSet> sequencedEntrySet() { + return ((SequencedMap) delegate).sequencedEntrySet(); + } + + /** + * Creates a shallow clone of this {@link LinkedHashObjectMap}. + *

+ * The returned {@code LinkedHashObjectMap} is a new instance with its own + * backing {@link LinkedHashMap}, so modifications to the clone's entries + * do not affect the original map and vice versa. + *

+ * Note: This is a shallow copy. The keys and values themselves + * are not cloned; if they are mutable objects, changes to them will + * be reflected in both the original and the clone. + *

+ * If a deep copy is required, use the {@link #deepCopy()} method + * provided by the {@link ObjectMap} + * interface (which extends {@link com.georgev22.skinoverlay.utilities.Copyable}). + * + * @return a shallow clone of this {@code LinkedHashObjectMap} + */ + @Override + public LinkedHashObjectMap clone() { + try { + //noinspection unchecked + LinkedHashObjectMap clone = (LinkedHashObjectMap) super.clone(); + if (this.delegate instanceof LinkedHashMap linkedHashMap) { + //noinspection unchecked + clone.delegate = (LinkedHashMap) linkedHashMap.clone(); + } else { + clone.delegate = new LinkedHashMap<>(this.delegate); + } + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); // should never happen + } + } + +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMap.java new file mode 100644 index 00000000..fe385580 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMap.java @@ -0,0 +1,513 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import com.georgev22.skinoverlay.utilities.Copyable; +import com.georgev22.skinoverlay.utilities.DeepCloner; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +/** + * An extension of the {@link Map} interface with additional utilities for + * fluent operations, conditional modifications, type-safe retrieval, and cloning. + * + *

This interface is the common base for specialized {@code ObjectMap} + * implementations such as {@link LinkedHashObjectMap}, {@link ConcurrentHashObjectMap}, + * {@link HashObjectMap}, and {@link TreeObjectMap}. + * + * @param the key type + * @param the value type + */ +public interface ObjectMap extends Map, Copyable> { + + /** + * Creates an unmodifiable {@link ObjectMap} containing the given entries. + *

+ * This method allows you to create a map from a variable number of key-value entries. + * The returned map cannot be modified; attempts to put, remove, or change entries + * will throw an {@link UnsupportedOperationException}. + *

+ * + * @param the type of keys maintained by the map + * @param the type of mapped values + * @param entries the key-value entries to include in the map; must not be {@code null} + * @return a new unmodifiable {@link ObjectMap} containing the specified entries + * @implNote The returned map is an instance of {@link UnmodifiableObjectMap}. + * @see UnmodifiableObjectMap + */ + @Contract("_ -> new") + @SafeVarargs + static @NonNull ObjectMap ofEntries(Entry @NonNull ... entries) { + //noinspection ConstantValue + if (entries == null) throw new IllegalArgumentException("Entries cannot be null"); + for (Entry entry : entries) { + if (entry == null) throw new IllegalArgumentException("Entries cannot be null"); + if (entry.getKey() == null) throw new IllegalArgumentException("Keys cannot be null"); + if (entry.getValue() == null) throw new IllegalArgumentException("Values cannot be null"); + } + return new UnmodifiableObjectMap<>(entries); + } + + @Contract(value = "_, _ -> new", pure = true) + static @NonNull Entry entry(K k, V v) { + return new AbstractMap.SimpleImmutableEntry<>(k, v); + } + + /** + * Adds or replaces a single entry, returning this map for fluent chaining. + * + * @param key the key + * @param value the value + * @return this map + */ + ObjectMap append(final K key, final V value); + + /** + * Adds or replaces all entries from the given map, returning this map for fluent chaining. + * + * @param map the map to append + * @return this map + */ + ObjectMap append(final Map map); + + /** + * Adds or replaces all entries from the given {@link ObjectMap}, returning this map for fluent chaining. + * + * @param map the map to append + * @return this map + */ + ObjectMap append(final ObjectMap map); + + /** + * Adds or replaces an entry if the given {@link Optional} contains a value. + * + * @param key the key + * @param value optional value (if empty, nothing is added) + * @return this map + */ + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + default ObjectMap append(final K key, final @NonNull Optional value) { + value.ifPresent(v -> append(key, v)); + return this; + } + + /** + * Adds or replaces an entry if the condition is true. + * + * @param key the key + * @param value the value + * @param ifTrue whether to insert + * @return this map + */ + ObjectMap append(final K key, final V value, boolean ifTrue); + + /** + * Adds or replaces an entry with one of two possible values depending on the condition. + * + * @param key the key + * @param valueIfTrue value if condition is true + * @param valueIfFalse value if condition is false + * @param ifTrue condition + * @return this map + */ + ObjectMap append(final K key, final V valueIfTrue, final V valueIfFalse, boolean ifTrue); + + /** + * Adds or replaces all entries from the given map if the condition is true. + * + * @param map the map + * @param ifTrue condition + * @return this map + */ + ObjectMap append(final Map map, boolean ifTrue); + + /** + * Adds or replaces all entries from one of two maps depending on the condition. + * + * @param mapIfTrue map if condition is true + * @param mapIfFalse map if condition is false + * @param ifTrue condition + * @return this map + */ + ObjectMap append(final Map mapIfTrue, final Map mapIfFalse, boolean ifTrue); + + /** + * Adds or replaces all entries from the given {@link ObjectMap} if the condition is true. + * + * @param map the map + * @param ifTrue condition + * @return this map + */ + ObjectMap append(final ObjectMap map, boolean ifTrue); + + /** + * Adds or replaces all entries from one of two maps depending on the condition. + * + * @param mapIfTrue map if condition is true + * @param mapIfFalse map if condition is false + * @param ifTrue condition + * @return this map + */ + ObjectMap append(final ObjectMap mapIfTrue, final Map mapIfFalse, boolean ifTrue); + + /** + * Removes an entry by key. + * + * @param key the key to remove + * @return this map + */ + ObjectMap removeEntry(final K key); + + /** + * Removes all entries with keys present in the given map. + * + * @param map the map whose keys should be removed + * @return this map + */ + ObjectMap removeEntries(final Map map); + + /** + * Removes all entries with keys present in the given {@link ObjectMap}. + * + * @param map the map whose keys should be removed + * @return this map + */ + ObjectMap removeEntries(final ObjectMap map); + + /** + * Removes an entry if the condition is true. + * + * @param key the key to remove + * @param ifTrue condition + * @return this map + */ + ObjectMap removeEntry(final K key, boolean ifTrue); + + /** + * Removes all entries from the given map if the condition is true. + * + * @param map the map whose keys should be removed + * @param ifTrue condition + * @return this map + */ + ObjectMap removeEntries(final Map map, boolean ifTrue); + + /** + * Removes all entries from the given {@link ObjectMap} if the condition is true. + * + * @param map the map whose keys should be removed + * @param ifTrue condition + * @return this map + */ + ObjectMap removeEntries(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); + + /** + * Converts this map into a new {@link HashObjectMap}. + * + * @return a new {@link HashObjectMap} containing the same entries + */ + default HashObjectMap toHashObjectMap() { + return new HashObjectMap<>(this); + } + + /** + * Converts this map into a new {@link ConcurrentHashObjectMap}. + * + * @return a new {@link ConcurrentHashObjectMap} containing the same entries + */ + default ConcurrentHashObjectMap toConcurrentObjectMap() { + return new ConcurrentHashObjectMap<>(this); + } + + /** + * Converts this map into a new {@link LinkedHashObjectMap}. + * + * @return a new {@link LinkedHashObjectMap} containing the same entries + */ + default LinkedHashObjectMap toLinkedObjectMap() { + return new LinkedHashObjectMap<>(this); + } + + /** + * Converts this map into a new {@link ObservableObjectMap}. + * + * @return a new {@link ObservableObjectMap} containing the same entries + */ + default ObservableObjectMap toObservableObjectMap() { + return new ObservableObjectMap<>(this); + } + + /** + * Converts this map into a new {@link TreeObjectMap}. + * + * @return a new {@link TreeObjectMap} containing the same entries + */ + default TreeObjectMap toTreeObjectMap() { + return new TreeObjectMap<>(this); + } + + /** + * Converts this map into a new unmodifiable wrapper. + * + * @return an {@link UnmodifiableObjectMap} containing the same entries + */ + default UnmodifiableObjectMap toUnmodifiableObjectMap() { + return new UnmodifiableObjectMap<>(this); + } + + /** + * Creates a new instance of the specified map type with the same entries. + *

The class must define a constructor accepting a {@link Map} or {@link ObjectMap}. + * + * @param clazz the map class + * @return a new map of the requested type + * @throws RuntimeException if no suitable constructor exists + */ + @NonNull + default ObjectMap toObjectMap(@NonNull Class> clazz) { + Constructor constructor; + try { + try { + constructor = clazz.getConstructor(Map.class); + } catch (NoSuchMethodException e) { + constructor = clazz.getConstructor(ObjectMap.class); + } + } catch (NoSuchMethodException e) { + throw new RuntimeException("No suitable constructor found for " + clazz.getName(), e); + } + + try { + //noinspection unchecked + return (ObjectMap) constructor.newInstance(this); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates a new empty {@link ObjectMap} of the same type as this instance. + * + * @return a new empty map + */ + ObjectMap newObjectMap(); + + /** + * Creates a new {@link ObjectMap} of the same type as this instance, + * containing all entries from the given map. + * + * @param map the source map + * @return a new populated map + */ + default ObjectMap newObjectMap(Map map) { + ObjectMap newObjectMap = newObjectMap(); + newObjectMap.putAll(map); + return newObjectMap; + } + + /** + * Creates a new {@link ObjectMap} of the same type as this instance, + * containing all entries from the given {@link ObjectMap}. + * + * @param map the source map + * @return a new populated map + */ + default ObjectMap newObjectMap(ObjectMap map) { + ObjectMap newObjectMap = newObjectMap(); + newObjectMap.putAll(map); + return newObjectMap; + } + + /** + * {@inheritDoc} + */ + @Override + default @NonNull ObjectMap shallowCopy() { + ObjectMap copy = newObjectMap(); + copy.putAll(this); + return copy; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + default @NonNull ObjectMap deepCopy() { + ObjectMap copy = newObjectMap(); + for (Entry entry : entrySet()) { + try { + copy.put((K) DeepCloner.cloneValue(entry.getKey()), (V) DeepCloner.cloneValue(entry.getValue())); + } catch (UnsupportedOperationException e) { + break; // target map does not support modifications + } catch (Exception ignored) { + } + } + return copy; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMaps.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMaps.java new file mode 100644 index 00000000..31b2eac6 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObjectMaps.java @@ -0,0 +1,173 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.Map; + +/** + * Utility class providing static factory methods for creating various {@link ObjectMap} implementations. + * + *

This class centralizes creation of {@link LinkedHashObjectMap}, {@link ConcurrentHashObjectMap}, + * {@link HashObjectMap}, {@link TreeObjectMap}, and {@link UnmodifiableObjectMap} instances. + * It supports both empty and pre-populated maps, improving readability and reusability.

+ */ +public final class ObjectMaps { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private ObjectMaps() { + // Prevent instantiation + } + + /** + * Creates a new empty {@link LinkedHashObjectMap}. + * + * @return a new {@link LinkedHashObjectMap} instance + */ + @Contract(" -> new") + public static @NonNull LinkedHashObjectMap newLinkedHashObjectMap() { + return new LinkedHashObjectMap<>(); + } + + /** + * Creates a new empty {@link ConcurrentHashObjectMap}. + * + * @return a new {@link ConcurrentHashObjectMap} instance + */ + @Contract(" -> new") + public static @NonNull ConcurrentHashObjectMap newConcurrentHashObjectMap() { + return new ConcurrentHashObjectMap<>(); + } + + /** + * Creates a new empty {@link HashObjectMap}. + * + * @return a new {@link HashObjectMap} instance + */ + @Contract(" -> new") + public static @NonNull HashObjectMap newHashObjectMap() { + return new HashObjectMap<>(); + } + + /** + * Creates a new empty {@link TreeObjectMap}. + * + * @return a new {@link TreeObjectMap} instance + */ + @Contract(" -> new") + public static @NonNull TreeObjectMap newTreeObjectMap() { + return new TreeObjectMap<>(); + } + + /** + * Creates a new {@link LinkedHashObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link LinkedHashObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull LinkedHashObjectMap newLinkedHashObjectMap(ObjectMap map) { + return new LinkedHashObjectMap<>(map); + } + + /** + * Creates a new {@link LinkedHashObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link LinkedHashObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull LinkedHashObjectMap newLinkedHashObjectMap(Map map) { + return new LinkedHashObjectMap<>(map); + } + + /** + * Creates a new {@link ConcurrentHashObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link ConcurrentHashObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull ConcurrentHashObjectMap newConcurrentHashObjectMap(ObjectMap map) { + return new ConcurrentHashObjectMap<>(map); + } + + /** + * Creates a new {@link ConcurrentHashObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link ConcurrentHashObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull ConcurrentHashObjectMap newConcurrentHashObjectMap(Map map) { + return new ConcurrentHashObjectMap<>(map); + } + + /** + * Creates a new {@link HashObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link HashObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull HashObjectMap newHashObjectMap(ObjectMap map) { + return new HashObjectMap<>(map); + } + + /** + * Creates a new {@link HashObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link HashObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull HashObjectMap newHashObjectMap(Map map) { + return new HashObjectMap<>(map); + } + + /** + * Creates a new {@link TreeObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link TreeObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull TreeObjectMap newTreeObjectMap(ObjectMap map) { + return new TreeObjectMap<>(map); + } + + /** + * Creates a new {@link TreeObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new {@link TreeObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull TreeObjectMap newTreeObjectMap(Map map) { + return new TreeObjectMap<>(map); + } + + /** + * Creates a new unmodifiable {@link ObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new unmodifiable {@link ObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull UnmodifiableObjectMap newUnmodifiableObjectMap(ObjectMap map) { + return new UnmodifiableObjectMap<>(map); + } + + /** + * Creates a new unmodifiable {@link ObjectMap} containing all entries from the given map. + * + * @param map the source map + * @return a new unmodifiable {@link ObjectMap} with the same mappings + */ + @Contract("_ -> new") + public static @NonNull UnmodifiableObjectMap newUnmodifiableObjectMap(Map map) { + return new UnmodifiableObjectMap<>(map); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObservableObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObservableObjectMap.java new file mode 100644 index 00000000..4d20fb91 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/ObservableObjectMap.java @@ -0,0 +1,282 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; + +/** + * An observable concurrent map implementation that extends {@link ConcurrentHashObjectMap} + * and allows registering listeners to react to changes in the map. + *

+ * This class notifies all registered {@link MapChangeListener}s whenever an entry + * is added, removed, or updated. It is especially useful in reactive systems, + * caches, and event-driven applications. + *

+ * Thread-safety: + *

    + *
  • The underlying storage is a {@link java.util.concurrent.ConcurrentHashMap}.
  • + *
  • Listeners are stored in a {@link CopyOnWriteArrayList}, so they can be + * safely added and removed while notifications are being dispatched.
  • + *
+ * + * @param key type + * @param value type + */ +public class ObservableObjectMap extends ConcurrentHashObjectMap { + + /** + * Registered listeners that will be notified when map entries change. + */ + private final List> listeners = new CopyOnWriteArrayList<>(); + + private int initialCapacity = -1; + private float loadFactor = -1; + + /** + * Creates an empty {@link ObservableObjectMap}. + */ + public ObservableObjectMap() { + super(); + } + + /** + * Creates a new {@link ObservableObjectMap} initialized with the entries from another {@link ObjectMap}. + * + * @param map the initial map + */ + public ObservableObjectMap(final ObjectMap map) { + super(map); + } + + /** + * Creates a new {@link ObservableObjectMap} initialized with the entries from another {@link Map}. + * + * @param map the initial map + */ + public ObservableObjectMap(final Map map) { + super(map); + } + + /** + * Creates a new {@link ObservableObjectMap} with a given initial capacity. + * + * @param initialCapacity initial capacity + */ + public ObservableObjectMap(final int initialCapacity) { + super(initialCapacity); + this.initialCapacity = initialCapacity; + } + + /** + * Creates a new {@link ObservableObjectMap} with a given initial capacity and load factor. + * + * @param initialCapacity initial capacity + * @param loadFactor load factor + */ + public ObservableObjectMap(final int initialCapacity, final float loadFactor) { + super(initialCapacity, loadFactor); + this.initialCapacity = initialCapacity; + this.loadFactor = loadFactor; + } + + /** + * Registers a listener to receive notifications when map entries change. + * + * @param listener listener to add + */ + public void addListener(@NonNull MapChangeListener listener) { + listeners.add(listener); + } + + /** + * Unregisters a previously registered listener. + * + * @param listener listener to remove + */ + public void removeListener(@NonNull MapChangeListener listener) { + listeners.remove(listener); + } + + /** + * Returns an unmodifiable view of the registered listeners. + * + * @return listeners list + */ + public List> getListeners() { + return Collections.unmodifiableList(listeners); + } + + /** + * {@inheritDoc} + */ + @Override + public V put(@NonNull K key, @NonNull V value) { + V oldValue = super.put(key, value); + if (oldValue == null) { + fireEntryAdded(key, value); + } else if (!Objects.equals(oldValue, value)) { + fireEntryUpdated(key, oldValue, value); + } + return oldValue; + } + + /** + * {@inheritDoc} + */ + @Override + public V putIfAbsent(@NonNull K key, @NonNull V value) { + V oldValue = super.putIfAbsent(key, value); + if (oldValue == null) { + fireEntryAdded(key, value); + } + return oldValue; + } + + /** + * {@inheritDoc} + */ + @Override + public void putAll(@NonNull Map m) { + for (Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); // reuse put logic for notifications + } + } + + /** + * {@inheritDoc} + */ + @Override + public V remove(@NonNull Object key) { + V oldValue = super.remove(key); + if (oldValue != null) { + fireEntryRemoved(key, oldValue); + } + return oldValue; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(Object key, Object value) { + boolean removed = super.remove(key, value); + if (removed) { + fireEntryRemoved(key, value); + } + return removed; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replace(K key, V oldValue, V newValue) { + boolean replaced = super.replace(key, oldValue, newValue); + if (replaced && !Objects.equals(oldValue, newValue)) { + fireEntryUpdated(key, oldValue, newValue); + } + return replaced; + } + + /** + * {@inheritDoc} + */ + @Override + public V replace(K key, V value) { + V oldValue = super.replace(key, value); + if (oldValue != null && !Objects.equals(oldValue, value)) { + fireEntryUpdated(key, oldValue, value); + } + return oldValue; + } + + /** + * {@inheritDoc} + */ + @Override + public void replaceAll(@NonNull BiFunction function) { + if (listeners.isEmpty()) { + super.replaceAll(function); + return; + } + + Map oldSnapshot = new HashMap<>(this); + super.replaceAll(function); + + for (Entry entry : entrySet()) { + V oldValue = oldSnapshot.get(entry.getKey()); + V newValue = entry.getValue(); + if (!Objects.equals(oldValue, newValue)) { + fireEntryUpdated(entry.getKey(), oldValue, newValue); + } + } + } + + private void fireEntryAdded(K key, V value) { + for (MapChangeListener listener : listeners) { + listener.entryAdded(key, value); + } + } + + private void fireEntryRemoved(Object key, @Nullable Object value) { + for (MapChangeListener listener : listeners) { + listener.entryRemoved(key, value); + } + } + + private void fireEntryUpdated(K key, @Nullable V oldValue, @NonNull V newValue) { + for (MapChangeListener listener : listeners) { + listener.entryUpdated(key, oldValue, newValue); + } + } + + /** + * Listener interface for receiving map change events. + * + * @param key type + * @param value type + */ + public interface MapChangeListener { + + /** + * Called when a new entry is added to the map. + * + * @param key the added key + * @param value the added value + */ + void entryAdded(K key, V value); + + /** + * Called when an entry is removed from the map. + * + * @param key the removed key + * @param value the removed value (may be {@code null}) + */ + void entryRemoved(Object key, @Nullable Object value); + + /** + * Called when an entry is updated in the map. + * + * @param key the updated key + * @param oldValue the previous value (may be {@code null}) + * @param newValue the new value + */ + void entryUpdated(K key, @Nullable V oldValue, @NonNull V newValue); + } + + /** + * {@inheritDoc} + */ + @Override + public ObservableObjectMap newObjectMap() { + if (initialCapacity > 0) { + return (loadFactor > 0) + ? new ObservableObjectMap<>(initialCapacity, loadFactor) + : new ObservableObjectMap<>(initialCapacity); + } + return new ObservableObjectMap<>(); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/TreeObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/TreeObjectMap.java new file mode 100644 index 00000000..90463aeb --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/TreeObjectMap.java @@ -0,0 +1,365 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import org.jspecify.annotations.NonNull; + +import java.util.*; + +public class TreeObjectMap extends AbstractObjectMap implements ObjectMap, NavigableMap, Cloneable { + + private Comparator comparator; + + /** + * Creates a TreeObjectMap instance. + */ + public TreeObjectMap() { + super(new TreeMap<>()); + this.comparator = null; + } + + /** + * Creates a TreeObjectMap instance with the specified comparator. + * + * @param comparator the comparator to order the keys + */ + public TreeObjectMap(Comparator comparator) { + super(new TreeMap<>(comparator)); + this.comparator = comparator; + } + + /** + * Creates a TreeObjectMap instance initialized with the given ObjectMap. + * + * @param map the initial ObjectMap + */ + public TreeObjectMap(final ObjectMap map) { + super(new TreeMap<>(map)); + if (map instanceof TreeObjectMap treeObjectMap) { + this.comparator = treeObjectMap.comparator; + } else { + this.comparator = null; + } + } + + /** + * Creates a TreeObjectMap instance initialized with the given Map. + * + * @param map the initial Map + */ + public TreeObjectMap(final Map map) { + super(new TreeMap<>(map)); + if (map instanceof TreeMap treeMap) { + this.comparator = treeMap.comparator(); + } else { + this.comparator = null; + } + } + + /** + * 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(new TreeMap<>(comparator)); + this.comparator = 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(new TreeMap<>(comparator)); + this.comparator = comparator; + putAll(map); + } + + /** + * {@inheritDoc} + */ + @Override + public TreeObjectMap newObjectMap() { + return new TreeObjectMap<>(comparator); + } + + /** + * Returns the comparator used to order the keys in this map. + * + * @return the comparator used to order the keys + */ + public Comparator comparator() { + return comparator; + } + + /** + * {@inheritDoc} + */ + @Override + public Entry lowerEntry(K key) { + return ((NavigableMap) delegate).lowerEntry(key); + } + + /** + * {@inheritDoc} + */ + @Override + public K lowerKey(K key) { + return ((NavigableMap) delegate).lowerKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry floorEntry(K key) { + return ((NavigableMap) delegate).floorEntry(key); + } + + /** + * {@inheritDoc} + */ + @Override + public K floorKey(K key) { + return ((NavigableMap) delegate).floorKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry ceilingEntry(K key) { + return ((NavigableMap) delegate).ceilingEntry(key); + } + + /** + * {@inheritDoc} + */ + @Override + public K ceilingKey(K key) { + return ((NavigableMap) delegate).ceilingKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry higherEntry(K key) { + return ((NavigableMap) delegate).higherEntry(key); + } + + /** + * {@inheritDoc} + */ + @Override + public K higherKey(K key) { + return ((NavigableMap) delegate).higherKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry firstEntry() { + return ((NavigableMap) delegate).firstEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry lastEntry() { + return ((NavigableMap) delegate).lastEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry pollFirstEntry() { + return ((NavigableMap) delegate).pollFirstEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public Entry pollLastEntry() { + return ((NavigableMap) delegate).pollLastEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableMap descendingMap() { + return ((NavigableMap) delegate).descendingMap(); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableSet navigableKeySet() { + return ((NavigableMap) delegate).navigableKeySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableSet descendingKeySet() { + return ((NavigableMap) delegate).descendingKeySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableMap subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) { + return ((NavigableMap) delegate).subMap(fromKey, fromInclusive, toKey, toInclusive); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableMap headMap(K toKey, boolean inclusive) { + return ((NavigableMap) delegate).headMap(toKey, inclusive); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableMap tailMap(K fromKey, boolean inclusive) { + return ((NavigableMap) delegate).tailMap(fromKey, inclusive); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SortedMap subMap(K fromKey, K toKey) { + return ((SortedMap) delegate).subMap(fromKey, toKey); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SortedMap headMap(K toKey) { + return ((SortedMap) delegate).headMap(toKey); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SortedMap tailMap(K fromKey) { + return ((SortedMap) delegate).tailMap(fromKey); + } + + /** + * {@inheritDoc} + */ + @Override + public K firstKey() { + return ((SortedMap) delegate).firstKey(); + } + + /** + * {@inheritDoc} + */ + @Override + public K lastKey() { + return ((SortedMap) delegate).lastKey(); + } + + /** + * {@inheritDoc} + */ + @Override + public V putFirst(K k, V v) { + return ((SortedMap) delegate).putFirst(k, v); + } + + /** + * {@inheritDoc} + */ + @Override + public V putLast(K k, V v) { + return ((SortedMap) delegate).putLast(k, v); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SequencedSet sequencedKeySet() { + return ((SequencedMap) delegate).sequencedKeySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SequencedCollection sequencedValues() { + return ((SequencedMap) delegate).sequencedValues(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull SequencedSet> sequencedEntrySet() { + return ((SequencedMap) delegate).sequencedEntrySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public NavigableMap reversed() { + return ((NavigableMap) delegate).reversed(); + } + + /** + * Creates a shallow clone of this {@link TreeObjectMap}. + *

+ * The returned {@code TreeObjectMap} is a new instance with its own + * backing {@link TreeMap}, so modifications to the clone's entries + * do not affect the original map and vice versa. + *

+ * Note: This is a shallow copy. The keys and values themselves + * are not cloned; if they are mutable objects, changes to them will + * be reflected in both the original and the clone. + *

+ * If a deep copy is required, use the {@link #deepCopy()} method + * provided by the {@link ObjectMap} + * interface (which extends {@link com.georgev22.skinoverlay.utilities.Copyable}). + * + * @return a shallow clone of this {@code TreeObjectMap} + */ + @Override + public TreeObjectMap clone() { + try { + @SuppressWarnings("unchecked") + TreeObjectMap clone = (TreeObjectMap) super.clone(); + if (this.delegate instanceof TreeMap treeMap) { + //noinspection unchecked + clone.delegate = (TreeMap) treeMap.clone(); + clone.comparator = treeMap.comparator(); + } else { + clone.delegate = new TreeMap<>(this.delegate); + clone.comparator = this.comparator; + } + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); // should never happen + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/UnmodifiableObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/UnmodifiableObjectMap.java new file mode 100644 index 00000000..151ba9cc --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/UnmodifiableObjectMap.java @@ -0,0 +1,310 @@ +package com.georgev22.skinoverlay.datastructures.maps; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.io.Serializable; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * 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 extends AbstractObjectMap implements ObjectMap, Serializable { + + @SafeVarargs + public UnmodifiableObjectMap(Entry @NonNull ... entries) { + super(new ConcurrentHashObjectMap<>()); + for (Entry entry : entries) { + this.delegate.put(entry.getKey(), entry.getValue()); + } + } + + public UnmodifiableObjectMap(ObjectMap map) { + super(map); + } + + public UnmodifiableObjectMap(Map map) { + super(map); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(K key, V value) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(Map map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(ObjectMap map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(K key, V value, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(K key, V valueIfTrue, V valueIfFalse, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(Map map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(Map mapIfTrue, Map mapIfFalse, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(ObjectMap map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap append(ObjectMap mapIfTrue, Map mapIfFalse, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap removeEntry(K key) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap removeEntries(Map map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap removeEntries(ObjectMap map) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap removeEntry(K key, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap removeEntries(Map map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap removeEntries(ObjectMap map, boolean ifTrue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + public V put(K key, V value) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V remove(Object key) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public void putAll(@NonNull Map m) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V putIfAbsent(K key, V value) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfAbsent(K key, @NonNull Function mappingFunction) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfPresent(K key, @NonNull BiFunction remappingFunction) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V compute(K key, @NonNull BiFunction remappingFunction) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V merge(K key, @NonNull V value, @NonNull BiFunction remappingFunction) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public void replaceAll(BiFunction function) { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + throw new UnsupportedOperationException("UnmodifiableObjectMap"); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Set keySet() { + return Collections.unmodifiableSet(super.keySet()); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Collection values() { + return Collections.unmodifiableCollection(super.values()); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Set> entrySet() { + Set> original = super.entrySet(); + Set> wrapped = new HashSet<>(); + + for (Entry entry : original) { + wrapped.add(new AbstractMap.SimpleImmutableEntry<>(entry)); + } + + return Collections.unmodifiableSet(wrapped); + } + + /** + * {@inheritDoc} + */ + @Override + public UnmodifiableObjectMap newObjectMap() { + return new UnmodifiableObjectMap<>(new HashObjectMap<>()); + } + + /** + * {@inheritDoc} + */ + @Override + public ObjectMap newObjectMap(Map map) { + return new UnmodifiableObjectMap<>(map); + } + + /** + * {@inheritDoc} + */ + @Override + public ObjectMap newObjectMap(ObjectMap map) { + return new UnmodifiableObjectMap<>(map); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/utilities/ObjectMapSerializerDeserializer.java b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/utilities/ObjectMapSerializerDeserializer.java new file mode 100644 index 00000000..8d5e7aed --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/datastructures/maps/utilities/ObjectMapSerializerDeserializer.java @@ -0,0 +1,39 @@ +package com.georgev22.skinoverlay.datastructures.maps.utilities; + +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.google.gson.*; +import org.jspecify.annotations.NonNull; + +import java.lang.reflect.Type; + +public class ObjectMapSerializerDeserializer implements JsonSerializer>, JsonDeserializer> { + + private static final String CLASSNAME = "CLASSNAME"; + private static final String INSTANCE = "INSTANCE"; + + @Override + public JsonElement serialize(@NonNull ObjectMap src, Type typeOfSrc, @NonNull JsonSerializationContext context) { + JsonObject retValue = new JsonObject(); + String className = src.getClass().getName(); + retValue.addProperty(CLASSNAME, className); + JsonElement elem = context.serialize(src); + retValue.add(INSTANCE, elem); + return retValue; + } + + @Override + public ObjectMap deserialize(@NonNull JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME); + String className = prim.getAsString(); + + Class clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + throw new JsonParseException(e.getMessage()); + } + return context.deserialize(jsonObject.get(INSTANCE), clazz); + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 8e6e5fb0..174ea7ac 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -39,8 +39,8 @@ public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { return; } - Optional> playerDataManagerOpt = EntityManagerRegistry.getManager(PlayerData.class); - Optional> skinManagerOpt = EntityManagerRegistry.getManager(Skin.class); + Optional> playerDataManagerOpt = EntityManagerRegistry.getInstance().getTyped(PlayerData.class); + Optional> skinManagerOpt = EntityManagerRegistry.getInstance().getTyped(Skin.class); if (playerDataManagerOpt.isEmpty() || skinManagerOpt.isEmpty()) { return; @@ -83,7 +83,7 @@ public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { public void onPlayerLeave(@NotNull SPlayerLeaveEvent event) { SPlayer player = event.getPlayer(); - EntityManagerRegistry.getManager(PlayerData.class) + EntityManagerRegistry.getInstance().getTyped(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 deleted file mode 100644 index b6f734b0..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/ConcurrentObjectMap.java +++ /dev/null @@ -1,504 +0,0 @@ -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 deleted file mode 100644 index b956f6bc..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/HashObjectMap.java +++ /dev/null @@ -1,504 +0,0 @@ -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 deleted file mode 100644 index 2b575295..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/LinkedObjectMap.java +++ /dev/null @@ -1,504 +0,0 @@ -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 deleted file mode 100644 index fea02e40..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/ObjectMap.java +++ /dev/null @@ -1,466 +0,0 @@ -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 deleted file mode 100644 index 515188ad..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/ObservableObjectMap.java +++ /dev/null @@ -1,217 +0,0 @@ -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 deleted file mode 100644 index d52f0614..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/Pair.java +++ /dev/null @@ -1,113 +0,0 @@ -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/TreeObjectMap.java b/common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java deleted file mode 100644 index c7c578af..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/TreeObjectMap.java +++ /dev/null @@ -1,513 +0,0 @@ -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 deleted file mode 100644 index bd93b23e..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/maps/UnmodifiableObjectMap.java +++ /dev/null @@ -1,705 +0,0 @@ -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 index 004d8ded..69a1449a 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java @@ -1,8 +1,8 @@ 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.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.utilities.Utils; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java index 941057ad..31b92912 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java @@ -3,7 +3,7 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java index ebeb96e1..303197f1 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider.java @@ -1,21 +1,54 @@ 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.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import org.jetbrains.annotations.NotNull; +/** + * Abstract provider responsible for handling {@link SGameProfile} retrieval, + * caching, and application for {@link SPlayer} instances. + */ public abstract class GameProfileProvider { protected final SkinOverlay skinOverlay = SkinOverlay.getInstance(); protected final ObjectMap sGameProfiles = new HashObjectMap<>(); + /** + * Retrieves the underlying platform-specific game profile object + * associated with the given player. + * + *

This may return objects such as Mojang's {@code GameProfile} + * depending on the platform implementation.

+ * + * @param player the player whose internal profile is requested + * @return the platform-specific game profile object (never {@code null}) + */ public abstract Object getInternalGameProfile(@NotNull SPlayer player); + /** + * Retrieves the {@link SGameProfile} associated with the given player. + * + *

Implementations may fetch, construct, or convert the profile + * from the platform-specific representation.

+ * + * @param player the player whose profile is requested + * @return the corresponding {@link SGameProfile} (never {@code null}) + */ public abstract SGameProfile getGameProfile(@NotNull SPlayer player); + /** + * Retrieves a cached {@link SGameProfile} if available, + * otherwise falls back to {@link #getGameProfile(SPlayer)}. + * + *

This method does not automatically store the result in the cache; + * caching behavior should be handled by the implementation if needed.

+ * + * @param player the player whose profile is requested + * @return the cached or newly retrieved {@link SGameProfile} + */ public SGameProfile getCachedGameProfile(@NotNull SPlayer player) { if (sGameProfiles.containsKey(player)) { return sGameProfiles.get(player); @@ -23,6 +56,15 @@ public SGameProfile getCachedGameProfile(@NotNull SPlayer player) { return getGameProfile(player); } + /** + * Applies any updates made to the player's {@link SGameProfile}. + * + *

This typically involves updating the player's skin or profile data + * on the client by sending the necessary packets or triggering a refresh + * depending on the platform.

+ * + * @param player the player whose updated profile should be applied + */ public abstract void applyUpdatedGameProfile(@NotNull SPlayer player); -} +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java index 792246c7..c13d8e54 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java @@ -6,18 +6,72 @@ import java.util.List; import java.util.UUID; +/** + * Abstract provider responsible for resolving and managing {@link SPlayer} instances + * across different platforms (e.g., Bukkit, Bungee, Velocity, etc.). + */ public abstract class PlayerProvider { + /** + * Resolves an {@link SPlayer} instance from a platform-specific player object. + * + *

The provided object may represent a player depending on the platform + * (e.g., {@code org.bukkit.entity.Player}, {@code com.velocitypowered.api.proxy.Player}, etc.).

+ * + * @param player the platform-specific player object + * @return the corresponding {@link SPlayer}, or {@code null} if not found or invalid + */ public abstract SPlayer getSPlayer(Object player); + /** + * Resolves an {@link SPlayer} instance from a {@link CommandIssuer}. + * + *

This is typically used when handling commands where the sender may + * or may not be a player.

+ * + * @param commandIssuer the command issuer + * @return the corresponding {@link SPlayer}, or {@code null} if the issuer is not a player + */ public abstract SPlayer getSPlayer(CommandIssuer commandIssuer); + /** + * Resolves an {@link SPlayer} instance by player name. + * + *

Implementations should consider case sensitivity and offline player handling + * depending on platform capabilities.

+ * + * @param name the player's name + * @return the corresponding {@link SPlayer}, or {@code null} if not found + */ public abstract SPlayer getSPlayer(String name); + /** + * Resolves an {@link SPlayer} instance by UUID. + * + * @param uuid the player's unique identifier + * @return the corresponding {@link SPlayer}, or {@code null} if not found + */ public abstract SPlayer getSPlayer(UUID uuid); + /** + * Retrieves a list of all currently online players. + * + *

The returned list should only include players that are fully connected + * and accessible via {@link SPlayer}.

+ * + * @return a list of online {@link SPlayer} instances + */ public abstract List getOnlinePlayers(); + /** + * Checks whether the given {@link SPlayer} is currently online. + * + *

This method safely handles potential exceptions and null values.

+ * + * @param player the player to check + * @return {@code true} if the player is online and has a valid underlying player object, + * otherwise {@code false} + */ public boolean isOnline(SPlayer player) { try { return player.isOnline() && player.getPlayer() != null; @@ -26,6 +80,15 @@ public boolean isOnline(SPlayer player) { } } + /** + * Checks whether a player with the given name is currently online. + * + *

This method attempts to resolve the player using {@link #getSPlayer(String)} + * and then delegates to {@link #isOnline(SPlayer)}.

+ * + * @param name the player's name + * @return {@code true} if the player is online, otherwise {@code false} + */ public boolean isOnline(String name) { try { SPlayer player = getSPlayer(name); @@ -35,6 +98,15 @@ public boolean isOnline(String name) { } } + /** + * Checks whether a player with the given UUID is currently online. + * + *

This method attempts to resolve the player using {@link #getSPlayer(UUID)} + * and then delegates to {@link #isOnline(SPlayer)}.

+ * + * @param uuid the player's unique identifier + * @return {@code true} if the player is online, otherwise {@code false} + */ public boolean isOnline(UUID uuid) { try { SPlayer player = getSPlayer(uuid); @@ -43,4 +115,4 @@ public boolean isOnline(UUID uuid) { return false; } } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java index 90a2bf8c..e820713e 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -62,7 +62,7 @@ public SkinProvider() { 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); + @NotNull Optional> skinEntityManager = EntityManagerRegistry.getInstance().getTyped(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()); diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java index 3017f48c..3423767b 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/AbstractRegistry.java @@ -1,15 +1,15 @@ package com.georgev22.skinoverlay.registry; -import com.georgev22.skinoverlay.maps.ConcurrentObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; -import com.georgev22.skinoverlay.maps.UnmodifiableObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ConcurrentHashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.UnmodifiableObjectMap; import org.jetbrains.annotations.NotNull; import java.util.Optional; public abstract class AbstractRegistry implements Registry { - protected final ObjectMap registry = new ConcurrentObjectMap<>(); + protected final ObjectMap registry = new ConcurrentHashObjectMap<>(); /** * {@inheritDoc} diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java index 12ca09c9..256e94c4 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/CommandTargetRegistry.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.registry; import com.georgev22.skinoverlay.command.CommandIssuer; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import org.jetbrains.annotations.NotNull; import java.util.function.Predicate; diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java index 34e45347..aa1583fb 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/EntityManagerRegistry.java @@ -1,81 +1,124 @@ package com.georgev22.skinoverlay.registry; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.SkinOverlay; 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 com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NonNull; -import java.util.Collections; -import java.util.Map; import java.util.Optional; +import java.util.logging.Level; /** - * Singleton registry for managing EntityManager instances by entity class. + * A centralized global registry for {@link EntityManager} instances. + *

+ * Each registered manager is indexed by the {@link Class} of the entity type it manages. + * This enables fast and type-safe lookup of storage handlers used throughout the API. + *

+ * + *

Key Characteristics

+ *
    + *
  • Singleton access via {@link #getInstance()}
  • + *
  • Automatically extracts keys from registered values using {@link EntityManager#getEntityClass()}
  • + *
  • Provides type-safe lookups with {@link #getTyped(Class)}
  • + *
+ * + *

This registry is primarily intended for internal use but remains public API for plugin extension.

+ * + * @see EntityManager + * @see Entity + * @see AbstractRegistry + * @see Registry */ -public class EntityManagerRegistry { - private static final ObjectMap, EntityManager> managers = new HashObjectMap<>(); +@ApiStatus.Internal +public final class EntityManagerRegistry + extends AbstractRegistry, EntityManager> { /** - * 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 + * Singleton instance of this registry. */ - 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); - } + private static final EntityManagerRegistry INSTANCE = new EntityManagerRegistry(); /** - * Registers a new EntityManager for a specific entity class, replacing any existing manager. + * Constructs the registry. *

- * **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. + * Restricted as private to enforce the singleton pattern. *

+ */ + private EntityManagerRegistry() { + } + + /** + * Returns the global singleton instance of this registry. * - * @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 + * @return the shared {@link EntityManagerRegistry} instance */ - public static boolean replaceOrRegisterManager(Class entityClass, EntityManager manager) { - boolean isReplacing = managers.containsKey(entityClass); - managers.put(entityClass, manager); - return isReplacing; + public static EntityManagerRegistry getInstance() { + return INSTANCE; } /** - * Retrieves the EntityManager for a specific entity class. + * Registers the given manager using the entity class returned by + * {@link EntityManager#getEntityClass()} as its key. * - * @param entityClass the class of the entity - * @param the type of entity - * @return the EntityManager instance, or {@code Optional.empty()} if not registered + * @param value the manager to register + * @throws IllegalArgumentException if a manager for the same entity class is already registered + * @see Registry#register(Object, Object) */ - @SuppressWarnings("unchecked") - public static @NotNull Optional> getManager(Class entityClass) { - return Optional.ofNullable((EntityManager) managers.get(entityClass)); + @Override + public void register(@NonNull EntityManager value) throws IllegalArgumentException { + super.register(value.getEntityClass(), value); } /** - * Checks if an EntityManager is registered for a specific entity class. + * Registers or replaces a manager based on {@link EntityManager#getEntityClass()}. * - * @param entityClass the class of the entity - * @param the type of entity - * @return {@code true} if an EntityManager is registered, {@code false} otherwise + * @param value the manager to register or replace + * @return {@code true} if a previous manager was replaced, {@code false} if newly registered + * @see Registry#replaceOrRegister(Object, Object) */ - public static boolean containsManager(Class entityClass) { - return managers.containsKey(entityClass); + @Override + public boolean replaceOrRegister(@NonNull EntityManager value) { + return super.replaceOrRegister(value.getEntityClass(), value); } - @Contract(pure = true) - public static @NotNull @UnmodifiableView Map, EntityManager> getManagers() { - return Collections.unmodifiableMap(managers); + /** + * Retrieves a typed {@link EntityManager} for the specified entity type. + * + *

This performs both lookup and runtime validation to ensure safe casting.

+ *

If type mismatch is detected, the result is empty and—when debug mode is enabled— + * a detailed {@link ClassCastException} is logged.

+ * + * @param the entity type handled by the requested manager + * @param clazz the entity class key + * @return an {@link Optional} containing the validated, typed manager if present + */ + @SuppressWarnings("unchecked") + public @NonNull Optional> getTyped(Class clazz) { + Optional> optionalRaw = super.get(clazz); + + if (optionalRaw.isEmpty()) { + return Optional.empty(); + } + + EntityManager rawManager = optionalRaw.get(); + + if (!clazz.isAssignableFrom(rawManager.getEntityClass())) { + if (OptionsUtil.DEBUG.getBooleanValue()) { + SkinOverlay.getInstance().getLogger().log( + Level.SEVERE, + "Failed to get typed manager", + new ClassCastException( + "EntityManager registered for " + + rawManager.getEntityClass().getName() + + " cannot be cast to EntityManager<" + clazz.getName() + ">" + ) + ); + } + return Optional.empty(); + } + + return Optional.of((EntityManager) rawManager); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java b/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java index 1c60f9c2..ffa76591 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/registry/Registry.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay.registry; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import org.jetbrains.annotations.NotNull; import java.util.Optional; diff --git a/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java b/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java index 4ef65f56..1af42d01 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java +++ b/common/src/main/java/com/georgev22/skinoverlay/skin/SGameProfile.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.skin; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import java.util.UUID; diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/EntityFactory.java b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityFactory.java new file mode 100644 index 00000000..d5f26887 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityFactory.java @@ -0,0 +1,34 @@ +package com.georgev22.skinoverlay.storage; + +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.storage.data.Entity; + +/** + * Factory interface used to create {@link Entity} instances. + * + *

This interface defines a generic constructor-like contract for entities, + * using a key-value map to pass arbitrary data or parameters required + * for construction. It is used both when creating new entities via + * {@code EntityManager#create} and when reconstructing entities + * from persisted data. + * + *

Implementations can use the provided map in any way they need, e.g., + * passing values to constructors, initializing default fields, or performing + * validation. + * + *

This is a functional interface and can be implemented using + * lambda expressions or method references. + * + * @param the type of entity created by this factory + */ +@FunctionalInterface +public interface EntityFactory { + + /** + * Creates a new entity instance using the provided data. + * + * @param data a map containing arbitrary parameters for entity construction + * @return a newly constructed entity instance + */ + E create(ObjectMap data); +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java index e4f82dec..d52bfd94 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java @@ -1,12 +1,14 @@ package com.georgev22.skinoverlay.storage; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.UnmodifiableObjectMap; import com.georgev22.skinoverlay.storage.data.Entity; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; /** @@ -16,12 +18,23 @@ */ public interface EntityManager { + /** + * Returns the class of the entity managed by this manager. + * + * @return the entity class + */ + default Class getEntityClass() { + return getManagedEntity().type(); + } + + ManagedEntity getManagedEntity(); + /** * Saves the specified entity. * * @param entity the entity to save */ - void save(@NotNull E entity); + void save(@NonNull E entity); /** * Finds an entity by its unique identifier. @@ -29,7 +42,7 @@ public interface EntityManager { * @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); + Optional findById(@NonNull String id); /** * Finds an entity by its unique identifier. @@ -37,7 +50,7 @@ public interface EntityManager { * @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) { + default Optional findById(@NonNull UUID uuid) { return findById(uuid.toString()); } @@ -46,7 +59,7 @@ default Optional findById(@NotNull UUID uuid) { * * @param entity the entity to delete */ - void delete(@NotNull E entity); + void delete(@NonNull E entity); /** * Loads an entity by its unique identifier. @@ -54,7 +67,7 @@ default Optional findById(@NotNull UUID uuid) { * @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); + CompletableFuture> load(@NonNull String id); /** * Loads an entity by its unique identifier. @@ -62,7 +75,7 @@ default Optional findById(@NotNull UUID uuid) { * @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) { + default CompletableFuture> load(@NonNull UUID uuid) { return load(uuid.toString()); } @@ -93,13 +106,20 @@ default void saveAll() { */ List getAll(); + /** + * Returns a view of the entities currently loaded in memory. + * + * @return an unmodifiable view of the loaded entities + */ + UnmodifiableObjectMap getLoadedEntities(); + /** * 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); + boolean exists(@NonNull String id); /** * Checks if an entity with the specified identifier exists. @@ -107,51 +127,40 @@ default void saveAll() { * @param uuid the unique identifier * @return {@code true} if the entity exists, {@code false} otherwise */ - default boolean exists(@NotNull UUID uuid) { + default boolean exists(@NonNull 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. + * Creates a new entity with the specified identifier and executes the provided consumer. * - * @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 + * @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 */ - default Optional getEntity(@NotNull UUID uuid, boolean loadIfExists) { - return getEntity(uuid.toString(), loadIfExists); + default Optional create(@NonNull String id, @NonNull Consumer consumer) { + return create(UUID.fromString(id), consumer); } /** * 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 + * @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 */ - Optional create(@NotNull String id, @NotNull Consumer consumer); + default Optional create(@NonNull UUID uuid, @NonNull Consumer consumer) { + return create(ObjectMap.ofEntries(ObjectMap.entry("id", uuid)), 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 + * @param data the data to create the entity with + * @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); - } + Optional create(@NonNull ObjectMap data, @NonNull Consumer consumer); /** * Gets the name of this entity manager. @@ -160,17 +169,9 @@ default Optional create(@NotNull UUID uuid, @NotNull Consumer consumer) { */ 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 -> { }); @@ -181,7 +182,6 @@ default void shutdown() { * * @param consumer the consumer to apply */ - @ApiStatus.Internal void shutdown(Consumer consumer); } diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/ManagedEntity.java b/common/src/main/java/com/georgev22/skinoverlay/storage/ManagedEntity.java new file mode 100644 index 00000000..12cca5e0 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/ManagedEntity.java @@ -0,0 +1,30 @@ +package com.georgev22.skinoverlay.storage; + + +import com.georgev22.skinoverlay.storage.data.Entity; + +/** + * Represents a managed storage entity definition. + *

+ * A {@code ManagedEntity} binds together: + *

    + *
  • a unique string key used for identification (e.g. file paths, config sections, SQL tables),
  • + *
  • the entity's base class type,
  • + *
  • and a factory used to create new entity instances.
  • + *
+ * + *

This record is intended to be used as a registry entry for storage systems, + * allowing consistent handling of multiple entity types (such as player data + * and void chests) across different storage backends. + * + * @param key unique identifier for this managed entity (e.g. {@code "playerData"}, {@code "skins"}) + * @param type the concrete {@link Entity} class handled by this entry + * @param factory factory responsible for constructing new entity instances + * @param the entity type + */ +public record ManagedEntity( + String key, + Class type, + EntityFactory factory +) { +} 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 index 8de7dcc5..94364f8d 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/data/Entity.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Entity.java @@ -1,9 +1,13 @@ package com.georgev22.skinoverlay.storage.data; +import com.georgev22.skinoverlay.storage.EntityManager; import com.georgev22.skinoverlay.utilities.CustomData; -import org.jetbrains.annotations.NotNull; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NonNull; import java.util.UUID; +import java.util.function.Consumer; /** * Represents a generic entity with a unique identifier. @@ -11,24 +15,24 @@ * This interface provides a base for all identifiable entities and includes * methods for comparison and equality checks. */ -public interface Entity extends Comparable { +public abstract class Entity implements Comparable { - /** - * Returns the universally unique identifier (UUID) of this entity. - * - * @return the UUID representing the identity of this entity - */ - UUID getId(); + protected final UUID uniqueId; + + protected final CustomData customData = new CustomData(); + public Entity(UUID uniqueId) { + this.uniqueId = uniqueId; + } /** - * Retrieves the custom data associated with this entity. - *

- * Note: Custom data are not saved, persisted or loaded from storage. + * Returns the universally unique identifier (UUID) of this entity. * - * @return The custom data associated with this entity. + * @return the UUID representing the identity of this entity */ - CustomData customData(); + public UUID getUniqueId() { + return uniqueId; + } /** * Compares this entity with the specified entity for order. @@ -40,8 +44,8 @@ public interface Entity extends Comparable { * 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()); + public int compareTo(@NonNull Entity o) { + return this.uniqueId.compareTo(o.uniqueId); } /** @@ -55,16 +59,98 @@ default int compareTo(@NotNull Entity o) { * {@code false} otherwise */ @Override - boolean equals(Object o); + public boolean equals(Object o) { + if (!(o instanceof Entity other)) { + return false; + } + return this.uniqueId.equals(other.uniqueId); + } + + /** + * Converts this {@link Entity} into its JSON string representation. + * The formatting of the resulting JSON string can be controlled. + * + * @param pretty if true, the JSON output will be formatted with indentation and line breaks for readability. + * if false, the JSON output will be compact, without unnecessary whitespace. + * @return a JSON string representing this {@link Entity}. + */ + public abstract String toJsonString(boolean pretty); + + /** + * Converts this {@link Entity} into its JSON representation. + * + * @return a JSON representing this {@link Entity}. + */ + public abstract JsonObject toJson(); + + /** + * Called immediately after this entity has been loaded from storage. + *

+ * This method is invoked exclusively by + * {@link EntityManager#load(String)} or + * {@link EntityManager#loadAll()}. + * It is intended to perform any internal post-loading initialization or processing + * that the system requires. + *

+ * Note: This method is for internal use only and should not be called + * directly by plugins or external code. + */ + @ApiStatus.Internal + public void postLoad() { + } + + /** + * Called immediately after this entity has been saved to storage. + *

+ * This method is invoked exclusively by + * {@link EntityManager#save(Entity)} or + * {@link EntityManager#saveAll()}. + * It is intended to perform any internal post-saving initialization or processing + * that the system requires. + *

+ * Note: This method is for internal use only and should not be called + * directly by plugins or external code. + */ + @ApiStatus.Internal + public void postSave() { + } + + /** + * Removes all storage data associated with this entity. + *

+ * This method is invoked exclusively by + * {@link EntityManager#delete(Entity)}. + * It ensures that any residual data left in storage is properly cleaned up. + *

+ * Note: This method is for internal use only and should not be called + * directly by plugins or external code. + */ + @ApiStatus.Internal + public void postDelete() { + } /** - * Performs a deep equality check between this entity and another object. + * Called immediately after this entity has been created by the {@link EntityManager} *

- * This method compares all relevant fields of the entities, not just the UUID. + * This method is invoked exclusively by + * {@link EntityManager#create(String, Consumer)} or + * {@link EntityManager#create(UUID, Consumer)}. + *

+ * Note: This method is for internal use only and should not be called + * directly by plugins or external code. + */ + @ApiStatus.Internal + public void postCreate() { + } + + /** + * Retrieves the custom data associated with this entity. + *

+ * Note: Custom data are not saved, persisted or loaded from storage. * - * @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 + * @return The custom data associated with this entity. */ - boolean equalsExact(Object o); + public CustomData customData() { + return customData; + } } 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 index c3fb6f12..84274ede 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/data/PlayerData.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/PlayerData.java @@ -1,6 +1,7 @@ package com.georgev22.skinoverlay.storage.data; import com.georgev22.skinoverlay.utilities.CustomData; +import com.google.gson.JsonObject; import java.util.Objects; import java.util.UUID; @@ -16,7 +17,7 @@ * * @author George */ -public class PlayerData implements Entity { +public class PlayerData extends Entity { /** * The UUID of the player. @@ -44,6 +45,7 @@ public class PlayerData implements Entity { * @param uuid the UUID of the player */ public PlayerData(UUID uuid) { + super(uuid); this.uuid = uuid; } @@ -53,7 +55,7 @@ public PlayerData(UUID uuid) { * @return the player's UUID */ @Override - public UUID getId() { + public UUID getUniqueId() { return this.uuid; } @@ -103,22 +105,6 @@ 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. @@ -132,6 +118,22 @@ public boolean equals(Object obj) { return Objects.equals(this.uuid, other.uuid); } + /** + * {@inheritDoc} + */ + @Override + public String toJsonString(boolean pretty) { + return ""; + } + + /** + * {@inheritDoc} + */ + @Override + public JsonObject toJson() { + return null; + } + /** * Returns a hash code value for this {@code PlayerData}. * The hash code is based solely on the player's UUID. 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 index fb4ef972..cfd318bf 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/data/Skin.java @@ -4,6 +4,7 @@ import com.georgev22.skinoverlay.storage.gson.SkinTypeAdapter; import com.georgev22.skinoverlay.utilities.CustomData; import com.georgev22.skinoverlay.utilities.skin.SkinParts; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -22,7 +23,7 @@ * while {@code equalsExact} compares all fields for strict equality. *

*/ -public class Skin implements Entity { +public class Skin extends Entity { /** * The custom data associated with this skin. @@ -50,6 +51,7 @@ public class Skin implements Entity { * @param id the unique identifier for the skin */ public Skin(UUID id) { + super(id); this.skinUniqueId = id; } @@ -59,7 +61,7 @@ public Skin(UUID id) { * @return the skin's UUID */ @Override - public UUID getId() { + public UUID getUniqueId() { return this.skinUniqueId; } @@ -123,21 +125,6 @@ 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. @@ -151,6 +138,22 @@ public boolean equals(Object obj) { return Objects.equals(this.skinUniqueId, other.skinUniqueId); } + /** + * {@inheritDoc} + */ + @Override + public String toJsonString(boolean pretty) { + return ""; + } + + /** + * {@inheritDoc} + */ + @Override + public JsonObject toJson() { + return null; + } + /** * Returns a hash code value for this {@code Skin}. * The hash code is based solely on the skin's UUID. diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java index b2ce191b..0825ce38 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/PlayerDataTypeAdapter.java @@ -29,7 +29,7 @@ public class PlayerDataTypeAdapter implements JsonSerializer, JsonDe public JsonElement serialize(@NotNull PlayerData user, Type type, @NotNull JsonSerializationContext jsonSerializationContext) { JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("entity_id", user.getId().toString()); + jsonObject.addProperty("entity_id", user.getUniqueId().toString()); jsonObject.add("defaultSkin", jsonSerializationContext.serialize(user.getDefaultSkin())); jsonObject.add("skin", jsonSerializationContext.serialize(user.getCurrentSkin())); diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java index 94ff1c4a..85eabd6a 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SkinTypeAdapter.java @@ -24,7 +24,7 @@ public class SkinTypeAdapter implements JsonSerializer, JsonDeserializer implements EntityManager { + + private static final ObjectMap< + Class, + EntityFactory + > FACTORIES = new HashObjectMap<>(); + private static final List> MANAGED_ENTITIES = List.of( + new ManagedEntity<>( + "playerData", + PlayerData.class, + data -> new PlayerData(data.get("id", UUID.class)) + ), + new ManagedEntity<>( + "skin", + Skin.class, + data -> new Skin(data.get("id", UUID.class)) + ) + ); + + static { + for (ManagedEntity entity : MANAGED_ENTITIES) { + FACTORIES.put(entity.type(), entity.factory()); + } + } + + public static List> getManagedEntities() { + return MANAGED_ENTITIES; + } + + protected final ObservableObjectMap loadedEntities = new ObservableObjectMap<>(); + protected final UnmodifiableObjectMap unmodifiableLoadedEntities = new UnmodifiableObjectMap<>(loadedEntities); + protected final SkinOverlay main = SkinOverlay.getInstance(); + protected final ExecutorService executorService = ExecutorManager.getInstance().getExecutor(ExecutorType.IO); + protected final Set saveQueue = ConcurrentHashMap.newKeySet(); + protected boolean isShuttingDown = false; + private final ManagedEntity managedEntity; + private final Lock saveLock = new ReentrantLock(); + + public AbstractEntityManager(ManagedEntity managedEntity) { + long saveInterval = OptionsUtil.SAVE_INTERVAL.getLongValue(); + ExecutorManager.getInstance().scheduleAtFixedRate(saveInterval, saveInterval, TimeUnit.SECONDS, this::batchSave); + this.managedEntity = managedEntity; + } + + @Override + public ManagedEntity getManagedEntity() { + return managedEntity; + } + + /** + * Saves the provided entity. + * + * @param entity the entity to be saved + */ + @Override + public void save(@NonNull E entity) { + // Update memory cache + this.loadedEntities.append(entity.getUniqueId().toString(), entity); + // Add to save queue + saveQueue.add(entity.getUniqueId()); + } + + @Override + public Optional findById(@NonNull String id) { + return Optional.ofNullable(loadedEntities.get(id)); + } + + @Override + public void saveAll(Consumer consumer) { + main.getLogger().info("Saving all " + this.getManagedEntity().key() + " data..."); + + for (E entity : new ArrayList<>(loadedEntities.values())) { + consumer.accept(entity); + saveQueue.add(entity.getUniqueId()); + } + + batchSave(); + } + + @Override + public List getAll() { + return new UnmodifiableArrayList<>(this.loadedEntities.values()); + } + + @Override + public UnmodifiableObjectMap getLoadedEntities() { + return this.unmodifiableLoadedEntities; + } + + @Override + public Optional create(@NonNull ObjectMap data, + @NonNull Consumer consumer) { + EntityFactory factory = FACTORIES.get(this.getEntityClass()); + if (factory == null) { + main.getLogger().log(Level.WARNING, "No entity factory registered for " + this.getManagedEntity().key()); + main.getLogger().log(Level.WARNING, "Valid factories: "); + for (Map.Entry, EntityFactory> entityClass : FACTORIES.entrySet()) { + main.getLogger().log(Level.WARNING, " - " + entityClass.getKey().getSimpleName()); + } + return Optional.empty(); + } + + E entity = this.getEntityClass().cast(factory.create(data)); + consumer.accept(entity); + + loadedEntities.put(entity.getUniqueId().toString(), entity); + saveQueue.add(entity.getUniqueId()); + entity.postCreate(); + + return Optional.of(entity); + } + + @Override + public void delete(@NonNull E entity) { + saveQueue.remove(entity.getUniqueId()); + this.loadedEntities.remove(entity.getUniqueId().toString()); + entity.postDelete(); + this.delete0(entity); + } + + @Override + public CompletableFuture> load(@NonNull String id) { + if (loadedEntities.containsKey(id)) { + return CompletableFuture.completedFuture(Optional.of(loadedEntities.get(id))); + } + return CompletableFuture.supplyAsync(() -> { + Optional entity = load0(id); + entity.ifPresent(Entity::postLoad); + return entity; + }, executorService); + } + + @Override + public String getName() { + return this.getClass().getCanonicalName(); + } + + @Override + public void shutdown(Consumer consumer) { + isShuttingDown = true; + + for (E entity : new ArrayList<>(loadedEntities.values())) { + consumer.accept(entity); + saveQueue.add(entity.getUniqueId()); + } + + saveLock.lock(); + try { + batchSave(); + } finally { + saveLock.unlock(); + } + + executorService.shutdown(); + try { + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + protected void batchSave() { + if (!saveLock.tryLock()) { + return; + } + + if (OptionsUtil.DEBUG.getBooleanValue()) + main.getLogger().info("Saving " + this.getManagedEntity().key() + " data..."); + + try { + if (saveQueue.isEmpty()) return; + + Set toSave = new HashSet<>(saveQueue); + saveQueue.clear(); + + for (UUID id : toSave) { + E entity = loadedEntities.get(id.toString()); + if (entity != null) { + saveEntityWithRetry(entity); + } + } + } finally { + saveLock.unlock(); + } + } + + protected void saveEntityWithRetry(E entity) { + int maxRetries = 3; + + for (int attempt = 1; attempt <= maxRetries; attempt++) { + if (save0(entity)) { + entity.postSave(); + return; + } + + main.getLogger().log( + Level.WARNING, + "Failed to save {0} {1}, retry {2}/{3}", + new Object[]{this.getManagedEntity().key(), entity.getUniqueId(), attempt, maxRetries} + ); + } + + main.getLogger().log( + Level.SEVERE, + "Failed to save {0} {1} after {2} retries", + new Object[]{this.getManagedEntity().key(), entity.getUniqueId(), maxRetries} + ); + } + + protected abstract boolean save0(@NonNull E entity); + + protected abstract void delete0(@NonNull E entity); + + protected abstract Optional load0(@NonNull String id); + + protected void logLoadAll(@NonNull List successList, @NonNull List failedList, long startTime) { + long duration = System.nanoTime() - startTime; + + this.main.getLogger().info("Finished loading entity " + getManagedEntity().key() + " data:"); + this.main.getLogger().info(" ✔ Loaded: " + successList.size()); + this.main.getLogger().info(" ✘ Failed: " + failedList.size()); + this.main.getLogger().info(" ⏱ Duration: " + (duration / 1_000_000.0) + " ms"); + + if (!failedList.isEmpty()) { + if (!OptionsUtil.DEBUG.getBooleanValue()) + this.main.getLogger().info("Enable DEBUG mode for more details."); + else + this.main.getLogger().warning("Failed to load the following " + this.getManagedEntity().key() + "s: " + String.join(", ", failedList)); + } + } + +} 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 deleted file mode 100644 index 9c20c13b..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/PlayerEntityManager.java +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index d8478047..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/SkinEntityManager.java +++ /dev/null @@ -1,155 +0,0 @@ -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/FileEntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/FileEntityManager.java new file mode 100644 index 00000000..f7252fff --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/FileEntityManager.java @@ -0,0 +1,228 @@ +package com.georgev22.skinoverlay.storage.manager.gson; + +import com.georgev22.skinoverlay.storage.ManagedEntity; +import com.georgev22.skinoverlay.storage.data.Entity; +import com.georgev22.skinoverlay.storage.manager.AbstractEntityManager; +import com.georgev22.skinoverlay.utilities.GsonUtils; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jspecify.annotations.NonNull; + +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.logging.Level; + +public class FileEntityManager extends AbstractEntityManager { + + private final File entitiesDirectory; + + public FileEntityManager(ManagedEntity managedEntity, File folder) { + super(managedEntity); + this.entitiesDirectory = folder; + if (!this.entitiesDirectory.exists()) { + //noinspection ResultOfMethodCallIgnored + this.entitiesDirectory.mkdirs(); + } + } + + /** + * Deletes the specified entity. + * + * @param entity the entity to delete + */ + protected void delete0(@NonNull E entity) { + if (entitiesDirectory != null) { + File file = new File(entitiesDirectory, entity.getUniqueId() + ".json"); + + if (file.exists()) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + } + + @Override + public Optional load0(@NonNull String id) { + File jsonFile = new File(entitiesDirectory, id + ".json"); + + if (jsonFile.exists()) { + try { + String jsonData = Files.readString(jsonFile.toPath(), StandardCharsets.UTF_8); + E entity = GsonUtils.fromJson(jsonData, getEntityClass()); + + if (entity == null) { + this.main.getLogger().log(Level.SEVERE, "Failed to load entity " + getEntityClass().getSimpleName() + " " + id); + return Optional.empty(); + } + this.loadedEntities.append(entity.getUniqueId().toString(), entity); + return Optional.of(entity); + } catch (IOException e) { + this.main.getLogger().log(Level.SEVERE, "Error while trying to load " + id + " entity " + getEntityClass().getSimpleName(), 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) { + main.getLogger().info("No entity " + getEntityClass().getSimpleName() + " data files found to load."); + return; + } + + int availableThreads = Math.max(4, Runtime.getRuntime().availableProcessors() / 2); + + List successList = Collections.synchronizedList(new ArrayList<>()); + List failedList = Collections.synchronizedList(new ArrayList<>()); + + long startTime = System.nanoTime(); + try (ForkJoinPool customPool = new ForkJoinPool(availableThreads)) { + try { + customPool.submit(() -> Arrays.stream(files).parallel().forEach(file -> { + String id = file.getName().replace(".json", ""); + + try { + long fileStart = System.nanoTime(); + Optional entity = this.load(id).get(); + if (entity.isPresent()) { + successList.add(id); + if (OptionsUtil.DEBUG.getBooleanValue()) { + this.main.getLogger().info("Loaded entity " + getEntityClass().getSimpleName() + " " + entity.get().getUniqueId()); + } + if (OptionsUtil.DEBUG.getBooleanValue()) { + long fileDuration = System.nanoTime() - fileStart; + this.main.getLogger().info("Loaded " + id + " in " + (fileDuration / 1_000_000.0) + " ms"); + } + } else { + failedList.add(id); + this.main.getLogger().warning("Failed to load entity " + getEntityClass().getSimpleName() + " " + id); + } + } catch (Exception e) { + failedList.add(id); + this.main.getLogger().log(Level.SEVERE, "Exception while loading entity " + getEntityClass().getSimpleName() + " " + id, e); + } + })).get(); + } catch (Exception e) { + this.main.getLogger().log(Level.SEVERE, "Fatal error during parallel entity " + getEntityClass().getSimpleName() + " load", e); + } + } + + logLoadAll(successList, failedList, startTime); + } + + /** + * 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(@NonNull String id) { + if (this.loadedEntities.containsKey(id)) { + return true; + } + if (this.entitiesDirectory != null) { + return new File(this.entitiesDirectory, id + ".json").exists(); + } else { + return false; + } + } + + protected boolean save0(@NonNull E entity) { + File tempFile; + try { + String jsonData; + try { + jsonData = GsonUtils.toJson(entity, true); + } catch (Exception e) { + main.getLogger().log(Level.SEVERE, "Failed to serialize entity " + getEntityClass().getSimpleName() + " data for " + entity.getUniqueId(), e); + return false; + } + + //noinspection ConstantConditions + if (jsonData == null) { + main.getLogger().log(Level.SEVERE, "Json data for {0} " + getEntityClass().getSimpleName() + " is null", entity.getUniqueId()); + return false; + } + + File targetFile = new File(entitiesDirectory, entity.getUniqueId() + ".json"); + tempFile = new File(entitiesDirectory, entity.getUniqueId() + ".tmp"); + + byte[] bytes = jsonData.getBytes(StandardCharsets.UTF_8); + + if (isShuttingDown) { + Files.write(tempFile.toPath(), bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + String readJson = Files.readString(tempFile.toPath()); + E parsed = GsonUtils.fromJson(readJson, getEntityClass()); + + if (parsed == null || !parsed.getUniqueId().equals(entity.getUniqueId())) { + throw new IOException("Parsed data is invalid or ID mismatch"); + } + + Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } else { + //noinspection resource + AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( + tempFile.toPath(), + StandardOpenOption.WRITE, StandardOpenOption.CREATE + ); + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + fileChannel.write(buffer, 0, entity.getUniqueId(), new CompletionHandler() { + @Override + public void completed(Integer result, Object attachment) { + try { + String readJson = Files.readString(tempFile.toPath()); + E parsed = GsonUtils.fromJson(readJson, getEntityClass()); + + if (parsed == null || !parsed.getUniqueId().equals(entity.getUniqueId())) { + throw new IOException("Parsed data is invalid or ID mismatch"); + } + + Files.move(tempFile.toPath(), targetFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + main.getLogger().log(Level.SEVERE, "Failed to save entity " + getEntityClass().getSimpleName() + " data for " + entity.getUniqueId(), e); + } finally { + try { + fileChannel.close(); + } catch (IOException e) { + main.getLogger().log(Level.WARNING, "Failed to close file channel for " + entity.getUniqueId(), e); + } + } + } + + @Override + public void failed(Throwable exc, Object attachment) { + main.getLogger().log(Level.SEVERE, "Asynchronous file write failed for " + entity.getUniqueId(), exc); + try { + fileChannel.close(); + } catch (IOException e) { + main.getLogger().log(Level.WARNING, "Failed to close file channel after failure for " + entity.getUniqueId(), e); + } + } + }); + } + return true; + + } catch (Exception e) { + main.getLogger().log(Level.SEVERE, "Error while saving entity " + getEntityClass().getSimpleName() + " data for " + entity.getUniqueId(), e); + } + return false; + } +} 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 deleted file mode 100644 index fe4af93f..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/PlayerFileManager.java +++ /dev/null @@ -1,312 +0,0 @@ -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 deleted file mode 100644 index fac9de6e..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/gson/SkinFileManager.java +++ /dev/null @@ -1,225 +0,0 @@ -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/task/ExecutorManager.java b/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorManager.java new file mode 100644 index 00000000..72aea819 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorManager.java @@ -0,0 +1,265 @@ +package com.georgev22.skinoverlay.task; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Centralized manager for handling all ExecutorServices in the system. + * + *

This manager ensures: + *

    + *
  • Executors are lazily created and reused
  • + *
  • Idle executors are automatically cleaned up
  • + *
  • Thread usage is controlled
  • + *
  • Metrics can be retrieved for monitoring
  • + *
+ * + *

Typical usage: + *

+ * ExecutorManager manager = new ExecutorManager();
+ * manager.submit(ExecutorType.IO, () -> doWork());
+ * 
+ */ +public class ExecutorManager { + + private static ExecutorManager instance; + + public static synchronized ExecutorManager getInstance() { + if (instance == null) { + instance = new ExecutorManager(); + } + return instance; + } + + private final Map executors = new ConcurrentHashMap<>(); + private volatile ScheduledExecutorService cleanupService; + + private final AtomicLong maxIdleTimeMillis = new AtomicLong(); + + private ExecutorManager() { + this.maxIdleTimeMillis.set(TimeUnit.MINUTES.toMillis(5)); + this.cleanupService = Executors.newSingleThreadScheduledExecutor( + new NamedThreadFactory("executor-cleanup") + ); + + scheduleCleanupTask(); + } + + /** + * Returns the ExecutorService for a given type. + * + * @param type executor type + * @return executor service + */ + public ExecutorService getExecutor(@NonNull ExecutorType type) { + ManagedExecutor executor = getOrCreate(type); + executor.touch(); + return executor.executor; + } + + /** + * Submits a Runnable task to the specified executor type. + * + * @param type executor type + * @param task task to execute + * @return Future representing the task + */ + public Future submit(ExecutorType type, Runnable task) { + ManagedExecutor executor = getOrCreate(type); + executor.touch(); + return executor.executor.submit(task); + } + + /** + * Submits a Callable task to the specified executor type. + * + * @param type executor type + * @param task task to execute + * @param return type + * @return Future representing the result + */ + public Future submit(ExecutorType type, Callable task) { + ManagedExecutor executor = getOrCreate(type); + executor.touch(); + return executor.executor.submit(task); + } + + /** + * Schedules a delayed task. + * + * @param delay delay time + * @param unit time unit + * @param task task to execute + * @return ScheduledFuture + */ + public ScheduledFuture schedule(long delay, TimeUnit unit, Runnable task) { + return getScheduler().schedule(task, delay, unit); + } + + /** + * Schedules a repeating task. + * + * @param initialDelay initial delay + * @param period period + * @param unit time unit + * @param task task + * @return ScheduledFuture + */ + public ScheduledFuture scheduleAtFixedRate(long initialDelay, long period, TimeUnit unit, Runnable task) { + return getScheduler().scheduleAtFixedRate(task, initialDelay, period, unit); + } + + /** + * Returns metrics for a given executor type. + * + * @param type executor type + * @return metrics or null if not created + */ + public ExecutorMetrics getMetrics(ExecutorType type) { + ManagedExecutor executor = executors.get(type); + if (executor == null) return null; + + ThreadPoolExecutor tpe = executor.threadPool; + return new ExecutorMetrics( + tpe.getPoolSize(), + tpe.getActiveCount(), + tpe.getQueue().size(), + tpe.getCompletedTaskCount() + ); + } + + /** + * Shuts down all executors managed by this instance. + */ + public void shutdown() { + executors.values().forEach(m -> m.executor.shutdown()); + cleanupService.shutdown(); + } + + private ManagedExecutor getOrCreate(ExecutorType type) { + return executors.computeIfAbsent(type, this::createExecutor); + } + + @Contract("_ -> new") + private @NonNull ManagedExecutor createExecutor(@NonNull ExecutorType type) { + ThreadPoolExecutor executor = switch (type) { + case IO -> new ThreadPoolExecutor( + 2, + 8, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + new NamedThreadFactory("skinoverlay-io"), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + case COMPUTE -> { + int totalCores = Runtime.getRuntime().availableProcessors(); + int pluginCores = Math.max(1, totalCores / 2); + yield new ThreadPoolExecutor( + pluginCores, + pluginCores, + 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + new NamedThreadFactory("skinoverlay-compute"), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + default -> throw new IllegalArgumentException("Unknown type: " + type); + }; + + executor.allowCoreThreadTimeOut(true); + + return new ManagedExecutor(executor); + } + + private ScheduledExecutorService getScheduler() { + return executors + .computeIfAbsent(ExecutorType.SCHEDULED, t -> { + ScheduledThreadPoolExecutor scheduler = + new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("scheduler")); + + scheduler.setKeepAliveTime(60, TimeUnit.SECONDS); + scheduler.allowCoreThreadTimeOut(true); + + return new ManagedExecutor(scheduler); + }).scheduledExecutor; + } + + /** + * Updates the maximum idle time before executors are removed. + * The cleanup task is restarted with the new timeout. + * + * @param timeout new idle timeout + * @param unit time unit + */ + public void setMaxIdleTime(long timeout, @NonNull TimeUnit unit) { + maxIdleTimeMillis.set(unit.toMillis(timeout)); + + cleanupService.shutdownNow(); + + cleanupService = Executors.newSingleThreadScheduledExecutor( + new NamedThreadFactory("executor-cleanup") + ); + + scheduleCleanupTask(); + } + + /** + * Schedules the cleanup task on the current cleanupService. + */ + private void scheduleCleanupTask() { + cleanupService.scheduleAtFixedRate(() -> { + long now = System.currentTimeMillis(); + executors.entrySet().removeIf(entry -> { + ManagedExecutor managed = entry.getValue(); + if (managed.isIdle(now, maxIdleTimeMillis.get())) { + managed.executor.shutdown(); + return true; + } + return false; + }); + }, 1, 1, TimeUnit.MINUTES); + } + + /** + * Internal wrapper for tracking executor usage. + */ + private static class ManagedExecutor { + final ExecutorService executor; + final ThreadPoolExecutor threadPool; + final ScheduledExecutorService scheduledExecutor; + + volatile long lastUsed; + + ManagedExecutor(ExecutorService executor) { + this.executor = executor; + this.lastUsed = System.currentTimeMillis(); + + if (executor instanceof ThreadPoolExecutor) { + this.threadPool = (ThreadPoolExecutor) executor; + } else { + this.threadPool = null; + } + + if (executor instanceof ScheduledExecutorService) { + this.scheduledExecutor = (ScheduledExecutorService) executor; + } else { + this.scheduledExecutor = null; + } + } + + void touch() { + this.lastUsed = System.currentTimeMillis(); + } + + boolean isIdle(long now, long maxIdle) { + if (threadPool == null) return false; + return (now - lastUsed) > maxIdle + && threadPool.getActiveCount() == 0 + && threadPool.getQueue().isEmpty(); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorMetrics.java b/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorMetrics.java new file mode 100644 index 00000000..5c78f990 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorMetrics.java @@ -0,0 +1,8 @@ +package com.georgev22.skinoverlay.task; + +/** + * Represents runtime metrics of an executor. + */ +public record ExecutorMetrics(int poolSize, int activeThreads, int queuedTasks, long completedTasks) { + +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorType.java b/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorType.java new file mode 100644 index 00000000..c33dc845 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/task/ExecutorType.java @@ -0,0 +1,26 @@ +package com.georgev22.skinoverlay.task; + +/** + * Defines available executor types. + */ +public enum ExecutorType { + + /** + * For blocking operations such as: + *
    + *
  • Database queries
  • + *
  • File I/O
  • + *
+ */ + IO, + + /** + * For CPU-intensive tasks. + */ + COMPUTE, + + /** + * For scheduled and repeating tasks. + */ + SCHEDULED +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/task/NamedThreadFactory.java b/common/src/main/java/com/georgev22/skinoverlay/task/NamedThreadFactory.java new file mode 100644 index 00000000..f2e8b12d --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/task/NamedThreadFactory.java @@ -0,0 +1,25 @@ +package com.georgev22.skinoverlay.task; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Custom ThreadFactory that assigns meaningful names to threads. + */ +public class NamedThreadFactory implements ThreadFactory { + + private final String name; + private final AtomicInteger counter = new AtomicInteger(); + + public NamedThreadFactory(String name) { + this.name = name; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setName(name + "-" + counter.incrementAndGet()); + thread.setDaemon(true); + return thread; + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java index c60900c9..14c49257 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/CustomData.java @@ -1,7 +1,8 @@ package com.georgev22.skinoverlay.utilities; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMaps; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,7 +25,7 @@ public final class CustomData implements Cloneable, Copyable { private ObjectMap customData; public CustomData() { - this.customData = ObjectMap.newConcurrentObjectMap(); + this.customData = ObjectMaps.newConcurrentHashObjectMap(); } /** @@ -195,7 +196,7 @@ public String toString() { public @NotNull CustomData clone() { try { CustomData clone = (CustomData) super.clone(); - clone.customData = ObjectMap.newConcurrentObjectMap(); + clone.customData = ObjectMaps.newConcurrentHashObjectMap(); for (Map.Entry entry : customData.entrySet()) { clone.customData.put(entry.getKey(), DeepCloner.cloneValue(entry.getValue())); } @@ -218,7 +219,7 @@ public String toString() { @Override public @NotNull CustomData deepCopy() { CustomData copy = new CustomData(); - copy.customData = ObjectMap.newConcurrentObjectMap(); + copy.customData = ObjectMaps.newConcurrentHashObjectMap(); for (Map.Entry entry : customData.entrySet()) { copy.customData.put(entry.getKey(), DeepCloner.cloneValue(entry.getValue())); } diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java index 722b8292..8af7468b 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/DeepCloner.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay.utilities; -import com.georgev22.skinoverlay.maps.*; +import com.georgev22.skinoverlay.datastructures.maps.*; import java.lang.reflect.Constructor; import java.math.BigDecimal; @@ -62,9 +62,9 @@ public final class DeepCloner { // === MAPS === register(ObjectMap.class, o -> switch (o) { case ObservableObjectMap ignored -> new ObservableObjectMap<>(); - case ConcurrentObjectMap ignored -> new ConcurrentObjectMap<>(); + case ConcurrentHashObjectMap ignored -> new ConcurrentHashObjectMap<>(); case HashObjectMap ignored -> new HashObjectMap<>(); - case LinkedObjectMap ignored -> new LinkedObjectMap<>(); + case LinkedHashObjectMap ignored -> new LinkedHashObjectMap<>(); case TreeObjectMap treeObjectMap -> new TreeObjectMap<>(treeObjectMap.comparator()); case UnmodifiableObjectMap ignored -> throw new UnsupportedOperationException("Cannot clone unmodifiable map"); diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/GsonUtils.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/GsonUtils.java new file mode 100644 index 00000000..7efbc351 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/GsonUtils.java @@ -0,0 +1,152 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.skin.SProperty; +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.gson.PlayerDataTypeAdapter; +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 com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.jspecify.annotations.NonNull; + +import java.io.Reader; +import java.io.Writer; + +/** + * Utility class for working with Gson, providing convenience methods for JSON serialization + * and deserialization, as well as a Gson instance pre-configured with custom type adapters. + * + */ +public class GsonUtils { + + /** + * Default Gson instance without pretty printing. + */ + private static final Gson defaultGson = builder().create(); + + /** + * Gson instance with pretty printing enabled. + */ + private static final Gson prettyGson = builder().setPrettyPrinting().create(); + + /** + * Creates a new {@link GsonBuilder} with registered type adapters. + * + * @return a configured {@link GsonBuilder} + */ + private static @NonNull GsonBuilder builder() { + return 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()); + } + + /** + * Converts an object to its JSON representation. + * + * @param obj the object to serialize + * @param pretty whether to pretty print the JSON output + * @return JSON string representing the object + */ + public static @NonNull String toJson(Object obj, boolean pretty) { + if (pretty) { + return prettyGson.toJson(obj); + } else { + return defaultGson.toJson(obj); + } + } + + /** + * Converts an arbitrary object to a {@link JsonObject} using Gson. + *

+ * This method serializes the given object into a Gson {@link JsonElement} tree + * and returns it as a {@link JsonObject}. If the object does not naturally + * serialize to a JSON object (for example, if it is a primitive, array, or null), + * a {@link IllegalArgumentException} is thrown. + *

+ * + * @param obj the object to convert to a {@link JsonObject}; must not be null + * @return a {@link JsonObject} representation of the object + * @throws NullPointerException if {@code obj} is null + * @throws IllegalArgumentException if the object cannot be represented as a {@link JsonObject} + * @see com.google.gson.Gson#toJsonTree(Object) + * @see com.google.gson.JsonElement#getAsJsonObject() + */ + public static @NonNull JsonObject toJsonObject(Object obj) { + JsonElement element = defaultGson.toJsonTree(obj); + + if (element.isJsonObject()) { + return element.getAsJsonObject(); + } else { + throw new IllegalArgumentException( + "Cannot convert object of type " + obj.getClass().getName() + " to JsonObject" + ); + } + } + + /** + * Writes an object to a writer in JSON format. + * + * @param obj the object to serialize + * @param pretty whether to pretty print the JSON output + * @param writer the writer to write the JSON to + */ + public static void write(Object obj, boolean pretty, Writer writer) { + if (pretty) { + prettyGson.toJson(obj, writer); + } else { + defaultGson.toJson(obj, writer); + } + } + + /** + * Deserializes a JSON string into an object of the specified class. + * + * @param the type of the desired object + * @param json the JSON string to deserialize + * @param clazz the class of T + * @return the deserialized object + */ + public static T fromJson(String json, Class clazz) { + return defaultGson.fromJson(json, clazz); + } + + /** + * Deserializes a JSON from a reader into an object of the specified class. + * + * @param the type of the desired object + * @param reader the reader to read the JSON from + * @param clazz the class of T + * @return the deserialized object + */ + public static T fromJson(Reader reader, Class clazz) { + return defaultGson.fromJson(reader, clazz); + } + + /** + * Converts an object into a {@link JsonElement} representation. + * + * @param src the object to convert + * @return a {@link JsonElement} representing the object + */ + public static JsonElement toJsonTree(Object src) { + return defaultGson.toJsonTree(src); + } + + /** + * Returns a Gson instance. + * + * @param pretty whether to return a pretty-printing Gson instance + * @return the configured {@link Gson} instance + */ + public static Gson getGson(boolean pretty) { + return pretty ? prettyGson : defaultGson; + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java index 62a19be4..1f1613e9 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/Utils.java @@ -1,10 +1,11 @@ package com.georgev22.skinoverlay.utilities; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMaps; +import com.georgev22.skinoverlay.datastructures.maps.TreeObjectMap; 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; @@ -12,8 +13,6 @@ 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; @@ -27,12 +26,8 @@ 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; @@ -702,7 +697,7 @@ public static Object deserializeObjectFromBytes(byte @NotNull [] byteArray) thro * @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. + * @return a {@link HashObjectMap(Map)} with the String List contents. */ public static @NotNull ObjectMap stringListToObjectMap(List stringList, final Class clazz) { return new HashObjectMap<>(stringListToHashMap(stringList, clazz)); @@ -790,7 +785,7 @@ public static String toRoman(int number) { if (number <= 0) { return String.valueOf(number); } - TreeObjectMap map = ObjectMap.newTreeObjectMap(); + TreeObjectMap map = ObjectMaps.newTreeObjectMap(); map .append(1000, "M") .append(900, "CM") @@ -2035,7 +2030,7 @@ interface Constructor0 { } public static class Cooldown { - private static final ObjectMap cooldownManagerObjectMap = ObjectMap.newHashObjectMap(); + private static final ObjectMap cooldownManagerObjectMap = ObjectMaps.newHashObjectMap(); private long start; private final int timeInSeconds; private final UUID id; diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java index 16769e49..8b16c796 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/SkinFileCache.java @@ -1,8 +1,8 @@ package com.georgev22.skinoverlay.utilities.config; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ConcurrentHashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.storage.data.Skin; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; import com.georgev22.skinoverlay.utilities.Utils; @@ -17,9 +17,9 @@ import java.util.logging.Level; public class SkinFileCache { - private final ObjectMap skinConfigurationFiles = new HashObjectMap<>(); + private final ObjectMap skinConfigurationFiles = new ConcurrentHashObjectMap<>(); - private final ObjectMap skinImages = new HashObjectMap<>(); + private final ObjectMap skinImages = new ConcurrentHashObjectMap<>(); private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java index 19f4c14a..72ea94b6 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java @@ -1,9 +1,9 @@ package com.georgev22.skinoverlay.utilities.skin; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; -import com.georgev22.skinoverlay.maps.UnmodifiableObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.UnmodifiableObjectMap; import com.georgev22.skinoverlay.utilities.SerializableBufferedImage; import com.georgev22.skinoverlay.utilities.skin.Section.SectionType; import org.jetbrains.annotations.NotNull; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce6bedba..74d38417 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ skinsrestorer = "15.7.3" yamlconfiguration = "3.0.2" jedis = "6.0.0" jetbrains-annotations = "26.0.2" +jspecify = "1.0.0" log4j-api = "3.0.0-beta2" reflect = "1.5.0" @@ -45,5 +46,6 @@ skinsrestorer-api = { module = "net.skinsrestorer:skinsrestorer-api", version.re 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" } +jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } 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/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index b39d29b1..948cfa68 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -100,7 +100,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { this.skinOverlay.getLogger().info("Player " + uuid + " has joined the server."); } this.skinOverlay.getScheduler().runAsyncTask(this.skinOverlay.getPlugin(), () -> { - EntityManagerRegistry.getManager(PlayerData.class) + EntityManagerRegistry.getInstance().getTyped(PlayerData.class) .flatMap(entityManager -> entityManager.findById(uuid)) .ifPresent(playerData -> { Skin skin = playerData.getCurrentSkin(); diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java index 9b0512ee..1ada9656 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java @@ -1,7 +1,7 @@ package com.georgev22.skinoverlay.providers; -import com.georgev22.skinoverlay.maps.HashObjectMap; -import com.georgev22.skinoverlay.maps.ObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.skin.SGameProfile; import com.georgev22.skinoverlay.skin.SProperty; From 3a8017bcf4414b7e2c346a6aaaca6ee884d2a744 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 5 May 2026 07:44:50 +0300 Subject: [PATCH 40/79] refactor!: rename and relocate SkinApplier to SkinRefresher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the old `SkinApplier` abstract class and move its responsibilities to the new `SkinRefresher` abstraction. - Rename `applySkin` methods to `refresh` across all platform implementations to better reflect the new purpose. - Move the `setSkin` logic (profile update + persistence) from `SkinApplier` into `SkinProvider`, which now acts as the central skin application service. - Update all references in commands, listeners, and platform initializers to use `SkinProvider#setSkin` and `SkinRefresher` instead of the removed `SkinApplier`. - Relocate all version‑specific refresher classes from the `appliers` package to the new `refreshers` package. --- .../skinoverlay/SkinOverlayBukkit.java | 42 ++++++------ .../LegacySkinApplier.java | 8 +-- .../SkinRefresher_1_17_R1.java} | 8 +-- .../SkinRefresher_1_18_R1.java} | 8 +-- .../SkinRefresher_1_18_R2.java} | 8 +-- .../SkinRefresher_1_19_R1.java} | 8 +-- .../SkinRefresher_1_19_R2.java} | 8 +-- .../SkinRefresher_1_19_R3.java} | 8 +-- .../SkinRefresher_1_20_R1.java} | 8 +-- .../SkinRefresher_1_20_R2.java} | 8 +-- .../SkinRefresher_1_20_R3.java} | 8 +-- .../SkinRefresher_1_20_R4.java} | 8 +-- .../SkinRefresher_1_21_R1.java} | 8 +-- .../refreshers/SkinRefresher_1_21_R2.java} | 8 +-- .../refreshers/SkinRefresher_1_21_R3.java} | 8 +-- .../refreshers/SkinRefresher_1_21_R4.java} | 8 +-- .../SkinRefresher_1_21_R5.java} | 8 +-- .../SkinRefresher_1_21_R6.java} | 8 +-- .../georgev22/skinoverlay/SkinOverlay.java | 21 +++--- .../skinoverlay/appliers/SkinApplier.java | 66 ------------------- .../command/commands/sub/ClearSubCommand.java | 4 +- .../command/commands/sub/WearSubCommand.java | 2 +- .../commands/sub/WearUrlSubCommand.java | 2 +- .../listeners/PlayerListeners.java | 2 +- .../skinoverlay/providers/SkinProvider.java | 29 ++++++++ .../NoopSkinRefresher.java} | 4 +- .../skinoverlay/refreshers/SkinRefresher.java | 66 +++++++++++++++++++ .../skinoverlay/SkinOverlayVelocity.java | 6 +- .../VelocitySkinRefresher.java} | 6 +- 29 files changed, 207 insertions(+), 179 deletions(-) rename bukkit/src/main/java/com/georgev22/skinoverlay/{appliers => refreshers}/LegacySkinApplier.java (98%) rename bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_17_R1.java => refreshers/SkinRefresher_1_17_R1.java} (94%) rename bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_18_R1.java => refreshers/SkinRefresher_1_18_R1.java} (94%) rename bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_18_R2.java => refreshers/SkinRefresher_1_18_R2.java} (94%) rename bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_19_R1.java => refreshers/SkinRefresher_1_19_R1.java} (94%) rename bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_19_R2.java => refreshers/SkinRefresher_1_19_R2.java} (94%) rename bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_19_R3.java => refreshers/SkinRefresher_1_19_R3.java} (94%) rename bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_20_R1.java => refreshers/SkinRefresher_1_20_R1.java} (94%) rename bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_20_R2.java => refreshers/SkinRefresher_1_20_R2.java} (94%) rename bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_20_R3.java => refreshers/SkinRefresher_1_20_R3.java} (94%) rename bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_20_R4.java => refreshers/SkinRefresher_1_20_R4.java} (94%) rename bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_21_R1.java => refreshers/SkinRefresher_1_21_R1.java} (93%) rename bukkit/versions/{mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java => mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java} (93%) rename bukkit/versions/{mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java => mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java} (93%) rename bukkit/versions/{mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java => mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java} (93%) rename bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_21_R5.java => refreshers/SkinRefresher_1_21_R5.java} (93%) rename bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/{appliers/SkinApplier_1_21_R6.java => refreshers/SkinRefresher_1_21_R6.java} (94%) delete mode 100644 common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java rename common/src/main/java/com/georgev22/skinoverlay/{appliers/NoopSkinApplier.java => refreshers/NoopSkinRefresher.java} (75%) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher.java rename velocity/src/main/java/com/georgev22/skinoverlay/{appliers/VelocitySkinApplier.java => refreshers/VelocitySkinRefresher.java} (76%) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 5667b3d3..6feb8257 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay; -import com.georgev22.skinoverlay.appliers.*; +import com.georgev22.skinoverlay.refreshers.*; import com.georgev22.skinoverlay.command.BukkitCommandManager; import com.georgev22.skinoverlay.hooks.SkinHookNoop; import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; @@ -34,77 +34,77 @@ public void onLoad() { this.skinOverlay.setPlayerProvider(new BukkitPlayerProvider()); if (BukkitMinecraftUtils.isPaper() && getCurrentVersion().isAboveOrEqual(V1_20_R1)) { - this.skinOverlay.setSkinApplier(new NoopSkinApplier()); + this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); 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()); + this.skinOverlay.setSkinRefresher(new LegacySkinApplier()); } case V1_17_R1 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_17_R1()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_17_R1()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_17_R1()); } case V1_18_R1 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_18_R1()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_18_R1()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_18_R1()); } case V1_18_R2 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_18_R2()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_18_R2()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_18_R2()); } case V1_19_R1 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R1()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_19_R1()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_19_R1()); } case V1_19_R2 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R2()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_19_R2()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_19_R2()); } case V1_19_R3 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R3()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_19_R3()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_19_R3()); } case V1_20_R1 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R1()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_20_R1()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_20_R1()); } case V1_20_R2 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R2()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_20_R2()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_20_R2()); } case V1_20_R3 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R3()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_20_R3()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_20_R3()); } case V1_21_R1 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R1()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R1()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R1()); } case V1_21_R2 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R2()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R2()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R2()); } case V1_21_R3 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R3()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R3()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R3()); } case V1_21_R4 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R4()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R4()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R4()); } case V1_21_R5 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R5()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R5()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R5()); } case V1_21_R6 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R6()); - this.skinOverlay.setSkinApplier(new SkinApplier_1_21_R6()); + this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R6()); } default -> { - this.skinOverlay.setSkinApplier(new NoopSkinApplier()); + this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); this.skinOverlay.setGameProfileProvider(new GameProfileProviderNoop()); this.getLogger().info("SkinOverlay does not support " + Bukkit.getBukkitVersion()); } @@ -114,7 +114,7 @@ public void onLoad() { if (Bukkit.getPluginManager().isPluginEnabled("SkinsRestorer")) { this.skinOverlay.setSkinHook(new SkinsRestorerHook()); this.skinOverlay.setGameProfileProvider(new GameProfileProvider_SkinsRestorer()); - this.skinOverlay.setSkinApplier(new NoopSkinApplier()); + this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); } else { this.skinOverlay.setSkinHook(new SkinHookNoop()); } @@ -156,7 +156,7 @@ public void onEnable() { skinOverlay.getMessageManager().subscribeSkinProperty((uuid, skin) -> { SPlayer player = this.skinOverlay.getPlayerProvider().getSPlayer(uuid); if (player != null) { - skinOverlay.getSkinApplier().setSkin(player, skin); + skinOverlay.getSkinProvider().setSkin(player, skin); } }); } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinApplier.java similarity index 98% rename from bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java rename to bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinApplier.java index bce0e1f2..6eefb07f 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/appliers/LegacySkinApplier.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinApplier.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.ReflectionException; import com.georgev22.skinoverlay.player.SPlayer; @@ -23,7 +23,7 @@ import static com.georgev22.skinoverlay.utilities.Utils.Reflection.*; @SuppressWarnings("deprecation") -public class LegacySkinApplier extends SkinApplier { +public class LegacySkinApplier extends SkinRefresher { private final Class playOutRespawn; private final Class playOutPlayerInfo; @@ -75,12 +75,12 @@ public LegacySkinApplier() { } @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; diff --git a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java similarity index 94% rename from bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java rename to bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java index 4b74ae03..50df9534 100644 --- a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_17_R1.java +++ b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -20,15 +20,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_17_R1 extends SkinApplier { +public class SkinRefresher_1_17_R1 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; diff --git a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java similarity index 94% rename from bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java rename to bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java index 6c3449c6..bdc860f4 100644 --- a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R1.java +++ b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -20,15 +20,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_18_R1 extends SkinApplier { +public class SkinRefresher_1_18_R1 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_18_R2.java similarity index 94% rename from bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_18_R2.java rename to bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java index 4e1c2ae1..8954baba 100644 --- 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/refreshers/SkinRefresher_1_18_R2.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -20,15 +20,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_18_R2 extends SkinApplier { +public class SkinRefresher_1_18_R2 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_19_R1.java similarity index 94% rename from bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R1.java rename to bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java index bc93ad11..588d927f 100644 --- 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/refreshers/SkinRefresher_1_19_R1.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -20,15 +20,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_19_R1 extends SkinApplier { +public class SkinRefresher_1_19_R1 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_19_R2.java similarity index 94% rename from bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R2.java rename to bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java index e45071fe..057660c7 100644 --- 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/refreshers/SkinRefresher_1_19_R2.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -21,15 +21,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_19_R2 extends SkinApplier { +public class SkinRefresher_1_19_R2 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_19_R3.java similarity index 94% rename from bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_19_R3.java rename to bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java index b8aedc08..f8c7479f 100644 --- 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/refreshers/SkinRefresher_1_19_R3.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -21,15 +21,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_19_R3 extends SkinApplier { +public class SkinRefresher_1_19_R3 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; diff --git a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java similarity index 94% rename from bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java rename to bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java index c6b8659a..37304cf8 100644 --- a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R1.java +++ b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -21,15 +21,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_20_R1 extends SkinApplier { +public class SkinRefresher_1_20_R1 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_20_R2.java similarity index 94% rename from bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R2.java rename to bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java index 047252aa..f435a9ef 100644 --- 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/refreshers/SkinRefresher_1_20_R2.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_20_R2 extends SkinApplier { +public class SkinRefresher_1_20_R2 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_20_R3.java similarity index 94% rename from bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R3.java rename to bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java index d164a225..0bfe35a5 100644 --- 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/refreshers/SkinRefresher_1_20_R3.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_20_R3 extends SkinApplier { +public class SkinRefresher_1_20_R3 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_20_R4.java similarity index 94% rename from bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_20_R4.java rename to bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java index abe1583f..0470f158 100644 --- 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/refreshers/SkinRefresher_1_20_R4.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_20_R4 extends SkinApplier { +public class SkinRefresher_1_20_R4 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_21_R1.java similarity index 93% rename from bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R1.java rename to bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java index f908708a..5fa284e8 100644 --- 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/refreshers/SkinRefresher_1_21_R1.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_21_R1 extends SkinApplier { +public class SkinRefresher_1_21_R1 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java similarity index 93% rename from bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java rename to bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java index 6a200737..a65c7e3a 100644 --- a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R3.java +++ b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_21_R3 extends SkinApplier { +public class SkinRefresher_1_21_R2 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java similarity index 93% rename from bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java rename to bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java index 7e99d036..707740bc 100644 --- a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R4.java +++ b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_21_R4 extends SkinApplier { +public class SkinRefresher_1_21_R3 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java similarity index 93% rename from bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java rename to bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java index d97a372c..0768ecd9 100644 --- a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R2.java +++ b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_21_R2 extends SkinApplier { +public class SkinRefresher_1_21_R4 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_21_R5.java similarity index 93% rename from bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R5.java rename to bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java index dc9a9934..3e4b1d2d 100644 --- 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/refreshers/SkinRefresher_1_21_R5.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -16,15 +16,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_21_R5 extends SkinApplier { +public class SkinRefresher_1_21_R5 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; 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/refreshers/SkinRefresher_1_21_R6.java similarity index 94% rename from bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier_1_21_R6.java rename to bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java index 84792a86..692431d8 100644 --- 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/refreshers/SkinRefresher_1_21_R6.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.player.SPlayer; @@ -17,15 +17,15 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinApplier_1_21_R6 extends SkinApplier { +public class SkinRefresher_1_21_R6 extends SkinRefresher { @Override - public void applySkin(@NotNull SPlayer player) { + public void refresh(@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) -> { + this.sendPackets(player).handleAsync((result, throwable) -> { if (throwable != null) { skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); return false; diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index 4b733f0a..f31bb3a3 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay; -import com.georgev22.skinoverlay.appliers.SkinApplier; +import com.georgev22.skinoverlay.refreshers.SkinRefresher; import com.georgev22.skinoverlay.command.CommandManager; import com.georgev22.skinoverlay.command.CompletionEngine; import com.georgev22.skinoverlay.command.commands.SkinOverlayMain; @@ -23,7 +23,6 @@ import com.georgev22.skinoverlay.storage.EntityManager; import com.georgev22.skinoverlay.storage.ManagedEntity; import com.georgev22.skinoverlay.storage.data.Entity; -import com.georgev22.skinoverlay.storage.gson.*; import com.georgev22.skinoverlay.storage.manager.AbstractEntityManager; import com.georgev22.skinoverlay.storage.manager.gson.FileEntityManager; import com.georgev22.skinoverlay.utilities.GsonUtils; @@ -65,7 +64,7 @@ public static SkinOverlay getInstance() { private PlayerProvider playerProvider; private GameProfileProvider gameProfileProvider; private SkinProvider skinProvider; - private SkinApplier skinApplier; + private SkinRefresher skinRefresher; private SkinHook skinHook; private boolean isOnlineMode; private boolean isProxy; @@ -336,21 +335,21 @@ public SkinProvider getSkinProvider() { } /** - * Gets the SkinApplier instance. + * Gets the SkinRefresher instance. * - * @return the SkinApplier + * @return the SkinRefresher */ - public SkinApplier getSkinApplier() { - return skinApplier; + public SkinRefresher getSkinRefresher() { + return skinRefresher; } /** - * Sets the SkinApplier instance. + * Sets the SkinRefresher instance. * - * @param skinApplier the SkinApplier + * @param skinRefresher the SkinRefresher */ - public void setSkinApplier(SkinApplier skinApplier) { - this.skinApplier = skinApplier; + public void setSkinRefresher(SkinRefresher skinRefresher) { + this.skinRefresher = skinRefresher; } /** diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java deleted file mode 100644 index 60e7e1c6..00000000 --- a/common/src/main/java/com/georgev22/skinoverlay/appliers/SkinApplier.java +++ /dev/null @@ -1,66 +0,0 @@ -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.getInstance().getTyped(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/commands/sub/ClearSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java index 88bd249c..bcaef54c 100644 --- 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 @@ -39,7 +39,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "ta return; } Skin skin = optionalSkin.get(); - mainPlugin.getSkinApplier().setSkin(player, skin); + mainPlugin.getSkinProvider().setSkin(player, skin); CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, new HashObjectMap().append("%player%", player.getName()), @@ -61,7 +61,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "ta return; } Skin skin = skinOptional.get(); - mainPlugin.getSkinApplier().setSkin(target, skin); + mainPlugin.getSkinProvider().setSkin(target, skin); CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, new HashObjectMap().append("%player%", target.getName()), 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 index bc27adfc..416de90e 100644 --- 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 @@ -65,7 +65,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer, return; } Skin skin = optionalSkin.get(); - mainPlugin.getSkinApplier() + mainPlugin.getSkinProvider() .setSkin(finalTarget, skin); CommandMessages.COMMAND_OVERLAY_DONE.msg( commandIssuer, 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 index 0fad8f53..93fc160f 100644 --- 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 @@ -85,7 +85,7 @@ private void applySkinToPlayer(CommandIssuer issuer, SPlayer player, SkinParts s } Skin skin = optionalSkin.get(); - mainPlugin.getSkinApplier().setSkin(player, skin); + mainPlugin.getSkinProvider().setSkin(player, skin); CommandMessages.COMMAND_OVERLAY_DONE.msg( issuer, diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 174ea7ac..86d817d6 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -74,7 +74,7 @@ public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { playerData.setDefaultSkin(defaultSkin); if (playerData.getCurrentSkin() != null && !playerData.getCurrentSkin().equals(defaultSkin)) { - mainPlugin.getSkinApplier().setSkin(player, playerData.getCurrentSkin()); + mainPlugin.getSkinProvider().setSkin(player, playerData.getCurrentSkin()); } skinManager.save(defaultSkin); diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java index e820713e..f47b6c61 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -7,6 +7,7 @@ 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.SerializableBufferedImage; import com.georgev22.skinoverlay.utilities.Utils; @@ -221,6 +222,34 @@ public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer }); } + + /** + * 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); + this.skinOverlay.getSkinRefresher().refresh(player, skin); + + Optional> optionalPlayerDataEntityManager = EntityManagerRegistry.getInstance().getTyped(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); + } + /** * Retrieves {@link SPlayer}'s {@link SGameProfile} bytes * diff --git a/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java b/common/src/main/java/com/georgev22/skinoverlay/refreshers/NoopSkinRefresher.java similarity index 75% rename from common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java rename to common/src/main/java/com/georgev22/skinoverlay/refreshers/NoopSkinRefresher.java index 0501492f..a96f4cef 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/appliers/NoopSkinApplier.java +++ b/common/src/main/java/com/georgev22/skinoverlay/refreshers/NoopSkinRefresher.java @@ -1,11 +1,11 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.player.SPlayer; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; -public class NoopSkinApplier extends SkinApplier { +public class NoopSkinRefresher extends SkinRefresher { @Override protected @NotNull CompletableFuture sendPackets(@NotNull SPlayer player) { diff --git a/common/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher.java b/common/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher.java new file mode 100644 index 00000000..a626de4c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher.java @@ -0,0 +1,66 @@ +package com.georgev22.skinoverlay.refreshers; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.storage.data.Skin; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +/** + * Responsible for refreshing a player's client state after their {@link SGameProfile} + * has been modified (e.g., skin/texture changes). + * + *

This class does not modify the player's skin data directly. Instead, + * it ensures that the updated profile is properly reflected on the client by + * triggering the necessary updates such as packet sending, respawning, or + * visibility refreshes.

+ * + *

Implementations are typically version-specific and may use platform-dependent + * mechanisms (e.g., NMS packets) to force the client to reload the player's appearance.

+ */ +public abstract class SkinRefresher { + + protected SkinOverlay skinOverlay = SkinOverlay.getInstance(); + + /** + * Triggers a client-side refresh for the given {@link SPlayer}. + * + *

This method should ensure that any previously applied changes to the player's + * {@link SGameProfile} (such as skin updates) become visible to the player and + * other clients.

+ * + * @param player the player whose client state should be refreshed + */ + public void refresh(@NotNull final SPlayer player) { + } + + /** + * Triggers a client-side refresh for the given {@link SPlayer} after a specific + * {@link Skin} has been applied. + * + *

This overload allows implementations to optionally use the provided + * {@link Skin} for additional logic during the refresh process.

+ * + * @param player the player whose client state should be refreshed + * @param skin the skin that was applied prior to the refresh + */ + public void refresh(@NotNull final SPlayer player, @NotNull final Skin skin) { + refresh(player); + } + + /** + * Sends the necessary packets to the client to force a visual update of the player's + * appearance. + * + *

This is typically the core mechanism used to synchronize the updated + * {@link SGameProfile} with the client.

+ * + * @param player the player whose client should receive the update packets + * @return a {@link CompletableFuture} that completes with {@code true} if the + * update was successful, or {@code false} otherwise + */ + protected abstract @NotNull CompletableFuture sendPackets(@NotNull SPlayer player); + +} diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index 948cfa68..75841646 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay; -import com.georgev22.skinoverlay.appliers.VelocitySkinApplier; +import com.georgev22.skinoverlay.refreshers.VelocitySkinRefresher; import com.georgev22.skinoverlay.command.VelocityCommandManager; import com.georgev22.skinoverlay.hooks.SkinHookNoop; import com.georgev22.skinoverlay.hooks.SkinsRestorerHook; @@ -76,7 +76,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { } else { this.skinOverlay.setSkinHook(new SkinHookNoop()); } - this.skinOverlay.setSkinApplier(new VelocitySkinApplier()); + this.skinOverlay.setSkinRefresher(new VelocitySkinRefresher()); this.skinOverlay.setGameProfileProvider(new VelocityGameProfileProvider()); this.skinOverlay.setPlayerProvider(new VelocityPlayerProvider(server)); this.skinOverlay.setAudienceProvider(new VelocityAudienceProvider(this, server)); @@ -105,7 +105,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { .ifPresent(playerData -> { Skin skin = playerData.getCurrentSkin(); if (skin != null) { - this.skinOverlay.getSkinApplier().setSkin( + this.skinOverlay.getSkinProvider().setSkin( this.skinOverlay.getPlayerProvider().getSPlayer(uuid), skin ); diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java b/velocity/src/main/java/com/georgev22/skinoverlay/refreshers/VelocitySkinRefresher.java similarity index 76% rename from velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java rename to velocity/src/main/java/com/georgev22/skinoverlay/refreshers/VelocitySkinRefresher.java index bf161726..bf9cae59 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/appliers/VelocitySkinApplier.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/refreshers/VelocitySkinRefresher.java @@ -1,4 +1,4 @@ -package com.georgev22.skinoverlay.appliers; +package com.georgev22.skinoverlay.refreshers; import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.player.SPlayer; @@ -7,12 +7,12 @@ import java.util.concurrent.CompletableFuture; -public class VelocitySkinApplier extends SkinApplier { +public class VelocitySkinRefresher extends SkinRefresher { private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); @Override - protected void applySkin(@NotNull SPlayer player, @NotNull Skin skin) { + public void refresh(@NotNull SPlayer player, @NotNull Skin skin) { skinOverlay.getMessageManager().publishSkinProperty(player.getUniqueId(), skin); } From 0c563d0373f5ae5166ff747ca14c91642786332c Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 5 May 2026 07:51:10 +0300 Subject: [PATCH 41/79] chore: update gradle to 8.14.4 --- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 5 ++- gradlew | 47 +++++++++++++++-------- gradlew.bat | 41 +++++++++++--------- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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 /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -133,22 +133,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" +# Stop when "xargs" is not available. +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 107acd32..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,34 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 From ea05044790e2ece9319ee3bb06984599080ca55a Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 18 Jun 2026 18:08:17 +0300 Subject: [PATCH 42/79] refactor: embed steve skin image instead of fetching from URL Replace runtime download of Steve skin from namemc.com with a bundled resource file to eliminate external dependency and network latency. --- .../skinoverlay/utilities/skin/SkinParts.java | 32 +++++++----------- common/src/main/resources/steve.png | Bin 0 -> 1109 bytes 2 files changed, 12 insertions(+), 20 deletions(-) create mode 100644 common/src/main/resources/steve.png diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java index 72ea94b6..749ae3da 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/skin/SkinParts.java @@ -22,30 +22,22 @@ 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(); - - 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); - } - } catch (IOException e) { - steveSkin = null; - throw new RuntimeException(e); + SerializableBufferedImage skin = null; + + try (InputStream stream = + SkinParts.class.getResourceAsStream("/steve.png")) { + + if (stream == null) { + throw new IllegalStateException("steve.png not found in resources"); } - steveSkin = new SerializableBufferedImage(ImageIO.read(new ByteArrayInputStream(output.toByteArray()))); - } catch (IOException e) { + skin = new SerializableBufferedImage(ImageIO.read(stream)); + + } catch (Exception e) { throw new RuntimeException(e); } + + steveSkin = skin; } private SerializableBufferedImage fullSkin; diff --git a/common/src/main/resources/steve.png b/common/src/main/resources/steve.png new file mode 100644 index 0000000000000000000000000000000000000000..aa33cd7c2d573f5b5295c6183d2357643310d581 GIT binary patch literal 1109 zcmV-b1giUqP)B34~HHbVx6vYDaobGly6@Bp3>VRXdzxMTlBCIw%w<91AZZ5u9mE|NsBAgl)Tt za#B5sQYa>xWJ;fCNsnGWFCYzoQ#3Rs5^qH;Z9^)MUPG*QT0bfkoMT3oUqG{aV!nuM zt$=MpDiXDOXT6Scy^C_TiENy5T*j7wy^eIZf^w21;vfJ300DGTPE!Ct=GbNc00P}f zL_t(|+U=I-a-%R5g@c5MW#f(6T`^XA*%Rhaj>+sg(I^^gO#8&G&jQV&|x4$E99xKI+<@r_RMN0y~W2xj2sJ zzP=BS=He){AyDTcRS>r9XmM>fihq9p{o~KOSJCd;Vq~{MbuQ9(&}@fZH$2wQ^e?~8 zPfm`*t{1kOo;nvpwBRkT@9ZC*onCx9zc|$o_jlHp@lu|Pp#;s=$_6|5@aeB&SVUnpUTNW2@2deGT}85Y7paH5V%fSAwZ)0-za&Mu?9*=co` zf%rpCe2Sz2xP%BXic~?}FzWcdPw^pZ$_bSMpWn3u6fQ98^*W_VjM6#~X*@H=Dmtj3 z(7{cC>tY8|fQ&(@j2Wo8cpy?>={l=O!L2PMp=1L@2n<7{A*o1#B?OjrO#u481tmaj zpp*i+bqF8=g94xgT%cJW${Gbe7(mXds2z+MGiFSlc|l#Oxdl})l7Ohe zVw4t!5Dd->E>;Z?PJA^NWDv+z-pWMG3fTl$l|ao0xda1qK?(!&LjPQlE|AyRl|Wr- zN|3dML|{Q6=L0K?K( Date: Thu, 18 Jun 2026 18:09:56 +0300 Subject: [PATCH 43/79] refactor: rename LegacySkinApplier to LegacySkinRefresher Renames the class to better reflect its purpose as a skin refresher rather than an applier. Also updates the reference in the Bukkit initialization code for legacy versions. --- .../java/com/georgev22/skinoverlay/SkinOverlayBukkit.java | 2 +- .../{LegacySkinApplier.java => LegacySkinRefresher.java} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/{LegacySkinApplier.java => LegacySkinRefresher.java} (99%) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 6feb8257..cf11a7eb 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -41,7 +41,7 @@ public void onLoad() { 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.setSkinRefresher(new LegacySkinApplier()); + this.skinOverlay.setSkinRefresher(new LegacySkinRefresher()); } case V1_17_R1 -> { this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_17_R1()); diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinApplier.java b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java similarity index 99% rename from bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinApplier.java rename to bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java index 6eefb07f..653ebcb0 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinApplier.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java @@ -23,7 +23,7 @@ import static com.georgev22.skinoverlay.utilities.Utils.Reflection.*; @SuppressWarnings("deprecation") -public class LegacySkinApplier extends SkinRefresher { +public class LegacySkinRefresher extends SkinRefresher { private final Class playOutRespawn; private final Class playOutPlayerInfo; @@ -34,7 +34,7 @@ public class LegacySkinApplier extends SkinRefresher { private Enum removePlayerEnum; private Enum addPlayerEnum; - public LegacySkinApplier() { + public LegacySkinRefresher() { try { packet = getNMSClass("Packet", "net.minecraft.network.protocol.Packet"); playOutHeldItemSlot = getNMSClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot"); From 04760ef0c2f4034b03390872803a002524f0514f Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 18 Jun 2026 19:02:06 +0300 Subject: [PATCH 44/79] refactor!: replace version enum with numeric version resolver and registry Replace the static MinecraftVersion enum with a dynamic numeric-based version resolver and a dedicated VersionRegistry. This eliminates the need to predefine version strings like "V1_21_R6" and instead allows version ranges to be defined numerically (e.g., 1.21.9 to 1.21.10). The version resolution logic is extracted from the main plugin class into a new SkinOverlayVersionResolver, which iterates through the VersionRegistry to find the appropriate provider and refresher. If no match is found, fallback to NoopSkinRefresher and GameProfileProviderNoop. Also improve reflection utilities (BukkitMinecraftUtils.MinecraftReflection) to support repackaged NMS and OBC packages correctly, simplifying the loading of classes across versions. --- .../skinoverlay/SkinOverlayBukkit.java | 89 +---- .../BukkitLegacyGameProfileProvider.java | 4 +- .../refreshers/LegacySkinRefresher.java | 19 +- .../utilities/BukkitMinecraftUtils.java | 370 ++++++++++-------- .../utilities/SkinOverlayVersionResolver.java | 31 ++ .../skinoverlay/utilities/VersionRange.java | 19 + .../utilities/VersionRegistry.java | 135 +++++++ 7 files changed, 407 insertions(+), 260 deletions(-) create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index cf11a7eb..d88fd932 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -13,13 +13,13 @@ import com.georgev22.skinoverlay.scheduler.MinecraftBukkitScheduler; import com.georgev22.skinoverlay.scheduler.MinecraftFoliaScheduler; import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils; +import com.georgev22.skinoverlay.utilities.SkinOverlayVersionResolver; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.V1_20_R1; -import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.getCurrentVersion; +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.getCurrent; public class SkinOverlayBukkit extends JavaPlugin { @@ -33,91 +33,20 @@ public void onLoad() { this.skinOverlay.setCommandManager(new BukkitCommandManager(this)); this.skinOverlay.setPlayerProvider(new BukkitPlayerProvider()); - if (BukkitMinecraftUtils.isPaper() && getCurrentVersion().isAboveOrEqual(V1_20_R1)) { - this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); - 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.setSkinRefresher(new LegacySkinRefresher()); - } - case V1_17_R1 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_17_R1()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_17_R1()); - } - case V1_18_R1 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_18_R1()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_18_R1()); - } - case V1_18_R2 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_18_R2()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_18_R2()); - } - case V1_19_R1 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R1()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_19_R1()); - } - case V1_19_R2 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R2()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_19_R2()); - } - case V1_19_R3 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_19_R3()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_19_R3()); - } - case V1_20_R1 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R1()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_20_R1()); - } - case V1_20_R2 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R2()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_20_R2()); - } - case V1_20_R3 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_20_R3()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_20_R3()); - } - case V1_21_R1 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R1()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R1()); - } - case V1_21_R2 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R2()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R2()); - } - case V1_21_R3 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R3()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R3()); - } - case V1_21_R4 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R4()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R4()); - } - case V1_21_R5 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R5()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R5()); - } - case V1_21_R6 -> { - this.skinOverlay.setGameProfileProvider(new GameProfileProvider_1_21_R6()); - this.skinOverlay.setSkinRefresher(new SkinRefresher_1_21_R6()); - } - default -> { - this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); - this.skinOverlay.setGameProfileProvider(new GameProfileProviderNoop()); - this.getLogger().info("SkinOverlay does not support " + Bukkit.getBukkitVersion()); - } - } - } - if (Bukkit.getPluginManager().isPluginEnabled("SkinsRestorer")) { this.skinOverlay.setSkinHook(new SkinsRestorerHook()); this.skinOverlay.setGameProfileProvider(new GameProfileProvider_SkinsRestorer()); this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); } else { this.skinOverlay.setSkinHook(new SkinHookNoop()); + if (BukkitMinecraftUtils.isPaper() && getCurrent().isAtLeast(1, 20)) { + this.skinOverlay.setSkinRefresher(new NoopSkinRefresher()); + this.skinOverlay.setGameProfileProvider(new PaperGameProfileProvider()); + } else { + new SkinOverlayVersionResolver(this.skinOverlay).resolve(); + } } + this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode()); this.skinOverlay.setProxy(false); if (BukkitMinecraftUtils.isFolia()) { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java index 58b67f0e..0f0868e7 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java @@ -14,7 +14,7 @@ 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.BukkitMinecraftUtils.MinecraftReflection.obcClass; import static com.georgev22.skinoverlay.utilities.Utils.Reflection.fetchMethodAndInvoke; public class BukkitLegacyGameProfileProvider extends GameProfileProvider { @@ -22,7 +22,7 @@ public class BukkitLegacyGameProfileProvider extends GameProfileProvider { @Override public GameProfile getInternalGameProfile(@NotNull SPlayer player) { try { - Class craftPlayerClass = getOBCClass("entity.CraftPlayer"); + Class craftPlayerClass = obcClass("entity.CraftPlayer"); Player bukkitPlayer = player.getPlayer(); return (GameProfile) fetchMethodAndInvoke(craftPlayerClass, "getProfile", bukkitPlayer, new Object[]{}, new Class[]{}); } catch (Exception e) { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java index 653ebcb0..03039254 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/refreshers/LegacySkinRefresher.java @@ -18,8 +18,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftReflection.getNMSClass; -import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftReflection.getOBCClass; +import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftReflection.*; import static com.georgev22.skinoverlay.utilities.Utils.Reflection.*; @SuppressWarnings("deprecation") @@ -36,11 +35,11 @@ public class LegacySkinRefresher extends SkinRefresher { public LegacySkinRefresher() { try { - packet = getNMSClass("Packet", "net.minecraft.network.protocol.Packet"); - playOutHeldItemSlot = getNMSClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot"); - playOutPosition = getNMSClass("PacketPlayOutPosition", "net.minecraft.network.protocol.game.PacketPlayOutPosition"); - playOutPlayerInfo = getNMSClass("PacketPlayOutPlayerInfo", "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo"); - playOutRespawn = getNMSClass("PacketPlayOutRespawn", "net.minecraft.network.protocol.game.PacketPlayOutRespawn"); + packet = nmsClass("Packet", "net.minecraft.network.protocol.Packet"); + playOutHeldItemSlot = nmsClass("PacketPlayOutHeldItemSlot", "net.minecraft.network.protocol.game.PacketPlayOutHeldItemSlot"); + playOutPosition = nmsClass("PacketPlayOutPosition", "net.minecraft.network.protocol.game.PacketPlayOutPosition"); + playOutPlayerInfo = nmsClass("PacketPlayOutPlayerInfo", "net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo"); + playOutRespawn = nmsClass("PacketPlayOutRespawn", "net.minecraft.network.protocol.game.PacketPlayOutRespawn"); try { removePlayerEnum = getEnum(playOutPlayerInfo, "EnumPlayerInfoAction", "REMOVE_PLAYER"); @@ -57,7 +56,7 @@ public LegacySkinRefresher() { addPlayerEnum = getEnum(playOutPlayerInfo, "Action", "ADD_PLAYER"); } catch (Exception e3) { try { - Class enumPlayerInfoAction = getNMSClass("EnumPlayerInfoAction", null); + Class enumPlayerInfoAction = nmsClass("EnumPlayerInfoAction", null); removePlayerEnum = getEnum(enumPlayerInfoAction, "REMOVE_PLAYER"); addPlayerEnum = getEnum(enumPlayerInfoAction, "ADD_PLAYER"); @@ -68,7 +67,7 @@ public LegacySkinRefresher() { } } - getHandleMethod = getOBCClass("entity.CraftPlayer").getDeclaredMethod("getHandle"); + getHandleMethod = obcClass("entity.CraftPlayer").getDeclaredMethod("getHandle"); } catch (Exception e) { throw new RuntimeException(e); } @@ -175,7 +174,7 @@ private void sendPacket(Object playerConnection, Object packet) throws Invocatio dimensionManager = getFieldByType(worldObject, "DimensionManager"); } catch (ReflectionException e) { try { - Class dimensionManagerClass = getNMSClass("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager"); + Class dimensionManagerClass = nmsClass("DimensionManager", "net.minecraft.world.level.dimension.DimensionManager"); for (Method m : dimensionManagerClass.getDeclaredMethods()) { if (m.getReturnType() == dimensionManagerClass && m.getParameterCount() == 1 && m.getParameterTypes()[0] == Integer.TYPE) { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java index 8d2c61bb..79b1c87d 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -18,6 +18,7 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import java.io.BufferedReader; @@ -650,254 +651,287 @@ public static boolean isPaper() { || 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; + /** + * Represents a Minecraft server version using a numeric format (major.minor.patch). + */ + public static final class MinecraftVersion implements Comparable { - 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 int major; + private final int minor; + private final int patch; + private final String nmsPackage; - private final SubVersionRange subVersionRange; + /** + * The current server version, parsed once during class initialization. + */ + private static final MinecraftVersion CURRENT; - MinecraftVersion(SubVersionRange subVersionRange) { - this.subVersionRange = subVersionRange; + static { + CURRENT = parse(Bukkit.getServer().getBukkitVersion()); } /** - * Returns the current minecraft server version. + * Constructs a new {@link MinecraftVersion}. * - * @return the current minecraft server version. + * @param major the major version (e.g. 1 or 26) + * @param minor the minor version (e.g. 21 or 1) + * @param patch the patch version (e.g. 4 or 11) */ - public static MinecraftVersion getCurrentVersion() { - return currentVersion; + public MinecraftVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.nmsPackage = parseNMSPackage(); + } + + public MinecraftVersion(int major, int minor, int patch, String nmsPackage) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.nmsPackage = nmsPackage; } /** - * Get the version number of the Minecraft server. + * Returns the current Minecraft server version. * - * @return The version number of the Minecraft server. + * @return the parsed server version */ - public static int getVersionNumber() { - return versionNumber; + public static MinecraftVersion getCurrent() { + return CURRENT; } /** - * Get the release number of the Minecraft server. + * Parses a Bukkit version string into a {@link MinecraftVersion}. + *

+ * Examples of supported formats: + *

    + *
  • {@code 1.21.4-R0.1-SNAPSHOT}
  • + *
  • {@code 26.1.1}
  • + *
* - * @return The release number of the Minecraft server. + * @param bukkitVersion the raw version string from Bukkit + * @return a parsed {@link MinecraftVersion}, or {@code 0.0.0} if parsing fails */ - public static int getReleaseNumber() { - return releaseNumber; - } + @Contract("_ -> new") + public static @NonNull MinecraftVersion parse(String bukkitVersion) { + try { + String versionPart = bukkitVersion.split("-")[0]; + String[] parts = versionPart.split("\\."); - @Contract(pure = true) - public static @NotNull String getCurrentVersionName() { - return currentVersion.name(); + int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + + return new MinecraftVersion(major, minor, patch, parseNMSPackage()); + } catch (Exception e) { + return new MinecraftVersion(0, 0, 0, "unknown"); + } } - @Contract(pure = true) - public static @NotNull String getCurrentVersionNameVtoLowerCase() { - return currentVersion.name().replace("V", "v"); + private static String parseNMSPackage() { + try { + String packageName = Bukkit.getServer() + .getClass() + .getPackage() + .getName(); + + String[] parts = packageName.split("\\."); + + return parts[parts.length - 1]; + } catch (Exception e) { + return "unknown"; + } } /** - * Check if the version is above or equal. + * Compares this version to another version. * - * @param minecraftVersion The {@link MinecraftVersion} to be checked. - * @return if the minecraft version is above or equal. + * @param other the other version + * @return a negative value if lower, positive if higher, 0 if equal */ - public boolean isAboveOrEqual(@NotNull MinecraftVersion minecraftVersion) { - return this.ordinal() >= minecraftVersion.ordinal(); + @Override + public int compareTo(@NonNull MinecraftVersion other) { + if (this.major != other.major) { + return Integer.compare(this.major, other.major); + } + if (this.minor != other.minor) { + return Integer.compare(this.minor, other.minor); + } + return Integer.compare(this.patch, other.patch); } /** - * Check if the version is above. + * Checks if this version is greater than or equal to the given version. + * Patch is ignored (assumes 0). * - * @param minecraftVersion The {@link MinecraftVersion} to be checked. - * @return if the minecraft version is above. + * @param major the major version + * @param minor the minor version + * @return {@code true} if this version is >= given version */ - public boolean isAbove(@NotNull MinecraftVersion minecraftVersion) { - return this.ordinal() > minecraftVersion.ordinal(); + public boolean isAtLeast(int major, int minor) { + return compareTo(new MinecraftVersion(major, minor, 0)) >= 0; } /** - * Check if the version is below or equal. + * Checks if this version is greater than or equal to the given version. * - * @param minecraftVersion The {@link MinecraftVersion} to be checked. - * @return if the minecraft version is below or equal. + * @param major the major version + * @param minor the minor version + * @param patch the patch version + * @return {@code true} if this version is >= given version */ - public boolean isBelowOrEqual(@NotNull MinecraftVersion minecraftVersion) { - return this.ordinal() <= minecraftVersion.ordinal(); + public boolean isAtLeast(int major, int minor, int patch) { + return compareTo(new MinecraftVersion(major, minor, patch)) >= 0; } /** - * Check if the version is below. + * Checks if this version is strictly lower than the given version. + * Patch is ignored (assumes 0). * - * @param minecraftVersion The {@link MinecraftVersion} to be checked. - * @return if the minecraft version is below. + * @param major the major version + * @param minor the minor version + * @return {@code true} if this version is < given version */ - public boolean isBelow(@NotNull MinecraftVersion minecraftVersion) { - return this.ordinal() < minecraftVersion.ordinal(); + public boolean isBelow(int major, int minor) { + return isBelow(major, minor, 0); + } + + + public boolean isBelow(int major, int minor, int patch) { + return compareTo(new MinecraftVersion(major, minor, patch)) < 0; } /** - * Check if the version is equal. + * Checks if this version is within a range: + * {@code [min, max)} (inclusive lower bound, exclusive upper bound). * - * @param minecraftVersion The {@link MinecraftVersion} to be checked. - * @return if the minecraft version is equal. + * @param minMajor minimum major version + * @param minMinor minimum minor version + * @param maxMajor maximum major version + * @param maxMinor maximum minor version + * @return {@code true} if within the specified range */ - public boolean isEqual(@NotNull MinecraftVersion minecraftVersion) { - return this.ordinal() == minecraftVersion.ordinal(); + public boolean isBetween( + int minMajor, int minMinor, + int maxMajor, int maxMinor + ) { + return isAtLeast(minMajor, minMinor) + && isBelow(maxMajor, maxMinor); } - /** - * Get the sub version range. - * - * @return The sub version range. - **/ - public SubVersionRange getSubVersionRange() { - return subVersionRange; + public boolean isBetween(int minMajor, int minMinor, int minPatch, int maxMajor, int maxMinor, int maxPatch) { + return isAtLeast(minMajor, minMinor, minPatch) + && isBelow(maxMajor, maxMinor, maxPatch); } - public static class SubVersionRange { - private final String version; - private final int start; - private final int end; + public boolean isVersion(int major, int minor, Integer patch) { + return this.major == major + && this.minor == minor + && (patch == null || this.patch == patch); + } - SubVersionRange(String version, int patch) { - this(version, patch, patch); - } + /** + * @return the major version + */ + public int getMajor() { + return major; + } - SubVersionRange(String version, int start, int end) { - this.version = version; - this.start = start; - this.end = end; - } + /** + * @return the minor version + */ + public int getMinor() { + return minor; + } - public String getVersion() { - return version; - } + /** + * @return the patch version + */ + public int getPatch() { + return patch; + } - public int getStart() { - return start; - } + public String getNMSPackage() { + return nmsPackage; + } - public int getEnd() { - return end; - } + /** + * Returns the version in {@code major.minor.patch} format. + * + * @return string representation of this version + */ + @Override + public String toString() { + return major + "." + minor + "." + patch; } } - public static class MinecraftReflection { + public static final 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 final String NET_MINECRAFT_PACKAGE = "net.minecraft"; + private static final String OBC_PACKAGE = "org.bukkit.craftbukkit"; - private static volatile Object theUnsafe; + private static final String NMS_SERVER_BASE = NET_MINECRAFT_PACKAGE + ".server"; - public static boolean isRepackaged() { - return MinecraftVersion.getCurrentVersion().isAboveOrEqual(MinecraftVersion.V1_17_R1); + private static final MinecraftVersion VERSION = MinecraftVersion.getCurrent(); + private static final boolean REPACKAGED_NMS = VERSION.isAtLeast(1, 17); + private static final boolean REPACKAGED_OBC = VERSION.isAtLeast(1, 20, 5) && isPaper(); + + private static String resolveNmsPackage() { + return REPACKAGED_NMS ? NMS_SERVER_BASE : NMS_SERVER_BASE + "." + VERSION.getNMSPackage(); } - @Contract(pure = true) - public static @NotNull String getNMSClassName(String className) { - return NET_MINECRAFT_SERVER_PACKAGE + '.' + MinecraftVersion.getCurrentVersionNameVtoLowerCase() + '.' + className; + private static String resolveObcPackage() { + return REPACKAGED_OBC ? OBC_PACKAGE : OBC_PACKAGE + "." + VERSION.getNMSPackage(); } - public static @NotNull Class getNMSClass(String className) throws ClassNotFoundException { - return Class.forName(getNMSClassName(className)); + private static ClassLoader loader() { + return Bukkit.class.getClassLoader(); } - public static Optional> getNMSOptionalClass(String className) { - return Utils.Reflection.optionalClass(getNMSClassName(className), Bukkit.class.getClassLoader()); + @Contract(pure = true) + public static @NonNull String nmsClassName(String className) { + return resolveNmsPackage() + "." + className; + } + + public static @NonNull Class nmsClass(String className) throws ClassNotFoundException { + return Class.forName(nmsClassName(className), false, loader()); } - public static @NotNull String getNMSClassName(String className, String fullClassName) { - return isRepackaged() ? fullClassName : getNMSClassName(className); + public static Optional> nmsOptionalClass(String className) { + return Utils.Reflection.optionalClass(nmsClassName(className), loader()); } - public static @NotNull Class getNMSClass(String className, String fullClassName) throws ClassNotFoundException { - return isRepackaged() ? Class.forName(fullClassName) : getNMSClass(className); + public static @NonNull Class nmsClass(String className, String fullName) throws ClassNotFoundException { + return Class.forName(REPACKAGED_NMS ? fullName : nmsClassName(className), false, loader()); } - public static Optional> getNMSOptionalClass(String className, String fullClassName) { - return isRepackaged() ? Utils.Reflection.optionalClass(fullClassName, Bukkit.class.getClassLoader()) : getNMSOptionalClass(className); + public static Optional> nmsOptionalClass(String className, String fullName) { + return Utils.Reflection.optionalClass( + REPACKAGED_NMS ? fullName : nmsClassName(className), + loader() + ); } @Contract(pure = true) - public static @NotNull String getOBCClassName(String className) { - return ORG_BUKKIT_CRAFTBUKKIT_PACKAGE + '.' + MinecraftVersion.getCurrentVersionNameVtoLowerCase() + '.' + className; + public static @NonNull String obcClassName(String className) { + return resolveObcPackage() + "." + className; + } + + public static @NonNull Class obcClass(String className) throws ClassNotFoundException { + return Class.forName(obcClassName(className), false, loader()); + } + + public static Optional> obcOptionalClass(String className) { + return Utils.Reflection.optionalClass(obcClassName(className), loader()); } - public static @NotNull Class getOBCClass(String className) throws ClassNotFoundException { - return Class.forName(getOBCClassName(className)); + public static boolean isNmsRepackaged() { + return REPACKAGED_NMS; } - public static Optional> getOBCOptionalClass(String className) { - return Utils.Reflection.optionalClass(getOBCClassName(className), Bukkit.class.getClassLoader()); + public static boolean isObcRepackaged() { + return REPACKAGED_OBC; } } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java new file mode 100644 index 00000000..f31f7e53 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/SkinOverlayVersionResolver.java @@ -0,0 +1,31 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.providers.GameProfileProviderNoop; +import com.georgev22.skinoverlay.refreshers.NoopSkinRefresher; + +public final class SkinOverlayVersionResolver { + + private final SkinOverlay skinOverlay; + + public SkinOverlayVersionResolver(SkinOverlay skinOverlay) { + this.skinOverlay = skinOverlay; + } + + public void resolve() { + BukkitMinecraftUtils.MinecraftVersion current = BukkitMinecraftUtils.MinecraftVersion.getCurrent(); + for (VersionRange range : VersionRegistry.RANGES) { + if (range.matches(current)) { + skinOverlay.setGameProfileProvider(range.provider().get()); + skinOverlay.setSkinRefresher(range.refresher().get()); + return; + } + } + + skinOverlay.setSkinRefresher(new NoopSkinRefresher()); + skinOverlay.setGameProfileProvider(new GameProfileProviderNoop()); + + skinOverlay.getLogger().info("SkinOverlay does not support " + current); + skinOverlay.getLogger().info("Install SkinsRestorer in order for SkinOverlay to use their API to apply skins."); + } +} \ No newline at end of file diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java new file mode 100644 index 00000000..7ba01efd --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java @@ -0,0 +1,19 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.providers.GameProfileProvider; +import com.georgev22.skinoverlay.refreshers.SkinRefresher; +import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion; +import org.jspecify.annotations.NonNull; + +import java.util.function.Supplier; + +public record VersionRange( + MinecraftVersion min, + MinecraftVersion max, + Supplier provider, + Supplier refresher +) { + public boolean matches(@NonNull MinecraftVersion v) { + return v.compareTo(min) >= 0 && v.compareTo(max) <= 0; + } +} \ No newline at end of file diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java new file mode 100644 index 00000000..2ff0fe35 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java @@ -0,0 +1,135 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.providers.*; +import com.georgev22.skinoverlay.refreshers.*; +import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion; + +import java.util.List; + +public final class VersionRegistry { + + private VersionRegistry() { + } + + public static final List RANGES = List.of( + + new VersionRange( + new MinecraftVersion(1, 8, 0), + new MinecraftVersion(1, 16, 5), + BukkitLegacyGameProfileProvider::new, + LegacySkinRefresher::new + ), + + new VersionRange( + new MinecraftVersion(1, 17, 0), + new MinecraftVersion(1, 17, 1), + GameProfileProvider_1_17_R1::new, + SkinRefresher_1_17_R1::new + ), + + new VersionRange( + new MinecraftVersion(1, 18, 0), + new MinecraftVersion(1, 18, 1), + GameProfileProvider_1_18_R1::new, + SkinRefresher_1_18_R1::new + ), + + new VersionRange( + new MinecraftVersion(1, 18, 2), + new MinecraftVersion(1, 18, 2), + GameProfileProvider_1_18_R2::new, + SkinRefresher_1_18_R2::new + ), + + new VersionRange( + new MinecraftVersion(1, 19, 0), + new MinecraftVersion(1, 19, 2), + GameProfileProvider_1_19_R1::new, + SkinRefresher_1_19_R1::new + ), + + new VersionRange( + new MinecraftVersion(1, 19, 3), + new MinecraftVersion(1, 19, 3), + GameProfileProvider_1_19_R2::new, + SkinRefresher_1_19_R2::new + ), + + new VersionRange( + new MinecraftVersion(1, 19, 4), + new MinecraftVersion(1, 19, 4), + GameProfileProvider_1_19_R3::new, + SkinRefresher_1_19_R3::new + ), + + new VersionRange( + new MinecraftVersion(1, 20, 0), + new MinecraftVersion(1, 20, 1), + GameProfileProvider_1_20_R1::new, + SkinRefresher_1_20_R1::new + ), + + new VersionRange( + new MinecraftVersion(1, 20, 2), + new MinecraftVersion(1, 20, 2), + GameProfileProvider_1_20_R2::new, + SkinRefresher_1_20_R2::new + ), + + new VersionRange( + new MinecraftVersion(1, 20, 3), + new MinecraftVersion(1, 20, 4), + GameProfileProvider_1_20_R3::new, + SkinRefresher_1_20_R3::new + ), + + new VersionRange( + new MinecraftVersion(1, 20, 5), + new MinecraftVersion(1, 20, 6), + GameProfileProvider_1_20_R4::new, + SkinRefresher_1_20_R4::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 0), + new MinecraftVersion(1, 21, 1), + GameProfileProvider_1_21_R1::new, + SkinRefresher_1_21_R1::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 2), + new MinecraftVersion(1, 21, 3), + GameProfileProvider_1_21_R2::new, + SkinRefresher_1_21_R2::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 4), + new MinecraftVersion(1, 21, 4), + GameProfileProvider_1_21_R3::new, + SkinRefresher_1_21_R3::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 5), + new MinecraftVersion(1, 21, 5), + GameProfileProvider_1_21_R4::new, + SkinRefresher_1_21_R4::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 6), + new MinecraftVersion(1, 21, 8), + GameProfileProvider_1_21_R5::new, + SkinRefresher_1_21_R5::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 9), + new MinecraftVersion(1, 21, 10), + GameProfileProvider_1_21_R6::new, + SkinRefresher_1_21_R6::new + ) + ); +} \ No newline at end of file From d1d1126ef677feae12383b42805123b088d37188 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 18 Jun 2026 19:11:37 +0300 Subject: [PATCH 45/79] refactor: simplify scheduler selection logic in Bukkit plugin Replace if-else block with a ternary operator for cleaner and more concise code when choosing between Folia and Bukkit schedulers. --- .../com/georgev22/skinoverlay/SkinOverlayBukkit.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index d88fd932..a3fc046f 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -49,15 +49,9 @@ public void onLoad() { this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode()); this.skinOverlay.setProxy(false); - if (BukkitMinecraftUtils.isFolia()) { - this.skinOverlay.setScheduler( - new MinecraftFoliaScheduler() - ); - } else { - this.skinOverlay.setScheduler( - new MinecraftBukkitScheduler() - ); - } + this.skinOverlay.setScheduler( + BukkitMinecraftUtils.isFolia() ? new MinecraftFoliaScheduler() : new MinecraftBukkitScheduler() + ); this.skinOverlay.onLoad(); } From 12491b56ee4760193a3575b39f67ceaf49e8fe6c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Jun 2026 16:34:32 +0000 Subject: [PATCH 46/79] chore(release): 8.0.0-beta.4 [skip ci] # [8.0.0-beta.4](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.3...v8.0.0-beta.4) (2026-06-18) ### Features * restructure data structures, storage, and task management ([d6519c9](https://github.com/GeorgeV220/SkinOverlay/commit/d6519c931d27534c406850d2c54b961a26a9b1ad)) ### BREAKING CHANGES * - Removed old `ConcurrentObjectMap`, `HashObjectMap`, `LinkedObjectMap`, `ObjectMap`, `ObservableObjectMap`, `Pair`, `TreeObjectMap`, `UnmodifiableObjectMap`, `PlayerEntityManager`, `SkinEntityManager`, `PlayerFileManager`, `SkinFileManager`, `PairDocument`. - `Entity.getId()` renamed to `Entity.getUniqueId()`. - `EntityManagerRegistry` is now a singleton via `getInstance()` and uses typed lookups via `getTyped(Class)`. - `EntityManager` load methods now return `CompletableFuture`. --- CHANGELOG.md | 19 +++++++++++++++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c02363..3c80730c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# [8.0.0-beta.4](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.3...v8.0.0-beta.4) (2026-06-18) + + +### Features + +* restructure data structures, storage, and task management ([d6519c9](https://github.com/GeorgeV220/SkinOverlay/commit/d6519c931d27534c406850d2c54b961a26a9b1ad)) + + +### BREAKING CHANGES + +* - Removed old `ConcurrentObjectMap`, `HashObjectMap`, `LinkedObjectMap`, + `ObjectMap`, `ObservableObjectMap`, `Pair`, `TreeObjectMap`, + `UnmodifiableObjectMap`, `PlayerEntityManager`, `SkinEntityManager`, + `PlayerFileManager`, `SkinFileManager`, `PairDocument`. +- `Entity.getId()` renamed to `Entity.getUniqueId()`. +- `EntityManagerRegistry` is now a singleton via `getInstance()` and + uses typed lookups via `getTyped(Class)`. +- `EntityManager` load methods now return `CompletableFuture`. + # [8.0.0-beta.3](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.2...v8.0.0-beta.3) (2025-11-30) diff --git a/README.md b/README.md index 569e4735..708b9334 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 - 8.0.0-beta.3 + 8.0.0-beta.4 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.3" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.4" } ``` diff --git a/gradle.properties b/gradle.properties index bc1974e3..be80bb82 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.3 +version = 8.0.0-beta.4 description = SkinOverlay From fecea1ec7e85ee807330f8bc6f59914b1c65c551 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 18 Jun 2026 19:38:41 +0300 Subject: [PATCH 47/79] fix: update repository URLs from GeorgeV22 to ShulkerLabs Replace all references of the old GeorgeV22 Maven repository with the new ShulkerLabs repository URLs in Gradle build scripts and documentation. This change updates the repository endpoints for both releases and snapshots to reflect the new project hosting location. --- README.md | 4 ++-- .../main/kotlin/buildlogic.java-conventions.gradle.kts | 4 ++-- gradle/publish.gradle | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 708b9334..029e66ed 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You can have your project depend on SkinOverlay as a dependency through the foll reposilite-repository GeorgeV22 Repository - https://repo.georgev22.com/releases + https://repo.shulkerlabs.com/releases @@ -40,7 +40,7 @@ You can include SkinOverlay into your gradle project using the following lines: ```groovy repositories { maven { - url "https://repo.georgev22.com/releases" + url "https://repo.shulkerlabs.com/releases" } } diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts index e1360543..df478842 100644 --- a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -11,10 +11,10 @@ repositories { mavenLocal() mavenCentral() maven { - url = uri("https://repo.georgev22.com/releases/") + url = uri("https://repo.shulkerlabs.com/releases/") } maven { - url = uri("https://repo.georgev22.com/snapshots/") + url = uri("https://repo.shulkerlabs.com/snapshots/") } maven { url = uri("https://repo.codemc.org/repository/maven-public/") diff --git a/gradle/publish.gradle b/gradle/publish.gradle index eebbb37b..dd3b7d37 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -50,9 +50,9 @@ publishing { } } maven { - name = "GeorgeV22" + name = "ShulkerLabs" - url = uri("https://repo.georgev22.com/releases") + url = uri("https://repo.shulkerlabs.com/releases") def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null @@ -72,9 +72,9 @@ publishing { } if (!isGitHub) { maven { - name = "GeorgeV22" + name = "ShulkerLabs-snapshots" - url = uri("https://repo.georgev22.com/snapshots") + url = uri("https://repo.shulkerlabs.com/snapshots") def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null From 10b785bfc9d0837eb1357bd56ae4dd64dad49aad Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Jun 2026 16:42:43 +0000 Subject: [PATCH 48/79] chore(release): 8.0.0-beta.5 [skip ci] # [8.0.0-beta.5](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.4...v8.0.0-beta.5) (2026-06-18) ### Bug Fixes * update repository URLs from GeorgeV22 to ShulkerLabs ([fecea1e](https://github.com/GeorgeV220/SkinOverlay/commit/fecea1ec7e85ee807330f8bc6f59914b1c65c551)) --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c80730c..8af9830d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [8.0.0-beta.5](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.4...v8.0.0-beta.5) (2026-06-18) + + +### Bug Fixes + +* update repository URLs from GeorgeV22 to ShulkerLabs ([fecea1e](https://github.com/GeorgeV220/SkinOverlay/commit/fecea1ec7e85ee807330f8bc6f59914b1c65c551)) + # [8.0.0-beta.4](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.3...v8.0.0-beta.4) (2026-06-18) diff --git a/README.md b/README.md index 029e66ed..6bddd5ac 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 - 8.0.0-beta.4 + 8.0.0-beta.5 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.4" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.5" } ``` diff --git a/gradle.properties b/gradle.properties index be80bb82..212f9284 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.4 +version = 8.0.0-beta.5 description = SkinOverlay From 51b2f825304c383e012681ffcd15a06ca2d34a7b Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 19 Jun 2026 05:05:39 +0300 Subject: [PATCH 49/79] refactor!: replace AudienceProvider with console audience and add placeholder hook Remove VelocityAudienceProvider and replace audience provider usage with direct Audience objects. Refactor CommandIssuer to use audience() method instead of separate sendMessage methods. Add PlaceholderAPI hook support in Bukkit and common modules. The changes simplify the audience handling by removing the need for a full AudienceProvider implementation on Velocity, and instead use the console command source directly. CommandIssuer now returns an Audience directly, reducing code duplication. PlaceholderAPI integration is added through a new PlaceholderHook interface. --- .../skinoverlay/SkinOverlayBukkit.java | 25 ++++- .../command/BukkitCommandIssuer.java | 20 ++-- .../hooks/placeholder/PlaceholderAPIHook.java | 65 ++++++++++++ .../georgev22/skinoverlay/SkinOverlay.java | 89 +++++++++++++---- .../skinoverlay/command/CommandIssuer.java | 39 ++------ .../skinoverlay/command/MethodCommand.java | 3 +- .../command/commands/sub/InfoSubCommand.java | 23 +++-- .../hooks/placeholder/PlaceholderHook.java | 52 ++++++++++ .../skinoverlay/message/MessageEntry.java | 4 +- velocity/build.gradle.kts | 1 - .../skinoverlay/SkinOverlayVelocity.java | 2 +- .../skinoverlay/VelocityAudienceProvider.java | 99 ------------------- .../command/VelocityCommandIssuer.java | 11 +-- 13 files changed, 247 insertions(+), 186 deletions(-) create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderHook.java delete mode 100644 velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index a3fc046f..8b3e4ffe 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -1,5 +1,6 @@ package com.georgev22.skinoverlay; +import com.georgev22.skinoverlay.hooks.placeholder.PlaceholderAPIHook; import com.georgev22.skinoverlay.refreshers.*; import com.georgev22.skinoverlay.command.BukkitCommandManager; import com.georgev22.skinoverlay.hooks.SkinHookNoop; @@ -23,10 +24,18 @@ public class SkinOverlayBukkit extends JavaPlugin { + private static SkinOverlayBukkit instance; + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + private BukkitAudiences bukkitAudiences; + + public static SkinOverlayBukkit getInstance() { + return instance; + } @Override public void onLoad() { + instance = this; this.skinOverlay.setPlugin(this); this.skinOverlay.setLogger(this.getLogger()); this.skinOverlay.setDataFolder(this.getDataFolder()); @@ -47,6 +56,10 @@ public void onLoad() { } } + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + this.skinOverlay.setPlaceholderHook(new PlaceholderAPIHook()); + } + this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode()); this.skinOverlay.setProxy(false); this.skinOverlay.setScheduler( @@ -57,8 +70,8 @@ public void onLoad() { @Override public void onEnable() { - this.skinOverlay.setAudienceProvider(BukkitAudiences.create(this)); - + this.bukkitAudiences = BukkitAudiences.create(this); + this.skinOverlay.setConsoleAudience(this.bukkitAudiences.console()); BukkitMinecraftUtils.registerListeners( this, new PlayerListeners() @@ -90,5 +103,13 @@ public void onEnable() { @Override public void onDisable() { skinOverlay.onDisable(); + if (this.bukkitAudiences != null) { + this.bukkitAudiences.close(); + this.bukkitAudiences = null; + } + } + + public BukkitAudiences getBukkitAudiences() { + return bukkitAudiences; } } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java index 53b25c20..65441ede 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java @@ -1,8 +1,9 @@ package com.georgev22.skinoverlay.command; -import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.SkinOverlayBukkit; import com.georgev22.skinoverlay.utilities.Utils; -import net.kyori.adventure.text.Component; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -23,17 +24,12 @@ public boolean isPlayer() { } @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; + public Audience audience() { + BukkitAudiences audienceProvider = SkinOverlayBukkit.getInstance().getBukkitAudiences(); + if (isPlayer()) { + return audienceProvider.player((Player) this.sender); } - SkinOverlay.getInstance().getAudienceProvider().player(this.getUniqueId()).sendMessage(component); + return audienceProvider.console(); } @Override diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java new file mode 100644 index 00000000..0645c913 --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java @@ -0,0 +1,65 @@ +package com.georgev22.skinoverlay.hooks.placeholder; + +import com.georgev22.skinoverlay.BuildParameters; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.storage.EntityManager; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import net.kyori.adventure.audience.Audience; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class PlaceholderAPIHook extends PlaceholderExpansion implements PlaceholderHook { + + @Override + public String resolve(Audience audience, String input) { + if (audience == null) return input; + try { + return me.clip.placeholderapi.PlaceholderAPI + .setBracketPlaceholders(audience instanceof Player player ? player : null, input); + } catch (Throwable ignored) { + return input; + } + } + + @Override + public @NotNull String getIdentifier() { + return BuildParameters.PLUGIN_NAME.toLowerCase(); + } + + @Override + public @NotNull String getAuthor() { + return BuildParameters.AUTHOR; + } + + @Override + public @NotNull String getVersion() { + return BuildParameters.VERSION; + } + + @Override + public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) { + if (player == null) return null; + + Optional> entityManagerOpt = EntityManagerRegistry.getInstance().getTyped(PlayerData.class); + if (entityManagerOpt.isEmpty()) return null; + + EntityManager entityManager = entityManagerOpt.get(); + + Optional playerDataOptional = entityManager.findById(player.getUniqueId()); + if (playerDataOptional.isEmpty()) return null; + + PlayerData playerData = playerDataOptional.get(); + + if (params.equalsIgnoreCase("overlay")) { + return playerData.getCurrentSkin().getSkinParts().getSkinName(); + } + + return null; + + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index f31bb3a3..fdbc3a77 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -1,5 +1,6 @@ package com.georgev22.skinoverlay; +import com.georgev22.skinoverlay.hooks.placeholder.PlaceholderHook; import com.georgev22.skinoverlay.refreshers.SkinRefresher; import com.georgev22.skinoverlay.command.CommandManager; import com.georgev22.skinoverlay.command.CompletionEngine; @@ -29,10 +30,12 @@ import com.georgev22.skinoverlay.utilities.config.FileManager; import com.georgev22.skinoverlay.utilities.config.SkinFileCache; import com.google.gson.Gson; -import net.kyori.adventure.platform.AudienceProvider; +import net.kyori.adventure.audience.Audience; import org.bspfsystems.yamlconfiguration.file.FileConfiguration; +import org.jetbrains.annotations.ApiStatus; import java.io.File; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -59,13 +62,14 @@ public static SkinOverlay getInstance() { private CommandManager commandManager; private Object plugin; private MinecraftScheduler scheduler; - private AudienceProvider audienceProvider; + private Audience consoleAudience; private Logger logger; private PlayerProvider playerProvider; private GameProfileProvider gameProfileProvider; private SkinProvider skinProvider; private SkinRefresher skinRefresher; private SkinHook skinHook; + private PlaceholderHook placeholderHook; private boolean isOnlineMode; private boolean isProxy; private File dataFolder; @@ -115,6 +119,17 @@ public void onLoad() { * Enables the plugin components. Should be called during plugin enable phase. */ public void onEnable() { + if (placeholderHook != null) { + try { + if (placeholderHook.register()) { + getLogger().info("Placeholder hook registered successfully!"); + } else { + getLogger().warning("Placeholder hook registration failed!"); + } + } catch (Exception e) { + getLogger().log(Level.WARNING, "Failed to register placeholder hook", e); + } + } CompletionEngine.registerResolver("@overlays", (commandIssuer, args) -> skinFileCache.getSkinConfigurationFiles().keySet()); this.commandManager.registerCommand(new SkinOverlayMain()); @@ -133,9 +148,17 @@ public void onDisable() { if (this.scheduler != null) { this.scheduler.cancelTasks(getPlugin()); } - if (this.audienceProvider != null) { - this.audienceProvider.close(); - audienceProvider = null; + + if (placeholderHook != null) { + try { + if (placeholderHook.unregister()) { + getLogger().info("Placeholder hook unregistered successfully!"); + } else { + getLogger().warning("Placeholder hook unregistration failed!"); + } + } catch (Exception e) { + getLogger().log(Level.WARNING, "Failed to unregister placeholder hook", e); + } } EntityManagerRegistry.getInstance().entries().forEach((key, value) -> value.shutdown()); @@ -253,22 +276,12 @@ public void setScheduler(MinecraftScheduler scheduler) { this.scheduler = scheduler; } - /** - * Gets the AudienceProvider instance. - * - * @return the AudienceProvider - */ - public AudienceProvider getAudienceProvider() { - return audienceProvider; + public Audience getConsoleAudience() { + return consoleAudience; } - /** - * Sets the AudienceProvider instance. - * - * @param audienceProvider the AudienceProvider - */ - public void setAudienceProvider(AudienceProvider audienceProvider) { - this.audienceProvider = audienceProvider; + public void setConsoleAudience(Audience consoleAudience) { + this.consoleAudience = consoleAudience; } /** @@ -370,6 +383,44 @@ public void setSkinHook(SkinHook skinHook) { this.skinHook = skinHook; } + /** + * Gets the PlaceholderHook instance. + * + * @return the PlaceholderHook + */ + public Optional getPlaceholderHook() { + return Optional.ofNullable(placeholderHook); + } + + /** + * Sets the active {@link PlaceholderHook} implementation. + *

+ * This method is intended for internal use only. The plugin automatically + * manages placeholder hook initialization and registration during its + * lifecycle. + *

+ * Calling this method manually is strongly discouraged unless you fully + * understand the placeholder hook lifecycle. In particular, registration + * should be handled manually by the caller, as the plugin's normal + * registration process occurs during {@code onEnable()}. Replacing the + * hook after or during this process without properly registering it may + * result in errors or undefined behavior. + *

+ * If a custom hook is supplied, it is the caller's responsibility to ensure + * that the hook is correctly initialized and that + * {@link PlaceholderHook#isRegistered()} returns {@code true} before the + * hook is used. + *

+ * This method should only be called after the plugin has completed its + * enable phase. + * + * @param placeholderHook the placeholder hook to use + */ + @ApiStatus.Internal + public void setPlaceholderHook(PlaceholderHook placeholderHook) { + this.placeholderHook = placeholderHook; + } + /** * Checks if the server is in online mode. * diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java index 0a0114ef..db3127af 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay.command; -import net.kyori.adventure.text.Component; +import net.kyori.adventure.audience.Audience; import org.jetbrains.annotations.NotNull; import java.util.UUID; @@ -20,40 +20,17 @@ public interface CommandIssuer { boolean isPlayer(); /** - * Sends a plain text message to the issuer. + * Returns the Kyori Adventure {@link Audience} associated with this 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. + *

An {@code Audience} represents any target that can receive messages, + * such as players, the console, command blocks, or other custom implementations.

* - * @param component the component to send - */ - void sendMessage(@NotNull Component component); - - /** - * Sends multiple rich text components to the issuer. + *

This can be used to send rich chat components, titles, action bars, + * and other Adventure-supported message types.

* - * @param components the components to send + * @return the audience associated with this issuer */ - default void sendMessage(Component @NotNull ... components) { - for (Component component : components) { - sendMessage(component); - } - } + Audience audience(); /** * Returns the underlying issuer object. diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java index bea33545..421423d5 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java @@ -3,6 +3,7 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.MessageBuilder; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import org.jetbrains.annotations.NotNull; @@ -56,7 +57,7 @@ public void execute(CommandIssuer sender, String @NotNull [] args, CommandContex method.setAccessible(true); invokeHandler(sender, args, context); } catch (Exception e) { - sender.sendMessage("§cError executing command."); + MessageBuilder.builder().appendMiniMessage("&cError executing command.").send(sender.audience()); SkinOverlay.getInstance().getLogger() .log(Level.SEVERE, "Error while executing command", e); } 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 index 59840c19..26df97cd 100644 --- 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 @@ -7,6 +7,7 @@ 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.MessageBuilder; import org.jetbrains.annotations.NotNull; @Subcommand("info") @@ -27,15 +28,17 @@ protected void handle(@NotNull CommandIssuer commandIssuer) { 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); + MessageBuilder.builder() + .appendln("Name: " + pluginName) + .appendln("Version: " + pluginVersion) + .appendln("Author: " + pluginAuthor) + .appendln("Description: " + pluginDescription) + .appendln("Website: " + pluginWebsite) + .appendln("CI Name: " + CIName) + .appendln("CI Build Number: " + CIBuildNumber) + .appendln("Commit: " + commit) + .appendln("Branch: " + branch) + .appendln("Build Time: " + buildTime) + .send(commandIssuer.audience()); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderHook.java b/common/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderHook.java new file mode 100644 index 00000000..51a8a118 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderHook.java @@ -0,0 +1,52 @@ +package com.georgev22.skinoverlay.hooks.placeholder; + +import net.kyori.adventure.audience.Audience; + +/** + * Represents a placeholder provider that can integrate with external + * placeholder systems and resolve placeholders in text. + *

+ * Implementations are responsible for handling registration and + * unregistration with the underlying placeholder service, as well as + * parsing and replacing placeholders for a given audience. + */ +public interface PlaceholderHook { + + /** + * Registers this placeholder hook with its underlying placeholder system. + * + * @return {@code true} if the hook was successfully registered, + * otherwise {@code false} + */ + boolean register(); + + /** + * Unregisters this placeholder hook from its underlying placeholder system. + * + * @return {@code true} if the hook was successfully unregistered, + * otherwise {@code false} + */ + boolean unregister(); + + /** + * Checks whether this placeholder hook is currently registered. + * + * @return {@code true} if the hook is registered, otherwise {@code false} + */ + boolean isRegistered(); + + /** + * Resolves placeholders contained within the specified input string + * for the provided audience. + *

+ * Implementations may use audience-specific data when replacing + * placeholders. If no placeholders are found, the original input + * may be returned unchanged. + * + * @param audience the audience for which placeholders should be resolved + * @param input the input text containing placeholders + * @return the input text with all applicable placeholders resolved + */ + String resolve(Audience audience, String input); + +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java index 31b92912..c91e887c 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java @@ -110,7 +110,7 @@ default void msg(CommandIssuer issuer, MessageBuilder builder = new MessageBuilder(); TagResolver resolver = StandardTags.defaults(); - Audience audience = SkinOverlay.getInstance().getAudienceProvider().player(issuer.getUniqueId()); + Audience audience = issuer.audience(); switch (type) { case ACTIONBAR -> { @@ -165,7 +165,7 @@ default void msgConsole() { */ default void msgConsole(Map map, boolean ignoreCase) { String[] messages = this.getMessages(); - Audience console = SkinOverlay.getInstance().getAudienceProvider().console(); + Audience console = SkinOverlay.getInstance().getConsoleAudience(); MessageBuilder messageBuilder = new MessageBuilder(); TagResolver resolver = StandardTags.defaults(); if (messages.length > 1) { diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index a2fb89c4..1025a353 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { exclude(group = "org.slf4j", module = "slf4j-api") } implementation(libs.bstats.velocity) - compileOnly(libs.adventure.platform.api) compileOnly(libs.log4j.api) } diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java index 75841646..a1718dc3 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -79,7 +79,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { this.skinOverlay.setSkinRefresher(new VelocitySkinRefresher()); this.skinOverlay.setGameProfileProvider(new VelocityGameProfileProvider()); this.skinOverlay.setPlayerProvider(new VelocityPlayerProvider(server)); - this.skinOverlay.setAudienceProvider(new VelocityAudienceProvider(this, server)); + this.skinOverlay.setConsoleAudience(server.getConsoleCommandSource()); this.skinOverlay.setOnlineMode(server.getConfiguration().isOnlineMode()); if (OptionsUtil.CONNECTION_TYPE.getStringValue().equalsIgnoreCase("PluginMessage")) { VelocityPluginMessageManager velocityPluginMessageManager = new VelocityPluginMessageManager(server); diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java deleted file mode 100644 index 12bbdaee..00000000 --- a/velocity/src/main/java/com/georgev22/skinoverlay/VelocityAudienceProvider.java +++ /dev/null @@ -1,99 +0,0 @@ -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/command/VelocityCommandIssuer.java b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java index cb3ca10c..0f5ced60 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/command/VelocityCommandIssuer.java @@ -3,7 +3,7 @@ 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 net.kyori.adventure.audience.Audience; import org.jetbrains.annotations.NotNull; import java.util.UUID; @@ -22,13 +22,8 @@ public boolean isPlayer() { } @Override - public void sendMessage(@NotNull String message) { - this.source.sendMessage(Component.text(message)); - } - - @Override - public void sendMessage(@NotNull Component component) { - this.source.sendMessage(component); + public Audience audience() { + return this.source; } @Override From c3f581e8a00f3501094a292a00bd616006f51bfa Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 19 Jun 2026 05:06:56 +0300 Subject: [PATCH 50/79] feat: add Placeholder and PlaceholderUtils for dynamic message replacement - Introduce a dedicated Placeholder class to replace ad-hoc placeholder handling within MessageBuilder, supporting both string placeholders and boolean states for inline conditional switches (e.g., `{autosell:enabled|disabled}`). - Add PlaceholderUtils utility for centralized placeholder replacement logic including PlaceholderAPI integration and inline switch resolution. - Refactor MessageBuilder to delegate placeholder resolution to the new Placeholder class, removing direct reliance on Utils.placeHolder and simplifying the API with methods like `addPlaceholder`, `addState`, and `addStates`. - Replace raw `ObjectMap` with `Placeholder` instance, improving separation of concerns and enabling more flexible placeholder merging and copying. - Add helper methods `appendln` for appending text with a trailing newline. --- .../skinoverlay/message/MessageBuilder.java | 212 ++++++----- .../skinoverlay/message/Placeholder.java | 335 ++++++++++++++++++ .../skinoverlay/message/PlaceholderUtils.java | 213 +++++++++++ 3 files changed, 672 insertions(+), 88 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/Placeholder.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/message/PlaceholderUtils.java diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java index 69a1449a..5b1b8882 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageBuilder.java @@ -1,11 +1,8 @@ package com.georgev22.skinoverlay.message; -import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; -import com.georgev22.skinoverlay.datastructures.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.ComponentLike; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; @@ -16,13 +13,10 @@ 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 org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.logging.Level; +import java.util.*; /** * A utility class for building and sending styled chat messages to players in a Minecraft server environment @@ -67,7 +61,7 @@ public class MessageBuilder { private TextColor currentColor = NamedTextColor.WHITE; private ClickEvent currentClickEvent; private HoverEvent currentHoverEvent; - private ObjectMap placeholders = new HashObjectMap<>(); + private Placeholder placeholder = new Placeholder(); /** * Constructs a new MessageBuilder instance. @@ -83,7 +77,7 @@ public MessageBuilder() { * @return A new MessageBuilder instance. */ @Contract(" -> new") - public static @NotNull MessageBuilder builder() { + public static @NonNull MessageBuilder builder() { return new MessageBuilder(); } @@ -103,7 +97,6 @@ public MessageBuilder() { * @return The MessageBuilder instance for method chaining. */ public MessageBuilder append(String text) { - text = replacePlaceholders(text); TextComponent.Builder textComponent = Component.text() .content(text) .color(currentColor); @@ -125,13 +118,46 @@ public MessageBuilder append(String text) { } /** - * Appends a component to the message. + * Appends a newline character to the message. * - * @param component The component to append. + *

This is equivalent to appending "\n".

+ * + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder appendln() { + return append("\n"); + } + + /** + * Appends a {@link ComponentLike} object to the message, followed by a newline. + * + *

This method appends the {@link ComponentLike}, and then appends a newline character.

+ * + * @param componentLike The {@link ComponentLike} object to append. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder appendln(ComponentLike componentLike) { + return append(componentLike).append("\n"); + } + + /** + * Appends the given plain text to the message, followed by a newline character. + * + * @param text The text to append. + * @return The MessageBuilder instance for method chaining. + */ + public MessageBuilder appendln(String text) { + return append(text).append("\n"); + } + + /** + * Appends a {@link ComponentLike} to the message. + * + * @param componentLike The {@link ComponentLike} to append. * @return The MessageBuilder instance for method chaining. */ - public MessageBuilder append(Component component) { - componentBuilder.append(component); + public MessageBuilder append(ComponentLike componentLike) { + componentBuilder.append(componentLike); return this; } @@ -178,8 +204,7 @@ public MessageBuilder appendMiniMessage(String miniMessage, TagResolver tagResol * @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); + private @NonNull Component miniMessage(String message, TagResolver tagResolver) { return MessageParser.miniMessage(message, tagResolver); } @@ -254,7 +279,6 @@ public MessageBuilder removeDecorate(TextDecoration... decorations) { * @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; } @@ -273,7 +297,6 @@ public MessageBuilder clickEvent(ClickEvent.Action action, String value) { * @return The MessageBuilder instance for method chaining. */ public MessageBuilder hoverEvent(String hoverText) { - hoverText = replacePlaceholders(hoverText); this.currentHoverEvent = HoverEvent.showText(miniMessage(hoverText, null)); return this; } @@ -292,8 +315,8 @@ public MessageBuilder hoverEvent(String hoverText) { * * @param audience The player to send the message to. */ - public void send(@NotNull Audience audience) { - audience.sendMessage(componentBuilder.build()); + public void send(@NonNull Audience audience) { + audience.sendMessage(build()); } /** @@ -309,7 +332,12 @@ public void send(@NotNull Audience audience) { * @return The constructed Component. */ public Component build() { - return componentBuilder.build(); + return MessageParser.miniMessage( + this.placeholder.resolve( + MiniMessage.miniMessage().serialize(componentBuilder.build()) + ), + TagResolver.empty() + ); } /** @@ -405,100 +433,108 @@ public MessageBuilder resetStyles() { public MessageBuilder reset() { this.componentBuilder = Component.text(); - this.placeholders.clear(); + this.placeholder = new Placeholder(); return this.resetStyles(); } /** - * Sets the placeholders for the message. + * Sets the target {@link Audience} for PlaceholderAPI integration. + *

+ * This allows placeholders to be resolved using PlaceholderAPI for the given target. * - * @param placeholders The placeholders to set. - * @return The MessageBuilder instance for method chaining. + * @param target The target player or server operator, can be null. + * @return The current {@link MessageBuilder} instance for method chaining. */ - public MessageBuilder placeholders(ObjectMap placeholders) { - this.placeholders = new HashObjectMap<>(placeholders); + public MessageBuilder placeholderContext(@Nullable Audience target) { + this.placeholder.setTarget(target); return this; } /** - * Clears all placeholders from the message. + * Adds a simple placeholder key-value pair that will be replaced in messages. + *

+ * Example usage: + *

{@code
+     * messageBuilder.addPlaceholder("%player%", player.getName());
+     * }
* - * @return The MessageBuilder instance for method chaining. + * @param key The placeholder key to replace (e.g., "%player%"). + * @param value The value to replace the placeholder with. + * @return The current {@link MessageBuilder} instance for method chaining. */ - public MessageBuilder clearPlaceholders() { - this.placeholders.clear(); + public MessageBuilder addPlaceholder(@NonNull String key, @NonNull String value) { + this.placeholder.addPlaceholder(key, value); return this; } /** - * Adds a placeholder to the message. + * Adds multiple boolean states for dynamic inline switches in messages. + *

+ * Example usage: + *

{@code
+     * Map states = Map.of("autosell", true, "linked", false);
+     * messageBuilder.addStates(states);
+     * // Then in message: "AutoSell is {autosell:enabled|disabled}, Chest is {linked:linked|not linked}"
+     * // Output: "AutoSell is enabled, Chest is not linked"
+     * }
* - * @param key The placeholder key. - * @param value The placeholder value. - * @return The MessageBuilder instance for method chaining. + * @param states A map containing key-boolean pairs representing states. + * @return The current {@link MessageBuilder} instance for method chaining. */ - public MessageBuilder addPlaceholder(String key, String value) { - this.placeholders.put(key, value); + public MessageBuilder addStates(@NonNull Map states) { + this.placeholder.addStates(states); 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.

+ * Adds a single boolean state for dynamic inline switches in messages. + *

+ * Example usage: + *

{@code
+     * messageBuilder.addState("autosell", true);
+     * // Then in message: "AutoSell is {autosell:enabled|disabled}"
+     * // Output: "AutoSell is enabled"
+     * }
* - * @return a Paper {@code net.kyori.adventure.text.Component} instance if Paper Adventure is present, - * otherwise {@code null} + * @param key The key representing the state. + * @param state The boolean value of the state. + * @return The current {@link MessageBuilder} instance for method chaining. */ - 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; - } + public MessageBuilder addState(@NonNull String key, boolean state) { + this.placeholder.addState(key, state); + return this; } /** - * Replaces placeholders in the given string with their corresponding values. + * Replaces the current {@link Placeholder} instance with a new one. + *

+ * This can be useful if you want to set multiple placeholders and states at once. * - * @param input The input string containing placeholders. - * @return The input string with placeholders replaced. + * @param placeholder The new {@link Placeholder} instance to use. + * @return The current {@link MessageBuilder} instance for method chaining. */ - private String replacePlaceholders(String input) { - return Utils.placeHolder(input, placeholders, true); + public MessageBuilder placeholders(@Nullable Placeholder placeholder) { + if (placeholder == null) { + return this; + } + this.placeholder.merge(placeholder); + return this; } - private String n(String... p) { - return String.join(".", p); + /** + * Adds multiple string placeholders at once from a map. + *

+ * Example usage: + *

{@code
+     * Map placeholders = Map.of("%player%", player.getName(), "%island%", island.getName());
+     * messageBuilder.placeholders(placeholders);
+     * }
+ * + * @param placeholders A map containing key-value pairs for placeholders. + * @return The current {@link MessageBuilder} instance for method chaining. + */ + public MessageBuilder placeholders(@NonNull Map placeholders) { + this.placeholder.addPlaceholders(placeholders); + return this; } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/Placeholder.java b/common/src/main/java/com/georgev22/skinoverlay/message/Placeholder.java new file mode 100644 index 00000000..f3f95a4c --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/Placeholder.java @@ -0,0 +1,335 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.datastructures.maps.ConcurrentHashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.utilities.Copyable; +import net.kyori.adventure.audience.Audience; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.util.Map; + +/** + * A utility class that manages placeholders and dynamic states for message formatting. + *

+ * Supports: + *

    + *
  • Simple string placeholders (e.g., %player%)
  • + *
  • Boolean states for inline switches (e.g., {autosell:enabled|disabled})
  • + *
  • Integration with {@link Audience} for PlaceholderAPI
  • + *
+ */ +@SuppressWarnings("UnusedReturnValue") +public class Placeholder implements Copyable { + + private final ObjectMap placeholders = new ConcurrentHashObjectMap<>(); + private final ObjectMap states = new ConcurrentHashObjectMap<>(); + private @Nullable Audience target; + + /** + * Creates an empty Placeholder instance. + */ + public Placeholder() { + } + + /** + * Creates a Placeholder instance with a target {@link Audience}. + * + * @param target The target player or server operator for PlaceholderAPI integration. + */ + public Placeholder(@Nullable Audience target) { + this.target = target; + } + + /** + * Sets the target {@link Audience} for PlaceholderAPI integration. + * + * @param target The target player or server operator. + * @return The current Placeholder instance for chaining. + */ + public Placeholder setTarget(@Nullable Audience target) { + this.target = target; + return this; + } + + /** + * Adds a simple placeholder key-value pair. + *

Example: addPlaceholder("%player%", "George")

+ * + * @param key The placeholder key. + * @param value The replacement value. + * @return The current Placeholder instance for chaining. + */ + public Placeholder addPlaceholder(@NonNull String key, @NonNull String value) { + placeholders.put(key, value); + return this; + } + + /** + * Adds multiple placeholders from a map. + * + * @param map A map containing key-value pairs for placeholders. + * @return The current Placeholder instance for chaining. + */ + public Placeholder addPlaceholders(@NonNull Map map) { + placeholders.putAll(map); + return this; + } + + /** + * Adds a boolean state for inline dynamic switches. + *

Example: addState("autosell", true) will allow "{autosell:enabled|disabled}" to resolve to "enabled".

+ * + * @param key The state key. + * @param state The boolean state. + * @return The current Placeholder instance for chaining. + */ + public Placeholder addState(@NonNull String key, boolean state) { + states.put(key, state); + return this; + } + + /** + * Merges placeholders and states from another {@link Placeholder} instance + * into this one. + *

+ * Any placeholders or states present in the provided instance will be added + * to this instance. If a key already exists, the value from the provided + * placeholder will overwrite the existing value. + * + * @param placeholder The {@link Placeholder} instance whose placeholders + * and states should be merged into this instance. + * @return The current {@link Placeholder} instance for chaining. + */ + public Placeholder merge(@NonNull Placeholder placeholder) { + placeholders.putAll(placeholder.placeholders); + states.putAll(placeholder.states); + return this; + } + + /** + * Adds multiple boolean states from a map. + * + * @param map A map containing key-boolean pairs for states. + * @return The current Placeholder instance for chaining. + */ + public Placeholder addStates(@NonNull Map map) { + states.putAll(map); + return this; + } + + /** + * Resolves all placeholders and states in the given input string. + *

This method will replace:

+ *
    + *
  • Simple placeholders (e.g., %player%)
  • + *
  • Inline switches using boolean states (e.g., {autosell:enabled|disabled})
  • + *
  • Integrates with PlaceholderAPI if target is set
  • + *
+ * + * @param input The input string containing placeholders and inline switches. + * @return The resolved string with placeholders replaced. + */ + public @NotNull String resolve(@NonNull String input) { + //noinspection ConstantValue + if (input == null) return ""; + + input = PlaceholderUtils.resolveInlineSwitches(input, states); + + return PlaceholderUtils.replace(target, input, placeholders, true); + } + + /** + * Returns a copy of this {@link Placeholder}. + *

+ * Although this method is named "shallowCopy", the concept of a shallow copy + * does not really apply here because a new {@link Placeholder} instance is + * created in the constructor with its own independent maps. All placeholders + * and states from this instance are copied into these new maps. Therefore, + * modifications to the returned instance do not affect the original. + * + * @return A new {@link Placeholder} instance containing the same placeholders, + * states, and target. + */ + @Override + public @NonNull Placeholder shallowCopy() { + return new Placeholder(target).merge(this); + } + + /** + * Returns a deep copy of this {@link Placeholder}. + *

+ * For this class, deepCopy is identical to shallowCopy because: + *

    + *
  • The keys and values (Strings and Booleans) are immutable.
  • + *
  • All internal maps are newly created in the constructor, ensuring + * independence from the original instance.
  • + *
+ * + * @return A new {@link Placeholder} instance containing the same data. + */ + @Override + public @NonNull Placeholder deepCopy() { + return this.shallowCopy(); + } + + /** + * Creates a new {@link Builder} for constructing a {@link Placeholder}. + * + * @return A new builder instance. + */ + @Contract(" -> new") + public static @NonNull Builder builder() { + return new Builder(); + } + + /** + * Creates a new {@link Builder} with a predefined {@link Audience}. + * + * @param target The target server operator. + * @return A new builder instance. + */ + @Contract("_ -> new") + public static @NonNull Builder builder(@NotNull Audience target) { + return new Builder(target); + } + + /** + * A fluent builder for creating {@link Placeholder} instances. + *

+ * This builder simplifies the construction of {@link Placeholder} objects + * by allowing chained configuration of: + *

    + *
  • Simple placeholders (e.g., %player%)
  • + *
  • Boolean states for inline switches (e.g., {autosell:enabled|disabled})
  • + *
  • Target {@link Audience} for PlaceholderAPI integration
  • + *
+ * + *

Example usage:

+ *
{@code
+     * Placeholder placeholder = new Placeholder.Builder(player)
+     *         .placeholder("%player%", player.getName())
+     *         .placeholder("%amount%", "100")
+     *         .state("autosell", true)
+     *         .build();
+     *
+     * String result = placeholder.resolve("Hello %player%! {autosell:Enabled|Disabled}");
+     * }
+ */ + public static class Builder { + + private final Placeholder placeholder; + + /** + * Creates a new builder with an empty {@link Placeholder} instance. + */ + public Builder() { + this.placeholder = new Placeholder(); + } + + /** + * Creates a new builder with a predefined {@link Audience} target. + *

+ * This target will be used when resolving placeholders via PlaceholderAPI. + * + * @param target The target {@link Audience}. + */ + public Builder(@NotNull Audience target) { + this.placeholder = new Placeholder(target); + } + + /** + * Sets or replaces the {@link Audience} target for PlaceholderAPI integration. + * + * @param target The target player or server operator. + * @return The current builder instance for chaining. + */ + public Builder target(@Nullable Audience target) { + placeholder.setTarget(target); + return this; + } + + /** + * Adds a single placeholder key-value pair. + * + *

Example:

+ *
{@code
+         * builder.placeholder("%player%", "George");
+         * }
+ * + * @param key The placeholder key (e.g., %player%). + * @param value The replacement value. + * @return The current builder instance for chaining. + */ + public Builder placeholder(@NonNull String key, @NonNull String value) { + placeholder.addPlaceholder(key, value); + return this; + } + + /** + * Adds multiple placeholders from a map. + * + * @param map A map containing placeholder key-value pairs. + * @return The current builder instance for chaining. + */ + public Builder placeholders(@NonNull Map map) { + placeholder.addPlaceholders(map); + return this; + } + + /** + * Adds a boolean state used for inline conditional switches. + * + *

Example:

+ *
{@code
+         * builder.state("autosell", true);
+         * }
+ *

+ * This allows constructs like: + *

{@code
+         * {autosell:enabled|disabled}
+         * }
+ * + * @param key The state key. + * @param value The boolean state value. + * @return The current builder instance for chaining. + */ + public Builder state(@NonNull String key, boolean value) { + placeholder.addState(key, value); + return this; + } + + /** + * Adds multiple boolean states from a map. + * + * @param map A map containing state key-boolean pairs. + * @return The current builder instance for chaining. + */ + public Builder states(@NonNull Map map) { + placeholder.addStates(map); + return this; + } + + /** + * Merges placeholders and states from another {@link Placeholder} instance. + * + * @param other The placeholder instance to merge from. + * @return The current builder instance for chaining. + */ + public Builder merge(@NonNull Placeholder other) { + placeholder.merge(other); + return this; + } + + /** + * Builds and returns the configured {@link Placeholder} instance. + * + * @return The constructed {@link Placeholder}. + */ + public @NotNull Placeholder build() { + return placeholder; + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/PlaceholderUtils.java b/common/src/main/java/com/georgev22/skinoverlay/message/PlaceholderUtils.java new file mode 100644 index 00000000..dd007238 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/message/PlaceholderUtils.java @@ -0,0 +1,213 @@ +package com.georgev22.skinoverlay.message; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.hooks.placeholder.PlaceholderHook; +import com.georgev22.skinoverlay.utilities.Utils; +import net.kyori.adventure.audience.Audience; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Utility class for handling placeholder replacements and + * inline conditional text switches. + *

+ * Supported features: + *

    + *
  • Basic placeholder replacement using a Map
  • + *
  • PlaceholderAPI support (if installed)
  • + *
  • Inline switch syntax: {@code {enabled|disabled}}
  • + *
  • Keyed inline switch syntax: {@code {autosell:enabled|disabled}}
  • + *
+ */ +public final class PlaceholderUtils { + + private PlaceholderUtils() { + // Utility class, prevent instantiation + } + + /** + * Replaces placeholders in a single string. + *

+ * Supports: + *

    + *
  • Simple placeholder replacement from the provided map
  • + *
  • Integration with PlaceholderAPI if target is a player
  • + *
+ * + * @param target Optional target player/operator for PlaceholderAPI integration + * @param input The input string to replace placeholders in + * @param placeholders Map of key-value placeholders to replace, can be null + * @param ignoreCase Whether to ignore case when replacing placeholders + * @return The string with all placeholders replaced + */ + public static @NotNull String replace( + @Nullable Audience target, + @NotNull String input, + @Nullable Map placeholders, + boolean ignoreCase + ) { + String result = input; + + if (placeholders != null && !placeholders.isEmpty()) { + for (Map.Entry entry : placeholders.entrySet()) { + result = ignoreCase + ? Utils.replaceIgnoreCase(result, entry.getKey(), entry.getValue()) + : result.replace(entry.getKey(), entry.getValue()); + } + } + + result = resolve(target, result); + + return result; + } + + /** + * Replaces placeholders in a list of strings. + * + * @param target Optional target player/operator for PlaceholderAPI integration + * @param input The input list of strings + * @param placeholders Map of key-value placeholders to replace, can be null + * @param ignoreCase Whether to ignore case when replacing placeholders + * @return A new list of strings with placeholders replaced + */ + public static @NotNull List replace( + @Nullable Audience target, + @NotNull List input, + @Nullable Map placeholders, + boolean ignoreCase + ) { + return input.stream() + .map(str -> replace(target, str, placeholders, ignoreCase)) + .collect(Collectors.toList()); + } + + /** + * Replaces placeholders in an array of strings. + * + * @param target Optional target player/operator for PlaceholderAPI integration + * @param input The input array of strings + * @param placeholders Map of key-value placeholders to replace, can be null + * @param ignoreCase Whether to ignore case when replacing placeholders + * @return A new array of strings with placeholders replaced + */ + public static @NotNull String @NonNull [] replace( + @Nullable Audience target, + @NotNull String[] input, + @Nullable Map placeholders, + boolean ignoreCase + ) { + String[] copy = Arrays.copyOf(input, input.length); + + for (int i = 0; i < copy.length; i++) { + copy[i] = replace(target, copy[i], placeholders, ignoreCase); + } + + return copy; + } + + private static final Pattern SIMPLE_SWITCH_PATTERN = + Pattern.compile("\\{([^:{}|]+)\\|([^{}|]+)}"); + + private static final Pattern KEYED_SWITCH_PATTERN = + Pattern.compile("\\{(\\w+):([^{}|]+)\\|([^{}|]+)}"); + + /** + * Resolves simple inline switches in a string. + *

+ * Example: + *

{@code
+     * resolveInlineSwitches("Feature is {enabled|disabled}", true);
+     * // Returns "Feature is enabled"
+     * }
+ * + * @param input Input string containing simple inline switches + * @param state Boolean state to determine which option to select (true -> first, false -> second) + * @return String with inline switches resolved + */ + public static @NotNull String resolveInlineSwitches( + @NotNull String input, + boolean state + ) { + Matcher matcher = SIMPLE_SWITCH_PATTERN.matcher(input); + StringBuilder builder = new StringBuilder(); + + while (matcher.find()) { + String first = matcher.group(1); + String second = matcher.group(2); + + String replacement = state ? first : second; + matcher.appendReplacement(builder, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(builder); + return builder.toString(); + } + + /** + * Resolves keyed inline switches in a string. + *

+ * Example: + *

{@code
+     * Map states = Map.of("autosell", true);
+     * resolveInlineSwitches("AutoSell is {autosell:enabled|disabled}", states);
+     * // Returns "AutoSell is enabled"
+     * }
+ * + * @param input Input string containing keyed inline switches + * @param states Map of key -> boolean state for each switch + * @return String with keyed inline switches resolved + */ + public static @NotNull String resolveInlineSwitches( + @NotNull String input, + @Nullable Map states + ) { + if (states == null || states.isEmpty()) { + return input; + } + + Matcher matcher = KEYED_SWITCH_PATTERN.matcher(input); + StringBuilder builder = new StringBuilder(); + + while (matcher.find()) { + String key = matcher.group(1); + String first = matcher.group(2); + String second = matcher.group(3); + + boolean state = states.getOrDefault(key, false); + String replacement = state ? first : second; + + matcher.appendReplacement(builder, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(builder); + return builder.toString(); + } + + /** + * Applies {@link PlaceholderHook} replacements if the target is an {@link Audience}. + * + * @param target Target player/operator for PlaceholderAPI, can be null + * @param input Input string to apply PlaceholderAPI placeholders to + * @return String with {@link PlaceholderHook} placeholders applied, or original string if unavailable + */ + private static String resolve( + @Nullable Audience target, + @NotNull String input + ) { + Optional optionalPlaceholderHook = SkinOverlay.getInstance().getPlaceholderHook(); + if (optionalPlaceholderHook.isEmpty()) + return input; + + PlaceholderHook placeholderHook = optionalPlaceholderHook.get(); + if (!placeholderHook.isRegistered()) + return input; + + return placeholderHook.resolve(target, input); + } +} \ No newline at end of file From 643fa5298526ebcb06d431b73a58e971dca9a363 Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 19 Jun 2026 07:32:33 +0300 Subject: [PATCH 51/79] refactor!: replace HashObjectMap with Placeholder for message formatting --- .../skinoverlay/command/BaseCommand.java | 37 +++---- .../skinoverlay/command/MethodCommand.java | 14 +-- .../command/commands/sub/ClearSubCommand.java | 17 +-- .../command/commands/sub/WearSubCommand.java | 26 +++-- .../commands/sub/WearUrlSubCommand.java | 27 +++-- .../skinoverlay/message/MessageEntry.java | 104 +++++++++--------- 6 files changed, 120 insertions(+), 105 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java index 0728bb17..89a33129 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/BaseCommand.java @@ -4,7 +4,7 @@ import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.command.processors.PostProcessor; import com.georgev22.skinoverlay.command.processors.PreProcessor; -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.Placeholder; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.registry.CommandTargetRegistry; import org.jetbrains.annotations.NotNull; @@ -96,19 +96,20 @@ public void execute(CommandIssuer sender, String @NotNull [] args, CommandContex // 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); + Placeholder.builder(sender.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .placeholder("%targets%", String.join(", ", targets)) + .build() + ); return; } // Permission check if (permission != null && !permission.isEmpty() && !sender.hasPermission(permission)) { CommandMessages.COMMAND_PERMISSION_DENIED.msg(sender, - new HashObjectMap() - .append("%command%", context.getData().getOr("command", "")), - true); + Placeholder.builder(sender.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .build()); return; } @@ -121,10 +122,9 @@ public void execute(CommandIssuer sender, String @NotNull [] args, CommandContex 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); + Placeholder.builder(sender.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .placeholder("%usage%", usage).build()); } } @@ -132,9 +132,9 @@ public void execute(CommandIssuer sender, String @NotNull [] args, CommandContex runPostprocessors(sender, context); } catch (Exception e) { CommandMessages.COMMAND_ERROR.msg(sender, - new HashObjectMap() - .append("%command%", context.getData().getOr("command", "")), - true); + Placeholder.builder(sender.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .build()); SkinOverlay.getInstance().getLogger() .log(Level.WARNING, "Error while executing command", e); } @@ -349,10 +349,9 @@ public ExecutionType getExecutionType() { 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); + Placeholder.builder(commandIssuer.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .placeholder("%usage%", usage).build()); } return false; } diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java index 421423d5..b6b6f75e 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java +++ b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java @@ -2,8 +2,8 @@ import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.annotation.*; -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; import com.georgev22.skinoverlay.message.MessageBuilder; +import com.georgev22.skinoverlay.message.Placeholder; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import org.jetbrains.annotations.NotNull; @@ -102,9 +102,9 @@ private void invokeHandler(@NotNull CommandIssuer sender, String[] args, Command 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); + Placeholder.builder(sender.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .placeholder("%arg%", argName).build()); return; } raw = argAnno.defaultValue(); @@ -114,9 +114,9 @@ private void invokeHandler(@NotNull CommandIssuer sender, String[] args, Command if (value == null) { CommandMessages.COMMAND_INVALID_ARGUMENT.msg(sender, - new HashObjectMap() - .append("%command%", context.getData().getOr("command", "")) - .append("%arg%", argName), true); + Placeholder.builder(sender.audience()) + .placeholder("%command%", context.getData().getOr("command", "")) + .placeholder("%arg%", argName).build()); return; } 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 index bcaef54c..bfe9219e 100644 --- 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 @@ -4,7 +4,7 @@ import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.Placeholder; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.storage.data.Skin; @@ -23,7 +23,8 @@ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "ta if (target == null) { if (!commandIssuer.isPlayer()) { CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, - new HashObjectMap().append("%command%", "clear ").append("%arg%", "target"), true); + Placeholder.builder(commandIssuer.audience()) + .placeholder("%command%", "clear ").placeholder("%arg%", "target").build()); return; } SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); @@ -42,14 +43,17 @@ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "ta mainPlugin.getSkinProvider().setSkin(player, skin); CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, - new HashObjectMap().append("%player%", player.getName()), - true + Placeholder.builder(commandIssuer.audience()).placeholder("%player%", player.getName()) + .build() ); }, 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); + CommandMessages.COMMAND_OFFLINE_PLAYER.msg( + commandIssuer, + Placeholder.builder(commandIssuer.audience()).placeholder("%player%", target.getName()) + .build()); return; } SkinParts skinParts = new SkinParts(null, "default"); @@ -64,8 +68,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "ta mainPlugin.getSkinProvider().setSkin(target, skin); CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, - new HashObjectMap().append("%player%", target.getName()), - true + Placeholder.builder(commandIssuer.audience()).build() ); }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); 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 index 416de90e..e2fa7dbf 100644 --- 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 @@ -4,7 +4,7 @@ import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.Placeholder; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.storage.data.Skin; @@ -15,6 +15,7 @@ import javax.imageio.ImageIO; import java.io.File; import java.io.IOException; +import java.util.Objects; import java.util.logging.Level; @Subcommand({"wear", "overlay"}) @@ -29,15 +30,15 @@ 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); + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, Placeholder.builder(commandIssuer.audience()) + .placeholder("%command%", "wear ").placeholder("%arg%", "overlay").build()); return; } if (target == null) { if (!commandIssuer.isPlayer()) { - CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap() - .append("%command%", "wear ").append("%arg%", "target"), true); + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, Placeholder.builder(commandIssuer.audience()) + .placeholder("%command%", "wear ").placeholder("%arg%", "target").build()); return; } target = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); @@ -46,7 +47,9 @@ protected void handle(@NotNull CommandIssuer commandIssuer, 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); + CommandMessages.COMMAND_OVERLAY_NOT_FOUND.msg( + commandIssuer, + Placeholder.builder(commandIssuer.audience()).placeholder("%overlay%", overlay).build()); return; } skinParts = new SkinParts(new SerializableBufferedImage(ImageIO.read(overlayFile)), overlay); @@ -69,12 +72,11 @@ protected void handle(@NotNull CommandIssuer commandIssuer, .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 + Placeholder.builder(commandIssuer.audience()) + .placeholder("%player%", finalTarget.getName()) + .placeholder("%url%", Objects.requireNonNullElse(skin.skinURL(), "")) + .placeholder("%name%", skin.getSkinParts().getSkinName()) + .placeholder("%skinParts%", skin.getSkinParts().toString()).build() ); }, 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 index 93fc160f..c3661e34 100644 --- 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 @@ -4,7 +4,7 @@ import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.command.annotation.*; import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand; -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.message.Placeholder; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.player.SPlayer; import com.georgev22.skinoverlay.storage.data.Skin; @@ -18,6 +18,7 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; +import java.util.Objects; import java.util.logging.Level; @Subcommand("url") @@ -37,12 +38,16 @@ protected void handle(@NotNull CommandIssuer commandIssuer, byte[] imageBytes = downloadImageBytes(url); if (imageBytes == null) { - CommandMessages.COMMAND_INVALID_URL.msg(commandIssuer, new HashObjectMap().append("%url%", url.toString()), true); + CommandMessages.COMMAND_INVALID_URL.msg(commandIssuer, + Placeholder.builder(commandIssuer.audience()) + .placeholder("%url%", url.toString()).build()); return; } if (target == null && !commandIssuer.isPlayer()) { - CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap().append("%command%", "url ").append("%arg%", "target"), true); + CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, + Placeholder.builder(commandIssuer.audience()) + .placeholder("%command%", "url ").placeholder("%arg%", "target").build()); return; } @@ -54,7 +59,8 @@ protected void handle(@NotNull CommandIssuer commandIssuer, } catch (Exception e) { mainPlugin.getLogger().log(Level.SEVERE, "Error executing WearUrlSubCommand:", e); - CommandMessages.COMMAND_ERROR.msg(commandIssuer, new HashObjectMap().append("%error%", e.getMessage()), true); + CommandMessages.COMMAND_ERROR.msg(commandIssuer, + Placeholder.builder(commandIssuer.audience()).placeholder("%error%", e.getMessage()).build()); } } @@ -80,7 +86,9 @@ protected void handle(@NotNull CommandIssuer commandIssuer, 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); + CommandMessages.COMMAND_ERROR.msg(issuer, + Placeholder.builder(issuer.audience()) + .placeholder("%error%", "Failed to retrieve or generate skin.").build()); return; } @@ -89,11 +97,10 @@ private void applySkinToPlayer(CommandIssuer issuer, SPlayer player, SkinParts s CommandMessages.COMMAND_OVERLAY_DONE.msg( issuer, - new HashObjectMap() - .append("%player%", player.getName()) - .append("%url%", skin.skinURL()) - .append("%overlay%", skin.getSkinParts().getSkinName()), - true + Placeholder.builder(issuer.audience()) + .placeholder("%player%", player.getName()) + .placeholder("%url%", Objects.requireNonNullElse(skin.skinURL(), "")) + .placeholder("%overlay%", skin.getSkinParts().getSkinName()).build() ); }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); } diff --git a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java index c91e887c..72983a07 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java +++ b/common/src/main/java/com/georgev22/skinoverlay/message/MessageEntry.java @@ -1,6 +1,5 @@ package com.georgev22.skinoverlay.message; - import com.georgev22.skinoverlay.SkinOverlay; import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; @@ -9,15 +8,13 @@ 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 org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; 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. *

@@ -26,8 +23,6 @@ */ public interface MessageEntry { - MessageEntry NONE = null; - /** * @return the path of this message in the configuration file */ @@ -71,7 +66,7 @@ enum MessageType { * @param issuer the receiver */ default void msg(CommandIssuer issuer) { - msg(issuer, new HashObjectMap<>(), false, MessageType.CHAT); + msg(issuer, null, MessageType.CHAT); } /** @@ -80,31 +75,29 @@ default void msg(CommandIssuer issuer) { * @param issuer the receiver */ default void msg(CommandIssuer issuer, MessageType type) { - msg(issuer, new HashObjectMap<>(), false, type); + msg(issuer, null, type); } /** * Sends a message with placeholder replacement. * - * @param issuer the receiver - * @param map placeholder replacements - * @param ignoreCase whether to ignore case when replacing + * @param issuer the receiver + * @param placeholder placeholder replacements */ - default void msg(CommandIssuer issuer, Map map, boolean ignoreCase) { - msg(issuer, map, ignoreCase, MessageType.CHAT); + default void msg(CommandIssuer issuer, Placeholder placeholder) { + msg(issuer, placeholder, 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 + * @param issuer the receiver + * @param placeholder placeholder replacements + * @param type how the message should be shown */ default void msg(CommandIssuer issuer, - Map map, boolean ignoreCase, - @NotNull MessageType type) { + @Nullable Placeholder placeholder, + @NonNull MessageType type) { String[] messages = this.getMessages(); MessageBuilder builder = new MessageBuilder(); @@ -113,22 +106,26 @@ default void msg(CommandIssuer issuer, Audience audience = issuer.audience(); switch (type) { - case ACTIONBAR -> { - audience.sendActionBar( - builder.appendMiniMessage( - placeHolder(messages[0], map, ignoreCase), - resolver - ) - .buildAndReset() - ); - } + case ACTIONBAR -> audience.sendActionBar( + builder + .placeholderContext(audience) + .placeholders(placeholder) + .appendMiniMessage(messages[0], resolver) + .buildAndReset() + ); case TITLE -> { Component title = builder - .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), resolver) + .placeholderContext(audience) + .placeholders(placeholder) + .appendMiniMessage(messages[0], resolver) .buildAndReset(); Component subtitle = messages.length > 1 - ? builder.appendMiniMessage(placeHolder(messages[1], map, ignoreCase), resolver).buildAndReset() + ? builder + .placeholderContext(audience) + .placeholders(placeholder) + .appendMiniMessage(messages[1], resolver) + .buildAndReset() : Component.text(""); audience.showTitle(Title.title(title, subtitle, Title.DEFAULT_TIMES)); @@ -137,12 +134,16 @@ default void msg(CommandIssuer issuer, if (messages.length > 1) { Arrays.stream(messages).forEach(msg -> audience.sendMessage(builder - .appendMiniMessage(placeHolder(msg, map, ignoreCase), resolver) + .placeholderContext(audience) + .placeholders(placeholder) + .appendMiniMessage(msg, resolver) .buildAndReset()) ); } else { audience.sendMessage(builder - .appendMiniMessage(placeHolder(messages[0], map, ignoreCase), resolver) + .placeholderContext(audience) + .placeholders(placeholder) + .appendMiniMessage(messages[0], resolver) .buildAndReset()); } } @@ -166,16 +167,21 @@ default void msgConsole() { default void msgConsole(Map map, boolean ignoreCase) { String[] messages = this.getMessages(); Audience console = SkinOverlay.getInstance().getConsoleAudience(); - MessageBuilder messageBuilder = new MessageBuilder(); + MessageBuilder builder = new MessageBuilder(); TagResolver resolver = StandardTags.defaults(); if (messages.length > 1) { - for (String message : messages) { - messageBuilder.appendMiniMessage(placeHolder(message, map, ignoreCase), resolver); - } - messageBuilder.send(console); + Arrays.stream(messages).forEach(msg -> console + .sendMessage(builder + .placeholderContext(console) + .placeholders(new HashObjectMap<>(map)) + .appendMiniMessage(msg, resolver) + .buildAndReset())); } else { - messageBuilder.appendMiniMessage(placeHolder(messages[0], map, ignoreCase), resolver); - messageBuilder.send(console); + console.sendMessage(builder + .placeholderContext(console) + .placeholders(new HashObjectMap<>(map)) + .appendMiniMessage(messages[0], resolver) + .buildAndReset()); } } @@ -184,17 +190,16 @@ default void msgConsole(Map map, boolean ignoreCase) { * */ default void msgAll() { - msgAll(Map.of(), false); + msgAll((Placeholder) null); } /** * Sends a message to all online players with placeholders. * - * @param map placeholder replacements - * @param ignoreCase whether to ignore case + * @param placeholder placeholder replacements */ - default void msgAll(Map map, boolean ignoreCase) { - SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().forEach(p -> msg(p, map, ignoreCase)); + default void msgAll(@Nullable Placeholder placeholder) { + SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().forEach(p -> msg(p, placeholder)); } /** @@ -209,11 +214,10 @@ default void msgAll(MessageType 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) + * @param placeholder placeholder replacements + * @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)); + default void msgAll(Placeholder placeholder, MessageType type) { + SkinOverlay.getInstance().getPlayerProvider().getOnlinePlayers().forEach(p -> msg(p, placeholder, type)); } } From 1b1e4b5647f7509cd1e8f8a1aa813c5d620a123b Mon Sep 17 00:00:00 2001 From: "George V." Date: Fri, 19 Jun 2026 07:38:20 +0300 Subject: [PATCH 52/79] chore: mark console audience methods as internal and experimental --- .../src/main/java/com/georgev22/skinoverlay/SkinOverlay.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index fdbc3a77..3b566dfd 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -276,10 +276,14 @@ public void setScheduler(MinecraftScheduler scheduler) { this.scheduler = scheduler; } + @ApiStatus.Internal + @ApiStatus.Experimental public Audience getConsoleAudience() { return consoleAudience; } + @ApiStatus.Internal + @ApiStatus.Experimental public void setConsoleAudience(Audience consoleAudience) { this.consoleAudience = consoleAudience; } From 423539f4156883df845fd645ac82da5eb49a8955 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 19 Jun 2026 04:42:18 +0000 Subject: [PATCH 53/79] chore(release): 8.0.0-beta.6 [skip ci] # [8.0.0-beta.6](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.5...v8.0.0-beta.6) (2026-06-19) ### Features * add Placeholder and PlaceholderUtils for dynamic message replacement ([c3f581e](https://github.com/GeorgeV220/SkinOverlay/commit/c3f581e8a00f3501094a292a00bd616006f51bfa)) --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af9830d..3fcfd922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [8.0.0-beta.6](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.5...v8.0.0-beta.6) (2026-06-19) + + +### Features + +* add Placeholder and PlaceholderUtils for dynamic message replacement ([c3f581e](https://github.com/GeorgeV220/SkinOverlay/commit/c3f581e8a00f3501094a292a00bd616006f51bfa)) + # [8.0.0-beta.5](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.4...v8.0.0-beta.5) (2026-06-18) diff --git a/README.md b/README.md index 6bddd5ac..db7ea181 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 - 8.0.0-beta.5 + 8.0.0-beta.6 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.5" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.6" } ``` diff --git a/gradle.properties b/gradle.properties index 212f9284..67580b28 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.5 +version = 8.0.0-beta.6 description = SkinOverlay From 9b8eeaac00e1462ddf894cb8bf982bc50614fac2 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 1 Jul 2026 11:17:06 +0300 Subject: [PATCH 54/79] refactor: improve version parsing with regex pattern Replace fragile split-based version parsing with a more robust regex pattern. This change handles edge cases like missing minor or patch versions correctly --- .../utilities/BukkitMinecraftUtils.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java index 79b1c87d..3483b3d7 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -655,6 +655,8 @@ public static boolean isPaper() { * Represents a Minecraft server version using a numeric format (major.minor.patch). */ public static final class MinecraftVersion implements Comparable { + private static final Pattern VERSION_PATTERN = + Pattern.compile("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?"); private final int major; private final int minor; @@ -712,20 +714,18 @@ public static MinecraftVersion getCurrent() { * @param bukkitVersion the raw version string from Bukkit * @return a parsed {@link MinecraftVersion}, or {@code 0.0.0} if parsing fails */ - @Contract("_ -> new") - public static @NonNull MinecraftVersion parse(String bukkitVersion) { - try { - String versionPart = bukkitVersion.split("-")[0]; - String[] parts = versionPart.split("\\."); - - int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; - int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; - int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + public static @NotNull MinecraftVersion parse(String bukkitVersion) { + Matcher matcher = VERSION_PATTERN.matcher(bukkitVersion); - return new MinecraftVersion(major, minor, patch, parseNMSPackage()); - } catch (Exception e) { + if (!matcher.find()) { return new MinecraftVersion(0, 0, 0, "unknown"); } + + int major = Integer.parseInt(matcher.group(1)); + int minor = matcher.group(2) != null ? Integer.parseInt(matcher.group(2)) : 0; + int patch = matcher.group(3) != null ? Integer.parseInt(matcher.group(3)) : 0; + + return new MinecraftVersion(major, minor, patch, parseNMSPackage()); } private static String parseNMSPackage() { From b19c54f765a60cee6244988219623400df435872 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 1 Jul 2026 12:14:56 +0300 Subject: [PATCH 55/79] fix: register PlaceholderAPI hook after plugin initialization - Move PlaceholderAPI hook registration to after data loading in Bukkit implementation to ensure proper initialization. - Add logging when placeholder hook is not set in common module. - Implement persist() and canRegister() methods in PlaceholderAPIHook for proper placeholder registration. --- .../com/georgev22/skinoverlay/SkinOverlayBukkit.java | 9 +++++---- .../hooks/placeholder/PlaceholderAPIHook.java | 10 ++++++++++ .../java/com/georgev22/skinoverlay/SkinOverlay.java | 2 ++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 8b3e4ffe..3dbc33ff 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -56,10 +56,6 @@ public void onLoad() { } } - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { - this.skinOverlay.setPlaceholderHook(new PlaceholderAPIHook()); - } - this.skinOverlay.setOnlineMode(Bukkit.getOnlineMode()); this.skinOverlay.setProxy(false); this.skinOverlay.setScheduler( @@ -96,6 +92,11 @@ public void onEnable() { } }); } + + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + this.skinOverlay.setPlaceholderHook(new PlaceholderAPIHook()); + } + // call onEnable skinOverlay.onEnable(); } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java index 0645c913..fc16dde8 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java @@ -41,6 +41,16 @@ public String resolve(Audience audience, String input) { return BuildParameters.VERSION; } + @Override + public boolean persist() { + return true; + } + + @Override + public boolean canRegister() { + return true; + } + @Override public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) { if (player == null) return null; diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index 3b566dfd..84b8c942 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -129,6 +129,8 @@ public void onEnable() { } catch (Exception e) { getLogger().log(Level.WARNING, "Failed to register placeholder hook", e); } + } else { + getLogger().info("Placeholder hook is not set!"); } CompletionEngine.registerResolver("@overlays", (commandIssuer, args) -> skinFileCache.getSkinConfigurationFiles().keySet()); From c00a51f36aa9b119200d28a2d158ad2c01b457e6 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 1 Jul 2026 12:15:12 +0300 Subject: [PATCH 56/79] feat: add multi-database support with MongoDB, MySQL, and SQLite Introduce a new `DatabaseManager` class to handle database initialization, shutdown, and data loading. Implement `MongoEntityManager` and `SQLEntityManager` for MongoDB and SQL-based storage, alongside the existing file-based `FileEntityManager`. Add `DataSourceProvider` for HikariCP connection pooling and `MongoProvider` for MongoDB client management. Update `OptionsUtil` to cache configuration values for performance and add new database-related config options. Refactor `SkinOverlay` to delegate database setup and loading to the new `DatabaseManager`. --- bukkit/build.gradle.kts | 2 + common/build.gradle.kts | 1 + .../georgev22/skinoverlay/SkinOverlay.java | 28 +- .../commands/sub/ReloadSubCommand.java | 2 + .../skinoverlay/storage/DatabaseManager.java | 141 ++++++++++ .../manager/mongo/MongoEntityManager.java | 107 ++++++++ .../storage/manager/mongo/MongoProvider.java | 27 ++ .../manager/sql/DataSourceProvider.java | 50 ++++ .../storage/manager/sql/SQLEntityManager.java | 143 ++++++++++ .../utilities/config/OptionsUtil.java | 251 ++++++++++++------ common/src/main/resources/config.yml | 19 ++ gradle/libs.versions.toml | 4 +- 12 files changed, 682 insertions(+), 93 deletions(-) create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/DatabaseManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoEntityManager.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoProvider.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/DataSourceProvider.java create mode 100644 common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/SQLEntityManager.java diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 22d7e9d1..bfe9f1fa 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -75,6 +75,8 @@ tasks.shadowJar { 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") + relocate("org.bson", "${project.property("packageName")}.lib.bson") + relocate("com.mongodb", "${project.property("packageName")}.lib.mongodb") } tasks.named("publish") { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index f6cf0d6b..7b3dd760 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(libs.jedis) implementation(libs.jspecify) + implementation(libs.mongodb.driver.sync) } configurations.configureEach { diff --git a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java index 84b8c942..cc8d6346 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java +++ b/common/src/main/java/com/georgev22/skinoverlay/SkinOverlay.java @@ -21,13 +21,13 @@ import com.georgev22.skinoverlay.providers.SkinProvider; import com.georgev22.skinoverlay.registry.EntityManagerRegistry; import com.georgev22.skinoverlay.scheduler.MinecraftScheduler; +import com.georgev22.skinoverlay.storage.DatabaseManager; import com.georgev22.skinoverlay.storage.EntityManager; import com.georgev22.skinoverlay.storage.ManagedEntity; import com.georgev22.skinoverlay.storage.data.Entity; -import com.georgev22.skinoverlay.storage.manager.AbstractEntityManager; -import com.georgev22.skinoverlay.storage.manager.gson.FileEntityManager; import com.georgev22.skinoverlay.utilities.GsonUtils; import com.georgev22.skinoverlay.utilities.config.FileManager; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import com.georgev22.skinoverlay.utilities.config.SkinFileCache; import com.google.gson.Gson; import net.kyori.adventure.audience.Audience; @@ -74,6 +74,7 @@ public static SkinOverlay getInstance() { private boolean isProxy; private File dataFolder; private MessageManager messageManager; + private DatabaseManager databaseManager; /** * Loads the plugin components. Should be called during plugin load phase. @@ -84,6 +85,7 @@ public void onLoad() { this.fileManager = FileManager.getInstance(); try { this.fileManager.loadFiles(); + OptionsUtil.reloadAll(); } catch (Exception e) { throw new RuntimeException(e); } @@ -98,21 +100,9 @@ public void onLoad() { } catch (Exception e) { getLogger().log(Level.SEVERE, "Error loading the language file: ", e); } - EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); - File folder = new File(this.dataFolder, "save-data"); - - if (!folder.exists() && folder.mkdirs()) { - this.getLogger().info( - "Created " + folder.getPath() + " folder" - ); - } - - for (ManagedEntity managedEntity : AbstractEntityManager.getManagedEntities()) { - if (entityManagerRegistry.get(managedEntity.type()).isEmpty()) { - registerEntityManager(entityManagerRegistry, managedEntity, - new FileEntityManager<>(managedEntity, new File(folder, managedEntity.key()))); - } - } + this.databaseManager = new DatabaseManager(this); + databaseManager.setupDatabase(); + databaseManager.loadAllData(); } /** @@ -163,7 +153,9 @@ public void onDisable() { } } - EntityManagerRegistry.getInstance().entries().forEach((key, value) -> value.shutdown()); + if (databaseManager != null) { + databaseManager.shutdown(); + } } /** 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 index 26846995..ba654151 100644 --- 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 @@ -10,6 +10,7 @@ import com.georgev22.skinoverlay.message.MessagesRegistry; import com.georgev22.skinoverlay.message.messages.CommandMessages; import com.georgev22.skinoverlay.message.messages.CoreMessages; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import org.jetbrains.annotations.NotNull; import java.util.logging.Level; @@ -30,6 +31,7 @@ protected void handle(@NotNull CommandIssuer commandIssuer) { } catch (Exception e) { mainPlugin.getLogger().log(Level.SEVERE, "Error loading the language file: ", e); } + OptionsUtil.reloadAll(); CoreMessages.PLUGIN_RELOAD.msg(commandIssuer); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/DatabaseManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/DatabaseManager.java new file mode 100644 index 00000000..95b4f4a7 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/DatabaseManager.java @@ -0,0 +1,141 @@ +package com.georgev22.skinoverlay.storage; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.registry.EntityManagerRegistry; +import com.georgev22.skinoverlay.storage.data.Entity; +import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.storage.data.Skin; +import com.georgev22.skinoverlay.storage.manager.AbstractEntityManager; +import com.georgev22.skinoverlay.storage.manager.gson.FileEntityManager; +import com.georgev22.skinoverlay.storage.manager.mongo.MongoEntityManager; +import com.georgev22.skinoverlay.storage.manager.mongo.MongoProvider; +import com.georgev22.skinoverlay.storage.manager.sql.DataSourceProvider; +import com.georgev22.skinoverlay.storage.manager.sql.SQLEntityManager; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; + +import java.io.File; +import java.util.Optional; +import java.util.logging.Level; + +public class DatabaseManager { + + private final SkinOverlay plugin; + + public DatabaseManager(SkinOverlay plugin) { + this.plugin = plugin; + } + + private boolean sql = false; + + public void setupDatabase() { + String dbType = OptionsUtil.DATABASE_TYPE.getStringValue().toLowerCase(); + + switch (dbType) { + case "file" -> setupFileDatabase(); + case "mysql", "sqlite" -> setupSQLDatabase(dbType); + case "mongodb" -> setupMongoDBDatabase(); + default -> throw new IllegalArgumentException("Unknown database type: " + dbType); + } + + plugin.getLogger().info("Database type: " + dbType); + } + + public void loadAllData() { + EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); + + // Load skins + Optional> voidEntityManager = + entityManagerRegistry.getTyped(Skin.class); + + if (voidEntityManager.isEmpty()) { + plugin.getLogger().log(Level.SEVERE, "Error retrieving skin entity manager!"); + return; + } + + voidEntityManager.get().loadAll(); + + // Load player data + Optional> playerEntityManager = + entityManagerRegistry.getTyped(PlayerData.class); + + if (playerEntityManager.isEmpty()) { + plugin.getLogger().log(Level.SEVERE, "Error retrieving player data entity manager!"); + return; + } + + playerEntityManager.get().loadAll(); + } + + public void shutdown() { + if (sql) { + DataSourceProvider.shutdown(); + } + EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); + for (EntityManager entityManager : entityManagerRegistry.entries().values()) { + entityManager.shutdown(); + } + } + + private void setupFileDatabase() { + EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); + File folder = new File(plugin.getDataFolder(), "save-data"); + + if (!folder.exists() && folder.mkdirs()) { + plugin.getLogger().info("Created " + folder.getPath() + " folder"); + } + + for (ManagedEntity managedEntity : AbstractEntityManager.getManagedEntities()) { + if (entityManagerRegistry.get(managedEntity.type()).isEmpty()) { + registerEntityManager(entityManagerRegistry, managedEntity, + new FileEntityManager<>(managedEntity, new File(folder, managedEntity.key()))); + } + } + } + + private void setupSQLDatabase(String type) { + sql = true; + switch (type) { + case "mysql" -> DataSourceProvider.initializeForMySQL( + OptionsUtil.DATABASE_HOST.getStringValue(), + OptionsUtil.DATABASE_PORT.getIntValue(), + OptionsUtil.DATABASE_DATABASE.getStringValue(), + OptionsUtil.DATABASE_USER.getStringValue(), + OptionsUtil.DATABASE_PASSWORD.getStringValue()); + case "sqlite" -> DataSourceProvider.initializeForSQLite( + plugin.getDataFolder().getPath() + File.separator + OptionsUtil.DATABASE_FILE_NAME.getStringValue() + ); + } + + EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); + + for (ManagedEntity managedEntity : AbstractEntityManager.getManagedEntities()) { + if (entityManagerRegistry.get(managedEntity.type()).isEmpty()) { + registerEntityManager(entityManagerRegistry, managedEntity, + new SQLEntityManager<>(managedEntity, DataSourceProvider.getDataSource())); + } + } + } + + private void setupMongoDBDatabase() { + EntityManagerRegistry entityManagerRegistry = EntityManagerRegistry.getInstance(); + + for (ManagedEntity managedEntity : AbstractEntityManager.getManagedEntities()) { + if (entityManagerRegistry.get(managedEntity.type()).isEmpty()) { + registerEntityManager(entityManagerRegistry, managedEntity, + new MongoEntityManager<>(managedEntity, MongoProvider.getDatabase())); + } + } + } + + private void registerEntityManager(EntityManagerRegistry entityManagerRegistry, + ManagedEntity managedEntity, + EntityManager entityManager) { + try { + entityManagerRegistry.register(managedEntity.type(), entityManager); + plugin.getLogger().info("Registered " + managedEntity.type().getSimpleName() + " entity manager"); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, + "Failed to register " + managedEntity.type().getSimpleName() + " entity manager", e); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoEntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoEntityManager.java new file mode 100644 index 00000000..218b2629 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoEntityManager.java @@ -0,0 +1,107 @@ +package com.georgev22.skinoverlay.storage.manager.mongo; + +import com.georgev22.skinoverlay.storage.ManagedEntity; +import com.georgev22.skinoverlay.storage.data.Entity; +import com.georgev22.skinoverlay.storage.manager.AbstractEntityManager; +import com.georgev22.skinoverlay.utilities.GsonUtils; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.ReplaceOptions; +import org.bson.Document; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; + +public class MongoEntityManager extends AbstractEntityManager { + + private MongoCollection collection; + + public MongoEntityManager(ManagedEntity managedEntity, @NonNull MongoDatabase database) { + super(managedEntity); + String collectionName = managedEntity.key(); + try { + this.collection = database.getCollection(collectionName); + } catch (Throwable throwable) { + database.createCollection(collectionName); + this.collection = database.getCollection(collectionName); + } + this.collection.createIndex(new Document("_id", 1)); + } + + @Override + protected void delete0(@NonNull E entity) { + executorService.execute(() -> collection.deleteOne(Filters.eq("_id", entity.getUniqueId().toString()))); + } + + @Override + public Optional load0(@NonNull String id) { + Document doc = collection.find(Filters.eq("_id", id)).first(); + if (doc == null) return Optional.empty(); + + try { + E entity = GsonUtils.fromJson(doc.toJson(), getEntityClass()); + if (entity != null) { + loadedEntities.append(id, entity); + } + return Optional.ofNullable(entity); + } catch (Exception e) { + main.getLogger().log(Level.SEVERE, "Error loading " + this.getManagedEntity().key() + " " + id + " from MongoDB", e); + return Optional.empty(); + } + } + + @Override + public void loadAll() { + List successList = Collections.synchronizedList(new ArrayList<>()); + List failedList = Collections.synchronizedList(new ArrayList<>()); + long startTime = System.nanoTime(); + try (MongoCursor cursor = collection.find().iterator()) { + while (cursor.hasNext()) { + Document doc = cursor.next(); + try { + E entity = GsonUtils.fromJson(doc.toJson(), getEntityClass()); + if (entity != null) { + loadedEntities.append(entity.getUniqueId().toString(), entity); + successList.add(entity.getUniqueId().toString()); + } else { + failedList.add(doc.get("_id").toString()); + } + } catch (Exception e) { + main.getLogger().log(Level.SEVERE, "Error loading document: " + doc.toJson(), e); + } + } + } + logLoadAll(successList, failedList, startTime); + } + + @Override + public boolean exists(@NonNull String id) { + return loadedEntities.containsKey(id); + } + + @Override + protected boolean save0(@NonNull E entity) { + try { + String json = GsonUtils.toJson(entity, false); + Document doc = Document.parse(json); + doc.put("_id", entity.getUniqueId().toString()); + + collection.replaceOne( + Filters.eq("_id", entity.getUniqueId().toString()), + doc, + new ReplaceOptions().upsert(true) + ); + return true; + } catch (Exception e) { + main.getLogger().log(Level.SEVERE, "Error saving " + + this.getManagedEntity().key() + " data to MongoDB for " + entity.getUniqueId(), e); + return false; + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoProvider.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoProvider.java new file mode 100644 index 00000000..fb75a884 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/mongo/MongoProvider.java @@ -0,0 +1,27 @@ +package com.georgev22.skinoverlay.storage.manager.mongo; + +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import org.jspecify.annotations.NonNull; + +public final class MongoProvider { + private static MongoClient client; + + public static @NonNull MongoDatabase getDatabase() { + if (client == null) { + client = MongoClients.create( + OptionsUtil.DATABASE_MONGO_HOST.getStringValue() + ":" + + OptionsUtil.DATABASE_MONGO_PORT.getIntValue() + ); + } + return client.getDatabase(OptionsUtil.DATABASE_MONGO_DATABASE.getStringValue()); + } + + public static void shutdown() { + if (client != null) { + client.close(); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/DataSourceProvider.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/DataSourceProvider.java new file mode 100644 index 00000000..f3e94920 --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/DataSourceProvider.java @@ -0,0 +1,50 @@ +package com.georgev22.skinoverlay.storage.manager.sql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import javax.sql.DataSource; + +public class DataSourceProvider { + + private static HikariDataSource dataSource; + + public static void initializeForMySQL(String host, int port, String database, String user, String password) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:mariadb://" + host + ":" + port + "/" + database + "?useSSL=false&characterEncoding=utf8"); + config.setUsername(user); + config.setPassword(password); + + config.setMaximumPoolSize(10); + config.setMinimumIdle(2); + config.setIdleTimeout(600000); + config.setMaxLifetime(1800000); + config.setConnectionTimeout(10000); + config.setPoolName("ProjectsMySQLPool"); + + dataSource = new HikariDataSource(config); + } + + public static void initializeForSQLite(String filePath) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:sqlite:" + filePath); + config.setConnectionTestQuery("SELECT 1"); + config.setMaximumPoolSize(1); + config.setPoolName("ProjectsSQLitePool"); + + dataSource = new HikariDataSource(config); + } + + public static DataSource getDataSource() { + if (dataSource == null) { + throw new IllegalStateException("DataSource is not initialized"); + } + return dataSource; + } + + public static void shutdown() { + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/SQLEntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/SQLEntityManager.java new file mode 100644 index 00000000..2535520b --- /dev/null +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/sql/SQLEntityManager.java @@ -0,0 +1,143 @@ +package com.georgev22.skinoverlay.storage.manager.sql; + +import com.georgev22.skinoverlay.storage.ManagedEntity; +import com.georgev22.skinoverlay.storage.data.Entity; +import com.georgev22.skinoverlay.storage.manager.AbstractEntityManager; +import com.georgev22.skinoverlay.utilities.GsonUtils; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import org.jspecify.annotations.NonNull; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.*; +import java.util.logging.Level; + +public class SQLEntityManager extends AbstractEntityManager { + private final DataSource dataSource; + + public SQLEntityManager(ManagedEntity managedEntity, @NonNull DataSource dataSource) { + super(managedEntity); + this.dataSource = dataSource; + try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE IF NOT EXISTS %1$s_data ( + %1$s_uuid CHAR(36) PRIMARY KEY, + data TEXT NOT NULL + ) + """.formatted(this.getManagedEntity().key())); + } catch (SQLException e) { + main.getLogger().log(Level.SEVERE, "Failed to create %s_data table".formatted(this.getManagedEntity().key()), e); + } + } + + @Override + public void delete0(@NonNull E entity) { + executorService.execute(() -> { + UUID id = entity.getUniqueId(); + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "DELETE FROM %1$s_data WHERE %1$s_uuid = ?" + .formatted(getManagedEntity().key()))) { + + stmt.setString(1, id.toString()); + stmt.executeUpdate(); + + } catch (SQLException e) { + main.getLogger().log(Level.SEVERE, + "Failed to delete " + getManagedEntity().key() + " data for " + id, e); + } + }); + } + + @Override + protected Optional load0(@NonNull String id) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "SELECT data FROM %1$s_data WHERE %1$s_uuid = ?".formatted(this.getManagedEntity().key()))) { + stmt.setString(1, id); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + String json = rs.getString("data"); + E entity = GsonUtils.fromJson(json, this.getEntityClass()); + if (entity == null) { + this.main.getLogger().log(Level.SEVERE, "Failed to load " + this.getManagedEntity().key() + " " + id); + return Optional.empty(); + } + loadedEntities.append(id, entity); + return Optional.of(entity); + } + } catch (SQLException e) { + main.getLogger().log(Level.SEVERE, "Error loading " + this.getManagedEntity().key() + " data for " + id, e); + } + return Optional.empty(); + } + + @Override + public void loadAll() { + List successList = Collections.synchronizedList(new ArrayList<>()); + List failedList = Collections.synchronizedList(new ArrayList<>()); + long startTime = System.nanoTime(); + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT %1$s_uuid, data FROM %1$s_data".formatted(this.getManagedEntity().key()))) { + + while (rs.next()) { + String id = rs.getString("%s_uuid".formatted(this.getManagedEntity().key())); + String json = rs.getString("data"); + + E entity = GsonUtils.fromJson(json, this.getEntityClass()); + if (entity != null) { + loadedEntities.put(id, entity); + successList.add(id); + if (OptionsUtil.DEBUG.getBooleanValue()) { + main.getLogger().info("Loaded entity " + entity.getUniqueId()); + } + } else { + failedList.add(id); + } + } + } catch (SQLException e) { + main.getLogger().log(Level.SEVERE, "Failed to load all " + this.getManagedEntity().key() + " data", e); + } + logLoadAll(successList, failedList, startTime); + } + + @Override + public boolean exists(@NonNull String id) { + if (loadedEntities.containsKey(id)) { + return true; + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "SELECT 1 FROM %1$s_data WHERE %1$s_uuid = ?".formatted(this.getManagedEntity().key()))) { + stmt.setString(1, id); + ResultSet rs = stmt.executeQuery(); + return rs.next(); + } catch (SQLException e) { + main.getLogger().log(Level.SEVERE, "Error checking existence for " + this.getManagedEntity().key() + " " + id, e); + return false; + } + } + + @Override + protected boolean save0(@NonNull E entity) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "REPLACE INTO %1$s_data (%1$s_uuid, data) VALUES (?, ?)" + .formatted(getManagedEntity().key()))) { + + stmt.setString(1, entity.getUniqueId().toString()); + stmt.setString(2, GsonUtils.toJson(entity, false)); + stmt.executeUpdate(); + return true; + + } catch (SQLException e) { + main.getLogger().log(Level.SEVERE, + "Error saving " + getManagedEntity().key() + " data for " + entity.getUniqueId(), e); + return false; + } + } +} diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java index a53be8d4..99d15111 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java @@ -4,6 +4,7 @@ import com.georgev22.skinoverlay.utilities.color.Color; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.List; import java.util.Optional; @@ -20,6 +21,30 @@ 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_FILE_NAME("database.SQL.SQLite file name", "SkinOverlay", Optional.empty()), + + 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_TYPE("database.type", "File", Optional.empty()), + EXPERIMENTAL_FEATURES("experimental features", false, Optional.empty()), METRICS("metrics", true, Optional.empty()), @@ -38,140 +63,218 @@ public enum OptionsUtil { REDIS_PORT("redis.port", 6379, Optional.empty()), REDIS_PASSWORD("redis.password", "", Optional.empty()), ; - private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); + private static final FileManager fileManager = FileManager.getInstance(); private final String pathName; - private final Object value; + private final Object defaultValue; private final Optional[] oldPaths; + private String resolvedPath; + private Object cachedValue; @SafeVarargs - @Contract(pure = true) - OptionsUtil(final String pathName, final Object value, Optional... oldPaths) { + OptionsUtil(final String pathName, final Object defaultValue, Optional... oldPaths) { this.pathName = pathName; - this.value = value; + this.defaultValue = defaultValue; this.oldPaths = oldPaths; } - public boolean getBooleanValue() { - return mainPlugin.getConfig().getBoolean(getPath(), Boolean.parseBoolean(String.valueOf(getDefaultValue()))); - } - - public Object getObjectValue() { - return mainPlugin.getConfig().get(getPath(), getDefaultValue()); - } - - public String getStringValue() { - return mainPlugin.getConfig().getString(getPath(), String.valueOf(getDefaultValue())); - } - - public @NotNull Long getLongValue() { - return mainPlugin.getConfig().getLong(getPath(), Long.parseLong(String.valueOf(getDefaultValue()))); - } - - public @NotNull Integer getIntValue() { - return mainPlugin.getConfig().getInt(getPath(), Integer.parseInt(String.valueOf(getDefaultValue()))); - } - - public @NotNull Double getDoubleValue() { - return mainPlugin.getConfig().getDouble(getPath(), Double.parseDouble(String.valueOf(getDefaultValue()))); + /** + * Reloads and caches all configuration options. + * + *

+ * This method must be called: + *

    + *
  • On plugin startup
  • + *
  • After a configuration reload
  • + *
+ * + *

+ * After this method is called, all getters operate in O(1) time + * without accessing the YAML configuration. + */ + public static void reloadAll() { + for (OptionsUtil option : values()) { + option.reload(); + } } - public @NotNull List getStringList() { - return mainPlugin.getConfig().getStringList(getPath()); + /** + * Reloads and caches the value of this option. + * + *

+ * The configuration path is resolved once and stored. + * The value is then read from the configuration and cached. + */ + public void reload() { + resolvedPath = null; + String path = getPath(); + Object val = fileManager.getConfig().getFileConfiguration().get(path); + cachedValue = (val != null) ? val : defaultValue; } /** - * Converts and return a String List of color codes to a List of Color classes that represent the colors. + * Returns the cached boolean value of this option. * - * @return a List of Color classes that represent the colors. + * @return the boolean value */ - public @NotNull List getColors() { - return getStringList().stream().map(Color::from).collect(Collectors.toList()); + public boolean getBooleanValue() { + if (cachedValue instanceof Boolean b) { + return b; + } + return Boolean.parseBoolean(String.valueOf(cachedValue)); } - public boolean getBooleanValue(String arg) { - return mainPlugin.getConfig().getBoolean(String.format(getPath(), arg), Boolean.parseBoolean(String.valueOf(getDefaultValue()))); + /** + * Returns the cached integer value of this option. + * + * @return the integer value + */ + public @NonNull Integer getIntValue() { + if (cachedValue instanceof Number n) { + return n.intValue(); + } + return Integer.parseInt(String.valueOf(cachedValue)); } - public Object getObjectValue(String arg) { - return mainPlugin.getConfig().get(getPath(), getDefaultValue()); + /** + * Returns the cached long value of this option. + * + * @return the long value + */ + public @NonNull Long getLongValue() { + if (cachedValue instanceof Number n) { + return n.longValue(); + } + return Long.parseLong(String.valueOf(cachedValue)); } - public String getStringValue(String arg) { - return mainPlugin.getConfig().getString(getPath(), String.valueOf(getDefaultValue())); + /** + * Returns the cached double value of this option. + * + * @return the double value + */ + public @NonNull Double getDoubleValue() { + if (cachedValue instanceof Number n) { + return n.doubleValue(); + } + return Double.parseDouble(String.valueOf(cachedValue)); } - public @NotNull Long getLongValue(String arg) { - return mainPlugin.getConfig().getLong(getPath(), Long.parseLong(String.valueOf(getDefaultValue()))); + /** + * Returns the cached float value of this option. + * + * @return the float value + */ + public @NonNull Float getFloatValue() { + if (cachedValue instanceof Number n) { + return n.floatValue(); + } + return Float.parseFloat(String.valueOf(cachedValue)); } - public @NotNull Integer getIntValue(String arg) { - return mainPlugin.getConfig().getInt(getPath(), Integer.parseInt(String.valueOf(getDefaultValue()))); + /** + * Returns the cached string value of this option. + * + * @return the string value + */ + public String getStringValue() { + return String.valueOf(cachedValue); } - public @NotNull Double getDoubleValue(String arg) { - return mainPlugin.getConfig().getDouble(getPath(), Double.parseDouble(String.valueOf(getDefaultValue()))); + /** + * Returns the cached string list value of this option. + * + * @return a list of strings, or an empty list if the value is not a list + */ + @SuppressWarnings("unchecked") + public @NonNull List getStringList() { + return cachedValue instanceof List + ? (List) cachedValue + : List.of(); } - public @NotNull List getStringList(String arg) { - return mainPlugin.getConfig().getStringList(getPath()); + /** + * Converts the cached string list into a list of {@link Color} objects. + * + * @return a list of parsed colors + */ + public @NonNull List getColors() { + return getStringList().stream() + .map(Color::from) + .collect(Collectors.toList()); } /** - * Converts and return a String List of color codes to a List of Color classes that represent the colors. + * Returns the cached string value wrapped in an {@link Optional}. * - * @return a List of Color classes that represent the colors. + * @return an optional containing the string value */ - public @NotNull List getColors(String arg) { - return getStringList().stream().map(Color::from).collect(Collectors.toList()); + public @NonNull Optional getOptionalStringValue() { + return Optional.ofNullable(getStringValue()); } /** - * Returns the path. + * Resolves and returns the configuration path for this option. + * + *

+ * The resolution order is: + *

    + *
  1. Cached path
  2. + *
  3. Current path
  4. + *
  5. Legacy paths
  6. + *
* - * @return the path. + *

+ * The resolved path is cached after the first lookup. + * + * @return the resolved configuration path */ - public @NotNull String getPath() { - if (mainPlugin.getConfig().get("Options." + getDefaultPath()) == null) { - for (Optional path : getOldPaths()) { - if (path.isPresent()) { - if (mainPlugin.getConfig().get("Options." + path.get()) != null) { - return "Options." + path.get(); - } + public @NonNull String getPath() { + if (resolvedPath != null) { + return resolvedPath; + } + + String base = "Options." + pathName; + if (fileManager.getConfig().getFileConfiguration().get(base) != null) { + return resolvedPath = base; + } + + for (Optional old : oldPaths) { + if (old.isPresent()) { + String oldPath = "Options." + old.get(); + if (fileManager.getConfig().getFileConfiguration().get(oldPath) != null) { + return resolvedPath = oldPath; } } } - return "Options." + getDefaultPath(); + + return resolvedPath = base; } /** - * Returns the default path. + * Returns the default (current) path name without the "Options." prefix. * - * @return the default path. + * @return the default path name */ @Contract(pure = true) - public @NotNull String getDefaultPath() { + public @NonNull String getDefaultPath() { return this.pathName; } /** - * Returns the old path if it exists. + * Returns the legacy paths associated with this option. * - * @return the old path if it exists. + * @return an array of legacy paths */ public Optional[] getOldPaths() { return oldPaths; } /** - * Returns the default value if the path have no value. + * Returns the default value of this option. * - * @return the default value if the path have no value. + * @return the default value */ public Object getDefaultValue() { - return value; - } - - public Optional getOptionalStringValue() { - return Optional.ofNullable(getStringValue()); + return defaultValue; } -} +} \ No newline at end of file diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 7cd07588..de796440 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -9,6 +9,25 @@ Options: locale: "en_US" commands: #Disable or enable plugin commands skinoverlay: true + database: + type: "File" #Types File, SQLite, MySQL, MongoDB + + # SQL database options + SQL: + host: "localhost" # SQL database host + port: 3306 # SQL database port + user: "youruser" # SQL database username + password: "yourpassword" # SQL database password + database: "SkinOverlay" # SQL database name + SQLite file name: "skinoverlay" # SQLite file name + + # MongoDB options + MongoDB: + host: "localhost" # MongoDB host + port: 27017 # MongoDB port + user: "youruser" # MongoDB username + password: "yourpassword" # MongoDB password + database: "skinoverlay" # MongoDB database name updater: enabled: true default skin uuid: "8667ba71-b85a-4004-af54-457a9734eed7" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74d38417..e9cd48ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ jetbrains-annotations = "26.0.2" jspecify = "1.0.0" log4j-api = "3.0.0-beta2" reflect = "1.5.0" +mongodb-driver-sync = "5.2.1" [plugins] gradleup-shadow = { id = "com.gradleup.shadow", version.ref = "gradleup-shadow" } @@ -48,4 +49,5 @@ jedis = { module = "redis.clients:jedis", version.ref = "jedis" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } 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 +reflect = { module = "net.lenni0451:Reflect", version.ref = "reflect" } +mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongodb-driver-sync" } \ No newline at end of file From 030b71a683c9a3c6022a2f72a012948d07d623f6 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 18:20:31 +0300 Subject: [PATCH 57/79] feat: integrate Quark library manager for dependency loading The Quark library manager is introduced to replace direct dependency implementations with runtime-loaded libraries across Bukkit and Velocity platforms. - Add Quark Gradle plugin (1.3.0) to Bukkit and Velocity build scripts - Migrate common module dependencies from `implementation` to `compileOnly` - Replace `shadowJar` configuration with `quark` blocks for relocations - Use Google Cloud Maven mirror for faster artifact resolution - Update Gradle Shadow plugin to 9.4.1 and MineskinClient to 3.2.6 - Refactor Bukkit audiences into a dedicated utility class - Simplify publication configuration and remove conditional Maven repos --- .../buildlogic.java-conventions.gradle.kts | 4 +- bukkit/build.gradle.kts | 81 +++++++++++++------ .../skinoverlay/SkinOverlayBukkit.java | 17 ++-- .../command/BukkitCommandIssuer.java | 4 +- .../utilities/BukkitAdventure.java | 27 +++++++ common/build.gradle.kts | 20 ++--- gradle/libs.versions.toml | 4 +- gradle/publish.gradle | 79 ++++-------------- velocity/build.gradle.kts | 47 +++++++++-- .../skinoverlay/SkinOverlayVelocity.java | 7 +- 10 files changed, 174 insertions(+), 116 deletions(-) create mode 100644 bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts index df478842..248a7f5e 100644 --- a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -9,7 +9,9 @@ if (file("$rootDir/build.local.gradle.kts").exists()) { repositories { mavenLocal() - mavenCentral() + maven { + url = uri("https://maven-central.storage-download.googleapis.com/maven2/") + } maven { url = uri("https://repo.shulkerlabs.com/releases/") } diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index bfe9f1fa..232f7665 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -1,12 +1,15 @@ plugins { id("buildlogic.java-conventions") alias(libs.plugins.gradleup.shadow) + id("org.bxteam.quark") version "1.3.0" } apply(from = "$rootDir/gradle/publish.gradle") repositories { - mavenCentral() + maven { + url = uri("https://maven-central.storage-download.googleapis.com/maven2/") + } } dependencies { @@ -14,15 +17,39 @@ dependencies { 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) + quark(libs.bstats.bukkit) + quark(libs.adventure.platform.bukkit) + quark(libs.adventure.text.minimessage) + quark(libs.adventure.text.serializer.legacy) + quark(libs.reflect) + + quark(libs.hikari) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + quark(libs.gson) + + // MINESKIN CLIENT + quark(libs.mineskinclient.client) { + exclude(group = "com.google.code.gson", module = "gson") + exclude(group = "com.google.guava", module = "guava") + } + quark(libs.mineskinclient.java11) { + exclude(group = "com.google.code.gson", module = "gson") + exclude(group = "com.google.guava", module = "guava") + } - implementation(project(":common")) { + // YAML + quark(libs.yamlconfiguration) { exclude(group = "org.slf4j", module = "slf4j-api") } + + quark(libs.jedis) + + quark(libs.jspecify) + quark(libs.mongodb.driver.sync) + + implementation(project(":common")) implementation(project(":bukkit:versions:mc1_17_R1", configuration = "reobf")) implementation(project(":bukkit:versions:mc1_18_R1", configuration = "reobf")) implementation(project(":bukkit:versions:mc1_18_R2", configuration = "reobf")) @@ -41,6 +68,30 @@ dependencies { implementation(project(":bukkit:versions:mc1_21_R6", configuration = "reobf")) } +quark { + platform = "bukkit" + repositories { + includeProjectRepositories(); + } + + relocate("org.mineskin", "${project.property("packageName")}.lib.mineskin") + relocate("com.google.gson", "${project.property("packageName")}.lib.gson") + relocate("com.google.errorprone", "${project.property("packageName")}.lib.gson.errorprone") + relocate("com.zaxxer", "${project.property("packageName")}.lib.zaxxer") + relocate("org.bstats", "${project.property("packageName")}.lib.bstats") + relocate("org.bspfsystems.yamlconfiguration", "${project.property("packageName")}.lib.yaml") + relocate("org.yaml.snakeyaml", "${project.property("packageName")}.lib.yaml") + relocate("org.intellij.lang", "${project.property("packageName")}.lib.jetbrains") + relocate("org.jetbrains", "${project.property("packageName")}.lib.jetbrains") + relocate("org.json", "${project.property("packageName")}.lib.json") + relocate("org.apache.commons.pool2", "${project.property("packageName")}.lib.pool2") + relocate("net.kyori", "${project.property("packageName")}.lib.kyori") + relocate("redis.clients", "${project.property("packageName")}.lib.jedis") + relocate("net.lenni0451.reflect", "${project.property("packageName")}.lib.reflect") + relocate("org.bson", "${project.property("packageName")}.lib.bson") + relocate("com.mongodb", "${project.property("packageName")}.lib.mongodb") +} + configurations.configureEach { resolutionStrategy { force(libs.adventure.platform.bukkit) @@ -61,22 +112,6 @@ configurations.configureEach { 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") - relocate("org.bson", "${project.property("packageName")}.lib.bson") - relocate("com.mongodb", "${project.property("packageName")}.lib.mongodb") } tasks.named("publish") { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java index 3dbc33ff..30d43062 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/SkinOverlayBukkit.java @@ -13,12 +13,13 @@ import com.georgev22.skinoverlay.providers.*; import com.georgev22.skinoverlay.scheduler.MinecraftBukkitScheduler; import com.georgev22.skinoverlay.scheduler.MinecraftFoliaScheduler; +import com.georgev22.skinoverlay.utilities.BukkitAdventure; import com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils; import com.georgev22.skinoverlay.utilities.SkinOverlayVersionResolver; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; +import org.bxteam.quark.bukkit.BukkitLibraryManager; import static com.georgev22.skinoverlay.utilities.BukkitMinecraftUtils.MinecraftVersion.getCurrent; @@ -27,7 +28,6 @@ public class SkinOverlayBukkit extends JavaPlugin { private static SkinOverlayBukkit instance; private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); - private BukkitAudiences bukkitAudiences; public static SkinOverlayBukkit getInstance() { return instance; @@ -36,6 +36,8 @@ public static SkinOverlayBukkit getInstance() { @Override public void onLoad() { instance = this; + BukkitLibraryManager libraryManager = new BukkitLibraryManager(this); + libraryManager.loadFromGradle(); this.skinOverlay.setPlugin(this); this.skinOverlay.setLogger(this.getLogger()); this.skinOverlay.setDataFolder(this.getDataFolder()); @@ -66,8 +68,7 @@ public void onLoad() { @Override public void onEnable() { - this.bukkitAudiences = BukkitAudiences.create(this); - this.skinOverlay.setConsoleAudience(this.bukkitAudiences.console()); + BukkitAdventure.init(); BukkitMinecraftUtils.registerListeners( this, new PlayerListeners() @@ -104,13 +105,7 @@ public void onEnable() { @Override public void onDisable() { skinOverlay.onDisable(); - if (this.bukkitAudiences != null) { - this.bukkitAudiences.close(); - this.bukkitAudiences = null; - } + BukkitAdventure.close(); } - public BukkitAudiences getBukkitAudiences() { - return bukkitAudiences; - } } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java index 65441ede..b8757738 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandIssuer.java @@ -1,6 +1,6 @@ package com.georgev22.skinoverlay.command; -import com.georgev22.skinoverlay.SkinOverlayBukkit; +import com.georgev22.skinoverlay.utilities.BukkitAdventure; import com.georgev22.skinoverlay.utilities.Utils; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.platform.bukkit.BukkitAudiences; @@ -25,7 +25,7 @@ public boolean isPlayer() { @Override public Audience audience() { - BukkitAudiences audienceProvider = SkinOverlayBukkit.getInstance().getBukkitAudiences(); + BukkitAudiences audienceProvider = BukkitAdventure.getBukkitAudiences(); if (isPlayer()) { return audienceProvider.player((Player) this.sender); } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java new file mode 100644 index 00000000..7b0f399f --- /dev/null +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitAdventure.java @@ -0,0 +1,27 @@ +package com.georgev22.skinoverlay.utilities; + +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.SkinOverlayBukkit; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; + +public class BukkitAdventure { + + private static BukkitAudiences bukkitAudiences; + + public static void init() { + bukkitAudiences = BukkitAudiences.create(SkinOverlayBukkit.getInstance()); + SkinOverlay.getInstance().setConsoleAudience(bukkitAudiences.console()); + } + + public static BukkitAudiences getBukkitAudiences() { + return bukkitAudiences; + } + + public static void close() { + if (bukkitAudiences != null) { + bukkitAudiences.close(); + bukkitAudiences = null; + } + } + +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 7b3dd760..f51bb24e 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -7,7 +7,9 @@ plugins { apply(from = "$rootDir/gradle/publish.gradle") repositories { - mavenCentral() + maven { + url = uri("https://maven-central.storage-download.googleapis.com/maven2/") + } } group = project.property("group") as String @@ -27,31 +29,31 @@ dependencies { compileOnly(libs.log4j.api) - implementation(libs.hikari) { + compileOnly(libs.hikari) { exclude(group = "org.slf4j", module = "slf4j-api") } - implementation(libs.gson) + compileOnly(libs.gson) // MINESKIN CLIENT - implementation(libs.mineskinclient.client) { + compileOnly(libs.mineskinclient.client) { exclude(group = "com.google.code.gson", module = "gson") exclude(group = "com.google.guava", module = "guava") } - implementation(libs.mineskinclient.java11) { + compileOnly(libs.mineskinclient.java11) { exclude(group = "com.google.code.gson", module = "gson") exclude(group = "com.google.guava", module = "guava") } // YAML - implementation(libs.yamlconfiguration) { + compileOnly(libs.yamlconfiguration) { exclude(group = "org.slf4j", module = "slf4j-api") } - implementation(libs.jedis) + compileOnly(libs.jedis) - implementation(libs.jspecify) - implementation(libs.mongodb.driver.sync) + compileOnly(libs.jspecify) + compileOnly(libs.mongodb.driver.sync) } configurations.configureEach { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9cd48ad..52ff4ff4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,12 +5,12 @@ 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" +gradleup-shadow = "9.4.1" hikari = "4.0.3" paperweight = "2.0.0-beta.17" gson = "2.13.1" placeholder-api = "2.11.6" -mineskinclient = "3.0.6-SNAPSHOT" +mineskinclient = "3.2.6" skinsrestorer = "15.7.3" yamlconfiguration = "3.0.2" jedis = "6.0.0" diff --git a/gradle/publish.gradle b/gradle/publish.gradle index dd3b7d37..db62ea4f 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -1,12 +1,12 @@ apply plugin: 'maven-publish' -static def getBranch() { +static String getBranch() { def process = 'git branch --show-current'.execute() process.waitFor() return process.text.trim() } -static def getHash() { +static String getHash() { def process = 'git rev-parse HEAD'.execute() process.waitFor() return process.text.trim() @@ -17,7 +17,7 @@ java { withSourcesJar() } -jar { +tasks.jar { manifest { attributes( 'Build-Jdk': "${System.properties['java.vendor']} ${System.properties['java.vm.version']}", @@ -30,74 +30,29 @@ jar { publishing { publications { - shadow(MavenPublication) { publication -> - project.shadow.component(publication) - artifactId = project.getName().toLowerCase() - groupId = ((String) project.getGroup()).toLowerCase() - artifact sourcesJar - artifact javadocJar + shadow(MavenPublication) { + from components.java + artifactId = project.name.toLowerCase() + groupId = project.group.toString().toLowerCase() + artifact(tasks.sourcesJar) + artifact(tasks.javadocJar) } } repositories { - def isGitHub = System.getenv("GITHUB_REPOSITORY") ? true : false - if (isGitHub) { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/${project.findProperty("GITHUB_REPOSITORY") ?: System.getenv("GITHUB_REPOSITORY")}") - credentials { - username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") - } - } - maven { - name = "ShulkerLabs" - - url = uri("https://repo.shulkerlabs.com/releases") - - def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : - System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null - def mavenPassword = System.getenv("MAVEN_PASSWORD") ? System.getenv("MAVEN_PASSWORD") : - System.getProperty("MAVEN_PASSWORD") ? System.getProperty("MAVEN_PASSWORD") : null - - if (mavenUsername != null && mavenPassword != null) { - credentials { - username = mavenUsername - password = mavenPassword - } - authentication { - basic(BasicAuthentication) - } - } - } - } - if (!isGitHub) { - maven { - name = "ShulkerLabs-snapshots" - - url = uri("https://repo.shulkerlabs.com/snapshots") - - def mavenUsername = System.getenv("MAVEN_USERNAME") ? System.getenv("MAVEN_USERNAME") : - System.getProperty("MAVEN_USERNAME") ? System.getProperty("MAVEN_USERNAME") : null - def mavenPassword = System.getenv("MAVEN_PASSWORD") ? System.getenv("MAVEN_PASSWORD") : - System.getProperty("MAVEN_PASSWORD") ? System.getProperty("MAVEN_PASSWORD") : null - - if (mavenUsername != null && mavenPassword != null) { - credentials { - username = mavenUsername - password = mavenPassword - } - authentication { - basic(BasicAuthentication) - } - } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/${project.findProperty('GITHUB_REPOSITORY') ?: System.getenv('GITHUB_REPOSITORY')}") + credentials { + username = project.findProperty('gpr.user') ?: System.getenv('GITHUB_ACTOR') + password = project.findProperty('gpr.token') ?: System.getenv('GITHUB_TOKEN') } } } } -javadoc { +tasks.javadoc { if (JavaVersion.current().isJava9Compatible()) { options.addBooleanOption('html5', true) options.addStringOption('Xdoclint:none', '-quiet') } -} +} \ No newline at end of file diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 1025a353..ecbd6245 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,12 +1,15 @@ plugins { id("buildlogic.java-conventions") alias(libs.plugins.gradleup.shadow) + id("org.bxteam.quark") version "1.3.0" } apply(from = "$rootDir/gradle/publish.gradle") repositories { - mavenCentral() + maven { + url = uri("https://maven-central.storage-download.googleapis.com/maven2/") + } } dependencies { @@ -16,7 +19,34 @@ dependencies { implementation(project(":common")) { exclude(group = "org.slf4j", module = "slf4j-api") } - implementation(libs.bstats.velocity) + + quark(libs.bstats.velocity) + quark(libs.hikari) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + quark(libs.gson) + + // MINESKIN CLIENT + quark(libs.mineskinclient.client) { + exclude(group = "com.google.code.gson", module = "gson") + exclude(group = "com.google.guava", module = "guava") + } + quark(libs.mineskinclient.java11) { + exclude(group = "com.google.code.gson", module = "gson") + exclude(group = "com.google.guava", module = "guava") + } + + // YAML + quark(libs.yamlconfiguration) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + quark(libs.jedis) + + quark(libs.jspecify) + quark(libs.mongodb.driver.sync) + compileOnly(libs.log4j.api) } @@ -24,9 +54,11 @@ configurations.configureEach { } -tasks.shadowJar { - archiveBaseName.set("skinoverlay") - archiveClassifier.set("velocity") +quark { + platform = "velocity" + repositories { + includeProjectRepositories(); + } relocate("org.mineskin", "${project.property("packageName")}.lib.mineskin") relocate("com.google.gson", "${project.property("packageName")}.lib.gson") relocate("com.google.errorprone", "${project.property("packageName")}.lib.gson.errorprone") @@ -41,6 +73,11 @@ tasks.shadowJar { relocate("redis.clients", "${project.property("packageName")}.lib.jedis") } +tasks.shadowJar { + archiveBaseName.set("skinoverlay") + archiveClassifier.set("velocity") +} + 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 a1718dc3..5542c127 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/SkinOverlayVelocity.java @@ -24,7 +24,9 @@ import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; +import org.bxteam.quark.velocity.VelocityLibraryManager; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import java.io.File; @@ -48,7 +50,10 @@ public class SkinOverlayVelocity { private final ProxyServer server; @Inject - public SkinOverlayVelocity(ProxyServer server, Logger logger, @DataDirectory @NotNull Path dataDirectory) { + public SkinOverlayVelocity(@NonNull ProxyServer server, Logger logger, @DataDirectory @NotNull Path dataDirectory) { + VelocityLibraryManager velocityLibraryManager + = new VelocityLibraryManager<>(this, logger, dataDirectory, server.getPluginManager()); + velocityLibraryManager.loadFromGradle(); this.server = server; this.skinOverlay.setLogger(new LoggerWrapper(logger)); this.skinOverlay.setProxy(true); From d19e4cb729aaf29e3e81e6938cd6c5eb1c6befcc Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 19:31:59 +0300 Subject: [PATCH 58/79] chore: remove deprecated JCenter repository from Gradle build JCenter has been sunset and is no longer receiving updates. This repository was already read-only and should not be used for resolving dependencies. --- .../src/main/kotlin/buildlogic.java-conventions.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts index 248a7f5e..3313660f 100644 --- a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -33,9 +33,6 @@ repositories { maven { url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") } - maven { - url = uri("https://jcenter.bintray.com/") - } maven { url = uri("https://jitpack.io") } From 75d09ee875a4792b0321e17719a3d235c519df1d Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 19:32:15 +0300 Subject: [PATCH 59/79] feat: make Consumer parameter nullable in entity creation methods Allow entity creation without a consumer callback by making the Consumer parameter nullable in both the interface and abstract implementation. This provides flexibility when no post-creation processing is needed. --- .../com/georgev22/skinoverlay/storage/EntityManager.java | 7 ++++--- .../skinoverlay/storage/manager/AbstractEntityManager.java | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java index d52bfd94..d2265206 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/EntityManager.java @@ -4,6 +4,7 @@ import com.georgev22.skinoverlay.datastructures.maps.UnmodifiableObjectMap; import com.georgev22.skinoverlay.storage.data.Entity; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.Optional; @@ -138,7 +139,7 @@ default boolean exists(@NonNull UUID uuid) { * @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(@NonNull String id, @NonNull Consumer consumer) { + default Optional create(@NonNull String id, @Nullable Consumer consumer) { return create(UUID.fromString(id), consumer); } @@ -149,7 +150,7 @@ default Optional create(@NonNull String id, @NonNull Consumer consumer) { * @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(@NonNull UUID uuid, @NonNull Consumer consumer) { + default Optional create(@NonNull UUID uuid, @Nullable Consumer consumer) { return create(ObjectMap.ofEntries(ObjectMap.entry("id", uuid)), consumer); } @@ -160,7 +161,7 @@ default Optional create(@NonNull UUID uuid, @NonNull Consumer consumer) { * @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(@NonNull ObjectMap data, @NonNull Consumer consumer); + Optional create(@NonNull ObjectMap data, @Nullable Consumer consumer); /** * Gets the name of this entity manager. diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/AbstractEntityManager.java b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/AbstractEntityManager.java index 55258bbe..667a4617 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/manager/AbstractEntityManager.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/manager/AbstractEntityManager.java @@ -16,6 +16,7 @@ import com.georgev22.skinoverlay.task.ExecutorType; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.util.*; import java.util.concurrent.*; @@ -115,7 +116,7 @@ public UnmodifiableObjectMap getLoadedEntities() { @Override public Optional create(@NonNull ObjectMap data, - @NonNull Consumer consumer) { + @Nullable Consumer consumer) { EntityFactory factory = FACTORIES.get(this.getEntityClass()); if (factory == null) { main.getLogger().log(Level.WARNING, "No entity factory registered for " + this.getManagedEntity().key()); @@ -127,7 +128,9 @@ public Optional create(@NonNull ObjectMap data, } E entity = this.getEntityClass().cast(factory.create(data)); - consumer.accept(entity); + if (consumer != null) { + consumer.accept(entity); + } loadedEntities.put(entity.getUniqueId().toString(), entity); saveQueue.add(entity.getUniqueId()); From ee3f94a9d3a4f4598aec1da28f5278b27bed4766 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 19:32:45 +0300 Subject: [PATCH 60/79] refactor: improve player data loading and skin initialization Create default player data if missing and load default skin image from provider, setting it as skin parts. Initialize current skin to default if none set, improving data consistency and reliability. --- .../listeners/PlayerListeners.java | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 86d817d6..3e0c048f 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -10,10 +10,13 @@ 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.SerializableBufferedImage; import com.georgev22.skinoverlay.utilities.Utils; import com.georgev22.skinoverlay.utilities.config.OptionsUtil; +import com.georgev22.skinoverlay.utilities.skin.SkinParts; import org.jetbrains.annotations.NotNull; +import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Optional; import java.util.UUID; @@ -49,12 +52,16 @@ public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { EntityManager playerDataManager = playerDataManagerOpt.get(); EntityManager skinManager = skinManagerOpt.get(); - PlayerData playerData = playerDataManager.findById(player.getUniqueId()).orElse(null); - - if (playerData == null) { - return; + Optional playerDataOptional = playerDataManager.findById(player.getUniqueId()); + if (playerDataOptional.isEmpty()) { + playerDataOptional = playerDataManager.create(player.getUniqueId(), null); + if (playerDataOptional.isEmpty()) { + return; + } } + PlayerData playerData = playerDataOptional.get(); + SGameProfile gameProfile = player.getGameProfile(); SProperty property = gameProfile.getProperty("textures"); @@ -67,13 +74,33 @@ public void onPlayerJoin(@NotNull SPlayerJoinEvent event) { } } + BufferedImage defaultSkinImage; + try { + defaultSkinImage = mainPlugin.getSkinProvider().getSkinImage(property); + } catch (IOException e) { + mainPlugin.getLogger().log(Level.SEVERE, "Error loading Default Skin:", e); + return; + } + + SerializableBufferedImage defaultSkinImageSerializable = new SerializableBufferedImage(defaultSkinImage); + UUID uuid = Utils.generateUUID("default" + player.getUniqueId()); - Skin defaultSkin = skinManager.findById(uuid).orElseGet(() -> new Skin(uuid)); + Optional defaultSkinOptional = skinManager.findById(uuid); + if (defaultSkinOptional.isEmpty()) { + defaultSkinOptional = skinManager.create(uuid, null); + if (defaultSkinOptional.isEmpty()) { + return; + } + } + Skin defaultSkin = defaultSkinOptional.get(); + defaultSkin.setSkinParts(new SkinParts(defaultSkinImageSerializable, "default")); defaultSkin.setProperty(property); playerData.setDefaultSkin(defaultSkin); - if (playerData.getCurrentSkin() != null && !playerData.getCurrentSkin().equals(defaultSkin)) { + if (playerData.getCurrentSkin() == null) { + playerData.setCurrentSkin(defaultSkin); + } else if (!playerData.getCurrentSkin().equals(defaultSkin)) { mainPlugin.getSkinProvider().setSkin(player, playerData.getCurrentSkin()); } From c6387b6fd379ac38b9ed2f96328ba44223f94de1 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 19:32:55 +0300 Subject: [PATCH 61/79] refactor: migrate Skin creation to use skinManager.create() Replace direct instantiation of `Skin` objects with the new `skinManager.create()` factory method, which returns an `Optional`. This ensures proper skin resource management and consistent creation errors. Add handling for empty optional results by throwing a `SkinException` and including this exception in the catch block for skin retrieval failures. This improves error reporting and prevents silent failures when skin creation fails. --- .../skinoverlay/providers/SkinProvider.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java index f47b6c61..81fb8986 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -78,15 +78,19 @@ public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer this.skinOverlay.getLogger().warning("Default skin not found for player: " + player.getName()); try { SProperty property = this.getSkin(player); - Skin skin = new Skin(skinUUID); - skin.setProperty(property); - skin.setSkinParts(skinParts); + Optional skinOptional = skinManager.create(skinUUID, skin -> { + skin.setProperty(property); + skin.setSkinParts(skinParts); + }); + if (skinOptional.isEmpty()) + throw new SkinException("Failed to create default skin for player: " + player.getName()); + Skin skin = skinOptional.get(); if (!this.skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { return Optional.of(skin); } skinManager.save(skin); return Optional.of(skin); - } catch (IOException | ExecutionException | InterruptedException e) { + } catch (IOException | ExecutionException | InterruptedException | SkinException e) { this.skinOverlay.getLogger().log(Level.SEVERE, "Failed to get skin for player: " + player.getName(), e); return Optional.empty(); } @@ -206,9 +210,14 @@ public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer return new SProperty(value, signature); }).join(); - Skin skin = new Skin(skinUUID); - skin.setSkinParts(skinParts); - skin.setProperty(property); + Optional skinOptional = skinManager.create(skinUUID, skin -> { + skin.setProperty(property); + skin.setSkinParts(skinParts); + }); + if (skinOptional.isEmpty()) { + throw new SkinException("Couldn't create skin for " + player.getName() + "!"); + } + Skin skin = skinOptional.get(); if (!skinOverlay.isProxy() && OptionsUtil.PROXY.getBooleanValue()) { return Optional.of(skin); } From 7e316b43a2060cdb6a0f2ec4c9ba3a0e92a25bd6 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 21:17:10 +0300 Subject: [PATCH 62/79] feat: implement SPlayer caching in PlayerProvider Introduce a ConcurrentMap-based cache for SPlayer instances in the PlayerProvider base class, along with cache methods (remove, clear). Refactor BukkitPlayerProvider and VelocityPlayerProvider to reuse cached SPlayer objects via a getOrCreate helper, replacing direct instantiation. This reduces object allocation and ensures consistent player state across lookups. Update method signatures with non-null annotations and adjust logic in BukkitPlayerProvider to return null instead of throwing IllegalArgumentException for offline players. Add cache cleanup in PlayerListeners on player quit to prevent memory leaks. --- .../providers/BukkitPlayerProvider.java | 71 ++++++++++--------- .../listeners/PlayerListeners.java | 1 + .../skinoverlay/providers/PlayerProvider.java | 41 ++++++++--- .../providers/VelocityPlayerProvider.java | 62 ++++++++-------- 4 files changed, 105 insertions(+), 70 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java index b861f7f5..12f76d1c 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitPlayerProvider.java @@ -7,6 +7,7 @@ import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.ArrayList; import java.util.List; @@ -14,72 +15,76 @@ public class BukkitPlayerProvider extends PlayerProvider { + private SPlayer getOrCreate(@NonNull Player player) { + return playerCache.computeIfAbsent( + player.getUniqueId(), + uuid -> new BukkitSPlayer(player) + ); + } + @Override - public SPlayer getSPlayer(Object player) { + public SPlayer getSPlayer(@NonNull Object player) { if (player instanceof Player bukkitPlayer) { - return new BukkitSPlayer(bukkitPlayer); + return getOrCreate(bukkitPlayer); } + if (player instanceof OfflinePlayer offlinePlayer) { if (offlinePlayer.isOnline() && offlinePlayer.getPlayer() != null) { - return new BukkitSPlayer(offlinePlayer.getPlayer()); + return getOrCreate(offlinePlayer.getPlayer()); } - throw new IllegalArgumentException("Player " + offlinePlayer.getName() + " is offline"); + return null; } + if (player instanceof String name) { - Player onlinePlayer = Bukkit.getPlayer(name); - if (onlinePlayer != null) { - return new BukkitSPlayer(onlinePlayer); - } - throw new IllegalArgumentException("Player " + name + " is null"); + return getSPlayer(name); } + if (player instanceof UUID uuid) { - Player onlinePlayer = Bukkit.getPlayer(uuid); - if (onlinePlayer != null) { - return new BukkitSPlayer(onlinePlayer); - } - throw new IllegalArgumentException("Player " + uuid + " is null"); + return getSPlayer(uuid); } + if (player instanceof CommandIssuer commandIssuer) { - if (!commandIssuer.isPlayer()) { - throw new IllegalArgumentException("CommandIssuer is not a player"); - } - return new BukkitSPlayer(commandIssuer.getIssuer()); + return getSPlayer(commandIssuer); } + return null; } @Override public SPlayer getSPlayer(@NotNull CommandIssuer commandIssuer) { if (!commandIssuer.isPlayer()) { - throw new IllegalArgumentException("CommandIssuer is not a player"); + return null; } - return new BukkitSPlayer(commandIssuer.getIssuer()); + + return getOrCreate(commandIssuer.getIssuer()); } @Override - public SPlayer getSPlayer(String name) { - Player onlinePlayer = Bukkit.getPlayer(name); - if (onlinePlayer != null) { - return new BukkitSPlayer(onlinePlayer); + public SPlayer getSPlayer(@NonNull String name) { + Player player = Bukkit.getPlayer(name); + if (player != null) { + return getOrCreate(player); } - throw new IllegalArgumentException("Player " + name + " is null"); + + return null; } @Override - public SPlayer getSPlayer(UUID uuid) { - Player onlinePlayer = Bukkit.getPlayer(uuid); - if (onlinePlayer != null) { - return new BukkitSPlayer(onlinePlayer); + public SPlayer getSPlayer(@NonNull UUID uuid) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + return getOrCreate(player); } - throw new IllegalArgumentException("Player " + uuid + " is null"); + + return null; } @Override - public List getOnlinePlayers() { + public @NonNull List getOnlinePlayers() { return new ArrayList<>( Bukkit.getOnlinePlayers().stream() - .map(BukkitSPlayer::new) + .map(this::getOrCreate) .toList() ); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java index 3e0c048f..9bd38568 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java +++ b/common/src/main/java/com/georgev22/skinoverlay/listeners/PlayerListeners.java @@ -113,6 +113,7 @@ public void onPlayerLeave(@NotNull SPlayerLeaveEvent event) { EntityManagerRegistry.getInstance().getTyped(PlayerData.class) .ifPresent(entityManager -> entityManager.findById(player.getUniqueId()) .ifPresent(entityManager::save)); + this.mainPlugin.getPlayerProvider().remove(player.getUniqueId()); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java index c13d8e54..839a4da2 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/PlayerProvider.java @@ -2,9 +2,14 @@ import com.georgev22.skinoverlay.command.CommandIssuer; import com.georgev22.skinoverlay.player.SPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; import java.util.List; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Abstract provider responsible for resolving and managing {@link SPlayer} instances @@ -12,6 +17,8 @@ */ public abstract class PlayerProvider { + protected final ConcurrentMap playerCache = new ConcurrentHashMap<>(); + /** * Resolves an {@link SPlayer} instance from a platform-specific player object. * @@ -21,7 +28,7 @@ public abstract class PlayerProvider { * @param player the platform-specific player object * @return the corresponding {@link SPlayer}, or {@code null} if not found or invalid */ - public abstract SPlayer getSPlayer(Object player); + public abstract @Nullable SPlayer getSPlayer(@NonNull Object player); /** * Resolves an {@link SPlayer} instance from a {@link CommandIssuer}. @@ -32,7 +39,7 @@ public abstract class PlayerProvider { * @param commandIssuer the command issuer * @return the corresponding {@link SPlayer}, or {@code null} if the issuer is not a player */ - public abstract SPlayer getSPlayer(CommandIssuer commandIssuer); + public abstract @Nullable SPlayer getSPlayer(@NotNull CommandIssuer commandIssuer); /** * Resolves an {@link SPlayer} instance by player name. @@ -43,7 +50,7 @@ public abstract class PlayerProvider { * @param name the player's name * @return the corresponding {@link SPlayer}, or {@code null} if not found */ - public abstract SPlayer getSPlayer(String name); + public abstract @Nullable SPlayer getSPlayer(@NotNull String name); /** * Resolves an {@link SPlayer} instance by UUID. @@ -51,7 +58,7 @@ public abstract class PlayerProvider { * @param uuid the player's unique identifier * @return the corresponding {@link SPlayer}, or {@code null} if not found */ - public abstract SPlayer getSPlayer(UUID uuid); + public abstract @Nullable SPlayer getSPlayer(@NotNull UUID uuid); /** * Retrieves a list of all currently online players. @@ -61,7 +68,7 @@ public abstract class PlayerProvider { * * @return a list of online {@link SPlayer} instances */ - public abstract List getOnlinePlayers(); + public abstract @NotNull List getOnlinePlayers(); /** * Checks whether the given {@link SPlayer} is currently online. @@ -72,7 +79,7 @@ public abstract class PlayerProvider { * @return {@code true} if the player is online and has a valid underlying player object, * otherwise {@code false} */ - public boolean isOnline(SPlayer player) { + public boolean isOnline(@NotNull SPlayer player) { try { return player.isOnline() && player.getPlayer() != null; } catch (Exception e) { @@ -89,9 +96,14 @@ public boolean isOnline(SPlayer player) { * @param name the player's name * @return {@code true} if the player is online, otherwise {@code false} */ - public boolean isOnline(String name) { + public boolean isOnline(@NotNull String name) { + //noinspection ConstantValue + if (name == null) return false; try { SPlayer player = getSPlayer(name); + if (player == null) { + return false; + } return isOnline(player); } catch (Exception e) { return false; @@ -107,12 +119,25 @@ public boolean isOnline(String name) { * @param uuid the player's unique identifier * @return {@code true} if the player is online, otherwise {@code false} */ - public boolean isOnline(UUID uuid) { + public boolean isOnline(@NotNull UUID uuid) { + //noinspection ConstantValue + if (uuid == null) return false; try { SPlayer player = getSPlayer(uuid); + if (player == null) { + return false; + } return isOnline(player); } catch (Exception e) { return false; } } + + public void remove(UUID uuid) { + playerCache.remove(uuid); + } + + public void clear() { + playerCache.clear(); + } } \ No newline at end of file diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java index fb362cb8..b852f2e0 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityPlayerProvider.java @@ -5,6 +5,7 @@ import com.georgev22.skinoverlay.player.VelocitySPlayer; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; +import org.jspecify.annotations.NonNull; import java.util.ArrayList; import java.util.List; @@ -19,66 +20,69 @@ public VelocityPlayerProvider(ProxyServer server) { this.server = server; } + private SPlayer getOrCreate(@NonNull Player player) { + return playerCache.computeIfAbsent( + player.getUniqueId(), + uuid -> new VelocitySPlayer(player) + ); + } + @Override - public SPlayer getSPlayer(Object player) { + public SPlayer getSPlayer(@NonNull Object player) { if (player instanceof Player velocityPlayer) { - return new VelocitySPlayer(velocityPlayer); + return getOrCreate(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"); + return getSPlayer(name); } + 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"); + return getSPlayer(uuid); } + if (player instanceof CommandIssuer commandIssuer) { - if (!commandIssuer.isPlayer()) { - throw new IllegalArgumentException("CommandIssuer is not a player"); - } - return new VelocitySPlayer(commandIssuer.getIssuer()); + return getSPlayer(commandIssuer); } + return null; } @Override - public SPlayer getSPlayer(CommandIssuer commandIssuer) { + public SPlayer getSPlayer(@NonNull CommandIssuer commandIssuer) { if (!commandIssuer.isPlayer()) { throw new IllegalArgumentException("CommandIssuer is not a player"); } - return new VelocitySPlayer(commandIssuer.getIssuer()); + + return getOrCreate(commandIssuer.getIssuer()); } @Override - public SPlayer getSPlayer(String name) { - Optional onlinePlayer = server.getPlayer(name); - if (onlinePlayer.isPresent()) { - return new VelocitySPlayer(onlinePlayer.get()); + public SPlayer getSPlayer(@NonNull String name) { + Optional player = server.getPlayer(name); + if (player.isPresent()) { + return getOrCreate(player.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()); + public SPlayer getSPlayer(@NonNull UUID uuid) { + Optional player = server.getPlayer(uuid); + if (player.isPresent()) { + return getOrCreate(player.get()); } + throw new IllegalArgumentException("Player " + uuid + " is not online"); } @Override - public List getOnlinePlayers() { + public @NonNull List getOnlinePlayers() { return new ArrayList<>( server.getAllPlayers().stream() - .map(VelocitySPlayer::new) + .map(this::getOrCreate) .toList() ); } -} +} \ No newline at end of file From 0a83c6f434010e94f3b50c734239409d47f57019 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 21:17:33 +0300 Subject: [PATCH 63/79] fix: add player placeholder to overlay reset success message --- .../skinoverlay/command/commands/sub/ClearSubCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index bfe9219e..47d7a044 100644 --- 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 @@ -68,7 +68,8 @@ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "ta mainPlugin.getSkinProvider().setSkin(target, skin); CommandMessages.COMMAND_OVERLAY_RESET.msg( commandIssuer, - Placeholder.builder(commandIssuer.audience()).build() + Placeholder.builder(commandIssuer.audience()).placeholder("%player%", target.getName()) + .build() ); }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable)); From 44d73a40e62eafdbd218c9f067d3d6b5b86b8296 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 21:18:40 +0300 Subject: [PATCH 64/79] feat: simplify target player resolution in WearSubCommand When target player is missing and issuer is a player, automatically use the issuer as the target before showing missing argument error. --- .../command/commands/sub/WearSubCommand.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 index e2fa7dbf..bc21da71 100644 --- 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 @@ -35,14 +35,20 @@ protected void handle(@NotNull CommandIssuer commandIssuer, return; } - if (target == null) { - if (!commandIssuer.isPlayer()) { - CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, Placeholder.builder(commandIssuer.audience()) - .placeholder("%command%", "wear ").placeholder("%arg%", "target").build()); - return; - } + if (target == null && commandIssuer.isPlayer()) { target = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId()); } + + if (target == null) { + CommandMessages.COMMAND_MISSING_ARGUMENT.msg( + commandIssuer, + Placeholder.builder(commandIssuer.audience()) + .placeholder("%command%", "wear ") + .placeholder("%arg%", "target") + .build() + ); + return; + } SkinParts skinParts; try { File overlayFile = new File(mainPlugin.getSkinsDataFolder(), overlay + ".png"); From f862b023c2f3f97fcf3f84ebf2810e2e18133515 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 2 Jul 2026 18:26:26 +0000 Subject: [PATCH 65/79] chore(release): 8.0.0-beta.7 [skip ci] # [8.0.0-beta.7](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.6...v8.0.0-beta.7) (2026-07-02) ### Bug Fixes * add player placeholder to overlay reset success message ([0a83c6f](https://github.com/GeorgeV220/SkinOverlay/commit/0a83c6f434010e94f3b50c734239409d47f57019)) * register PlaceholderAPI hook after plugin initialization ([b19c54f](https://github.com/GeorgeV220/SkinOverlay/commit/b19c54f765a60cee6244988219623400df435872)) ### Features * add multi-database support with MongoDB, MySQL, and SQLite ([c00a51f](https://github.com/GeorgeV220/SkinOverlay/commit/c00a51f36aa9b119200d28a2d158ad2c01b457e6)) * implement SPlayer caching in PlayerProvider ([7e316b4](https://github.com/GeorgeV220/SkinOverlay/commit/7e316b43a2060cdb6a0f2ec4c9ba3a0e92a25bd6)) * integrate Quark library manager for dependency loading ([030b71a](https://github.com/GeorgeV220/SkinOverlay/commit/030b71a683c9a3c6022a2f72a012948d07d623f6)) * make Consumer parameter nullable in entity creation methods ([75d09ee](https://github.com/GeorgeV220/SkinOverlay/commit/75d09ee875a4792b0321e17719a3d235c519df1d)) * simplify target player resolution in WearSubCommand ([44d73a4](https://github.com/GeorgeV220/SkinOverlay/commit/44d73a40e62eafdbd218c9f067d3d6b5b86b8296)) --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fcfd922..92333058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# [8.0.0-beta.7](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.6...v8.0.0-beta.7) (2026-07-02) + + +### Bug Fixes + +* add player placeholder to overlay reset success message ([0a83c6f](https://github.com/GeorgeV220/SkinOverlay/commit/0a83c6f434010e94f3b50c734239409d47f57019)) +* register PlaceholderAPI hook after plugin initialization ([b19c54f](https://github.com/GeorgeV220/SkinOverlay/commit/b19c54f765a60cee6244988219623400df435872)) + + +### Features + +* add multi-database support with MongoDB, MySQL, and SQLite ([c00a51f](https://github.com/GeorgeV220/SkinOverlay/commit/c00a51f36aa9b119200d28a2d158ad2c01b457e6)) +* implement SPlayer caching in PlayerProvider ([7e316b4](https://github.com/GeorgeV220/SkinOverlay/commit/7e316b43a2060cdb6a0f2ec4c9ba3a0e92a25bd6)) +* integrate Quark library manager for dependency loading ([030b71a](https://github.com/GeorgeV220/SkinOverlay/commit/030b71a683c9a3c6022a2f72a012948d07d623f6)) +* make Consumer parameter nullable in entity creation methods ([75d09ee](https://github.com/GeorgeV220/SkinOverlay/commit/75d09ee875a4792b0321e17719a3d235c519df1d)) +* simplify target player resolution in WearSubCommand ([44d73a4](https://github.com/GeorgeV220/SkinOverlay/commit/44d73a40e62eafdbd218c9f067d3d6b5b86b8296)) + # [8.0.0-beta.6](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.5...v8.0.0-beta.6) (2026-06-19) diff --git a/README.md b/README.md index db7ea181..77549c59 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 - 8.0.0-beta.6 + 8.0.0-beta.7 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.6" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.7" } ``` diff --git a/gradle.properties b/gradle.properties index 67580b28..01b450e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.6 +version = 8.0.0-beta.7 description = SkinOverlay From 2d04c3bcd5218d045b9636ab9327d7e9ec4b40c7 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 21:44:49 +0300 Subject: [PATCH 66/79] feat: add placeholder field to skin configs and integrate with PlaceholderAPI Add a `placeholder` field to skin YAML configuration files, enabling custom display names for each skin in PlaceholderAPI responses. Update `PlaceholderAPIHook` to resolve the `%skinoverlay_overlay%` placeholder by reading the new field from the skin's config. If the skin is named "custom", it returns "Custom". Logs an error if the skin configuration file is missing and returns "Unknown" as fallback. --- .../hooks/placeholder/PlaceholderAPIHook.java | 22 ++++++++++++++++++- .../utilities/config/OverlayOptionsUtil.java | 2 ++ common/src/main/resources/skins-cfg/alley.yml | 3 ++- .../resources/skins-cfg/bubbo_transparent.yml | 3 ++- .../main/resources/skins-cfg/fire_demon.yml | 3 ++- common/src/main/resources/skins-cfg/flame.yml | 3 ++- .../main/resources/skins-cfg/frog_pijama.yml | 3 ++- common/src/main/resources/skins-cfg/glare.yml | 3 ++- .../src/main/resources/skins-cfg/hoodie.yml | 3 ++- .../src/main/resources/skins-cfg/migrator.yml | 3 ++- .../src/main/resources/skins-cfg/mustache.yml | 3 ++- .../src/main/resources/skins-cfg/pirate.yml | 3 ++- .../main/resources/skins-cfg/policeman.yml | 3 ++- .../src/main/resources/skins-cfg/smoking.yml | 3 ++- 14 files changed, 47 insertions(+), 13 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java index fc16dde8..d010bf2c 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java @@ -1,20 +1,28 @@ package com.georgev22.skinoverlay.hooks.placeholder; import com.georgev22.skinoverlay.BuildParameters; +import com.georgev22.skinoverlay.SkinOverlay; +import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.registry.EntityManagerRegistry; import com.georgev22.skinoverlay.storage.EntityManager; import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.utilities.config.OverlayOptionsUtil; +import com.georgev22.skinoverlay.utilities.config.SkinConfigurationFile; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import net.kyori.adventure.audience.Audience; +import org.bspfsystems.yamlconfiguration.file.FileConfiguration; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Optional; +import java.util.logging.Level; public class PlaceholderAPIHook extends PlaceholderExpansion implements PlaceholderHook { + private final SkinOverlay skinOverlay = SkinOverlay.getInstance(); + @Override public String resolve(Audience audience, String input) { if (audience == null) return input; @@ -66,7 +74,19 @@ public boolean canRegister() { PlayerData playerData = playerDataOptional.get(); if (params.equalsIgnoreCase("overlay")) { - return playerData.getCurrentSkin().getSkinParts().getSkinName(); + String skinName = playerData.getCurrentSkin().getSkinParts().getSkinName(); + if (skinName.equalsIgnoreCase("custom")) { + return "Custom"; + } + SkinConfigurationFile skinConfigurationFile = this.skinOverlay.getSkinFileCache().getCacheSkinConfig(skinName); + + if (skinConfigurationFile == null) { + this.skinOverlay.getLogger().log(Level.SEVERE, "SkinConfigurationFile cannot be null", new SkinException("SkinConfigurationFile cannot be null")); + return "Unknown"; + } + + FileConfiguration fileConfiguration = skinConfigurationFile.getFileConfiguration(); + return OverlayOptionsUtil.PLACEHOLDER.getStringValue(fileConfiguration); } return null; diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java index 87f86b43..a7ed5810 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OverlayOptionsUtil.java @@ -36,6 +36,8 @@ public enum OverlayOptionsUtil { PARTS_PLAYER_RIGHT_PANTS("player.right_pants", false, Optional.empty()), PARTS_PLAYER_HAT("player.hat", false, Optional.empty()), + + PLACEHOLDER("placeholder", "", Optional.empty()), ; private static final SkinOverlay mainPlugin = SkinOverlay.getInstance(); diff --git a/common/src/main/resources/skins-cfg/alley.yml b/common/src/main/resources/skins-cfg/alley.yml index f52f3d9b..2cb7aef2 100644 --- a/common/src/main/resources/skins-cfg/alley.yml +++ b/common/src/main/resources/skins-cfg/alley.yml @@ -12,4 +12,5 @@ Options: left_sleeve: true jacket: true right_pants: true - left_pants: true \ No newline at end of file + left_pants: true + placeholder: "Alley" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/bubbo_transparent.yml b/common/src/main/resources/skins-cfg/bubbo_transparent.yml index 541f7c74..5ab5a6f1 100644 --- a/common/src/main/resources/skins-cfg/bubbo_transparent.yml +++ b/common/src/main/resources/skins-cfg/bubbo_transparent.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Bubbo Transparent" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/fire_demon.yml b/common/src/main/resources/skins-cfg/fire_demon.yml index bf99dbde..660461eb 100644 --- a/common/src/main/resources/skins-cfg/fire_demon.yml +++ b/common/src/main/resources/skins-cfg/fire_demon.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Fire Demon" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/flame.yml b/common/src/main/resources/skins-cfg/flame.yml index bf99dbde..848fa473 100644 --- a/common/src/main/resources/skins-cfg/flame.yml +++ b/common/src/main/resources/skins-cfg/flame.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Flame" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/frog_pijama.yml b/common/src/main/resources/skins-cfg/frog_pijama.yml index 9b558ef8..7484637f 100644 --- a/common/src/main/resources/skins-cfg/frog_pijama.yml +++ b/common/src/main/resources/skins-cfg/frog_pijama.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Frog Pijama" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/glare.yml b/common/src/main/resources/skins-cfg/glare.yml index d22dabd7..9af662f5 100644 --- a/common/src/main/resources/skins-cfg/glare.yml +++ b/common/src/main/resources/skins-cfg/glare.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: true - left_pants: true \ No newline at end of file + left_pants: true + placeholder: "Glare" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/hoodie.yml b/common/src/main/resources/skins-cfg/hoodie.yml index bf99dbde..b9056573 100644 --- a/common/src/main/resources/skins-cfg/hoodie.yml +++ b/common/src/main/resources/skins-cfg/hoodie.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Hoodie" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/migrator.yml b/common/src/main/resources/skins-cfg/migrator.yml index ebf552c0..fced9684 100644 --- a/common/src/main/resources/skins-cfg/migrator.yml +++ b/common/src/main/resources/skins-cfg/migrator.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Migrator" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/mustache.yml b/common/src/main/resources/skins-cfg/mustache.yml index 55e95363..4f709d71 100644 --- a/common/src/main/resources/skins-cfg/mustache.yml +++ b/common/src/main/resources/skins-cfg/mustache.yml @@ -12,4 +12,5 @@ Options: left_sleeve: true jacket: true right_pants: true - left_pants: true \ No newline at end of file + left_pants: true + placeholder: "Mustache" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/pirate.yml b/common/src/main/resources/skins-cfg/pirate.yml index bf99dbde..7cab8f8d 100644 --- a/common/src/main/resources/skins-cfg/pirate.yml +++ b/common/src/main/resources/skins-cfg/pirate.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Pirate" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/policeman.yml b/common/src/main/resources/skins-cfg/policeman.yml index 5bac9999..9f37009b 100644 --- a/common/src/main/resources/skins-cfg/policeman.yml +++ b/common/src/main/resources/skins-cfg/policeman.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Policeman" \ No newline at end of file diff --git a/common/src/main/resources/skins-cfg/smoking.yml b/common/src/main/resources/skins-cfg/smoking.yml index 5bac9999..adc12565 100644 --- a/common/src/main/resources/skins-cfg/smoking.yml +++ b/common/src/main/resources/skins-cfg/smoking.yml @@ -12,4 +12,5 @@ Options: left_sleeve: false jacket: false right_pants: false - left_pants: false \ No newline at end of file + left_pants: false + placeholder: "Smoking" \ No newline at end of file From 71e990cc4b67af64dcb41776ddb7a484a2fb4012 Mon Sep 17 00:00:00 2001 From: "George V." Date: Thu, 2 Jul 2026 22:59:28 +0300 Subject: [PATCH 67/79] feat: improve placeholder API hook with configurable skin names - Replace hardcoded skin name strings with configurable options - Remove unused imports and deprecated Exception usage - Add null safety checks and new placeholder "overlay_raw" - Refactor overlay logic into dedicated helper methods --- .../hooks/placeholder/PlaceholderAPIHook.java | 66 ++++++++++++------- .../utilities/config/OptionsUtil.java | 4 ++ common/src/main/resources/config.yml | 6 ++ 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java index d010bf2c..4f6153e6 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/hooks/placeholder/PlaceholderAPIHook.java @@ -2,22 +2,20 @@ import com.georgev22.skinoverlay.BuildParameters; import com.georgev22.skinoverlay.SkinOverlay; -import com.georgev22.skinoverlay.exceptions.SkinException; import com.georgev22.skinoverlay.registry.EntityManagerRegistry; import com.georgev22.skinoverlay.storage.EntityManager; import com.georgev22.skinoverlay.storage.data.PlayerData; +import com.georgev22.skinoverlay.utilities.config.OptionsUtil; import com.georgev22.skinoverlay.utilities.config.OverlayOptionsUtil; import com.georgev22.skinoverlay.utilities.config.SkinConfigurationFile; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import net.kyori.adventure.audience.Audience; -import org.bspfsystems.yamlconfiguration.file.FileConfiguration; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.util.Optional; -import java.util.logging.Level; public class PlaceholderAPIHook extends PlaceholderExpansion implements PlaceholderHook { @@ -35,17 +33,17 @@ public String resolve(Audience audience, String input) { } @Override - public @NotNull String getIdentifier() { + public @NonNull String getIdentifier() { return BuildParameters.PLUGIN_NAME.toLowerCase(); } @Override - public @NotNull String getAuthor() { + public @NonNull String getAuthor() { return BuildParameters.AUTHOR; } @Override - public @NotNull String getVersion() { + public @NonNull String getVersion() { return BuildParameters.VERSION; } @@ -60,8 +58,8 @@ public boolean canRegister() { } @Override - public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) { - if (player == null) return null; + public @Nullable String onRequest(OfflinePlayer player, @NonNull String params) { + if (player == null || params.isEmpty()) return null; Optional> entityManagerOpt = EntityManagerRegistry.getInstance().getTyped(PlayerData.class); if (entityManagerOpt.isEmpty()) return null; @@ -73,23 +71,45 @@ public boolean canRegister() { PlayerData playerData = playerDataOptional.get(); - if (params.equalsIgnoreCase("overlay")) { - String skinName = playerData.getCurrentSkin().getSkinParts().getSkinName(); - if (skinName.equalsIgnoreCase("custom")) { - return "Custom"; - } - SkinConfigurationFile skinConfigurationFile = this.skinOverlay.getSkinFileCache().getCacheSkinConfig(skinName); + return handleParams(playerData, params); + } + + private @Nullable String handleParams(PlayerData data, @NonNull String params) { + return switch (params.toLowerCase()) { + case "overlay" -> getOverlay(data); + case "overlay_raw" -> data.getCurrentSkin().getSkinParts().getSkinName(); + default -> null; + }; + } - if (skinConfigurationFile == null) { - this.skinOverlay.getLogger().log(Level.SEVERE, "SkinConfigurationFile cannot be null", new SkinException("SkinConfigurationFile cannot be null")); - return "Unknown"; - } + private String getOverlay(@NonNull PlayerData playerData) { + String skinName = playerData.getCurrentSkin() + .getSkinParts() + .getSkinName(); - FileConfiguration fileConfiguration = skinConfigurationFile.getFileConfiguration(); - return OverlayOptionsUtil.PLACEHOLDER.getStringValue(fileConfiguration); + if (skinName == null) { + return OptionsUtil.UNKNOWN_SKIN_NAME.getStringValue(); } - return null; + if (skinName.equalsIgnoreCase("default")) { + return OptionsUtil.DEFAULT_SKIN_NAME.getStringValue(); + } + + if (skinName.equalsIgnoreCase("custom")) { + return OptionsUtil.CUSTOM_SKIN_NAME.getStringValue(); + } + + if (skinName.startsWith("customURL-")) { + return OptionsUtil.CUSTOM_SKIN_URL_NAME.getStringValue(); + } + + SkinConfigurationFile file = skinOverlay.getSkinFileCache() + .getCacheSkinConfig(skinName); + + if (file == null) { + return OptionsUtil.UNKNOWN_SKIN_NAME.getStringValue(); + } + return OverlayOptionsUtil.PLACEHOLDER.getStringValue(file.getFileConfiguration()); } } diff --git a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java index 99d15111..9ac9a2b9 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java +++ b/common/src/main/java/com/georgev22/skinoverlay/utilities/config/OptionsUtil.java @@ -54,6 +54,10 @@ public enum OptionsUtil { UPDATER("updater.enabled", true, Optional.empty()), DEFAULT_SKIN_UUID("default skin uuid", "8667ba71-b85a-4004-af54-457a9734eed7", Optional.empty()), + DEFAULT_SKIN_NAME("default skin name", "default", Optional.empty()), + CUSTOM_SKIN_NAME("custom skin name", "Custom", Optional.empty()), + CUSTOM_SKIN_URL_NAME("custom skin url name", "Custom URL", Optional.empty()), + UNKNOWN_SKIN_NAME("unknown skin name", "Unknown", Optional.empty()), SKIN_HOOK("skin hook", "SkinsRestorer", Optional.empty()), LOCALE("locale", "en_US", Optional.empty()), diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index de796440..1e96f396 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -30,7 +30,13 @@ Options: database: "skinoverlay" # MongoDB database name updater: enabled: true + default skin uuid: "8667ba71-b85a-4004-af54-457a9734eed7" + default skin name: "default" + custom skin name: "Custom" + custom skin url name: "Custom URL" + unknown skin name: "Unknown" + #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). From d395607994a24278eb863f6503d533f3ebfaf919 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 2 Jul 2026 20:05:52 +0000 Subject: [PATCH 68/79] chore(release): 8.0.0-beta.8 [skip ci] # [8.0.0-beta.8](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.7...v8.0.0-beta.8) (2026-07-02) ### Features * add placeholder field to skin configs and integrate with PlaceholderAPI ([2d04c3b](https://github.com/GeorgeV220/SkinOverlay/commit/2d04c3bcd5218d045b9636ab9327d7e9ec4b40c7)) * improve placeholder API hook with configurable skin names ([71e990c](https://github.com/GeorgeV220/SkinOverlay/commit/71e990cc4b67af64dcb41776ddb7a484a2fb4012)) --- CHANGELOG.md | 8 ++++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92333058..d39dd3e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [8.0.0-beta.8](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.7...v8.0.0-beta.8) (2026-07-02) + + +### Features + +* add placeholder field to skin configs and integrate with PlaceholderAPI ([2d04c3b](https://github.com/GeorgeV220/SkinOverlay/commit/2d04c3bcd5218d045b9636ab9327d7e9ec4b40c7)) +* improve placeholder API hook with configurable skin names ([71e990c](https://github.com/GeorgeV220/SkinOverlay/commit/71e990cc4b67af64dcb41776ddb7a484a2fb4012)) + # [8.0.0-beta.7](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.6...v8.0.0-beta.7) (2026-07-02) diff --git a/README.md b/README.md index 77549c59..a83689c5 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 - 8.0.0-beta.7 + 8.0.0-beta.8 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.7" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.8" } ``` diff --git a/gradle.properties b/gradle.properties index 01b450e9..305a791b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.7 +version = 8.0.0-beta.8 description = SkinOverlay From cb74575c68b3214b10e12b1ee3718af481aa2eb6 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 7 Jul 2026 21:10:47 +0300 Subject: [PATCH 69/79] fix: maven publish --- gradle/publish.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle/publish.gradle b/gradle/publish.gradle index db62ea4f..8ba6e7b5 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -34,8 +34,6 @@ publishing { from components.java artifactId = project.name.toLowerCase() groupId = project.group.toString().toLowerCase() - artifact(tasks.sourcesJar) - artifact(tasks.javadocJar) } } repositories { From 787f584495e7f8ad876e9d66aa82716a22fcce93 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 7 Jul 2026 22:08:08 +0300 Subject: [PATCH 70/79] fix: catch specific NoSuchMethodError instead of generic Exception --- .../com/georgev22/skinoverlay/command/BukkitCommandManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java index 5e5874f9..3ca92972 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/command/BukkitCommandManager.java @@ -22,7 +22,7 @@ protected void registerCommand0(@NotNull BaseCommand command) { CommandMap commandMap; try { commandMap = plugin.getServer().getCommandMap(); - } catch (Exception e) { + } catch (NoSuchMethodError e) { Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); commandMapField.setAccessible(true); commandMap = (CommandMap) commandMapField.get(Bukkit.getServer()); From f139e8525be5ebe2febce562e3896ea5b8026c0f Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 7 Jul 2026 22:27:21 +0300 Subject: [PATCH 71/79] refactor: rename `world` variable to `serverLevel` in skin refreshers Consistently rename the local variable `world` to `serverLevel` across all version-specific skin refresher implementations. This change also replaces direct access to `entityPlayer.server` or `entityPlayer.gameMode.level` with the more reliable `serverLevel.getServer().getPlayerList()` to obtain the server's player list. --- .../refreshers/SkinRefresher_1_17_R1.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_18_R1.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_18_R2.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_19_R1.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_19_R2.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_19_R3.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_20_R1.java | 16 ++++++++-------- .../refreshers/SkinRefresher_1_20_R2.java | 8 ++++---- .../refreshers/SkinRefresher_1_20_R3.java | 8 ++++---- .../refreshers/SkinRefresher_1_20_R4.java | 8 ++++---- .../refreshers/SkinRefresher_1_21_R1.java | 8 ++++---- .../refreshers/SkinRefresher_1_21_R2.java | 8 ++++---- .../refreshers/SkinRefresher_1_21_R3.java | 8 ++++---- .../refreshers/SkinRefresher_1_21_R4.java | 8 ++++---- .../refreshers/SkinRefresher_1_21_R5.java | 8 ++++---- .../refreshers/SkinRefresher_1_21_R6.java | 8 ++++---- 16 files changed, 92 insertions(+), 92 deletions(-) diff --git a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java index 50df9534..53e0641c 100644 --- a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java +++ b/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java @@ -54,17 +54,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.getLevel(); + ServerLevel serverLevel = entityPlayer.getLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionType(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionType(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), true); sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, List.of(entityPlayer))); @@ -78,9 +78,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java index bdc860f4..18d8cd66 100644 --- a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java +++ b/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java @@ -54,17 +54,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.getLevel(); + ServerLevel serverLevel = entityPlayer.getLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionType(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionType(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), true); sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, List.of(entityPlayer))); @@ -78,9 +78,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java b/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java index 8954baba..e9fa4ee6 100644 --- a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java +++ b/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java @@ -54,17 +54,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.getLevel(); + ServerLevel serverLevel = entityPlayer.getLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeRegistration(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionTypeRegistration(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), true); sendPacket(entityPlayer, new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.REMOVE_PLAYER, @@ -80,9 +80,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java b/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java index 588d927f..dbbd5a8c 100644 --- a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java +++ b/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java @@ -54,17 +54,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.getLevel(); + ServerLevel serverLevel = entityPlayer.getLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionTypeId(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), true, entityPlayer.getLastDeathLocation() ); @@ -82,9 +82,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java b/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java index 057660c7..ec9f4842 100644 --- a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java +++ b/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java @@ -55,17 +55,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.getLevel(); + ServerLevel serverLevel = entityPlayer.getLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionTypeId(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), ClientboundRespawnPacket.KEEP_ALL_DATA, entityPlayer.getLastDeathLocation() ); @@ -81,9 +81,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java b/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java index f8c7479f..f5c64868 100644 --- a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java +++ b/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java @@ -55,17 +55,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.getLevel(); + ServerLevel serverLevel = entityPlayer.getLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionTypeId(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), ClientboundRespawnPacket.KEEP_ALL_DATA, entityPlayer.getLastDeathLocation() ); @@ -81,9 +81,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java index 37304cf8..dc573626 100644 --- a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java +++ b/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java @@ -55,17 +55,17 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); ServerPlayerGameMode gameMode = entityPlayer.gameMode; ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( - world.dimensionTypeId(), - world.dimension(), - BiomeManager.obfuscateSeed(world.getSeed()), + serverLevel.dimensionTypeId(), + serverLevel.dimension(), + BiomeManager.obfuscateSeed(serverLevel.getSeed()), gameMode.getGameModeForPlayer(), gameMode.getPreviousGameModeForPlayer(), - world.isDebug(), - world.isFlat(), + serverLevel.isDebug(), + serverLevel.isFlat(), ClientboundRespawnPacket.KEEP_ALL_DATA, entityPlayer.getLastDeathLocation(), entityPlayer.getPortalCooldown() @@ -82,9 +82,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java b/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java index f435a9ef..125f7ead 100644 --- a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java +++ b/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java @@ -49,9 +49,9 @@ public void refresh(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -68,9 +68,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java b/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java index 0bfe35a5..8749c3c9 100644 --- a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java +++ b/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java b/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java index 0470f158..adbab6b8 100644 --- a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java +++ b/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance mobEffect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java b/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java index 5fa284e8..9dc24367 100644 --- a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java +++ b/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java index a65c7e3a..d5265648 100644 --- a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java +++ b/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java index 707740bc..0560e2ad 100644 --- a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java +++ b/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java index 0768ecd9..54b1ddb2 100644 --- a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java +++ b/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.serverLevel(); + ServerLevel serverLevel = entityPlayer.serverLevel(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.server.getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java b/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java index 3e4b1d2d..9e8c4c4f 100644 --- a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java +++ b/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java @@ -50,9 +50,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.gameMode.level; + ServerLevel serverLevel = entityPlayer.level(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -69,9 +69,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.gameMode.level.getServer().getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { diff --git a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java b/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java index 692431d8..408ae26f 100644 --- a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java +++ b/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java @@ -51,9 +51,9 @@ public void refresh(@NotNull SPlayer player) { final CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; final ServerPlayer entityPlayer = craftPlayer.getHandle(); - ServerLevel world = entityPlayer.gameMode.level; + ServerLevel serverLevel = entityPlayer.level(); - CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(world); + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); ClientboundRespawnPacket respawn = new ClientboundRespawnPacket( spawnInfo, ClientboundRespawnPacket.KEEP_ALL_DATA @@ -76,9 +76,9 @@ public void refresh(@NotNull SPlayer player) { entityPlayer.resetSentInfo(); - PlayerList playerList = entityPlayer.gameMode.level.getServer().getPlayerList(); + PlayerList playerList = serverLevel.getServer().getPlayerList(); playerList.sendPlayerPermissionLevel(entityPlayer); - playerList.sendLevelInfo(entityPlayer, world); + playerList.sendLevelInfo(entityPlayer, serverLevel); playerList.sendAllPlayerInfo(entityPlayer); for (MobEffectInstance effect : entityPlayer.getActiveEffects()) { From eb3fc330ea0a8fa02873442f18be3cd0dea19387 Mon Sep 17 00:00:00 2001 From: "George V." Date: Tue, 7 Jul 2026 23:31:22 +0300 Subject: [PATCH 72/79] fix: use original property names when updating GameProfile properties Update the 1_21_R6 GameProfile provider to preserve and reuse the original property name when rebuilding the internal PropertyMap. The provider was incorrectly creating properties without passing the original name, causing invalid mappings because the property value was used as the name. This only affected the 1.21.10 implementation where the property name handling was missing. Use the existing property key from the GameProfile instead of hardcoding or reconstructing property names, ensuring properties such as `textures` are transferred correctly across versions. --- .../providers/BukkitLegacyGameProfileProvider.java | 7 ++++--- .../providers/PaperGameProfileProvider.java | 5 +++-- .../providers/GameProfileProvider_1_17_R1.java | 7 ++++--- .../providers/GameProfileProvider_1_18_R1.java | 7 ++++--- .../providers/GameProfileProvider_1_18_R2.java | 7 ++++--- .../providers/GameProfileProvider_1_19_R1.java | 7 ++++--- .../providers/GameProfileProvider_1_19_R2.java | 7 ++++--- .../providers/GameProfileProvider_1_19_R3.java | 7 ++++--- .../providers/GameProfileProvider_1_20_R1.java | 7 ++++--- .../providers/GameProfileProvider_1_20_R2.java | 7 ++++--- .../providers/GameProfileProvider_1_20_R3.java | 7 ++++--- .../providers/GameProfileProvider_1_20_R4.java | 7 ++++--- .../providers/GameProfileProvider_1_21_R1.java | 7 ++++--- .../providers/GameProfileProvider_1_21_R2.java | 7 ++++--- .../providers/GameProfileProvider_1_21_R3.java | 7 ++++--- .../providers/GameProfileProvider_1_21_R4.java | 7 ++++--- .../providers/GameProfileProvider_1_21_R5.java | 7 ++++--- .../providers/GameProfileProvider_1_21_R6.java | 7 ++++--- .../skinoverlay/hooks/SkinsRestorerHook.java | 2 +- .../providers/GameProfileProvider_SkinsRestorer.java | 3 ++- .../georgev22/skinoverlay/providers/SkinProvider.java | 11 ++++++++--- .../com/georgev22/skinoverlay/skin/SProperty.java | 2 +- .../storage/gson/SPropertyTypeAdapter.java | 4 +++- .../providers/VelocityGameProfileProvider.java | 6 +++--- 24 files changed, 89 insertions(+), 63 deletions(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java index 0f0868e7..18f5c1f7 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/BukkitLegacyGameProfileProvider.java @@ -48,7 +48,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -58,7 +59,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java index c6f2b257..6946e1d3 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/providers/PaperGameProfileProvider.java @@ -24,7 +24,7 @@ public SGameProfile getGameProfile(@NotNull SPlayer 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()) + profileProperty.getName(), new SProperty(profileProperty.getName(), profileProperty.getValue(), profileProperty.getSignature()) )); return sGameProfiles.append(player, gameProfile).get(player); } @@ -36,7 +36,8 @@ public void applyUpdatedGameProfile(@NotNull SPlayer player) { 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()))); + .forEach((name, property) -> playerProfile.getProperties() + .add(new ProfileProperty(name, property.value(), property.signature()))); bukkitPlayer.setPlayerProfile(playerProfile); } } 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 index c438eabb..8a27733c 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 94ea19e9..52b78d30 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 38805043..ad539bee 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 33d78ab3..a87b5bc3 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 26b79873..65ea073a 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index bd6e6d66..36f91338 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index c5122ad2..65f7d0eb 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.getValue(), property.getSignature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 038a1962..243a3bba 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 4a88d466..4b70673b 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index fe36740e..1def3ea0 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 429a4ecc..508676f8 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 530f1f4c..ae896e78 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 48570f5b..c8c808f4 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index f04e8a16..d2056b1e 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index f5facc3c..c9d846ee 100644 --- 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 @@ -28,7 +28,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.getProperties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.getProperties().forEach((s, property) -> propertyObjectMap + .append(s, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -38,7 +39,7 @@ 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()))); + gameProfile.getProperties().forEach((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); } } 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 index 5370a897..29687d9c 100644 --- 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 @@ -33,7 +33,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { } ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); - gameProfile.properties().forEach((s, property) -> propertyObjectMap.append(s, new SProperty(property.value(), property.signature()))); + gameProfile.properties().forEach((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), property.value(), property.signature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.name(), gameProfile.id(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -43,9 +44,9 @@ 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(); + ImmutableMultimap.Builder newProperties = ImmutableMultimap.builder(); for (Map.Entry entry : gameProfile.getProperties().entrySet()) { - newProperties.put(entry.getKey(), new Property(entry.getValue().value(), entry.getValue().signature())); + newProperties.put(entry.getKey(), new Property(entry.getKey(), entry.getValue().value(), entry.getValue().signature())); } RStream.of(properties) diff --git a/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java index d0229b6b..71c67970 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java +++ b/common/src/main/java/com/georgev22/skinoverlay/hooks/SkinsRestorerHook.java @@ -55,7 +55,7 @@ public SProperty getProperty(@NotNull SPlayer player) { } catch (DataRequestException e) { return null; } - return property.map(skinProperty -> new SProperty(skinProperty.getValue(), skinProperty.getSignature())).orElse(null); + return property.map(skinProperty -> new SProperty("textures", skinProperty.getValue(), skinProperty.getSignature())).orElse(null); } } 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 index 48c99bb0..a653569c 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_SkinsRestorer.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_SkinsRestorer.java @@ -28,7 +28,8 @@ public SGameProfile getInternalGameProfile(@NotNull SPlayer player) { 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)); + property.map(skinProperty -> new SProperty("textures", skinProperty.getValue(), skinProperty.getSignature())) + .ifPresent(sProperty -> gameProfile.setProperty("textures", sProperty)); return gameProfile; } diff --git a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java index 81fb8986..c5c1b93c 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java +++ b/common/src/main/java/com/georgev22/skinoverlay/providers/SkinProvider.java @@ -205,9 +205,10 @@ public CompletableFuture> retrieveOrGenerateSkin(@NotNull SPlayer }) .thenCompose(jobReference -> jobReference.getOrLoadSkin(mineskinClient)) .thenApply(skinInfo -> { + String name = "textures"; String value = skinInfo.texture().data().value(); String signature = skinInfo.texture().data().signature(); - return new SProperty(value, signature); + return new SProperty(name, value, signature); }).join(); Optional skinOptional = skinManager.create(skinUUID, skin -> { @@ -463,7 +464,11 @@ public SProperty getJavaSkin(final SPlayer player) throws IOException { 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()); + property = new SProperty( + object.getAsJsonObject().get("name").getAsString(), + object.getAsJsonObject().get("value").getAsString(), + object.getAsJsonObject().get("signature").getAsString() + ); } } return property; @@ -479,7 +484,7 @@ public SProperty getJavaSkin(final SPlayer player) throws IOException { 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()); + return new SProperty("textures", json.getAsJsonObject().get("value").getAsString(), json.getAsJsonObject().get("signature").getAsString()); } /** diff --git a/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java b/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java index edb5de40..9dc8b9e6 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java +++ b/common/src/main/java/com/georgev22/skinoverlay/skin/SProperty.java @@ -1,5 +1,5 @@ package com.georgev22.skinoverlay.skin; -public record SProperty(String value, String signature) { +public record SProperty(String name, String value, String signature) { } diff --git a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java index a9b87de9..77a4ac26 100644 --- a/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java +++ b/common/src/main/java/com/georgev22/skinoverlay/storage/gson/SPropertyTypeAdapter.java @@ -29,10 +29,11 @@ public class SPropertyTypeAdapter implements public @NotNull SProperty deserialize(@NotNull JsonElement jsonElement, Type typeOfT, JsonDeserializationContext jsonDeserializationContext) { JsonObject jsonObject = jsonElement.getAsJsonObject(); + String name = jsonObject.has("name") ? jsonObject.get("name").getAsString() : "textures"; String value = jsonObject.get("value").getAsString(); String signature = jsonObject.get("signature").getAsString(); - return new SProperty(value, signature); + return new SProperty(name, value, signature); } /** @@ -46,6 +47,7 @@ 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; diff --git a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java index 1ada9656..e906bb2e 100644 --- a/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java +++ b/velocity/src/main/java/com/georgev22/skinoverlay/providers/VelocityGameProfileProvider.java @@ -27,7 +27,7 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { ObjectMap propertyObjectMap = new HashObjectMap<>(); GameProfile gameProfile = this.getInternalGameProfile(player); gameProfile.getProperties().forEach(property -> - propertyObjectMap.append(property.getName(), new SProperty(property.getValue(), property.getSignature()))); + propertyObjectMap.append(property.getName(), new SProperty(property.getName(), property.getValue(), property.getSignature()))); SGameProfile sGameProfile = new SGameProfile(gameProfile.getName(), gameProfile.getId(), propertyObjectMap); return sGameProfiles.append(player, sGameProfile).get(player); } @@ -36,8 +36,8 @@ public SGameProfile getGameProfile(@NotNull SPlayer player) { 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()))); + getGameProfile(player).getProperties().forEach((name, property) -> propertyList.add( + new GameProfile.Property(property.name(), property.value(), property.signature()))); velocityPlayer.setGameProfileProperties(propertyList); } } From 08d64abd03fc4e92af7ba246db0f26c87f74460a Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 8 Jul 2026 14:50:06 +0300 Subject: [PATCH 73/79] refactor: rename version-specific classes and modules to remove 'R' suffix Remove the 'R' suffix from version-specific module names, class names, and registration code to align with the actual Minecraft version numbers rather than internal mappings. This includes renaming all bukkit version modules (e.g., mc1_20_R4 -> mc1_20_5), their corresponding Java classes, and the version registration in VersionRegistry. Additionally, remove the Reflect library dependency and replace its usage with direct reflection in the 1.21.9 GameProfileProvider. --- bukkit/build.gradle.kts | 34 +++++----- .../utilities/VersionRegistry.java | 64 +++++++++---------- .../{mc1_17_R1 => mc1_17}/build.gradle.kts | 0 .../providers/GameProfileProvider_1_17.java} | 2 +- .../refreshers/SkinRefresher_1_17.java} | 2 +- .../{mc1_18_R1 => mc1_18}/build.gradle.kts | 0 .../providers/GameProfileProvider_1_18.java} | 2 +- .../refreshers/SkinRefresher_1_18.java} | 2 +- .../{mc1_18_R2 => mc1_18_2}/build.gradle.kts | 0 .../GameProfileProvider_1_18_2.java} | 2 +- .../refreshers/SkinRefresher_1_18_2.java} | 2 +- .../{mc1_19_R1 => mc1_19}/build.gradle.kts | 0 .../providers/GameProfileProvider_1_19.java} | 2 +- .../refreshers/SkinRefresher_1_19.java} | 2 +- .../{mc1_19_R2 => mc1_19_3}/build.gradle.kts | 0 .../GameProfileProvider_1_19_3.java} | 2 +- .../refreshers/SkinRefresher_1_19_3.java} | 2 +- .../{mc1_19_R3 => mc1_19_4}/build.gradle.kts | 0 .../GameProfileProvider_1_19_4.java} | 2 +- .../refreshers/SkinRefresher_1_19_4.java} | 2 +- .../{mc1_20_R1 => mc1_20}/build.gradle.kts | 0 .../providers/GameProfileProvider_1_20.java} | 2 +- .../refreshers/SkinRefresher_1_20.java} | 2 +- .../{mc1_20_R2 => mc1_20_2}/build.gradle.kts | 0 .../GameProfileProvider_1_20_2.java} | 2 +- .../refreshers/SkinRefresher_1_20_2.java} | 2 +- .../{mc1_20_R3 => mc1_20_3}/build.gradle.kts | 0 .../GameProfileProvider_1_20_3.java} | 2 +- .../refreshers/SkinRefresher_1_20_3.java} | 2 +- .../{mc1_20_R4 => mc1_20_5}/build.gradle.kts | 0 .../GameProfileProvider_1_20_5.java} | 2 +- .../refreshers/SkinRefresher_1_20_5.java} | 2 +- .../{mc1_21_R1 => mc1_21}/build.gradle.kts | 0 .../providers/GameProfileProvider_1_21.java} | 2 +- .../refreshers/SkinRefresher_1_21.java} | 2 +- .../{mc1_21_R2 => mc1_21_2}/build.gradle.kts | 0 .../GameProfileProvider_1_21_2.java} | 2 +- .../refreshers/SkinRefresher_1_21_2.java} | 2 +- .../{mc1_21_R3 => mc1_21_4}/build.gradle.kts | 0 .../GameProfileProvider_1_21_4.java} | 2 +- .../refreshers/SkinRefresher_1_21_4.java} | 2 +- .../{mc1_21_R4 => mc1_21_5}/build.gradle.kts | 0 .../providers/GameProfileProvider_1_21_5.java | 45 +++++++++++++ .../refreshers/SkinRefresher_1_21_5.java} | 2 +- .../{mc1_21_R5 => mc1_21_6}/build.gradle.kts | 0 .../GameProfileProvider_1_21_6.java} | 2 +- .../refreshers/SkinRefresher_1_21_6.java} | 2 +- .../{mc1_21_R6 => mc1_21_9}/build.gradle.kts | 1 - .../GameProfileProvider_1_21_9.java} | 27 ++++++-- .../refreshers/SkinRefresher_1_21_9.java} | 2 +- .../GameProfileProvider_1_21_R4.java | 45 ------------- gradle/libs.versions.toml | 2 - settings.gradle.kts | 32 +++++----- 53 files changed, 159 insertions(+), 151 deletions(-) rename bukkit/versions/{mc1_17_R1 => mc1_17}/build.gradle.kts (100%) rename bukkit/versions/{mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java => mc1_17/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17.java} (96%) rename bukkit/versions/{mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java => mc1_17/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17.java} (98%) rename bukkit/versions/{mc1_18_R1 => mc1_18}/build.gradle.kts (100%) rename bukkit/versions/{mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java => mc1_18/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18.java} (96%) rename bukkit/versions/{mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java => mc1_18/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18.java} (98%) rename bukkit/versions/{mc1_18_R2 => mc1_18_2}/build.gradle.kts (100%) rename bukkit/versions/{mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java => mc1_18_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_2.java} (96%) rename bukkit/versions/{mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java => mc1_18_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_2.java} (98%) rename bukkit/versions/{mc1_19_R1 => mc1_19}/build.gradle.kts (100%) rename bukkit/versions/{mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java => mc1_19/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19.java} (96%) rename bukkit/versions/{mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java => mc1_19/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19.java} (98%) rename bukkit/versions/{mc1_19_R2 => mc1_19_3}/build.gradle.kts (100%) rename bukkit/versions/{mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java => mc1_19_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_3.java} (96%) rename bukkit/versions/{mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java => mc1_19_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_3.java} (98%) rename bukkit/versions/{mc1_19_R3 => mc1_19_4}/build.gradle.kts (100%) rename bukkit/versions/{mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java => mc1_19_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_4.java} (96%) rename bukkit/versions/{mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java => mc1_19_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_4.java} (98%) rename bukkit/versions/{mc1_20_R1 => mc1_20}/build.gradle.kts (100%) rename bukkit/versions/{mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java => mc1_20/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20.java} (96%) rename bukkit/versions/{mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java => mc1_20/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20.java} (98%) rename bukkit/versions/{mc1_20_R2 => mc1_20_2}/build.gradle.kts (100%) rename bukkit/versions/{mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java => mc1_20_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_2.java} (96%) rename bukkit/versions/{mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java => mc1_20_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_2.java} (98%) rename bukkit/versions/{mc1_20_R3 => mc1_20_3}/build.gradle.kts (100%) rename bukkit/versions/{mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java => mc1_20_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_3.java} (96%) rename bukkit/versions/{mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java => mc1_20_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_3.java} (98%) rename bukkit/versions/{mc1_20_R4 => mc1_20_5}/build.gradle.kts (100%) rename bukkit/versions/{mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java => mc1_20_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_5.java} (96%) rename bukkit/versions/{mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java => mc1_20_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_5.java} (98%) rename bukkit/versions/{mc1_21_R1 => mc1_21}/build.gradle.kts (100%) rename bukkit/versions/{mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java => mc1_21/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21.java} (96%) rename bukkit/versions/{mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java => mc1_21/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21.java} (98%) rename bukkit/versions/{mc1_21_R2 => mc1_21_2}/build.gradle.kts (100%) rename bukkit/versions/{mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java => mc1_21_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_2.java} (96%) rename bukkit/versions/{mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java => mc1_21_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_2.java} (98%) rename bukkit/versions/{mc1_21_R3 => mc1_21_4}/build.gradle.kts (100%) rename bukkit/versions/{mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java => mc1_21_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_4.java} (96%) rename bukkit/versions/{mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java => mc1_21_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_4.java} (98%) rename bukkit/versions/{mc1_21_R4 => mc1_21_5}/build.gradle.kts (100%) create mode 100644 bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_5.java rename bukkit/versions/{mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java => mc1_21_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_5.java} (98%) rename bukkit/versions/{mc1_21_R5 => mc1_21_6}/build.gradle.kts (100%) rename bukkit/versions/{mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java => mc1_21_6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_6.java} (96%) rename bukkit/versions/{mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java => mc1_21_6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_6.java} (98%) rename bukkit/versions/{mc1_21_R6 => mc1_21_9}/build.gradle.kts (93%) rename bukkit/versions/{mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java => mc1_21_9/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_9.java} (79%) rename bukkit/versions/{mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java => mc1_21_9/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_9.java} (98%) delete mode 100644 bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R4.java diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 232f7665..fa8b728d 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { quark(libs.adventure.platform.bukkit) quark(libs.adventure.text.minimessage) quark(libs.adventure.text.serializer.legacy) - quark(libs.reflect) quark(libs.hikari) { exclude(group = "org.slf4j", module = "slf4j-api") @@ -50,22 +49,22 @@ dependencies { quark(libs.mongodb.driver.sync) implementation(project(":common")) - 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")) + implementation(project(":bukkit:versions:mc1_17", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_18", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_18_2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_19", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_19_3", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_19_4", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_3", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_20_5", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_2", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_4", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_5", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_6", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_9", configuration = "reobf")) } quark { @@ -87,7 +86,6 @@ quark { 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") relocate("org.bson", "${project.property("packageName")}.lib.bson") relocate("com.mongodb", "${project.property("packageName")}.lib.mongodb") } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java index 2ff0fe35..cb02f38c 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java @@ -23,113 +23,113 @@ private VersionRegistry() { new VersionRange( new MinecraftVersion(1, 17, 0), new MinecraftVersion(1, 17, 1), - GameProfileProvider_1_17_R1::new, - SkinRefresher_1_17_R1::new + GameProfileProvider_1_17::new, + SkinRefresher_1_17::new ), new VersionRange( new MinecraftVersion(1, 18, 0), new MinecraftVersion(1, 18, 1), - GameProfileProvider_1_18_R1::new, - SkinRefresher_1_18_R1::new + GameProfileProvider_1_18::new, + SkinRefresher_1_18::new ), new VersionRange( new MinecraftVersion(1, 18, 2), new MinecraftVersion(1, 18, 2), - GameProfileProvider_1_18_R2::new, - SkinRefresher_1_18_R2::new + GameProfileProvider_1_18_2::new, + SkinRefresher_1_18_2::new ), new VersionRange( new MinecraftVersion(1, 19, 0), new MinecraftVersion(1, 19, 2), - GameProfileProvider_1_19_R1::new, - SkinRefresher_1_19_R1::new + GameProfileProvider_1_19::new, + SkinRefresher_1_19::new ), new VersionRange( new MinecraftVersion(1, 19, 3), new MinecraftVersion(1, 19, 3), - GameProfileProvider_1_19_R2::new, - SkinRefresher_1_19_R2::new + GameProfileProvider_1_19_3::new, + SkinRefresher_1_19_3::new ), new VersionRange( new MinecraftVersion(1, 19, 4), new MinecraftVersion(1, 19, 4), - GameProfileProvider_1_19_R3::new, - SkinRefresher_1_19_R3::new + GameProfileProvider_1_19_4::new, + SkinRefresher_1_19_4::new ), new VersionRange( new MinecraftVersion(1, 20, 0), new MinecraftVersion(1, 20, 1), - GameProfileProvider_1_20_R1::new, - SkinRefresher_1_20_R1::new + GameProfileProvider_1_20::new, + SkinRefresher_1_20::new ), new VersionRange( new MinecraftVersion(1, 20, 2), new MinecraftVersion(1, 20, 2), - GameProfileProvider_1_20_R2::new, - SkinRefresher_1_20_R2::new + GameProfileProvider_1_20_2::new, + SkinRefresher_1_20_2::new ), new VersionRange( new MinecraftVersion(1, 20, 3), new MinecraftVersion(1, 20, 4), - GameProfileProvider_1_20_R3::new, - SkinRefresher_1_20_R3::new + GameProfileProvider_1_20_3::new, + SkinRefresher_1_20_3::new ), new VersionRange( new MinecraftVersion(1, 20, 5), new MinecraftVersion(1, 20, 6), - GameProfileProvider_1_20_R4::new, - SkinRefresher_1_20_R4::new + GameProfileProvider_1_20_5::new, + SkinRefresher_1_20_5::new ), new VersionRange( new MinecraftVersion(1, 21, 0), new MinecraftVersion(1, 21, 1), - GameProfileProvider_1_21_R1::new, - SkinRefresher_1_21_R1::new + GameProfileProvider_1_21::new, + SkinRefresher_1_21::new ), new VersionRange( new MinecraftVersion(1, 21, 2), new MinecraftVersion(1, 21, 3), - GameProfileProvider_1_21_R2::new, - SkinRefresher_1_21_R2::new + GameProfileProvider_1_21_2::new, + SkinRefresher_1_21_2::new ), new VersionRange( new MinecraftVersion(1, 21, 4), new MinecraftVersion(1, 21, 4), - GameProfileProvider_1_21_R3::new, - SkinRefresher_1_21_R3::new + GameProfileProvider_1_21_4::new, + SkinRefresher_1_21_4::new ), new VersionRange( new MinecraftVersion(1, 21, 5), new MinecraftVersion(1, 21, 5), - GameProfileProvider_1_21_R4::new, - SkinRefresher_1_21_R4::new + GameProfileProvider_1_21_5::new, + SkinRefresher_1_21_5::new ), new VersionRange( new MinecraftVersion(1, 21, 6), new MinecraftVersion(1, 21, 8), - GameProfileProvider_1_21_R5::new, - SkinRefresher_1_21_R5::new + GameProfileProvider_1_21_6::new, + SkinRefresher_1_21_6::new ), new VersionRange( new MinecraftVersion(1, 21, 9), new MinecraftVersion(1, 21, 10), - GameProfileProvider_1_21_R6::new, - SkinRefresher_1_21_R6::new + GameProfileProvider_1_21_9::new, + SkinRefresher_1_21_9::new ) ); } \ No newline at end of file diff --git a/bukkit/versions/mc1_17_R1/build.gradle.kts b/bukkit/versions/mc1_17/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_17_R1/build.gradle.kts rename to bukkit/versions/mc1_17/build.gradle.kts 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/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17.java similarity index 96% rename from bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java rename to bukkit/versions/mc1_17/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17.java index 8a27733c..0ec43d81 100644 --- a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17_R1.java +++ b/bukkit/versions/mc1_17/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_17.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_17_R1 extends GameProfileProvider { +public class GameProfileProvider_1_17 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java b/bukkit/versions/mc1_17/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17.java similarity index 98% rename from bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java rename to bukkit/versions/mc1_17/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17.java index 53e0641c..8fc4f5b0 100644 --- a/bukkit/versions/mc1_17_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17_R1.java +++ b/bukkit/versions/mc1_17/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_17.java @@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_17_R1 extends SkinRefresher { +public class SkinRefresher_1_17 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_18_R1/build.gradle.kts b/bukkit/versions/mc1_18/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_18_R1/build.gradle.kts rename to bukkit/versions/mc1_18/build.gradle.kts 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/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18.java similarity index 96% rename from bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java rename to bukkit/versions/mc1_18/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18.java index 52b78d30..44ef5d57 100644 --- a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R1.java +++ b/bukkit/versions/mc1_18/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_18_R1 extends GameProfileProvider { +public class GameProfileProvider_1_18 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java b/bukkit/versions/mc1_18/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18.java similarity index 98% rename from bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java rename to bukkit/versions/mc1_18/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18.java index 18d8cd66..ea468b99 100644 --- a/bukkit/versions/mc1_18_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R1.java +++ b/bukkit/versions/mc1_18/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18.java @@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_18_R1 extends SkinRefresher { +public class SkinRefresher_1_18 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_18_R2/build.gradle.kts b/bukkit/versions/mc1_18_2/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_18_R2/build.gradle.kts rename to bukkit/versions/mc1_18_2/build.gradle.kts 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_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_2.java similarity index 96% rename from bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java rename to bukkit/versions/mc1_18_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_2.java index ad539bee..dc25109c 100644 --- a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_R2.java +++ b/bukkit/versions/mc1_18_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_18_2.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_18_R2 extends GameProfileProvider { +public class GameProfileProvider_1_18_2 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java b/bukkit/versions/mc1_18_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_2.java similarity index 98% rename from bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java rename to bukkit/versions/mc1_18_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_2.java index e9fa4ee6..c5728f34 100644 --- a/bukkit/versions/mc1_18_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_R2.java +++ b/bukkit/versions/mc1_18_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_18_2.java @@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_18_R2 extends SkinRefresher { +public class SkinRefresher_1_18_2 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_19_R1/build.gradle.kts b/bukkit/versions/mc1_19/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_19_R1/build.gradle.kts rename to bukkit/versions/mc1_19/build.gradle.kts 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/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19.java similarity index 96% rename from bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java rename to bukkit/versions/mc1_19/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19.java index a87b5bc3..02b75806 100644 --- a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R1.java +++ b/bukkit/versions/mc1_19/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_19_R1 extends GameProfileProvider { +public class GameProfileProvider_1_19 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java b/bukkit/versions/mc1_19/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19.java similarity index 98% rename from bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java rename to bukkit/versions/mc1_19/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19.java index dbbd5a8c..e84c2fe5 100644 --- a/bukkit/versions/mc1_19_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R1.java +++ b/bukkit/versions/mc1_19/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19.java @@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_19_R1 extends SkinRefresher { +public class SkinRefresher_1_19 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_19_R2/build.gradle.kts b/bukkit/versions/mc1_19_3/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_19_R2/build.gradle.kts rename to bukkit/versions/mc1_19_3/build.gradle.kts 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_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_3.java similarity index 96% rename from bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java rename to bukkit/versions/mc1_19_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_3.java index 65ea073a..bb8f4d3e 100644 --- a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R2.java +++ b/bukkit/versions/mc1_19_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_3.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_19_R2 extends GameProfileProvider { +public class GameProfileProvider_1_19_3 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java b/bukkit/versions/mc1_19_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_3.java similarity index 98% rename from bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java rename to bukkit/versions/mc1_19_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_3.java index ec9f4842..43612ab2 100644 --- a/bukkit/versions/mc1_19_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R2.java +++ b/bukkit/versions/mc1_19_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_3.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_19_R2 extends SkinRefresher { +public class SkinRefresher_1_19_3 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_19_R3/build.gradle.kts b/bukkit/versions/mc1_19_4/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_19_R3/build.gradle.kts rename to bukkit/versions/mc1_19_4/build.gradle.kts 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_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_4.java similarity index 96% rename from bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java rename to bukkit/versions/mc1_19_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_4.java index 36f91338..3c916da5 100644 --- a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_R3.java +++ b/bukkit/versions/mc1_19_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_19_4.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_19_R3 extends GameProfileProvider { +public class GameProfileProvider_1_19_4 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java b/bukkit/versions/mc1_19_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_4.java similarity index 98% rename from bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java rename to bukkit/versions/mc1_19_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_4.java index f5c64868..f724152b 100644 --- a/bukkit/versions/mc1_19_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_R3.java +++ b/bukkit/versions/mc1_19_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_19_4.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_19_R3 extends SkinRefresher { +public class SkinRefresher_1_19_4 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_20_R1/build.gradle.kts b/bukkit/versions/mc1_20/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_20_R1/build.gradle.kts rename to bukkit/versions/mc1_20/build.gradle.kts 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/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20.java similarity index 96% rename from bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java rename to bukkit/versions/mc1_20/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20.java index 65f7d0eb..4071dd56 100644 --- a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R1.java +++ b/bukkit/versions/mc1_20/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_20_R1 extends GameProfileProvider { +public class GameProfileProvider_1_20 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java b/bukkit/versions/mc1_20/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20.java similarity index 98% rename from bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java rename to bukkit/versions/mc1_20/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20.java index dc573626..365d0fbd 100644 --- a/bukkit/versions/mc1_20_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R1.java +++ b/bukkit/versions/mc1_20/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_20_R1 extends SkinRefresher { +public class SkinRefresher_1_20 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_20_R2/build.gradle.kts b/bukkit/versions/mc1_20_2/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_20_R2/build.gradle.kts rename to bukkit/versions/mc1_20_2/build.gradle.kts 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_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_2.java similarity index 96% rename from bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java rename to bukkit/versions/mc1_20_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_2.java index 243a3bba..250f73b0 100644 --- a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R2.java +++ b/bukkit/versions/mc1_20_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_2.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_20_R2 extends GameProfileProvider { +public class GameProfileProvider_1_20_2 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java b/bukkit/versions/mc1_20_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_2.java similarity index 98% rename from bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java rename to bukkit/versions/mc1_20_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_2.java index 125f7ead..199c3bac 100644 --- a/bukkit/versions/mc1_20_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R2.java +++ b/bukkit/versions/mc1_20_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_2.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_20_R2 extends SkinRefresher { +public class SkinRefresher_1_20_2 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_20_R3/build.gradle.kts b/bukkit/versions/mc1_20_3/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_20_R3/build.gradle.kts rename to bukkit/versions/mc1_20_3/build.gradle.kts 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_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_3.java similarity index 96% rename from bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java rename to bukkit/versions/mc1_20_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_3.java index 4b70673b..b4bbf320 100644 --- a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R3.java +++ b/bukkit/versions/mc1_20_3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_3.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_20_R3 extends GameProfileProvider { +public class GameProfileProvider_1_20_3 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java b/bukkit/versions/mc1_20_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_3.java similarity index 98% rename from bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java rename to bukkit/versions/mc1_20_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_3.java index 8749c3c9..3a5e8787 100644 --- a/bukkit/versions/mc1_20_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R3.java +++ b/bukkit/versions/mc1_20_3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_3.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_20_R3 extends SkinRefresher { +public class SkinRefresher_1_20_3 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_20_R4/build.gradle.kts b/bukkit/versions/mc1_20_5/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_20_R4/build.gradle.kts rename to bukkit/versions/mc1_20_5/build.gradle.kts diff --git a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java b/bukkit/versions/mc1_20_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_5.java similarity index 96% rename from bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java rename to bukkit/versions/mc1_20_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_5.java index ae896e78..b1ad4141 100644 --- a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R2.java +++ b/bukkit/versions/mc1_20_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_5.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_21_R2 extends GameProfileProvider { +public class GameProfileProvider_1_20_5 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java b/bukkit/versions/mc1_20_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_5.java similarity index 98% rename from bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java rename to bukkit/versions/mc1_20_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_5.java index adbab6b8..e4f728a3 100644 --- a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_R4.java +++ b/bukkit/versions/mc1_20_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_20_5.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_20_R4 extends SkinRefresher { +public class SkinRefresher_1_20_5 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_21_R1/build.gradle.kts b/bukkit/versions/mc1_21/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_21_R1/build.gradle.kts rename to bukkit/versions/mc1_21/build.gradle.kts 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/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21.java similarity index 96% rename from bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java rename to bukkit/versions/mc1_21/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21.java index c8c808f4..1a45151f 100644 --- a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R3.java +++ b/bukkit/versions/mc1_21/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_21_R3 extends GameProfileProvider { +public class GameProfileProvider_1_21 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java b/bukkit/versions/mc1_21/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21.java similarity index 98% rename from bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java rename to bukkit/versions/mc1_21/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21.java index 54b1ddb2..94b5b922 100644 --- a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R4.java +++ b/bukkit/versions/mc1_21/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_21_R4 extends SkinRefresher { +public class SkinRefresher_1_21 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_21_R2/build.gradle.kts b/bukkit/versions/mc1_21_2/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_21_R2/build.gradle.kts rename to bukkit/versions/mc1_21_2/build.gradle.kts diff --git a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java b/bukkit/versions/mc1_21_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_2.java similarity index 96% rename from bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java rename to bukkit/versions/mc1_21_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_2.java index 1def3ea0..f4907416 100644 --- a/bukkit/versions/mc1_20_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_20_R4.java +++ b/bukkit/versions/mc1_21_2/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_2.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_20_R4 extends GameProfileProvider { +public class GameProfileProvider_1_21_2 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java b/bukkit/versions/mc1_21_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_2.java similarity index 98% rename from bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java rename to bukkit/versions/mc1_21_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_2.java index 0560e2ad..11d213ee 100644 --- a/bukkit/versions/mc1_21_R3/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R3.java +++ b/bukkit/versions/mc1_21_2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_2.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_21_R3 extends SkinRefresher { +public class SkinRefresher_1_21_2 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_21_R3/build.gradle.kts b/bukkit/versions/mc1_21_4/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_21_R3/build.gradle.kts rename to bukkit/versions/mc1_21_4/build.gradle.kts 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_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_4.java similarity index 96% rename from bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java rename to bukkit/versions/mc1_21_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_4.java index 508676f8..babb1fcd 100644 --- a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R1.java +++ b/bukkit/versions/mc1_21_4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_4.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_21_R1 extends GameProfileProvider { +public class GameProfileProvider_1_21_4 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java b/bukkit/versions/mc1_21_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_4.java similarity index 98% rename from bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java rename to bukkit/versions/mc1_21_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_4.java index 9dc24367..2b2bb553 100644 --- a/bukkit/versions/mc1_21_R1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R1.java +++ b/bukkit/versions/mc1_21_4/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_4.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_21_R1 extends SkinRefresher { +public class SkinRefresher_1_21_4 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_21_R4/build.gradle.kts b/bukkit/versions/mc1_21_5/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_21_R4/build.gradle.kts rename to bukkit/versions/mc1_21_5/build.gradle.kts diff --git a/bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_5.java b/bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_5.java new file mode 100644 index 00000000..51df9b19 --- /dev/null +++ b/bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_5.java @@ -0,0 +1,45 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import 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_5 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((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), 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((name, property) -> internalGameProfile.getProperties() + .put(name, new Property(property.name(), property.value(), property.signature()))); + } +} diff --git a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java b/bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_5.java similarity index 98% rename from bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java rename to bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_5.java index d5265648..9fc929a2 100644 --- a/bukkit/versions/mc1_21_R2/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R2.java +++ b/bukkit/versions/mc1_21_5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_5.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_21_R2 extends SkinRefresher { +public class SkinRefresher_1_21_5 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_21_R5/build.gradle.kts b/bukkit/versions/mc1_21_6/build.gradle.kts similarity index 100% rename from bukkit/versions/mc1_21_R5/build.gradle.kts rename to bukkit/versions/mc1_21_6/build.gradle.kts 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_6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_6.java similarity index 96% rename from bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java rename to bukkit/versions/mc1_21_6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_6.java index c9d846ee..ff8bb55d 100644 --- a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R5.java +++ b/bukkit/versions/mc1_21_6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_6.java @@ -12,7 +12,7 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public class GameProfileProvider_1_21_R5 extends GameProfileProvider { +public class GameProfileProvider_1_21_6 extends GameProfileProvider { @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java b/bukkit/versions/mc1_21_6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_6.java similarity index 98% rename from bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java rename to bukkit/versions/mc1_21_6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_6.java index 9e8c4c4f..5382619d 100644 --- a/bukkit/versions/mc1_21_R5/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R5.java +++ b/bukkit/versions/mc1_21_6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_6.java @@ -16,7 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_21_R5 extends SkinRefresher { +public class SkinRefresher_1_21_6 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { diff --git a/bukkit/versions/mc1_21_R6/build.gradle.kts b/bukkit/versions/mc1_21_9/build.gradle.kts similarity index 93% rename from bukkit/versions/mc1_21_R6/build.gradle.kts rename to bukkit/versions/mc1_21_9/build.gradle.kts index 4cb1ce07..7a525e8d 100644 --- a/bukkit/versions/mc1_21_R6/build.gradle.kts +++ b/bukkit/versions/mc1_21_9/build.gradle.kts @@ -6,7 +6,6 @@ dependencies { paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") compileOnly(project(":common")) compileOnly(libs.auth.lib.modern) - compileOnly(libs.reflect) } tasks.reobfJar { 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_9/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_9.java similarity index 79% rename from bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java rename to bukkit/versions/mc1_21_9/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_9.java index 29687d9c..375cd161 100644 --- a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R6.java +++ b/bukkit/versions/mc1_21_9/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_9.java @@ -9,15 +9,28 @@ 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.lang.reflect.Field; import java.util.Map; -public class GameProfileProvider_1_21_R6 extends GameProfileProvider { +public class GameProfileProvider_1_21_9 extends GameProfileProvider { + + private static final Field PROPERTIES_FIELD; + + static { + try { + PROPERTIES_FIELD = PropertyMap.class.getDeclaredField("properties"); + PROPERTIES_FIELD.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); @@ -49,10 +62,10 @@ public void applyUpdatedGameProfile(@NotNull SPlayer player) { newProperties.put(entry.getKey(), new Property(entry.getKey(), entry.getValue().value(), entry.getValue().signature())); } - RStream.of(properties) - .withSuper() - .fields() - .by("properties") - .set(newProperties.build()); + try { + PROPERTIES_FIELD.set(properties, newProperties.build()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } } } diff --git a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java b/bukkit/versions/mc1_21_9/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_9.java similarity index 98% rename from bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java rename to bukkit/versions/mc1_21_9/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_9.java index 408ae26f..0326289e 100644 --- a/bukkit/versions/mc1_21_R6/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_R6.java +++ b/bukkit/versions/mc1_21_9/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_9.java @@ -17,7 +17,7 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -public class SkinRefresher_1_21_R6 extends SkinRefresher { +public class SkinRefresher_1_21_9 extends SkinRefresher { @Override public void refresh(@NotNull SPlayer player) { 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 deleted file mode 100644 index d2056b1e..00000000 --- a/bukkit/versions/mc1_21_R4/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_R4.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.georgev22.skinoverlay.providers; - -import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; -import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; -import com.georgev22.skinoverlay.player.SPlayer; -import com.georgev22.skinoverlay.skin.SGameProfile; -import com.georgev22.skinoverlay.skin.SProperty; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; -import 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((name, property) -> propertyObjectMap - .append(name, new SProperty(property.name(), 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((name, property) -> internalGameProfile.getProperties() - .put(name, new Property(property.name(), property.value(), property.signature()))); - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52ff4ff4..bde0fd00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,6 @@ jedis = "6.0.0" jetbrains-annotations = "26.0.2" jspecify = "1.0.0" log4j-api = "3.0.0-beta2" -reflect = "1.5.0" mongodb-driver-sync = "5.2.1" [plugins] @@ -49,5 +48,4 @@ jedis = { module = "redis.clients:jedis", version.ref = "jedis" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j-api" } -reflect = { module = "net.lenni0451:Reflect", version.ref = "reflect" } mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongodb-driver-sync" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a8c93935..b7e4ebd7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,20 +12,20 @@ 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("bukkit:versions:mc1_17") +include("bukkit:versions:mc1_18") +include("bukkit:versions:mc1_18_2") +include("bukkit:versions:mc1_19") +include("bukkit:versions:mc1_19_3") +include("bukkit:versions:mc1_19_4") +include("bukkit:versions:mc1_20") +include("bukkit:versions:mc1_20_2") +include("bukkit:versions:mc1_20_3") +include("bukkit:versions:mc1_20_5") +include("bukkit:versions:mc1_21") +include("bukkit:versions:mc1_21_2") +include("bukkit:versions:mc1_21_4") +include("bukkit:versions:mc1_21_5") +include("bukkit:versions:mc1_21_6") +include("bukkit:versions:mc1_21_9") include("velocity") \ No newline at end of file From f8e4a33aab07035592ec7107498cab9f0d5b5697 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 8 Jul 2026 15:48:36 +0300 Subject: [PATCH 74/79] feat: add Minecraft 1.21.11 version support and refactor VersionRange - Add GameProfileProvider_1_21_11 and SkinRefresher_1_21_11 for Minecraft 1.21.11 support. - Refactor VersionRange to use a single-version constructor and add documentation. - Update VersionRegistry to include the new version range and fix existing range definitions. - Upgrade Gradle to 9.6.1, Paperweight to 2.0.0-beta.21, and Blossom to 2.2.0. - Fix build-info description fallback and update gradlew scripts. --- build-info/build.gradle.kts | 2 +- bukkit/build.gradle.kts | 1 + .../skinoverlay/utilities/VersionRange.java | 35 +++++++ .../utilities/VersionRegistry.java | 12 +-- bukkit/versions/mc1_21_11/build.gradle.kts | 19 ++++ .../GameProfileProvider_1_21_11.java | 71 +++++++++++++ .../refreshers/SkinRefresher_1_21_11.java | 97 ++++++++++++++++++ gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 11 +- gradlew.bat | 36 +++---- settings.gradle.kts | 1 + 13 files changed, 252 insertions(+), 41 deletions(-) create mode 100644 bukkit/versions/mc1_21_11/build.gradle.kts create mode 100644 bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java create mode 100644 bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_11.java diff --git a/build-info/build.gradle.kts b/build-info/build.gradle.kts index cdd1a1df..7aefe288 100644 --- a/build-info/build.gradle.kts +++ b/build-info/build.gradle.kts @@ -15,7 +15,7 @@ sourceSets { property("pluginId", project.property("pluginName").toString().lowercase()) property("pluginName", project.property("pluginName").toString()) property("version", version.toString()) - property("description", rootProject.description) + property("description", rootProject.description ?: "unknown") property("author", project.property("author").toString()) property("url", "https://github.com/GeorgeV220/SkinOverlay") property("commit", indraGit.commit()?.name ?: "unknown") diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index fa8b728d..a7bf7eab 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { implementation(project(":bukkit:versions:mc1_21_5", configuration = "reobf")) implementation(project(":bukkit:versions:mc1_21_6", configuration = "reobf")) implementation(project(":bukkit:versions:mc1_21_9", configuration = "reobf")) + implementation(project(":bukkit:versions:mc1_21_11", configuration = "reobf")) } quark { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java index 7ba01efd..0301f680 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRange.java @@ -7,12 +7,47 @@ import java.util.function.Supplier; +/** + * Represents a supported Minecraft version range and the components that should + * be used for that range. + * + *

A {@code VersionRange} defines the minimum and maximum Minecraft versions + * supported by a specific {@link GameProfileProvider} and {@link SkinRefresher}. + * The providers are created lazily using {@link Supplier} instances, allowing + * implementations to be initialized only when required.

+ * + * @param min the minimum supported Minecraft version (inclusive) + * @param max the maximum supported Minecraft version (inclusive) + * @param provider supplier that creates the {@link GameProfileProvider} for this range + * @param refresher supplier that creates the {@link SkinRefresher} for this range + */ public record VersionRange( MinecraftVersion min, MinecraftVersion max, Supplier provider, Supplier refresher ) { + + /** + * Creates a version range that only matches a single Minecraft version. + * + * @param version the Minecraft version supported by this range + * @param provider supplier that creates the {@link GameProfileProvider} for this version + * @param refresher supplier that creates the {@link SkinRefresher} for this version + */ + public VersionRange(MinecraftVersion version, Supplier provider, Supplier refresher) { + this(version, version, provider, refresher); + } + + /** + * Checks whether the specified Minecraft version is contained within this range. + * + *

The comparison is inclusive, meaning that versions equal to {@link #min()} + * or {@link #max()} will match.

+ * + * @param v the Minecraft version to check + * @return {@code true} if the version is within this range, otherwise {@code false} + */ public boolean matches(@NonNull MinecraftVersion v) { return v.compareTo(min) >= 0 && v.compareTo(max) <= 0; } diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java index cb02f38c..eccc4fcc 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java @@ -35,7 +35,6 @@ private VersionRegistry() { ), new VersionRange( - new MinecraftVersion(1, 18, 2), new MinecraftVersion(1, 18, 2), GameProfileProvider_1_18_2::new, SkinRefresher_1_18_2::new @@ -49,14 +48,12 @@ private VersionRegistry() { ), new VersionRange( - new MinecraftVersion(1, 19, 3), new MinecraftVersion(1, 19, 3), GameProfileProvider_1_19_3::new, SkinRefresher_1_19_3::new ), new VersionRange( - new MinecraftVersion(1, 19, 4), new MinecraftVersion(1, 19, 4), GameProfileProvider_1_19_4::new, SkinRefresher_1_19_4::new @@ -70,7 +67,6 @@ private VersionRegistry() { ), new VersionRange( - new MinecraftVersion(1, 20, 2), new MinecraftVersion(1, 20, 2), GameProfileProvider_1_20_2::new, SkinRefresher_1_20_2::new @@ -105,14 +101,12 @@ private VersionRegistry() { ), new VersionRange( - new MinecraftVersion(1, 21, 4), new MinecraftVersion(1, 21, 4), GameProfileProvider_1_21_4::new, SkinRefresher_1_21_4::new ), new VersionRange( - new MinecraftVersion(1, 21, 5), new MinecraftVersion(1, 21, 5), GameProfileProvider_1_21_5::new, SkinRefresher_1_21_5::new @@ -130,6 +124,12 @@ private VersionRegistry() { new MinecraftVersion(1, 21, 10), GameProfileProvider_1_21_9::new, SkinRefresher_1_21_9::new + ), + + new VersionRange( + new MinecraftVersion(1, 21, 11), + GameProfileProvider_1_21_11::new, + SkinRefresher_1_21_11::new ) ); } \ No newline at end of file diff --git a/bukkit/versions/mc1_21_11/build.gradle.kts b/bukkit/versions/mc1_21_11/build.gradle.kts new file mode 100644 index 00000000..47b9ec08 --- /dev/null +++ b/bukkit/versions/mc1_21_11/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +dependencies { + paperweight.paperDevBundle("1.21.11-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_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java b/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java new file mode 100644 index 00000000..115cf8fd --- /dev/null +++ b/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java @@ -0,0 +1,71 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.google.common.collect.ImmutableMultimap; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Field; +import java.util.Map; + +public class GameProfileProvider_1_21_11 extends GameProfileProvider { + + private static final Field PROPERTIES_FIELD; + + static { + try { + PROPERTIES_FIELD = PropertyMap.class.getDeclaredField("properties"); + PROPERTIES_FIELD.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + + @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((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), 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.getKey(), entry.getValue().value(), entry.getValue().signature())); + } + + try { + PROPERTIES_FIELD.set(properties, newProperties.build()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_11.java b/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_11.java new file mode 100644 index 00000000..3d09cbd2 --- /dev/null +++ b/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_1_21_11.java @@ -0,0 +1,97 @@ +package com.georgev22.skinoverlay.refreshers; + +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 SkinRefresher_1_21_11 extends SkinRefresher { + + @Override + public void refresh(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + this.sendPackets(player).handleAsync((result, throwable) -> { + if (throwable != null) { + skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); + return false; + } + return result; + }).thenAccept(result -> this.skinOverlay.getScheduler().runTask(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 serverLevel = entityPlayer.level(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); + 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 = serverLevel.getServer().getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, serverLevel); + 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index bde0fd00..308fd403 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ auth-lib-modern = "6.0.57" auth-lib-legacy = "3.11.50" gradleup-shadow = "9.4.1" hikari = "4.0.3" -paperweight = "2.0.0-beta.17" +paperweight = "2.0.0-beta.21" gson = "2.13.1" placeholder-api = "2.11.6" mineskinclient = "3.2.6" @@ -22,7 +22,7 @@ mongodb-driver-sync = "5.2.1" [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" } +blossom = { id = "net.kyori.blossom", version = "2.2.0" } [libraries] bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55baabb587c669f562ae36f953de2481846..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 40183 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 delta 35506 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 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH= -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -: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 +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index b7e4ebd7..9f43406b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,4 +28,5 @@ include("bukkit:versions:mc1_21_4") include("bukkit:versions:mc1_21_5") include("bukkit:versions:mc1_21_6") include("bukkit:versions:mc1_21_9") +include("bukkit:versions:mc1_21_11") include("velocity") \ No newline at end of file From 86bf989b8bf1a71dcac8085d843942681b267be9 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 8 Jul 2026 17:36:25 +0300 Subject: [PATCH 75/79] feat: add support for Minecraft 26.1 and 26.2 versions --- bukkit/build.gradle.kts | 1 + .../utilities/VersionRegistry.java | 7 ++ .../GameProfileProvider_1_21_11.java | 1 - bukkit/versions/mc26_1/build.gradle.kts | 25 +++++ .../providers/GameProfileProvider_26_1.java | 70 +++++++++++++ .../refreshers/SkinRefresher_26_1.java | 97 +++++++++++++++++++ settings.gradle.kts | 1 + 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 bukkit/versions/mc26_1/build.gradle.kts create mode 100644 bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_26_1.java create mode 100644 bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_26_1.java diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index a7bf7eab..f7ebfc0f 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation(project(":bukkit:versions:mc1_21_6", configuration = "reobf")) implementation(project(":bukkit:versions:mc1_21_9", configuration = "reobf")) implementation(project(":bukkit:versions:mc1_21_11", configuration = "reobf")) + implementation(project(":bukkit:versions:mc26_1")) } quark { diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java index eccc4fcc..45dd6846 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/VersionRegistry.java @@ -130,6 +130,13 @@ private VersionRegistry() { new MinecraftVersion(1, 21, 11), GameProfileProvider_1_21_11::new, SkinRefresher_1_21_11::new + ), + + new VersionRange( + new MinecraftVersion(26, 1, 0), + new MinecraftVersion(26, 2, 99), + GameProfileProvider_26_1::new, + SkinRefresher_26_1::new ) ); } \ No newline at end of file diff --git a/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java b/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java index 115cf8fd..269f014b 100644 --- a/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java +++ b/bukkit/versions/mc1_21_11/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_1_21_11.java @@ -30,7 +30,6 @@ public class GameProfileProvider_1_21_11 extends GameProfileProvider { } } - @Override public @NotNull GameProfile getInternalGameProfile(@NotNull SPlayer player) { Player bukkitPlayer = player.getPlayer(); diff --git a/bukkit/versions/mc26_1/build.gradle.kts b/bukkit/versions/mc26_1/build.gradle.kts new file mode 100644 index 00000000..15d8ddfd --- /dev/null +++ b/bukkit/versions/mc26_1/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.paperweight.userdev) +} + +java.disableAutoTargetJvm() + +dependencies { + paperweight.paperDevBundle("26.1.2.build.+") + compileOnly(project(":common")) + compileOnly(libs.auth.lib.modern) +} + +tasks.named("reobfJar") { + enabled = false +} + +java { + toolchain.languageVersion = JavaLanguageVersion.of(25) +} + +tasks { + withType().configureEach { + options.release = 21 + } +} \ No newline at end of file diff --git a/bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_26_1.java b/bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_26_1.java new file mode 100644 index 00000000..3d68feac --- /dev/null +++ b/bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/providers/GameProfileProvider_26_1.java @@ -0,0 +1,70 @@ +package com.georgev22.skinoverlay.providers; + +import com.georgev22.skinoverlay.datastructures.maps.HashObjectMap; +import com.georgev22.skinoverlay.datastructures.maps.ObjectMap; +import com.georgev22.skinoverlay.player.SPlayer; +import com.georgev22.skinoverlay.skin.SGameProfile; +import com.georgev22.skinoverlay.skin.SProperty; +import com.google.common.collect.ImmutableMultimap; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Field; +import java.util.Map; + +public class GameProfileProvider_26_1 extends GameProfileProvider { + + private static final Field PROPERTIES_FIELD; + + static { + try { + PROPERTIES_FIELD = PropertyMap.class.getDeclaredField("properties"); + PROPERTIES_FIELD.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + @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((name, property) -> propertyObjectMap + .append(name, new SProperty(property.name(), 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.getKey(), entry.getValue().value(), entry.getValue().signature())); + } + + try { + PROPERTIES_FIELD.set(properties, newProperties.build()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_26_1.java b/bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_26_1.java new file mode 100644 index 00000000..a965e2b4 --- /dev/null +++ b/bukkit/versions/mc26_1/src/main/java/com/georgev22/skinoverlay/refreshers/SkinRefresher_26_1.java @@ -0,0 +1,97 @@ +package com.georgev22.skinoverlay.refreshers; + +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 SkinRefresher_26_1 extends SkinRefresher { + + @Override + public void refresh(@NotNull SPlayer player) { + this.skinOverlay.getScheduler().createDelayedTask(skinOverlay.getPlugin(), () -> { + Player bukkitPlayer = player.getPlayer(); + bukkitPlayer.hidePlayer(skinOverlay.getPlugin(), bukkitPlayer); + bukkitPlayer.showPlayer(skinOverlay.getPlugin(), bukkitPlayer); + this.sendPackets(player).handleAsync((result, throwable) -> { + if (throwable != null) { + skinOverlay.getLogger().log(Level.SEVERE, "Error updating skin", throwable); + return false; + } + return result; + }).thenAccept(result -> this.skinOverlay.getScheduler().runTask(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 serverLevel = entityPlayer.level(); + + CommonPlayerSpawnInfo spawnInfo = entityPlayer.createCommonSpawnInfo(serverLevel); + 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 = serverLevel.getServer().getPlayerList(); + playerList.sendPlayerPermissionLevel(entityPlayer); + playerList.sendLevelInfo(entityPlayer, serverLevel); + 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/settings.gradle.kts b/settings.gradle.kts index 9f43406b..47c73db4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,5 @@ include("bukkit:versions:mc1_21_5") include("bukkit:versions:mc1_21_6") include("bukkit:versions:mc1_21_9") include("bukkit:versions:mc1_21_11") +include("bukkit:versions:mc26_1") include("velocity") \ No newline at end of file From eb740153162f3938bf02ceb2441d9aee17cd99fd Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 8 Jul 2026 17:51:09 +0300 Subject: [PATCH 76/79] chore: update GitHub Actions to JDK 21/25 and latest action versions Update GitHub Actions workflow to use the latest versions of actions/setup-java (v5), actions/cache (v6), actions/checkout (v7), actions/upload-artifact (v7), and actions/setup-node (v6). Support both JDK 21 and JDK 25 in the setup-java step, replacing the previous single JDK 21 configuration. --- .github/workflows/gradle.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index a292fa85..00bfe7b2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,20 +10,22 @@ jobs: steps: - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK + uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '21' + java-version: | + 21 + 25 - name: Cache Gradle id: cache-gradle - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.gradle key: ${{ runner.os }}-gradle - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Grant execute permission to gradlew run: chmod +x gradlew @@ -37,7 +39,7 @@ jobs: MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: path: "**/build/libs" @@ -48,25 +50,27 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK + uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '21' + java-version: | + 21 + 25 - name: Set up Node.js v20.x - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "20.x" - name: Cache Gradle id: cache-gradle - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.gradle key: ${{ runner.os }}-gradle - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Grant execute permission for gradlew run: chmod +x gradlew From 391b3ffbfcf4a187aaa2a114e69b3fcf40fc73c3 Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 8 Jul 2026 17:51:19 +0300 Subject: [PATCH 77/79] docs: improve Javadoc clarity for version comparison in BukkitMinecraftUtils The Javadoc for the `isBelow` method now explicitly uses `{@code <}` to indicate the less-than comparison, making the documentation clearer and more formal. --- .../georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java index 3483b3d7..5c9e6168 100644 --- a/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java +++ b/bukkit/src/main/java/com/georgev22/skinoverlay/utilities/BukkitMinecraftUtils.java @@ -790,7 +790,7 @@ public boolean isAtLeast(int major, int minor, int patch) { * * @param major the major version * @param minor the minor version - * @return {@code true} if this version is < given version + * @return {@code true} if this version is {@code <} given version */ public boolean isBelow(int major, int minor) { return isBelow(major, minor, 0); From d3909933f28532de28a10438072590e3ecd0e09f Mon Sep 17 00:00:00 2001 From: "George V." Date: Wed, 8 Jul 2026 17:56:01 +0300 Subject: [PATCH 78/79] chore: reorder Java versions in CI workflow --- .github/workflows/gradle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 00bfe7b2..7e601786 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -15,8 +15,8 @@ jobs: with: distribution: 'temurin' java-version: | - 21 25 + 21 - name: Cache Gradle id: cache-gradle @@ -55,8 +55,8 @@ jobs: with: distribution: 'temurin' java-version: | - 21 25 + 21 - name: Set up Node.js v20.x uses: actions/setup-node@v6 From a0a5df61caee57306e2bffd9686bf630dbd0e615 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 8 Jul 2026 18:05:32 +0000 Subject: [PATCH 79/79] chore(release): 8.0.0-beta.9 [skip ci] # [8.0.0-beta.9](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.8...v8.0.0-beta.9) (2026-07-08) ### Bug Fixes * catch specific NoSuchMethodError instead of generic Exception ([787f584](https://github.com/GeorgeV220/SkinOverlay/commit/787f584495e7f8ad876e9d66aa82716a22fcce93)) * maven publish ([cb74575](https://github.com/GeorgeV220/SkinOverlay/commit/cb74575c68b3214b10e12b1ee3718af481aa2eb6)) * use original property names when updating GameProfile properties ([eb3fc33](https://github.com/GeorgeV220/SkinOverlay/commit/eb3fc330ea0a8fa02873442f18be3cd0dea19387)) ### Features * add Minecraft 1.21.11 version support and refactor VersionRange ([f8e4a33](https://github.com/GeorgeV220/SkinOverlay/commit/f8e4a33aab07035592ec7107498cab9f0d5b5697)) * add support for Minecraft 26.1 and 26.2 versions ([86bf989](https://github.com/GeorgeV220/SkinOverlay/commit/86bf989b8bf1a71dcac8085d843942681b267be9)) --- CHANGELOG.md | 15 +++++++++++++++ README.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d39dd3e7..c0eb28f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# [8.0.0-beta.9](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.8...v8.0.0-beta.9) (2026-07-08) + + +### Bug Fixes + +* catch specific NoSuchMethodError instead of generic Exception ([787f584](https://github.com/GeorgeV220/SkinOverlay/commit/787f584495e7f8ad876e9d66aa82716a22fcce93)) +* maven publish ([cb74575](https://github.com/GeorgeV220/SkinOverlay/commit/cb74575c68b3214b10e12b1ee3718af481aa2eb6)) +* use original property names when updating GameProfile properties ([eb3fc33](https://github.com/GeorgeV220/SkinOverlay/commit/eb3fc330ea0a8fa02873442f18be3cd0dea19387)) + + +### Features + +* add Minecraft 1.21.11 version support and refactor VersionRange ([f8e4a33](https://github.com/GeorgeV220/SkinOverlay/commit/f8e4a33aab07035592ec7107498cab9f0d5b5697)) +* add support for Minecraft 26.1 and 26.2 versions ([86bf989](https://github.com/GeorgeV220/SkinOverlay/commit/86bf989b8bf1a71dcac8085d843942681b267be9)) + # [8.0.0-beta.8](https://github.com/GeorgeV220/SkinOverlay/compare/v8.0.0-beta.7...v8.0.0-beta.8) (2026-07-02) diff --git a/README.md b/README.md index a83689c5..4d7a6f82 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 - 8.0.0-beta.8 + 8.0.0-beta.9 provided @@ -45,7 +45,7 @@ repositories { } dependencies { - compileOnly "com.georgev22:skinoverlay:8.0.0-beta.8" + compileOnly "com.georgev22:skinoverlay:8.0.0-beta.9" } ``` diff --git a/gradle.properties b/gradle.properties index 305a791b..40c0b1ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,5 +5,5 @@ bukkitMain = com.georgev22.skinoverlay.SkinOverlayBukkit bungeeMain = com.georgev22.skinoverlay.SkinOverlayBungee pluginName = SkinOverlay author = GeorgeV22 -version = 8.0.0-beta.8 +version = 8.0.0-beta.9 description = SkinOverlay