diff --git a/pom.xml b/pom.xml index 2c1cb4e..b931547 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ CustomItemsLib jar - 1.1.0-SNAPSHOT + 1.1.0-JL.10 Rocologo's CustomItems Library https://github.com/Rocologo/CustomItemsLib CustomItemsLib is a library with code shared between MobHunting and BagOfGold. @@ -24,7 +24,7 @@ - 4.5.6-SNAPSHOT + 4.5.6-JL.2 8.5.5-SNAPSHOT diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 720dbfa..cd49fa3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,7 +7,7 @@ author: Rocologo depend: [] loadbefore: [BagOfGold, MobHunting] softdepend: [Reserve, Vault, PerWorldInventory, Citizens, Essentials, ProtocolLib, PacketListenerApi, ItemFrameShops, Multiverse-Core, TitleManager, CMI, CMILib, BossBarAPI, BarAPI, ActionBar, TitleAPI, TitleManager, ActionBarAPI, ActionAnnouncer] -api-version: 1.13 +api-version: 1.21 commands: customitemslib: diff --git a/src/one/lindegaard/CustomItemsLib/Core.java b/src/one/lindegaard/CustomItemsLib/Core.java index cf87b03..1324d13 100644 --- a/src/one/lindegaard/CustomItemsLib/Core.java +++ b/src/one/lindegaard/CustomItemsLib/Core.java @@ -32,7 +32,9 @@ import one.lindegaard.CustomItemsLib.config.ConfigManager; import one.lindegaard.CustomItemsLib.messages.Messages; import one.lindegaard.CustomItemsLib.rewards.CoreRewardManager; +import one.lindegaard.CustomItemsLib.rewards.RewardSecurity; import one.lindegaard.CustomItemsLib.rewards.RewardBlockManager; +import one.lindegaard.CustomItemsLib.rewards.TokenSpendStore; import one.lindegaard.CustomItemsLib.storage.DataStoreException; import one.lindegaard.CustomItemsLib.storage.DataStoreManager; import one.lindegaard.CustomItemsLib.storage.IDataStore; @@ -54,6 +56,8 @@ public class Core extends JavaPlugin { private static DataStoreManager mDataStoreManager; private static PlayerSettingsManager mPlayerSettingsManager; private static CoreRewardManager mCoreRewardManager; + private static RewardSecurity mRewardSecurity; + private static TokenSpendStore mTokenSpendStore; private static CompatibilityManager mCompatibilityManager; private CommandDispatcher mCommandDispatcher; private SpigetUpdater mSpigetUpdater; @@ -117,6 +121,14 @@ public void onEnable() { mMessages.setLanguage(mConfig.language + ".lang"); mMessages.debug("Loading config.yml file, version %s", config_version); + mRewardSecurity = new RewardSecurity(this); + try { + mRewardSecurity.initialize(); + } catch (IOException ex) { + Bukkit.getConsoleSender().sendMessage(PREFIX_ERROR + "Could not initialize security.yml: " + ex.getMessage()); + throw new RuntimeException("[CustomItemsLib] Could not initialize security.yml", ex); + } + List itemtypes = Arrays.asList("SKULL", "ITEM", "KILLER", "KILLED", "GRINGOTTS_STYLE"); if (!itemtypes.contains(mConfig.rewardItemtype)) { Bukkit.getConsoleSender().sendMessage(PREFIX + ChatColor.RED @@ -153,6 +165,14 @@ public void onEnable() { return; } + mTokenSpendStore = new TokenSpendStore(this); + try { + mTokenSpendStore.initialize(); + } catch (Exception ex) { + Bukkit.getConsoleSender().sendMessage(PREFIX_ERROR + "Could not initialize token spend store: " + ex.getMessage()); + throw new RuntimeException("[CustomItemsLib] Could not initialize token spend store", ex); + } + mDataStoreManager = new DataStoreManager(plugin, mStore); mPlayerSettingsManager = new PlayerSettingsManager(plugin); mCoreRewardManager = new CoreRewardManager(plugin); @@ -175,6 +195,7 @@ public void onEnable() { // Hook into Vault or Reserve mEconomyManager = new EconomyManager(this); + Bukkit.getPluginManager().registerEvents(new EconomyProviderListener(), this); if (!mEconomyManager.isActive()) return; @@ -238,6 +259,14 @@ public static CoreRewardManager getCoreRewardManager() { return mCoreRewardManager; } + public static RewardSecurity getRewardSecurity() { + return mRewardSecurity; + } + + public static TokenSpendStore getTokenSpendStore() { + return mTokenSpendStore; + } + public SpigetUpdater getSpigetUpdater() { return mSpigetUpdater; } diff --git a/src/one/lindegaard/CustomItemsLib/EconomyManager.java b/src/one/lindegaard/CustomItemsLib/EconomyManager.java index 6869922..4250a96 100644 --- a/src/one/lindegaard/CustomItemsLib/EconomyManager.java +++ b/src/one/lindegaard/CustomItemsLib/EconomyManager.java @@ -1,6 +1,7 @@ package one.lindegaard.CustomItemsLib; import java.math.BigDecimal; +import java.util.Collection; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; @@ -68,6 +69,27 @@ private void setVersion(String version) { this.version = version; } + private RegisteredServiceProvider getPreferredVaultProvider() { + Collection> providers = plugin.getServer().getServicesManager() + .getRegistrations(Economy.class); + + RegisteredServiceProvider bagOfGoldProvider = null; + for (RegisteredServiceProvider provider : providers) { + if (provider == null || provider.getProvider() == null) + continue; + if ("BagOfGold".equalsIgnoreCase(provider.getProvider().getName())) { + if (bagOfGoldProvider == null + || provider.getPriority().compareTo(bagOfGoldProvider.getPriority()) > 0) + bagOfGoldProvider = provider; + } + } + + if (bagOfGoldProvider != null) + return bagOfGoldProvider; + + return plugin.getServer().getServicesManager().getRegistration(Economy.class); + } + /** * Find and configure a suitable economy provider * @@ -75,22 +97,25 @@ private void setVersion(String version) { */ public Boolean setupEconomy() { Plugin economyProvider = null; + EcoType previousType = Type; + String previousVersion = getVersion(); /* * Attempt to find Vault for Economy handling */ try { - RegisteredServiceProvider vaultEcoProvider = plugin.getServer().getServicesManager() - .getRegistration(net.milkbowl.vault.economy.Economy.class); + RegisteredServiceProvider vaultEcoProvider = getPreferredVaultProvider(); if (vaultEcoProvider != null) { /* * Flag as using Vault hooks */ vaultEconomy = vaultEcoProvider.getProvider(); + reserveEconomy = null; setVersion(String.format("%s %s", vaultEcoProvider.getProvider().getName(), "via Vault")); - Bukkit.getConsoleSender().sendMessage( - Core.PREFIX + "CustomItemsLib is using " + getVersion() + " as Economy Provider"); Type = EcoType.VAULT; + if (previousType != Type || !previousVersion.equals(getVersion())) + Bukkit.getConsoleSender() + .sendMessage(Core.PREFIX + "CustomItemsLib is using " + getVersion() + " as Economy Provider"); return true; } } catch (NoClassDefFoundError ex) { @@ -105,12 +130,18 @@ public Boolean setupEconomy() { * Flat as using Reserve Hooks. */ reserveEconomy = ((Reserve) economyProvider).economy(); + vaultEconomy = null; setVersion(String.format("%s %s", reserveEconomy.name(), "via Reserve")); - Bukkit.getConsoleSender() - .sendMessage(Core.PREFIX + "CustomItemsLib is using " + getVersion() + " as Economy Provider"); Type = EcoType.RESERVE; + if (previousType != Type || !previousVersion.equals(getVersion())) + Bukkit.getConsoleSender() + .sendMessage(Core.PREFIX + "CustomItemsLib is using " + getVersion() + " as Economy Provider"); return true; } + vaultEconomy = null; + reserveEconomy = null; + setVersion(""); + Type = EcoType.NONE; return false; } diff --git a/src/one/lindegaard/CustomItemsLib/EconomyProviderListener.java b/src/one/lindegaard/CustomItemsLib/EconomyProviderListener.java new file mode 100644 index 0000000..6a604c5 --- /dev/null +++ b/src/one/lindegaard/CustomItemsLib/EconomyProviderListener.java @@ -0,0 +1,25 @@ +package one.lindegaard.CustomItemsLib; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.event.server.ServerLoadEvent; + +public class EconomyProviderListener implements Listener { + + @EventHandler(priority = EventPriority.MONITOR) + public void onPluginEnable(PluginEnableEvent event) { + if (!"BagOfGold".equalsIgnoreCase(event.getPlugin().getName())) + return; + + if (Core.getEconomyManager() != null) + Core.getEconomyManager().setupEconomy(); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onServerLoad(ServerLoadEvent event) { + if (Core.getEconomyManager() != null) + Core.getEconomyManager().setupEconomy(); + } +} diff --git a/src/one/lindegaard/CustomItemsLib/compatibility/ProtocolLibHelper.java b/src/one/lindegaard/CustomItemsLib/compatibility/ProtocolLibHelper.java index 44ee6b1..89bb407 100644 --- a/src/one/lindegaard/CustomItemsLib/compatibility/ProtocolLibHelper.java +++ b/src/one/lindegaard/CustomItemsLib/compatibility/ProtocolLibHelper.java @@ -1,88 +1,78 @@ -package one.lindegaard.CustomItemsLib.compatibility; - -import java.util.Iterator; +package one.lindegaard.CustomItemsLib.compatibility; + import java.util.List; - -import org.bukkit.GameMode; +import org.bukkit.ChatColor; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import com.comphenix.packetwrapper.WrapperPlayServerCollect; -import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.ProtocolManager; -import com.comphenix.protocol.events.ListenerPriority; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketContainer; -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.reflect.StructureModifier; - -import one.lindegaard.CustomItemsLib.Core; - -public class ProtocolLibHelper { - - private static ProtocolManager protocolManager; - +import com.comphenix.packetwrapper.WrapperPlayServerCollect; +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.StructureModifier; + +import one.lindegaard.CustomItemsLib.Core; + +public class ProtocolLibHelper { + + private static ProtocolManager protocolManager; + public static void enableProtocolLib() { protocolManager = ProtocolLibrary.getProtocolManager(); - ProtocolLibrary.getProtocolManager() - .addPacketListener(new PacketAdapter(Core.getInstance(), ListenerPriority.HIGHEST, - PacketType.Play.Server.SET_SLOT, PacketType.Play.Server.WINDOW_ITEMS, - PacketType.Play.Server.RECIPES, PacketType.Play.Server.RECIPE_UPDATE) { + // Only hook packet types that carry inventory items on modern Paper versions. + // Registering every enum value causes ProtocolLib warnings for legacy/unregistered packets. + protocolManager.addPacketListener(new PacketAdapter(Core.getInstance(), ListenerPriority.HIGHEST, + PacketType.Play.Server.SET_SLOT, PacketType.Play.Server.WINDOW_ITEMS) { @Override public void onPacketSending(PacketEvent event) { - if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) { - PacketContainer packet = event.getPacket().deepClone(); - StructureModifier sm = packet.getItemModifier(); - for (int i = 0; i < sm.size(); i++) { - ItemStack is = sm.getValues().get(i); - if (is.hasItemMeta()) { - ItemMeta itemMeta = is.getItemMeta(); - if (itemMeta.hasLore()) { - List lore = itemMeta.getLore(); - Iterator itr = lore.iterator(); - while (itr.hasNext()) { - String str = itr.next(); - if (str.startsWith("Hidden(")) - if (event.getPlayer().getGameMode() == GameMode.SURVIVAL) - itr.remove(); - } - itemMeta.setLore(lore); - is.setItemMeta(itemMeta); - } - } + boolean hideInternalLore = shouldHideInternalLore(event); + if (!hideInternalLore) { + return; + } + + PacketContainer packet = event.getPacket().deepClone(); + boolean changed = false; + + StructureModifier itemModifier = packet.getItemModifier(); + for (int i = 0; i < itemModifier.size(); i++) { + ItemStack itemStack = itemModifier.read(i); + ItemStack sanitized = sanitizeHiddenLore(itemStack); + if (sanitized != itemStack) { + itemModifier.write(i, sanitized); + changed = true; } - event.setPacket(packet); } - else if (event.getPacketType() == PacketType.Play.Server.WINDOW_ITEMS) { - PacketContainer packet = event.getPacket().deepClone(); - StructureModifier> modifiers = packet.getItemListModifier(); - for (int j = 0; j < modifiers.size(); j++) { - List itemStackList = modifiers.getValues().get(j); - for (int i = 0; i < itemStackList.size(); i++) { - ItemStack is = itemStackList.get(i); - if (is.hasItemMeta()) { - ItemMeta itemMeta = is.getItemMeta(); - if (itemMeta.hasLore()) { - List lore = itemMeta.getLore(); - Iterator itr = lore.iterator(); - while (itr.hasNext()) { - String str = itr.next(); - if (str.startsWith("Hidden(")) - if (event.getPlayer().getGameMode() == GameMode.SURVIVAL) { -// BagOfGold.getInstance().getMessages().debug("ProtocolLibHelper:ItemSlots=%s", event.getPacket().getItemSlots().toString()); - itr.remove(); - } - } - itemMeta.setLore(lore); - is.setItemMeta(itemMeta); - } - } + StructureModifier> listModifier = packet.getItemListModifier(); + for (int i = 0; i < listModifier.size(); i++) { + List itemList = listModifier.read(i); + if (itemList == null || itemList.isEmpty()) { + continue; + } + + boolean listChanged = false; + for (int j = 0; j < itemList.size(); j++) { + ItemStack original = itemList.get(j); + ItemStack sanitized = sanitizeHiddenLore(original); + if (sanitized != original) { + itemList.set(j, sanitized); + listChanged = true; } } + + if (listChanged) { + listModifier.write(i, itemList); + changed = true; + } + } + + if (changed) { event.setPacket(packet); } } @@ -91,15 +81,82 @@ else if (event.getPacketType() == PacketType.Play.Server.WINDOW_ITEMS) { }); } - public static ProtocolManager getProtocolmanager() { - return protocolManager; + private static boolean shouldHideInternalLore(PacketEvent event) { + if (event == null) { + return false; + } + + Player player = event.getPlayer(); + if (player == null) { + return false; + } + + // Hidden(...) lore is internal metadata and must never be visible to clients. + // Always strip it for packet-rendered items (Java + Bedrock via Geyser/Floodgate). + return true; + } + + private static boolean isInternalHiddenLoreLine(String line) { + if (line == null || line.isEmpty()) { + return false; + } + + String plain = ChatColor.stripColor(line); + if (plain == null) { + return false; + } + + return plain.trim().startsWith("Hidden("); } - public static void pickupMoney(Player player, Entity ent) { - WrapperPlayServerCollect wpsc = new WrapperPlayServerCollect(); - wpsc.setCollectedEntityId(ent.getEntityId()); - wpsc.setCollectorEntityId(player.getEntityId()); - wpsc.sendPacket(player); + private static ItemStack sanitizeHiddenLore(ItemStack itemStack) { + if (itemStack == null || !itemStack.hasItemMeta()) { + return itemStack; + } + + ItemMeta itemMeta = itemStack.getItemMeta(); + if (itemMeta == null || !itemMeta.hasLore()) { + return itemStack; + } + + List lore = itemMeta.getLore(); + if (lore == null || lore.isEmpty()) { + return itemStack; + } + + List filtered = new java.util.ArrayList<>(lore.size()); + boolean removed = false; + for (String line : lore) { + if (isInternalHiddenLoreLine(line)) { + removed = true; + continue; + } + filtered.add(line); + } + + if (!removed) { + return itemStack; + } + + ItemStack cloned = itemStack.clone(); + ItemMeta clonedMeta = cloned.getItemMeta(); + if (clonedMeta == null) { + return itemStack; + } + clonedMeta.setLore(filtered.isEmpty() ? null : filtered); + cloned.setItemMeta(clonedMeta); + return cloned; } -} + public static ProtocolManager getProtocolmanager() { + return protocolManager; + } + + public static void pickupMoney(Player player, Entity ent) { + WrapperPlayServerCollect wpsc = new WrapperPlayServerCollect(); + wpsc.setCollectedEntityId(ent.getEntityId()); + wpsc.setCollectorEntityId(player.getEntityId()); + wpsc.sendPacket(player); + } + +} diff --git a/src/one/lindegaard/CustomItemsLib/rewards/CoreCustomItems.java b/src/one/lindegaard/CustomItemsLib/rewards/CoreCustomItems.java index 2e14808..02b4e33 100644 --- a/src/one/lindegaard/CustomItemsLib/rewards/CoreCustomItems.java +++ b/src/one/lindegaard/CustomItemsLib/rewards/CoreCustomItems.java @@ -1,388 +1,422 @@ -package one.lindegaard.CustomItemsLib.rewards; - +package one.lindegaard.CustomItemsLib.rewards; + import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; +import java.util.Base64; import java.util.UUID; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; - -import one.lindegaard.Core.shared.Skins; -import one.lindegaard.Core.v1_10_R1.Skins_1_10_R1; -import one.lindegaard.Core.v1_11_R1.Skins_1_11_R1; -import one.lindegaard.Core.v1_10_R1.Skins_1_12_R1; -import one.lindegaard.Core.v1_13_R1.Skins_1_13_R1; -import one.lindegaard.Core.v1_13_R2.Skins_1_13_R2; -import one.lindegaard.Core.v1_14_R1.Skins_1_14_R1; -import one.lindegaard.Core.v1_15_R1.Skins_1_15_R1; -import one.lindegaard.Core.v1_16_R1.Skins_1_16_R1; -import one.lindegaard.Core.v1_16_R2.Skins_1_16_R2; -import one.lindegaard.Core.v1_16_R3.Skins_1_16_R3; -import one.lindegaard.Core.v1_17_R1.Skins_1_17_R1; -import one.lindegaard.Core.v1_18_R1.Skins_1_18_R1; -import one.lindegaard.Core.v1_19_R1.Skins_1_19_R1; -import one.lindegaard.Core.v1_19_R2.Skins_1_19_R2; -import one.lindegaard.Core.v1_19_R3.Skins_1_19_R3; -import one.lindegaard.Core.v1_20_R1.Skins_1_20_R1; -import one.lindegaard.Core.v1_20_R2.Skins_1_20_R2; -import one.lindegaard.Core.v1_20_R3.Skins_1_20_R3; -import one.lindegaard.Core.v1_21_R1.Skins_1_21_R1; -import one.lindegaard.Core.v1_21_R2.Skins_1_21_R2; -import one.lindegaard.Core.v1_21_R3.Skins_1_21_R3; -import one.lindegaard.Core.v1_21_R4.Skins_1_21_R4; -import one.lindegaard.Core.v1_21_R5.Skins_1_21_R5; -import one.lindegaard.Core.v1_8_R1.Skins_1_8_R1; -import one.lindegaard.Core.v1_8_R2.Skins_1_8_R2; -import one.lindegaard.Core.v1_8_R3.Skins_1_8_R3; -import one.lindegaard.Core.v1_9_R1.Skins_1_9_R1; -import one.lindegaard.Core.v1_9_R2.Skins_1_9_R2; -import one.lindegaard.CustomItemsLib.Core; -import one.lindegaard.CustomItemsLib.PlayerSettings; -import one.lindegaard.CustomItemsLib.Strings; -import one.lindegaard.CustomItemsLib.mobs.MobType; -import one.lindegaard.CustomItemsLib.server.Servers; - -public class CoreCustomItems { - - //Plugin plugin; - - //public CoreCustomItems(Plugin plugin) { - // this.plugin = plugin; - //} - - // How to get Playerskin - // https://www.spigotmc.org/threads/how-to-get-a-players-texture.244966/ - - /** - * Return an ItemStack with the Players head texture. - * - * @param name - * @param money - * @return - */ - public static Skins getSkinsClass() { - String version; - Skins sk = null; - try { - version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3]; - } catch (ArrayIndexOutOfBoundsException e) { - // MC 1.21.5+ dropped versioned package names — fall back to Mojang API for skin retrieval - Bukkit.getLogger().warning("[CustomItemsLib] Unversioned CraftBukkit package detected (MC 1.21.5+) — skin lookup will use Mojang API instead of NMS."); - return null; - } - // https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-16/ - if (version.equals("v1_21_R5")) { - sk = new Skins_1_21_R5(); - } else if (version.equals("v1_21_R4")) { - sk = new Skins_1_21_R4(); - } else if (version.equals("v1_21_R3")) { - sk = new Skins_1_21_R3(); - } else if (version.equals("v1_21_R2")) { - sk = new Skins_1_21_R2(); - } else if (version.equals("v1_21_R1")) { - sk = new Skins_1_21_R1(); - } else if (version.equals("v1_20_R3")) { - sk = new Skins_1_20_R3(); - } else if (version.equals("v1_20_R2")) { - sk = new Skins_1_20_R2(); - } else if (version.equals("v1_20_R1")) { - sk = new Skins_1_20_R1(); - } else if (version.equals("v1_19_R3")) { - sk = new Skins_1_19_R3(); - } else if (version.equals("v1_19_R2")) { - sk = new Skins_1_19_R2(); - } else if (version.equals("v1_19_R1")) { - sk = new Skins_1_19_R1(); - } else if (version.equals("v1_18_R1")) { - sk = new Skins_1_18_R1(); - } else if (version.equals("v1_17_R1")) { - sk = new Skins_1_17_R1(); - } else if (version.equals("v1_16_R3")) { - sk = new Skins_1_16_R3(); - } else if (version.equals("v1_16_R2")) { - sk = new Skins_1_16_R2(); - } else if (version.equals("v1_16_R1")) { - sk = new Skins_1_16_R1(); - } else if (version.equals("v1_15_R1")) { - sk = new Skins_1_15_R1(); - } else if (version.equals("v1_14_R1")) { - sk = new Skins_1_14_R1(); - } else if (version.equals("v1_13_R2")) { - sk = new Skins_1_13_R2(); - } else if (version.equals("v1_13_R1")) { - sk = new Skins_1_13_R1(); - } else if (version.equals("v1_12_R1")) { - sk = new Skins_1_12_R1(); - } else if (version.equals("v1_11_R1")) { - sk = new Skins_1_11_R1(); - } else if (version.equals("v1_10_R1")) { - sk = new Skins_1_10_R1(); - } else if (version.equals("v1_9_R2")) { - sk = new Skins_1_9_R2(); - } else if (version.equals("v1_9_R1")) { - sk = new Skins_1_9_R1(); - } else if (version.equals("v1_8_R3")) { - sk = new Skins_1_8_R3(); - } else if (version.equals("v1_8_R2")) { - sk = new Skins_1_8_R2(); - } else if (version.equals("v1_8_R1")) { - sk = new Skins_1_8_R1(); - } - return sk; - } - - /** - * Return an ItemStack with a custom texture. If Mojang changes the way they - * calculate Signatures this method will stop working. - * - * @param mPlayerUUID - * @param mDisplayName - * @param mTextureValue - * @param mTextureSignature - * @param money - * @return ItemStack with custom texture. - */ +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; + +import one.lindegaard.Core.shared.Skins; +import one.lindegaard.Core.v1_10_R1.Skins_1_10_R1; +import one.lindegaard.Core.v1_11_R1.Skins_1_11_R1; +import one.lindegaard.Core.v1_10_R1.Skins_1_12_R1; +import one.lindegaard.Core.v1_13_R1.Skins_1_13_R1; +import one.lindegaard.Core.v1_13_R2.Skins_1_13_R2; +import one.lindegaard.Core.v1_14_R1.Skins_1_14_R1; +import one.lindegaard.Core.v1_15_R1.Skins_1_15_R1; +import one.lindegaard.Core.v1_16_R1.Skins_1_16_R1; +import one.lindegaard.Core.v1_16_R2.Skins_1_16_R2; +import one.lindegaard.Core.v1_16_R3.Skins_1_16_R3; +import one.lindegaard.Core.v1_17_R1.Skins_1_17_R1; +import one.lindegaard.Core.v1_18_R1.Skins_1_18_R1; +import one.lindegaard.Core.v1_19_R1.Skins_1_19_R1; +import one.lindegaard.Core.v1_19_R2.Skins_1_19_R2; +import one.lindegaard.Core.v1_19_R3.Skins_1_19_R3; +import one.lindegaard.Core.v1_20_R1.Skins_1_20_R1; +import one.lindegaard.Core.v1_20_R2.Skins_1_20_R2; +import one.lindegaard.Core.v1_20_R3.Skins_1_20_R3; +import one.lindegaard.Core.v1_21_R1.Skins_1_21_R1; +import one.lindegaard.Core.v1_21_R2.Skins_1_21_R2; +import one.lindegaard.Core.v1_21_R3.Skins_1_21_R3; +import one.lindegaard.Core.v1_21_R4.Skins_1_21_R4; +import one.lindegaard.Core.v1_21_R5.Skins_1_21_R5; +import one.lindegaard.Core.v1_8_R1.Skins_1_8_R1; +import one.lindegaard.Core.v1_8_R2.Skins_1_8_R2; +import one.lindegaard.Core.v1_8_R3.Skins_1_8_R3; +import one.lindegaard.Core.v1_9_R1.Skins_1_9_R1; +import one.lindegaard.Core.v1_9_R2.Skins_1_9_R2; +import one.lindegaard.CustomItemsLib.Core; +import one.lindegaard.CustomItemsLib.PlayerSettings; +import one.lindegaard.CustomItemsLib.mobs.MobType; +import one.lindegaard.CustomItemsLib.server.Servers; + +public class CoreCustomItems { + + //Plugin plugin; + + //public CoreCustomItems(Plugin plugin) { + // this.plugin = plugin; + //} + + // How to get Playerskin + // https://www.spigotmc.org/threads/how-to-get-a-players-texture.244966/ + + /** + * Return an ItemStack with the Players head texture. + * + * @param name + * @param money + * @return + */ + public static Skins getSkinsClass() { + String version; + Skins sk = null; + try { + version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3]; + } catch (ArrayIndexOutOfBoundsException e) { + // MC 1.21.5+ dropped versioned package names — fall back to Mojang API for skin retrieval + Bukkit.getLogger().warning("[CustomItemsLib] Unversioned CraftBukkit package detected (MC 1.21.5+) — skin lookup will use Mojang API instead of NMS."); + return null; + } + // https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-16/ + if (version.equals("v1_21_R5")) { + sk = new Skins_1_21_R5(); + } else if (version.equals("v1_21_R4")) { + sk = new Skins_1_21_R4(); + } else if (version.equals("v1_21_R3")) { + sk = new Skins_1_21_R3(); + } else if (version.equals("v1_21_R2")) { + sk = new Skins_1_21_R2(); + } else if (version.equals("v1_21_R1")) { + sk = new Skins_1_21_R1(); + } else if (version.equals("v1_20_R3")) { + sk = new Skins_1_20_R3(); + } else if (version.equals("v1_20_R2")) { + sk = new Skins_1_20_R2(); + } else if (version.equals("v1_20_R1")) { + sk = new Skins_1_20_R1(); + } else if (version.equals("v1_19_R3")) { + sk = new Skins_1_19_R3(); + } else if (version.equals("v1_19_R2")) { + sk = new Skins_1_19_R2(); + } else if (version.equals("v1_19_R1")) { + sk = new Skins_1_19_R1(); + } else if (version.equals("v1_18_R1")) { + sk = new Skins_1_18_R1(); + } else if (version.equals("v1_17_R1")) { + sk = new Skins_1_17_R1(); + } else if (version.equals("v1_16_R3")) { + sk = new Skins_1_16_R3(); + } else if (version.equals("v1_16_R2")) { + sk = new Skins_1_16_R2(); + } else if (version.equals("v1_16_R1")) { + sk = new Skins_1_16_R1(); + } else if (version.equals("v1_15_R1")) { + sk = new Skins_1_15_R1(); + } else if (version.equals("v1_14_R1")) { + sk = new Skins_1_14_R1(); + } else if (version.equals("v1_13_R2")) { + sk = new Skins_1_13_R2(); + } else if (version.equals("v1_13_R1")) { + sk = new Skins_1_13_R1(); + } else if (version.equals("v1_12_R1")) { + sk = new Skins_1_12_R1(); + } else if (version.equals("v1_11_R1")) { + sk = new Skins_1_11_R1(); + } else if (version.equals("v1_10_R1")) { + sk = new Skins_1_10_R1(); + } else if (version.equals("v1_9_R2")) { + sk = new Skins_1_9_R2(); + } else if (version.equals("v1_9_R1")) { + sk = new Skins_1_9_R1(); + } else if (version.equals("v1_8_R3")) { + sk = new Skins_1_8_R3(); + } else if (version.equals("v1_8_R2")) { + sk = new Skins_1_8_R2(); + } else if (version.equals("v1_8_R1")) { + sk = new Skins_1_8_R1(); + } + return sk; + } + + /** + * Return an ItemStack with a custom texture. If Mojang changes the way they + * calculate Signatures this method will stop working. + * + * @param mPlayerUUID + * @param mDisplayName + * @param mTextureValue + * @param mTextureSignature + * @param money + * @return ItemStack with custom texture. + */ public static ItemStack getCustomtexture(Reward reward, String mTextureValue, String mTextureSignature) { ItemStack skull = CoreCustomItems.getDefaultPlayerHead(1); - if (mTextureSignature.isEmpty() || mTextureValue.isEmpty()) + if (mTextureValue.isEmpty()) return skull; // add custom texture to skull SkullMeta skullMeta = (SkullMeta) skull.getItemMeta(); - GameProfile profile = new GameProfile(reward.getSkinUUID(), reward.getDisplayName()); - if (mTextureSignature.isEmpty()) - profile.getProperties().put("textures", new Property("textures", mTextureValue)); - else - profile.getProperties().put("textures", new Property("textures", mTextureValue, mTextureSignature)); - Field profileField = null; - try { - profileField = skullMeta.getClass().getDeclaredField("profile"); - } catch (NoSuchFieldException | SecurityException e) { - e.printStackTrace(); - return skull; - } - profileField.setAccessible(true); - try { - profileField.set(skullMeta, profile); - } catch (IllegalArgumentException | IllegalAccessException e) { - e.printStackTrace(); - } - skull.setItemMeta(skullMeta); - - // add displayname and lores to skull - skull = Reward.setDisplayNameAndHiddenLores(skull, reward); - return skull; - } - - /** - * Return an ItemStack with the Players head texture. - * - * @param name - * @param money - * @return - */ - public static ItemStack getPlayerHead(UUID uuid, String name, int amount, double money) { - ItemStack skull = CoreCustomItems.getDefaultPlayerHead(amount); - skull.setAmount(amount); - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - PlayerSettings ps = Core.getPlayerSettingsManager().getPlayerSettings(offlinePlayer); - if (ps.getTexture() == null || ps.getSignature() == null || ps.getTexture().isEmpty() - || ps.getSignature().isEmpty()) { - Core.getMessages().debug("No skin found i database"); - String[] onlineSkin = new String[2]; - if (offlinePlayer.isOnline()) { - Player player = (Player) offlinePlayer; - Skins sk = CoreCustomItems.getSkinsClass(); - if (sk != null) { - Core.getMessages().debug("Trying to fecth skin from Online Player Profile"); - onlineSkin = sk.getSkin(player); - } else { - Core.getMessages().debug("Trying to fecth skin from Minecraft Servers"); - onlineSkin = getSkinFromUUID(uuid); - } - } - - if ((onlineSkin == null || onlineSkin[0] == null || onlineSkin[0].isEmpty() || onlineSkin[1] == null - || onlineSkin[1].isEmpty()) && Servers.isMC112OrNewer()) - return getPlayerHeadOwningPlayer(uuid, name, amount, money); - - if (onlineSkin != null && onlineSkin[0] != null && !onlineSkin[0].isEmpty() && onlineSkin[1] != null - && !onlineSkin[1].isEmpty()) { - ps.setTexture(onlineSkin[0]); - ps.setSignature(onlineSkin[1]); - Core.getPlayerSettingsManager().setPlayerSettings(ps); + // Modern API path (Paper/Spigot 1.20+): build a PlayerProfile from the encoded texture value. + URL skinUrl = getSkinUrlFromTextureValue(mTextureValue); + if (skinUrl != null) { + UUID profileUuid = reward.getSkinUUID() != null ? reward.getSkinUUID() : UUID.randomUUID(); + PlayerProfile ownerProfile = Bukkit.createPlayerProfile(profileUuid); + PlayerTextures textures = ownerProfile.getTextures(); + textures.setSkin(skinUrl); + ownerProfile.setTextures(textures); + skullMeta.setOwnerProfile(ownerProfile); } else { - Core.getMessages().debug("Empty skin"); + throw new IllegalArgumentException("Could not decode skin URL from texture value."); + } + } catch (Throwable modernApiFailure) { + // Legacy fallback for old servers relying on direct GameProfile field injection. + GameProfile profile = new GameProfile(reward.getSkinUUID(), reward.getDisplayName()); + if (mTextureSignature.isEmpty()) + profile.getProperties().put("textures", new Property("textures", mTextureValue)); + else + profile.getProperties().put("textures", new Property("textures", mTextureValue, mTextureSignature)); + Field profileField = null; + + try { + profileField = skullMeta.getClass().getDeclaredField("profile"); + } catch (NoSuchFieldException | SecurityException e) { + Core.getMessages().debug("Unable to set skull profile by reflection: %s", e.getMessage()); return skull; } - } else { - if (offlinePlayer.isOnline()) { - Player player = (Player) offlinePlayer; - Skins sk = CoreCustomItems.getSkinsClass(); - if (sk != null) { - String[] skin = sk.getSkin(player); - if (skin != null && skin[0] != null && !skin[0].equals(ps.getTexture())) { - Core.getMessages().debug("%s has changed skin, updating database with new skin. (%s,%s)", - player.getName(), ps.getTexture(), skin[0]); - ps.setTexture(skin[0]); - ps.setSignature(skin[1]); - Core.getPlayerSettingsManager().setPlayerSettings(ps); - } - } - } else - Core.getMessages().debug("%s using skin from skin Cache", offlinePlayer.getName()); + profileField.setAccessible(true); + try { + profileField.set(skullMeta, profile); + } catch (IllegalArgumentException | IllegalAccessException e) { + Core.getMessages().debug("Unable to apply legacy skull texture profile: %s", e.getMessage()); + } } - - skull = new ItemStack(getCustomtexture(new Reward(offlinePlayer.getName(), money, RewardType.KILLED, uuid), - ps.getTexture(), ps.getSignature())); - skull.setAmount(amount); + skull.setItemMeta(skullMeta); + + // add displayname and lores to skull + skull = Reward.setDisplayNameAndHiddenLores(skull, reward); return skull; } - private static String[] getSkinFromUUID(UUID uuid) { + private static URL getSkinUrlFromTextureValue(String textureValue) { try { - URL url_1 = new URL( - "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); - InputStreamReader reader_1; - reader_1 = new InputStreamReader(url_1.openStream()); - - JsonElement json = new JsonParser().parse(reader_1); - if (json.isJsonObject()) { - JsonObject textureProperty = json.getAsJsonObject().get("properties").getAsJsonArray().get(0) - .getAsJsonObject(); - String texture = textureProperty.get("value").getAsString(); - String signature = textureProperty.get("signature").getAsString(); - - return new String[] { texture, signature }; - } else { - Core.getMessages().debug("(1) Could not get skin data from session servers!"); + String decoded = new String(Base64.getDecoder().decode(textureValue), StandardCharsets.UTF_8); + JsonElement json = new JsonParser().parse(decoded); + if (!json.isJsonObject()) return null; - } - - } catch (IOException e) { - Core.getMessages().debug("(2)Could not get skin data from session servers!"); + JsonObject root = json.getAsJsonObject(); + if (!root.has("textures")) + return null; + JsonObject textures = root.getAsJsonObject("textures"); + if (textures == null || !textures.has("SKIN")) + return null; + JsonObject skin = textures.getAsJsonObject("SKIN"); + if (skin == null || !skin.has("url")) + return null; + return new URL(skin.get("url").getAsString()); + } catch (Exception e) { return null; } } - + + /** + * Return an ItemStack with the Players head texture. + * + * @param name + * @param money + * @return + */ + public static ItemStack getPlayerHead(UUID uuid, String name, int amount, double money) { + ItemStack skull = CoreCustomItems.getDefaultPlayerHead(amount); + skull.setAmount(amount); + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); + PlayerSettings ps = Core.getPlayerSettingsManager().getPlayerSettings(offlinePlayer); + if (ps.getTexture() == null || ps.getSignature() == null || ps.getTexture().isEmpty() + || ps.getSignature().isEmpty()) { + Core.getMessages().debug("No skin found i database"); + String[] onlineSkin = new String[2]; + if (offlinePlayer.isOnline()) { + Player player = (Player) offlinePlayer; + Skins sk = CoreCustomItems.getSkinsClass(); + if (sk != null) { + Core.getMessages().debug("Trying to fecth skin from Online Player Profile"); + onlineSkin = sk.getSkin(player); + } else { + Core.getMessages().debug("Trying to fecth skin from Minecraft Servers"); + onlineSkin = getSkinFromUUID(uuid); + } + } + + if ((onlineSkin == null || onlineSkin[0] == null || onlineSkin[0].isEmpty() || onlineSkin[1] == null + || onlineSkin[1].isEmpty()) && Servers.isMC112OrNewer()) + return getPlayerHeadOwningPlayer(uuid, name, amount, money); + + if (onlineSkin != null && onlineSkin[0] != null && !onlineSkin[0].isEmpty() && onlineSkin[1] != null + && !onlineSkin[1].isEmpty()) { + ps.setTexture(onlineSkin[0]); + ps.setSignature(onlineSkin[1]); + Core.getPlayerSettingsManager().setPlayerSettings(ps); + } else { + Core.getMessages().debug("Empty skin"); + return skull; + } + } else { + if (offlinePlayer.isOnline()) { + Player player = (Player) offlinePlayer; + Skins sk = CoreCustomItems.getSkinsClass(); + if (sk != null) { + String[] skin = sk.getSkin(player); + if (skin != null && skin[0] != null && !skin[0].equals(ps.getTexture())) { + Core.getMessages().debug("%s has changed skin, updating database with new skin. (%s,%s)", + player.getName(), ps.getTexture(), skin[0]); + ps.setTexture(skin[0]); + ps.setSignature(skin[1]); + Core.getPlayerSettingsManager().setPlayerSettings(ps); + } + } + } else + Core.getMessages().debug("%s using skin from skin Cache", offlinePlayer.getName()); + } + + skull = new ItemStack(getCustomtexture(new Reward(offlinePlayer.getName(), money, RewardType.KILLED, uuid), + ps.getTexture(), ps.getSignature())); + skull.setAmount(amount); + return skull; + } + + private static String[] getSkinFromUUID(UUID uuid) { + try { + URL url_1 = new URL( + "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false"); + InputStreamReader reader_1; + reader_1 = new InputStreamReader(url_1.openStream()); + + JsonElement json = new JsonParser().parse(reader_1); + if (json.isJsonObject()) { + JsonObject textureProperty = json.getAsJsonObject().get("properties").getAsJsonArray().get(0) + .getAsJsonObject(); + String texture = textureProperty.get("value").getAsString(); + String signature = textureProperty.get("signature").getAsString(); + + return new String[] { texture, signature }; + } else { + Core.getMessages().debug("(1) Could not get skin data from session servers!"); + return null; + } + + } catch (IOException e) { + Core.getMessages().debug("(2)Could not get skin data from session servers!"); + return null; + } + } + private static ItemStack getPlayerHeadOwningPlayer(UUID uuid, String name, int amount, double money) { ItemStack skull = CoreCustomItems.getDefaultPlayerHead(amount); SkullMeta skullMeta = (SkullMeta) skull.getItemMeta(); skull.setItemMeta(skullMeta); - skull = Reward.setDisplayNameAndHiddenLores(skull, name, money, new ArrayList(Arrays.asList( - "Hidden(0):" + name, "Hidden(1):" + String.format(Locale.ENGLISH, "%.5f", money), - "Hidden(2):" + RewardType.KILLED.getType(), "Hidden(4):" + uuid, - "Hidden(5):" - + Strings.encode(String.format(Locale.ENGLISH, "%.5f", money) + RewardType.KILLED.getType()), - Core.getMessages().getString("core.reward.lore")))); + skull = Reward.setDisplayNameAndHiddenLores(skull, + new Reward(name, money, RewardType.KILLED, uuid)); Core.getMessages().debug("CustomItems: set the skin using OwningPlayer/Owner (%s)", name); return skull; } - - public static ItemStack getCustomHead(MobType minecraftMob, String name, int amount, double money, UUID skinUUID) { - ItemStack skull; - switch (minecraftMob) { - case Skeleton: - skull = CoreCustomItems.getDefaultSkeletonHead(amount); - skull = Reward.setDisplayNameAndHiddenLores(skull, - new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); - break; - - case WitherSkeleton: - skull = CoreCustomItems.getDefaultWitherSkeletonHead(amount); - skull = Reward.setDisplayNameAndHiddenLores(skull, - new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); - break; - - case Zombie: - skull = CoreCustomItems.getDefaultZombieHead(amount); - skull = Reward.setDisplayNameAndHiddenLores(skull, - new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); - break; - - case PvpPlayer: - skull = getPlayerHead(skinUUID, name, amount, money); - break; - - case Creeper: - skull = CoreCustomItems.getDefaultCreeperHead(amount); - skull = Reward.setDisplayNameAndHiddenLores(skull, - new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); - break; - - case EnderDragon: - skull = CoreCustomItems.getDefaultEnderDragonHead(amount); - skull = Reward.setDisplayNameAndHiddenLores(skull, - new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); - break; - - default: - ItemStack is = new ItemStack( - getCustomtexture(new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID), - minecraftMob.getTextureValue(), minecraftMob.getTextureSignature())); - is.setAmount(amount); - return is; - } - return skull; - } - - private static ItemStack getDefaultSkeletonHead(int amount) { - if (Servers.isMC113OrNewer()) - return new ItemStack(Material.SKELETON_SKULL, amount); - else - return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 0); - } - - private static ItemStack getDefaultWitherSkeletonHead(int amount) { - if (Servers.isMC113OrNewer()) - return new ItemStack(Material.WITHER_SKELETON_SKULL, amount); - else - return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 1); - } - - private static ItemStack getDefaultZombieHead(int amount) { - if (Servers.isMC113OrNewer()) - return new ItemStack(Material.ZOMBIE_HEAD, amount); - else - return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 2); - } - - private static ItemStack getDefaultPlayerHead(int amount) { - if (Servers.isMC113OrNewer()) - return new ItemStack(Material.PLAYER_HEAD, amount); - else - return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 3); - } - - private static ItemStack getDefaultCreeperHead(int amount) { - if (Servers.isMC113OrNewer()) - return new ItemStack(Material.CREEPER_HEAD, amount); - else - return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 4); - } - - private static ItemStack getDefaultEnderDragonHead(int amount) { - if (Servers.isMC113OrNewer()) - return new ItemStack(Material.DRAGON_HEAD, amount); - else - return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 5); - } - -} + + public static ItemStack getCustomHead(MobType minecraftMob, String name, int amount, double money, UUID skinUUID) { + ItemStack skull; + switch (minecraftMob) { + case Skeleton: + skull = CoreCustomItems.getDefaultSkeletonHead(amount); + skull = Reward.setDisplayNameAndHiddenLores(skull, + new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); + break; + + case WitherSkeleton: + skull = CoreCustomItems.getDefaultWitherSkeletonHead(amount); + skull = Reward.setDisplayNameAndHiddenLores(skull, + new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); + break; + + case Zombie: + skull = CoreCustomItems.getDefaultZombieHead(amount); + skull = Reward.setDisplayNameAndHiddenLores(skull, + new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); + break; + + case PvpPlayer: + skull = getPlayerHead(skinUUID, name, amount, money); + break; + + case Creeper: + skull = CoreCustomItems.getDefaultCreeperHead(amount); + skull = Reward.setDisplayNameAndHiddenLores(skull, + new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); + break; + + case EnderDragon: + skull = CoreCustomItems.getDefaultEnderDragonHead(amount); + skull = Reward.setDisplayNameAndHiddenLores(skull, + new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID)); + break; + + default: + ItemStack is = new ItemStack( + getCustomtexture(new Reward(minecraftMob.getFriendlyName(), money, RewardType.KILLED, skinUUID), + minecraftMob.getTextureValue(), minecraftMob.getTextureSignature())); + is.setAmount(amount); + return is; + } + return skull; + } + + private static ItemStack getDefaultSkeletonHead(int amount) { + if (Servers.isMC113OrNewer()) + return new ItemStack(Material.SKELETON_SKULL, amount); + else + return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 0); + } + + private static ItemStack getDefaultWitherSkeletonHead(int amount) { + if (Servers.isMC113OrNewer()) + return new ItemStack(Material.WITHER_SKELETON_SKULL, amount); + else + return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 1); + } + + private static ItemStack getDefaultZombieHead(int amount) { + if (Servers.isMC113OrNewer()) + return new ItemStack(Material.ZOMBIE_HEAD, amount); + else + return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 2); + } + + private static ItemStack getDefaultPlayerHead(int amount) { + if (Servers.isMC113OrNewer()) + return new ItemStack(Material.PLAYER_HEAD, amount); + else + return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 3); + } + + private static ItemStack getDefaultCreeperHead(int amount) { + if (Servers.isMC113OrNewer()) + return new ItemStack(Material.CREEPER_HEAD, amount); + else + return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 4); + } + + private static ItemStack getDefaultEnderDragonHead(int amount) { + if (Servers.isMC113OrNewer()) + return new ItemStack(Material.DRAGON_HEAD, amount); + else + return new ItemStack(Material.matchMaterial("SKULL_ITEM"), amount, (short) 5); + } + +} diff --git a/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardListeners.java b/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardListeners.java index e76cd75..c3b22c5 100644 --- a/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardListeners.java +++ b/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardListeners.java @@ -38,7 +38,6 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.plugin.Plugin; @@ -998,10 +997,14 @@ else if (cursorMoney > 1) break; case SWAP_WITH_CURSOR: if (Reward.isReward(isCurrentSlot) && Reward.isReward(isCursor)) { - ItemMeta imCurrent = isCurrentSlot.getItemMeta(); - ItemMeta imCursor = isCursor.getItemMeta(); - Reward reward1 = new Reward(imCurrent.getLore()); - Reward reward2 = new Reward(imCursor.getLore()); + Reward reward1 = Reward.getReward(isCurrentSlot); + Reward reward2 = Reward.getReward(isCursor); + if (reward1 == null || reward2 == null) { + event.setCancelled(true); + Core.getMessages().debug( + "SWAP_WITH_CURSOR: cancelled because reward metadata could not be parsed from PDC."); + break; + } int amount_reward1 = isCurrentSlot.getAmount(); int amount_reward2 = isCursor.getAmount(); if (reward2.isMoney() && slotType == SlotType.ARMOR) { diff --git a/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardManager.java b/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardManager.java index a76585e..5eda457 100644 --- a/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardManager.java +++ b/src/one/lindegaard/CustomItemsLib/rewards/CoreRewardManager.java @@ -181,6 +181,24 @@ public double removeBagOfGoldFromPlayer(Player player, double amount) { if (reward.checkHash()) { if (reward.isMoney()) { double saldo = Tools.round(reward.getMoney()); + double consumedFromToken = saldo > toBeTaken ? toBeTaken : saldo; + TokenSpendStore.MarkResult spendResult = TokenSpendStore.MarkResult.MARKED; + if (Core.getTokenSpendStore() != null) { + spendResult = Core.getTokenSpendStore().markTokenSpent(reward.getTokenUUID(), + player.getUniqueId(), "inventory-spend", consumedFromToken); + } + if (spendResult == TokenSpendStore.MarkResult.DUPLICATE) { + Core.getMessages().debug( + "Rejected duplicated inventory token while spending for %s (token=%s, slot=%s).", + player.getName(), reward.getTokenUUID(), slot); + player.getInventory().clear(slot); + continue; + } + if (spendResult == TokenSpendStore.MarkResult.ERROR) { + Core.getMessages().debug( + "Token spend store unavailable while spending inventory token for %s. Falling back to signature-only check.", + player.getName()); + } if (saldo > toBeTaken) { reward.setMoney(Tools.round(saldo - toBeTaken)); is = Reward.setDisplayNameAndHiddenLores(is, reward); diff --git a/src/one/lindegaard/CustomItemsLib/rewards/PickupRewards.java b/src/one/lindegaard/CustomItemsLib/rewards/PickupRewards.java index dbd2300..1a06d60 100644 --- a/src/one/lindegaard/CustomItemsLib/rewards/PickupRewards.java +++ b/src/one/lindegaard/CustomItemsLib/rewards/PickupRewards.java @@ -1,70 +1,111 @@ -package one.lindegaard.CustomItemsLib.rewards; - +package one.lindegaard.CustomItemsLib.rewards; + import one.lindegaard.CustomItemsLib.Core; import one.lindegaard.CustomItemsLib.Tools; -import one.lindegaard.CustomItemsLib.compatibility.BagOfGoldCompat; import one.lindegaard.CustomItemsLib.compatibility.ProtocolLibCompat; import one.lindegaard.CustomItemsLib.compatibility.ProtocolLibHelper; - -import org.bukkit.ChatColor; -import org.bukkit.entity.Item; -import org.bukkit.entity.Player; - -public class PickupRewards { - - private Core plugin; - - public PickupRewards(Core plugin) { - this.plugin = plugin; - } - + +import org.bukkit.ChatColor; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; + +public class PickupRewards { + + private Core plugin; + + public PickupRewards(Core plugin) { + this.plugin = plugin; + } + public void rewardPlayer(Player player, Item item, CallBack callBack) { if (Reward.isReward(item)) { Reward reward = Reward.getReward(item); + if (reward == null) { + callBack.setCancelled(false); + return; + } if (reward.isBagOfGoldReward() || reward.isItemReward()) { - callBack.setCancelled(true); + // BagOfGold loads after CustomItemsLib, so retry economy discovery on demand. + if (!Core.getEconomyManager().isActive()) + Core.getEconomyManager().setupEconomy(); - boolean succes = false; - if (BagOfGoldCompat.isSupported()) { - succes = Core.getEconomyManager().depositPlayer(player, reward.getMoney()); - if (succes) { - item.remove(); - if (Core.getCoreRewardManager().getDroppedMoney().containsKey(item.getEntityId())) - Core.getCoreRewardManager().getDroppedMoney().remove(item.getEntityId()); - if (ProtocolLibCompat.isSupported()) - ProtocolLibHelper.pickupMoney(player, item); + if (!reward.checkHash()) { + Core.getMessages().debug("Rejected reward token for %s because signature verification failed.", player.getName()); + callBack.setCancelled(true); + item.remove(); + if (Core.getCoreRewardManager().getDroppedMoney().containsKey(item.getEntityId())) + Core.getCoreRewardManager().getDroppedMoney().remove(item.getEntityId()); + return; + } - if (reward.getMoney() == 0) { - Core.getMessages().debug("%s picked up a %s" + ChatColor.RESET + " (# of rewards left=%s)", - player.getName(), reward.isItemReward() ? "ITEM" : reward.getDisplayName(), - Core.getCoreRewardManager().getDroppedMoney().size()); + boolean succes = Core.getEconomyManager().depositPlayer(player, reward.getMoney()); + if (succes) { + TokenSpendStore.MarkResult spendResult = TokenSpendStore.MarkResult.MARKED; + if (Core.getTokenSpendStore() != null) { + spendResult = Core.getTokenSpendStore().markTokenSpent(reward.getTokenUUID(), + player.getUniqueId(), "pickup", reward.getMoney()); + } + if (spendResult != TokenSpendStore.MarkResult.MARKED) { + // Roll back if we deposited but could not mark token consumption. + Core.getEconomyManager().withdrawPlayer(player, reward.getMoney()); + if (spendResult == TokenSpendStore.MarkResult.DUPLICATE) { + Core.getMessages().debug( + "Rejected duplicated reward token for %s (token=%s). Deposit rolled back.", + player.getName(), reward.getTokenUUID()); + callBack.setCancelled(true); + item.remove(); + if (Core.getCoreRewardManager().getDroppedMoney().containsKey(item.getEntityId())) + Core.getCoreRewardManager().getDroppedMoney().remove(item.getEntityId()); } else { Core.getMessages().debug( - "%s picked up a %s" + ChatColor.RESET + " with a value:%s (# of rewards left=%s)(PickupRewards)", - player.getName(), reward.isItemReward() ? "ITEM" : reward.getDisplayName(), - Tools.format(Tools.round(reward.getMoney())), - Core.getCoreRewardManager().getDroppedMoney().size()); - if (!Core.getPlayerSettingsManager().getPlayerSettings(player).isMuted()) - Core.getMessages().playerActionBarMessageQueue(player, - Core.getMessages().getString("core.moneypickup", "money", - Tools.format(reward.getMoney()), "rewardname", - ChatColor.valueOf(Core.getConfigManager().rewardTextColor) - + (reward.getDisplayName().isEmpty() - ? Core.getConfigManager().bagOfGoldName - : reward.getDisplayName()))); + "Token store unavailable for %s (token=%s). Deposit rolled back and default pickup allowed.", + player.getName(), reward.getTokenUUID()); + callBack.setCancelled(false); } + return; + } + + callBack.setCancelled(true); + item.remove(); + if (Core.getCoreRewardManager().getDroppedMoney().containsKey(item.getEntityId())) + Core.getCoreRewardManager().getDroppedMoney().remove(item.getEntityId()); + if (ProtocolLibCompat.isSupported()) + ProtocolLibHelper.pickupMoney(player, item); + + if (reward.getMoney() == 0) { + Core.getMessages().debug("%s picked up a %s" + ChatColor.RESET + " (# of rewards left=%s)", + player.getName(), reward.isItemReward() ? "ITEM" : reward.getDisplayName(), + Core.getCoreRewardManager().getDroppedMoney().size()); } else { - callBack.setCancelled(true); + Core.getMessages().debug( + "%s picked up a %s" + ChatColor.RESET + " with a value:%s (# of rewards left=%s)(PickupRewards)", + player.getName(), reward.isItemReward() ? "ITEM" : reward.getDisplayName(), + Tools.format(Tools.round(reward.getMoney())), + Core.getCoreRewardManager().getDroppedMoney().size()); + if (!Core.getPlayerSettingsManager().getPlayerSettings(player).isMuted()) + Core.getMessages().playerActionBarMessageQueue(player, + Core.getMessages().getString("core.moneypickup", "money", + Tools.format(reward.getMoney()), "rewardname", + ChatColor.valueOf(Core.getConfigManager().rewardTextColor) + + (reward.getDisplayName().isEmpty() + ? Core.getConfigManager().bagOfGoldName + : reward.getDisplayName()))); } + } else { + // If no economy provider is active, let vanilla pickup handle the item. + callBack.setCancelled(false); + Core.getMessages().debug( + "Could not deposit reward value to economy for %s; allowing default pickup (reward=%s, value=%s).", + player.getName(), reward.getDisplayName(), Tools.format(Tools.round(reward.getMoney()))); } } } } - - public interface CallBack { - - void setCancelled(boolean canceled); - - } - -} + + public interface CallBack { + + void setCancelled(boolean canceled); + + } + +} diff --git a/src/one/lindegaard/CustomItemsLib/rewards/Reward.java b/src/one/lindegaard/CustomItemsLib/rewards/Reward.java index a1b415b..9d5e6b9 100644 --- a/src/one/lindegaard/CustomItemsLib/rewards/Reward.java +++ b/src/one/lindegaard/CustomItemsLib/rewards/Reward.java @@ -1,14 +1,15 @@ package one.lindegaard.CustomItemsLib.rewards; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; - -import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.ChatColor; +import org.bukkit.NamespacedKey; import org.bukkit.block.Block; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; @@ -17,6 +18,8 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.metadata.MetadataValue; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; import one.lindegaard.CustomItemsLib.Core; import one.lindegaard.CustomItemsLib.Strings; @@ -24,21 +27,41 @@ public class Reward { - public final static String MH_REWARD_DATA_NEW = "MH:HiddenRewardDataNew"; - - private String displayname = ""; // Hidden(0) - private double money = 0; // Hidden(1) - the value of the reward - private RewardType rewardType = null; // Hidden(2) - private UUID skinUUID; // Hidden(4) - private String encodedHash; // Hidden(5) - private int id; // only used when the Reward is placed as a Block and when saved + public static final String MH_REWARD_DATA_NEW = "MH:HiddenRewardDataNew"; + + private static final int TOKEN_VERSION = 2; + private static final String LEGACY_0 = "Hidden(0):"; + private static final String LEGACY_1 = "Hidden(1):"; + private static final String LEGACY_2 = "Hidden(2):"; + private static final String LEGACY_4 = "Hidden(4):"; + private static final String LEGACY_5 = "Hidden(5):"; + + private static final String PDC_VERSION = "reward_version"; + private static final String PDC_TYPE = "reward_type"; + private static final String PDC_VALUE = "reward_value_per_unit"; + private static final String PDC_TOKEN = "reward_token_uuid"; + private static final String PDC_SIG = "reward_signature"; + private static final String PDC_KEY_ID = "reward_key_id"; + private static final String PDC_DISPLAY = "reward_display_name"; + private static final String PDC_SKIN_UUID = "reward_skin_uuid"; + + private String displayname = ""; + private double money = 0; + private RewardType rewardType = null; + private UUID skinUUID; + private String encodedHash; + private int id; + + private String tokenUuid; + private String keyId; + private String signature; public Reward() { this.displayname = "Skull"; this.money = 0; this.rewardType = RewardType.BAGOFGOLD; this.skinUUID = UUID.fromString(RewardType.BAGOFGOLD.getUUID()); - this.encodedHash = Strings.encode(makeDecodedHash()); + rotateTokenAndSign(); this.id = 0; } @@ -49,14 +72,21 @@ public Reward(Reward reward) { this.skinUUID = reward.getSkinUUID(); this.encodedHash = reward.getEncodedHash(); this.id = reward.getUniqueID(); + this.tokenUuid = reward.getTokenUUID(); + this.keyId = reward.getKeyId(); + this.signature = reward.getSignature(); } public Reward(String displayName, double money, RewardType rewardType, UUID skinUUID) { this.displayname = displayName; this.money = money; - this.rewardType = rewardType; + this.rewardType = rewardType == null ? RewardType.BAGOFGOLD : rewardType; this.skinUUID = skinUUID; - this.encodedHash = Strings.encode(makeDecodedHash()); + rotateTokenAndSign(); + } + + public Reward(List lore) { + setReward(lore); } public int getUniqueID() { @@ -67,178 +97,237 @@ public void setUniqueID(int id) { this.id = id; } - public Reward(List lore) { - setReward(lore); + private static NamespacedKey pdcKey(String path) { + if (Core.getInstance() == null) { + return null; + } + return new NamespacedKey(Core.getInstance(), path); } - private String makeDecodedHash() { - return String.format(Locale.ENGLISH, "%.5f", money) + rewardType.getType(); + private String canonicalPayload() { + String safeDisplay = displayname == null ? "" : displayname; + String safeSkin = skinUUID == null ? "" : skinUUID.toString(); + String safeType = rewardType == null ? RewardType.BAGOFGOLD.getType() : rewardType.getType(); + String displayEncoded = Base64.getEncoder().encodeToString(safeDisplay.getBytes(StandardCharsets.UTF_8)); + return "v=" + TOKEN_VERSION + "|type=" + safeType + "|value=" + + String.format(Locale.ENGLISH, "%.5f", money) + "|token=" + (tokenUuid == null ? "" : tokenUuid) + + "|skin=" + safeSkin + "|display=" + displayEncoded + "|key=" + (keyId == null ? "" : keyId); } private String makeDecodedHashOld() { - return String.format(Locale.ENGLISH, "%.5f", money) + rewardType.getUUID(); + return String.format(Locale.ENGLISH, "%.5f", money) + + (rewardType == null ? RewardType.BAGOFGOLD.getUUID() : rewardType.getUUID()); + } + + private String makeDecodedHash() { + return String.format(Locale.ENGLISH, "%.5f", money) + + (rewardType == null ? RewardType.BAGOFGOLD.getType() : rewardType.getType()); + } + + private void ensureType() { + if (rewardType == null) { + rewardType = RewardType.BAGOFGOLD; + } + } + + private void ensureKeyId() { + if (keyId != null && !keyId.isEmpty()) { + return; + } + if (Core.getRewardSecurity() != null && Core.getRewardSecurity().isLoaded()) { + keyId = Core.getRewardSecurity().getActiveKeyId(); + } else { + keyId = "k1"; + } + } + + private void ensureTokenUuid() { + if (tokenUuid == null || tokenUuid.isEmpty()) { + tokenUuid = UUID.randomUUID().toString(); + } + } + + private void updateSignature() { + ensureType(); + ensureTokenUuid(); + ensureKeyId(); + if (Core.getRewardSecurity() != null && Core.getRewardSecurity().isLoaded()) { + signature = Core.getRewardSecurity().sign(keyId, canonicalPayload()); + encodedHash = signature; + } else { + signature = ""; + encodedHash = Strings.encode(makeDecodedHash()); + } + } + + public void rotateTokenAndSign() { + tokenUuid = UUID.randomUUID().toString(); + updateSignature(); } public boolean checkHash() { - if (this.encodedHash != null) + if (tokenUuid != null && !tokenUuid.isEmpty() && signature != null && !signature.isEmpty()) { + if (Core.getRewardSecurity() == null || !Core.getRewardSecurity().isLoaded()) { + return false; + } + return Core.getRewardSecurity().verify(keyId, canonicalPayload(), signature); + } + if (this.encodedHash != null) { return makeDecodedHash().equals(Strings.decode(this.encodedHash)) || makeDecodedHashOld().equals(Strings.decode(this.encodedHash)); - else - return true; + } + return true; } public void updateEncodedHash() { - this.encodedHash = Strings.encode(makeDecodedHash()); + updateSignature(); + } + + private static RewardType parseRewardType(String rewardTypeStr) { + if (rewardTypeStr == null || rewardTypeStr.isEmpty()) { + return RewardType.BAGOFGOLD; + } + try { + return RewardType.valueOf(rewardTypeStr); + } catch (Exception ignored) { + } + + if (RewardType.BAGOFGOLD.getUUID().equals(rewardTypeStr)) + return RewardType.BAGOFGOLD; + if (RewardType.ITEM.getUUID().equals(rewardTypeStr)) + return RewardType.ITEM; + if (RewardType.KILLED.getUUID().equals(rewardTypeStr)) + return RewardType.KILLED; + if (RewardType.KILLER.getUUID().equals(rewardTypeStr)) + return RewardType.KILLER; + + return RewardType.BAGOFGOLD; } public void setReward(List lore) { String moneyStr = "", rewardTypeStr = ""; + tokenUuid = null; + keyId = null; + signature = null; + encodedHash = null; + if (lore == null) { + rewardType = RewardType.BAGOFGOLD; + money = 0; + displayname = Core.getConfigManager() != null ? Core.getConfigManager().bagOfGoldName : "BagOfGold"; + rotateTokenAndSign(); + return; + } + for (int n = 0; n < lore.size(); n++) { String str = lore.get(n); - // DisplayName - if (str.startsWith("Hidden(0):")) - this.displayname = str.substring(10); - - // Money - else if (str.startsWith("Hidden(1):")) { - moneyStr = str.substring(10); // Dont remove this line + if (str.startsWith(LEGACY_0)) { + this.displayname = str.substring(LEGACY_0.length()); + } else if (str.startsWith(LEGACY_1)) { + moneyStr = str.substring(LEGACY_1.length()); this.money = Double.valueOf(moneyStr); - } - - // RewardType - else if (str.startsWith("Hidden(2):")) { - rewardTypeStr = str.substring(10); // Dont remove this line - try { - this.rewardType = RewardType.valueOf(rewardTypeStr); - } catch (Exception e) { - if (RewardType.BAGOFGOLD.getUUID().equals(rewardTypeStr)) - rewardType = RewardType.BAGOFGOLD; - else if (RewardType.ITEM.getUUID().equals(rewardTypeStr)) - rewardType = RewardType.ITEM; - else if (RewardType.KILLED.getUUID().equals(rewardTypeStr)) - rewardType = RewardType.KILLED; - else if (RewardType.KILLER.getUUID().equals(rewardTypeStr)) - rewardType = RewardType.KILLER; - else - rewardType = RewardType.BAGOFGOLD; - } - } - - // Skin UUID - else if (str.startsWith("Hidden(4):")) - this.skinUUID = (str.length() > 10) ? UUID.fromString(str.substring(10)) : null; - - // Hash - else if (str.startsWith("Hidden(5):")) { - this.encodedHash = str.substring(10); + } else if (str.startsWith(LEGACY_2)) { + rewardTypeStr = str.substring(LEGACY_2.length()); + this.rewardType = parseRewardType(rewardTypeStr); + } else if (str.startsWith(LEGACY_4)) { + this.skinUUID = (str.length() > LEGACY_4.length()) ? UUID.fromString(str.substring(LEGACY_4.length())) + : null; + } else if (str.startsWith(LEGACY_5)) { + this.encodedHash = str.substring(LEGACY_5.length()); String compareHash = Strings.encode(moneyStr + rewardTypeStr); if (!encodedHash.equalsIgnoreCase(compareHash)) { Bukkit.getConsoleSender().sendMessage(Core.PREFIX + ChatColor.RED + "[Warning] A player has tried to change the value of a BagOfGold Item. Value set to 0!"); money = 0; - updateEncodedHash(); + encodedHash = Strings.encode(makeDecodedHash()); } } } + if (displayname == null || displayname.isEmpty()) { + displayname = Core.getConfigManager() != null ? Core.getConfigManager().bagOfGoldName : "BagOfGold"; + } + ensureType(); + rotateTokenAndSign(); } public ArrayList getHiddenLore() { - ArrayList lores = new ArrayList(Arrays.asList("Hidden(0):" + displayname, // displayname - "Hidden(1):" + String.format(Locale.ENGLISH, "%.5f", money), // value - "Hidden(2):" + rewardType.getType(), // type - "Hidden(4):" + (skinUUID == null ? "" : skinUUID.toString()), // SkinUUID - "Hidden(5):" + encodedHash)); // Hash + ArrayList lores = new ArrayList(); if (rewardType != RewardType.BAGOFGOLD) lores.add(Core.getMessages().getString("core.reward.lore")); return lores; - } - /** - * @return the displayname - */ public String getDisplayName() { return displayname; } - /** - * @return the money - */ public double getMoney() { return money; } - /** - * @return the uuid - */ public RewardType getRewardType() { return rewardType; } - /** - * @param displayName the displayName to set - */ + public String getTokenUUID() { + return tokenUuid; + } + + public String getKeyId() { + return keyId; + } + + public String getSignature() { + return signature; + } + public void setDisplayname(String displayName) { this.displayname = displayName; + updateSignature(); } - /** - * @param money the money to set - */ public void setMoney(double money) { this.money = money; - updateEncodedHash(); + updateSignature(); } - /** - * @param rewardType the uuid to set - */ public void setRewardType(RewardType rewardType) { this.rewardType = rewardType; - updateEncodedHash(); + updateSignature(); } - /** - * Get the skin UUID for the reward - * - * @return - */ public UUID getSkinUUID() { return skinUUID; } - /** - * Set the skin UUID for the reward - * - * @param skinUUID - */ public void setSkinUUID(UUID skinUUID) { this.skinUUID = skinUUID; + updateSignature(); } - /** - * @return the hash - */ public String getEncodedHash() { return encodedHash; } - /** - * @param hash the hash to set - */ public void setHash(String hash) { this.encodedHash = hash; + this.signature = hash; } public String toString() { return "{Description=" + displayname + ", money=" + String.format(Locale.ENGLISH, "%.5f", money) + ", type=" - + rewardType.getType() + ", Skin=" + skinUUID + ", id=" + id + "}"; + + rewardType.getType() + ", Skin=" + skinUUID + ", token=" + tokenUuid + ", id=" + id + "}"; } public boolean equals(Reward reward) { + if (reward == null) + return false; + if (skinUUID == null && reward.getSkinUUID() != null) + return false; + if (skinUUID != null && !skinUUID.equals(reward.getSkinUUID())) + return false; return displayname.equalsIgnoreCase(reward.getDisplayName()) && money == reward.money - && rewardType == reward.getRewardType() && skinUUID.equals(reward.getSkinUUID()) && checkHash(); + && rewardType == reward.getRewardType() && checkHash(); } public void save(ConfigurationSection section) { @@ -247,32 +336,38 @@ public void save(ConfigurationSection section) { section.set("type", rewardType.getType()); section.set("skinuuid", skinUUID == null ? "" : skinUUID.toString()); section.set("hash", encodedHash == null ? "" : Strings.decode(encodedHash)); + section.set("token_uuid", tokenUuid == null ? "" : tokenUuid); + section.set("token_key_id", keyId == null ? "" : keyId); + section.set("token_sig", signature == null ? "" : signature); } public void read(ConfigurationSection section) throws InvalidConfigurationException { if (section.contains("displayname")) displayname = section.getString("displayname"); else - displayname = section.getString("description"); // old config name + displayname = section.getString("description"); money = Double.valueOf(section.getString("money").replace(",", ".")); if (section.contains("type")) - rewardType = RewardType.valueOf(section.getString("type")); + rewardType = parseRewardType(section.getString("type")); else { - String uuid = section.getString("uuid"); // old config name - if (RewardType.BAGOFGOLD.getUUID().equals(uuid)) - rewardType = RewardType.BAGOFGOLD; - else if (RewardType.ITEM.getUUID().equals(uuid)) - rewardType = RewardType.ITEM; - else if (RewardType.KILLED.getUUID().equals(uuid)) - rewardType = RewardType.KILLED; - else if (RewardType.KILLER.getUUID().equals(uuid)) - rewardType = RewardType.KILLER; - else - rewardType = RewardType.BAGOFGOLD; + String uuid = section.getString("uuid"); + rewardType = parseRewardType(uuid); + } + + skinUUID = null; + String skin = section.getString("skinuuid", ""); + if (skin != null && !skin.isEmpty()) { + skinUUID = UUID.fromString(skin); } - skinUUID = UUID.fromString(section.getString("skinuuid")); + tokenUuid = section.getString("token_uuid", ""); + keyId = section.getString("token_key_id", ""); + signature = section.getString("token_sig", ""); encodedHash = Strings.encode(section.getString("hash", makeDecodedHash())); + if (tokenUuid == null || tokenUuid.isEmpty() || keyId == null || keyId.isEmpty() || signature == null + || signature.isEmpty()) { + rotateTokenAndSign(); + } } public boolean isMoney() { @@ -296,56 +391,102 @@ public boolean isItemReward() { } public static boolean isReward(Item item) { - return item.hasMetadata(MH_REWARD_DATA_NEW) || isReward(item.getItemStack()); + return item != null && (item.hasMetadata(MH_REWARD_DATA_NEW) || isReward(item.getItemStack())); } public static Reward getReward(Item item) { - if (item.hasMetadata(MH_REWARD_DATA_NEW)) + if (item != null && item.hasMetadata(MH_REWARD_DATA_NEW)) { for (MetadataValue mv : item.getMetadata(MH_REWARD_DATA_NEW)) { if (mv.value() instanceof Reward) - return (Reward) item.getMetadata(MH_REWARD_DATA_NEW).get(0).value(); + return (Reward) mv.value(); } - return getReward(item.getItemStack()); + } + return item == null ? null : getReward(item.getItemStack()); } - public static boolean isReward(ItemStack itemStack) { - if (itemStack != null && itemStack.hasItemMeta() && itemStack.getItemMeta().hasLore() - && itemStack.getItemMeta().getLore().size() > 2) { - String lore = itemStack.getItemMeta().getLore().get(2); - if (lore.startsWith("Hidden(2):")) { - lore = lore.substring(10); - return lore.equals(RewardType.BAGOFGOLD.getType()) || lore.equals(RewardType.KILLED.getType()) - || lore.equals(RewardType.KILLER.getType()) || lore.equals(RewardType.ITEM.getType()) - || lore.equals(RewardType.BAGOFGOLD.getUUID()) || lore.equals(RewardType.KILLED.getUUID()) - || lore.equals(RewardType.KILLER.getUUID()) || lore.equals(RewardType.ITEM.getUUID()); - } else - return false; - - } else + private static boolean hasPdcReward(ItemMeta itemMeta) { + if (itemMeta == null) return false; + NamespacedKey versionKey = pdcKey(PDC_VERSION); + NamespacedKey typeKey = pdcKey(PDC_TYPE); + NamespacedKey valueKey = pdcKey(PDC_VALUE); + NamespacedKey tokenKey = pdcKey(PDC_TOKEN); + NamespacedKey sigKey = pdcKey(PDC_SIG); + NamespacedKey keyIdKey = pdcKey(PDC_KEY_ID); + if (versionKey == null || typeKey == null || valueKey == null || tokenKey == null || sigKey == null + || keyIdKey == null) { + return false; + } + PersistentDataContainer pdc = itemMeta.getPersistentDataContainer(); + return pdc.has(versionKey, PersistentDataType.INTEGER) && pdc.has(typeKey, PersistentDataType.STRING) + && pdc.has(valueKey, PersistentDataType.DOUBLE) && pdc.has(tokenKey, PersistentDataType.STRING) + && pdc.has(sigKey, PersistentDataType.STRING) && pdc.has(keyIdKey, PersistentDataType.STRING); } - public static boolean isHead(ItemStack itemStack) { - if (itemStack != null && itemStack.hasItemMeta() && itemStack.getItemMeta().hasLore() - && itemStack.getItemMeta().getLore().size() > 2) { - String lore = itemStack.getItemMeta().getLore().get(2); - if (lore.startsWith("Hidden(2):")) { - lore = lore.substring(10); - return lore.equals(RewardType.KILLED.getType()) || lore.equals(RewardType.KILLER.getType()) - || lore.equals(RewardType.KILLED.getUUID()) || lore.equals(RewardType.KILLER.getUUID()); - } else - return false; + public static boolean isReward(ItemStack itemStack) { + if (itemStack == null || !itemStack.hasItemMeta()) + return false; + return hasPdcReward(itemStack.getItemMeta()); + } - } else + public static boolean isLegacyReward(ItemStack itemStack) { + if (itemStack == null || !itemStack.hasItemMeta()) + return false; + ItemMeta meta = itemStack.getItemMeta(); + if (!meta.hasLore() || meta.getLore() == null || meta.getLore().size() < 3) + return false; + String lore = meta.getLore().get(2); + if (!lore.startsWith(LEGACY_2)) return false; + String type = lore.substring(LEGACY_2.length()); + return type.equals(RewardType.BAGOFGOLD.getType()) || type.equals(RewardType.KILLED.getType()) + || type.equals(RewardType.KILLER.getType()) || type.equals(RewardType.ITEM.getType()) + || type.equals(RewardType.BAGOFGOLD.getUUID()) || type.equals(RewardType.KILLED.getUUID()) + || type.equals(RewardType.KILLER.getUUID()) || type.equals(RewardType.ITEM.getUUID()); + } + + private static Reward fromPdc(ItemStack itemStack) { + if (itemStack == null || !itemStack.hasItemMeta()) + return null; + ItemMeta itemMeta = itemStack.getItemMeta(); + if (!hasPdcReward(itemMeta)) + return null; + + PersistentDataContainer pdc = itemMeta.getPersistentDataContainer(); + Reward reward = new Reward(); + reward.displayname = pdc.getOrDefault(pdcKey(PDC_DISPLAY), PersistentDataType.STRING, + (itemMeta.hasDisplayName() ? ChatColor.stripColor(itemMeta.getDisplayName()) : "BagOfGold")); + reward.money = pdc.getOrDefault(pdcKey(PDC_VALUE), PersistentDataType.DOUBLE, 0D); + reward.rewardType = parseRewardType(pdc.getOrDefault(pdcKey(PDC_TYPE), PersistentDataType.STRING, + RewardType.BAGOFGOLD.getType())); + String skin = pdc.getOrDefault(pdcKey(PDC_SKIN_UUID), PersistentDataType.STRING, ""); + reward.skinUUID = skin.isEmpty() ? null : UUID.fromString(skin); + reward.tokenUuid = pdc.getOrDefault(pdcKey(PDC_TOKEN), PersistentDataType.STRING, ""); + reward.keyId = pdc.getOrDefault(pdcKey(PDC_KEY_ID), PersistentDataType.STRING, ""); + reward.signature = pdc.getOrDefault(pdcKey(PDC_SIG), PersistentDataType.STRING, ""); + reward.encodedHash = reward.signature; + return reward; } public static Reward getReward(ItemStack itemStack) { + return fromPdc(itemStack); + } + + public static Reward getLegacyReward(ItemStack itemStack) { + if (!isLegacyReward(itemStack)) + return null; return new Reward(itemStack.getItemMeta().getLore()); } + public static ItemStack migrateLegacyReward(ItemStack itemStack) { + Reward legacy = getLegacyReward(itemStack); + if (legacy == null) + return itemStack; + return setDisplayNameAndHiddenLores(itemStack, legacy); + } + public static boolean isReward(Block block) { - return block.hasMetadata(MH_REWARD_DATA_NEW); + return block != null && block.hasMetadata(MH_REWARD_DATA_NEW); } public static Reward getReward(Block block) { @@ -353,24 +494,39 @@ public static Reward getReward(Block block) { } public static boolean isReward(Entity entity) { - return entity.hasMetadata(MH_REWARD_DATA_NEW); + return entity != null && entity.hasMetadata(MH_REWARD_DATA_NEW); } public static Reward getReward(Entity entity) { return (Reward) entity.getMetadata(MH_REWARD_DATA_NEW).get(0).value(); } + private static void writeRewardPdc(ItemMeta itemMeta, Reward reward) { + PersistentDataContainer pdc = itemMeta.getPersistentDataContainer(); + pdc.set(pdcKey(PDC_VERSION), PersistentDataType.INTEGER, TOKEN_VERSION); + pdc.set(pdcKey(PDC_TYPE), PersistentDataType.STRING, + reward.getRewardType() == null ? RewardType.BAGOFGOLD.getType() : reward.getRewardType().getType()); + pdc.set(pdcKey(PDC_VALUE), PersistentDataType.DOUBLE, reward.getMoney()); + pdc.set(pdcKey(PDC_TOKEN), PersistentDataType.STRING, reward.getTokenUUID()); + pdc.set(pdcKey(PDC_SIG), PersistentDataType.STRING, reward.getSignature()); + pdc.set(pdcKey(PDC_KEY_ID), PersistentDataType.STRING, reward.getKeyId()); + pdc.set(pdcKey(PDC_DISPLAY), PersistentDataType.STRING, + reward.getDisplayName() == null ? "" : reward.getDisplayName()); + pdc.set(pdcKey(PDC_SKIN_UUID), PersistentDataType.STRING, + reward.getSkinUUID() == null ? "" : reward.getSkinUUID().toString()); + } + /** - * setDisplayNameAndHiddenLores: add the Display name and the (hidden) Lores. - * The lores identifies the reward and contain secret information. - * - * @param skull - The base itemStack without the information. - * @param reward - The reward information is added to the ItemStack - * @return the updated ItemStack. + * setDisplayNameAndHiddenLores: add the Display name and secure metadata. */ public static ItemStack setDisplayNameAndHiddenLores(ItemStack skull, Reward reward) { ItemMeta skullMeta = skull.getItemMeta(); - skullMeta.setLore(reward.getHiddenLore()); + if (skullMeta == null) { + return skull; + } + + reward.rotateTokenAndSign(); + skullMeta.setLore(null); if (reward.getRewardType() == RewardType.BAGOFGOLD) { skullMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', @@ -379,9 +535,8 @@ public static ItemStack setDisplayNameAndHiddenLores(ItemStack skull, Reward rew } else if (reward.getRewardType() == RewardType.ITEM) if (reward.getMoney() == 0) - skullMeta.setDisplayName( - ChatColor.translateAlternateColorCodes('&', Core.getConfigManager().itemDisplayNameFormatNoValue - .replace("{name}", reward.getDisplayName()))); + skullMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', + Core.getConfigManager().itemDisplayNameFormatNoValue.replace("{name}", reward.getDisplayName()))); else skullMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', Core.getConfigManager().itemDisplayNameFormat.replace("{name}", reward.getDisplayName()) @@ -406,33 +561,67 @@ else if (reward.getRewardType() == RewardType.KILLER) skullMeta.setDisplayName(ChatColor.translateAlternateColorCodes('&', Core.getConfigManager().killerHeadDisplayNameFormat.replace("{name}", reward.getDisplayName()) .replace("{value}", Tools.format(reward.getMoney())))); + + writeRewardPdc(skullMeta, reward); skull.setItemMeta(skullMeta); return skull; } public static ItemStack setDisplayNameAndHiddenLores(ItemStack skull, String name, double value, List lores) { - ItemMeta skullMeta = skull.getItemMeta(); - skullMeta.setLore(lores); - skullMeta.setDisplayName( - ChatColor.translateAlternateColorCodes('&', Core.getConfigManager().bagOfGoldDisplayNameFormat - .replace("{name}", name).replace("{value}", Tools.format(value)))); - skull.setItemMeta(skullMeta); - return skull; + RewardType rewardType = RewardType.BAGOFGOLD; + UUID skinUuid = null; + List visibleLore = new ArrayList(); + if (lores != null) { + for (String line : lores) { + if (line == null) + continue; + if (line.startsWith(LEGACY_2)) { + rewardType = parseRewardType(line.substring(LEGACY_2.length())); + } else if (line.startsWith(LEGACY_4)) { + String raw = line.substring(LEGACY_4.length()); + if (!raw.isEmpty()) { + skinUuid = UUID.fromString(raw); + } + } else if (!line.trim().startsWith("Hidden(")) { + visibleLore.add(line); + } + } + } + + Reward reward = new Reward(name, value, rewardType, skinUuid); + ItemStack updated = setDisplayNameAndHiddenLores(skull, reward); + if (!visibleLore.isEmpty()) { + ItemMeta skullMeta = updated.getItemMeta(); + if (skullMeta != null) { + skullMeta.setLore(visibleLore); + updated.setItemMeta(skullMeta); + } + } + return updated; } public static boolean isFakeReward(Item item) { - ItemStack itemStack = item.getItemStack(); + ItemStack itemStack = item == null ? null : item.getItemStack(); return isFakeReward(itemStack); } public static boolean isFakeReward(ItemStack itemStack) { if (itemStack != null && itemStack.hasItemMeta() && itemStack.getItemMeta().hasDisplayName() && itemStack.getItemMeta().getDisplayName().contains(Core.getConfigManager().bagOfGoldName)) { - if (!itemStack.getItemMeta().hasLore()) { - return true; - } + return !isReward(itemStack); } return false; } + + public static boolean isHead(ItemStack itemStack) { + if (!isReward(itemStack)) { + return false; + } + Reward reward = getReward(itemStack); + if (reward == null) { + return false; + } + return reward.isKilledHeadReward() || reward.isKillerHeadReward(); + } } diff --git a/src/one/lindegaard/CustomItemsLib/rewards/RewardSecurity.java b/src/one/lindegaard/CustomItemsLib/rewards/RewardSecurity.java new file mode 100644 index 0000000..d9cf3f1 --- /dev/null +++ b/src/one/lindegaard/CustomItemsLib/rewards/RewardSecurity.java @@ -0,0 +1,139 @@ +package one.lindegaard.CustomItemsLib.rewards; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import one.lindegaard.CustomItemsLib.Core; + +public class RewardSecurity { + + private static final int SECRET_BYTES = 32; + private static final String HMAC_ALGORITHM = "HmacSHA256"; + + private final Core plugin; + private final File file; + + private final Map keys = new HashMap<>(); + private String activeKeyId = "k1"; + private boolean loaded = false; + + public RewardSecurity(Core plugin) { + this.plugin = plugin; + this.file = new File(plugin.getDataFolder(), "security.yml"); + } + + public synchronized void initialize() throws IOException { + if (!file.exists()) { + createDefaultConfig(); + } + + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + activeKeyId = config.getString("hmac.active-key-id", "k1"); + + keys.clear(); + ConfigurationSection sec = config.getConfigurationSection("hmac.keys"); + if (sec != null) { + Set ids = sec.getKeys(false); + for (String id : ids) { + String encoded = sec.getString(id, "").trim(); + if (!encoded.isEmpty()) { + keys.put(id, Base64.getDecoder().decode(encoded)); + } + } + } + + if (keys.isEmpty()) { + createDefaultConfig(); + config = YamlConfiguration.loadConfiguration(file); + activeKeyId = config.getString("hmac.active-key-id", "k1"); + sec = config.getConfigurationSection("hmac.keys"); + if (sec != null) { + for (String id : sec.getKeys(false)) { + String encoded = sec.getString(id, "").trim(); + if (!encoded.isEmpty()) { + keys.put(id, Base64.getDecoder().decode(encoded)); + } + } + } + } + + if (!keys.containsKey(activeKeyId)) { + byte[] randomSecret = randomSecret(); + keys.put(activeKeyId, randomSecret); + config.set("hmac.keys." + activeKeyId, Base64.getEncoder().encodeToString(randomSecret)); + config.save(file); + } + + loaded = true; + } + + private void createDefaultConfig() throws IOException { + YamlConfiguration config = new YamlConfiguration(); + config.set("security-format-version", 1); + config.set("hmac.active-key-id", "k1"); + config.set("hmac.accept-legacy-keys", true); + config.set("hmac.keys.k1", Base64.getEncoder().encodeToString(randomSecret())); + config.save(file); + } + + private byte[] randomSecret() { + SecureRandom secureRandom = new SecureRandom(); + byte[] bytes = new byte[SECRET_BYTES]; + secureRandom.nextBytes(bytes); + return bytes; + } + + public synchronized boolean isLoaded() { + return loaded; + } + + public synchronized String getActiveKeyId() { + return activeKeyId; + } + + public synchronized String sign(String keyId, String payload) { + if (!loaded || payload == null || keyId == null) { + return ""; + } + byte[] key = keys.get(keyId); + if (key == null) { + return ""; + } + + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + mac.init(new SecretKeySpec(key, HMAC_ALGORITHM)); + byte[] signed = mac.doFinal(payload.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(signed); + } catch (GeneralSecurityException ex) { + return ""; + } + } + + public synchronized boolean verify(String keyId, String payload, String signature) { + if (!loaded || keyId == null || payload == null || signature == null || signature.isEmpty()) { + return false; + } + + String expected = sign(keyId, payload); + if (expected.isEmpty()) { + return false; + } + + return MessageDigest.isEqual(expected.getBytes(java.nio.charset.StandardCharsets.UTF_8), + signature.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } +} diff --git a/src/one/lindegaard/CustomItemsLib/rewards/TokenSpendStore.java b/src/one/lindegaard/CustomItemsLib/rewards/TokenSpendStore.java new file mode 100644 index 0000000..055f340 --- /dev/null +++ b/src/one/lindegaard/CustomItemsLib/rewards/TokenSpendStore.java @@ -0,0 +1,144 @@ +package one.lindegaard.CustomItemsLib.rewards; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.UUID; + +import com.mysql.cj.jdbc.MysqlDataSource; + +import one.lindegaard.CustomItemsLib.Core; + +public class TokenSpendStore { + + public enum MarkResult { + MARKED, + DUPLICATE, + ERROR + } + + private final Core plugin; + + public TokenSpendStore(Core plugin) { + this.plugin = plugin; + } + + public void initialize() throws Exception { + try (Connection connection = openConnection(); Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE IF NOT EXISTS mh_spent_tokens (" + + " token_uuid VARCHAR(64) NOT NULL PRIMARY KEY," + + " player_uuid VARCHAR(36)," + + " source VARCHAR(64)," + + " amount DOUBLE," + + " spent_at BIGINT NOT NULL" + + ")"); + } + } + + public MarkResult markTokenSpent(String tokenUuid, UUID playerUuid, String source, double amount) { + if (tokenUuid == null || tokenUuid.isEmpty()) { + return MarkResult.ERROR; + } + + String sql; + boolean mysql = isMySQL(); + if (mysql) { + sql = "INSERT IGNORE INTO mh_spent_tokens(token_uuid, player_uuid, source, amount, spent_at) VALUES(?,?,?,?,?)"; + } else { + sql = "INSERT OR IGNORE INTO mh_spent_tokens(token_uuid, player_uuid, source, amount, spent_at) VALUES(?,?,?,?,?)"; + } + + try (Connection connection = openConnection(); PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, tokenUuid); + statement.setString(2, playerUuid == null ? null : playerUuid.toString()); + statement.setString(3, source == null ? "unknown" : source); + statement.setDouble(4, amount); + statement.setLong(5, System.currentTimeMillis()); + + int updated = statement.executeUpdate(); + return updated > 0 ? MarkResult.MARKED : MarkResult.DUPLICATE; + } catch (SQLException ex) { + if (mysql && isDuplicateKey(ex)) { + return MarkResult.DUPLICATE; + } + Core.getMessages().debug("TokenSpendStore markTokenSpent failed for %s: %s", tokenUuid, ex.getMessage()); + return MarkResult.ERROR; + } catch (Exception ex) { + Core.getMessages().debug("TokenSpendStore markTokenSpent failed for %s: %s", tokenUuid, ex.getMessage()); + return MarkResult.ERROR; + } + } + + public boolean isTokenSpent(String tokenUuid) { + if (tokenUuid == null || tokenUuid.isEmpty()) { + return false; + } + + try (Connection connection = openConnection(); + PreparedStatement statement = connection + .prepareStatement("SELECT 1 FROM mh_spent_tokens WHERE token_uuid=? LIMIT 1")) { + statement.setString(1, tokenUuid); + try (ResultSet rs = statement.executeQuery()) { + return rs.next(); + } + } catch (Exception ex) { + Core.getMessages().debug("TokenSpendStore isTokenSpent failed for %s: %s", tokenUuid, ex.getMessage()); + return false; + } + } + + public boolean revokeToken(String tokenUuid, String source) { + return markTokenSpent(tokenUuid, null, source == null ? "manual-revoke" : source, 0D) == MarkResult.MARKED; + } + + public int countTokens() { + try (Connection connection = openConnection(); + Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT COUNT(*) FROM mh_spent_tokens")) { + if (rs.next()) { + return rs.getInt(1); + } + return 0; + } catch (Exception ex) { + Core.getMessages().debug("TokenSpendStore countTokens failed: %s", ex.getMessage()); + return 0; + } + } + + private boolean isMySQL() { + return Core.getConfigManager().databaseType != null + && Core.getConfigManager().databaseType.equalsIgnoreCase("mysql"); + } + + private boolean isDuplicateKey(SQLException ex) { + return ex.getErrorCode() == 1062 || (ex.getSQLState() != null && ex.getSQLState().startsWith("23")); + } + + private Connection openConnection() throws Exception { + if (isMySQL()) { + Class.forName("com.mysql.cj.jdbc.Driver"); + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUser(Core.getConfigManager().databaseUsername); + dataSource.setPassword(Core.getConfigManager().databasePassword); + if (Core.getConfigManager().databaseHost.contains(":")) { + dataSource.setServerName(Core.getConfigManager().databaseHost.split(":")[0]); + dataSource.setPort(Integer.parseInt(Core.getConfigManager().databaseHost.split(":")[1])); + } else { + dataSource.setServerName(Core.getConfigManager().databaseHost); + } + dataSource.setDatabaseName(Core.getConfigManager().databaseName); + Connection connection = dataSource.getConnection(); + connection.setAutoCommit(true); + return connection; + } + + Connection connection = DriverManager + .getConnection("jdbc:sqlite:" + plugin.getDataFolder().getPath() + "/" + Core.getConfigManager().databaseName + + ".db"); + connection.setAutoCommit(true); + return connection; + } +} diff --git a/src/one/lindegaard/CustomItemsLib/server/Servers.java b/src/one/lindegaard/CustomItemsLib/server/Servers.java index 3a4aa1c..34383bb 100644 --- a/src/one/lindegaard/CustomItemsLib/server/Servers.java +++ b/src/one/lindegaard/CustomItemsLib/server/Servers.java @@ -4,180 +4,183 @@ public class Servers { + private static final VersionInfo SERVER_VERSION = detectServerVersion(); + + private static final class VersionInfo { + private final int major; + private final int minor; + private final int patch; + private final boolean valid; + + private VersionInfo(int major, int minor, int patch, boolean valid) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.valid = valid; + } + } + + private static VersionInfo detectServerVersion() { + try { + String bukkitVersion = Bukkit.getBukkitVersion(); + String coreVersion = bukkitVersion.split("-", 2)[0]; + String[] numbers = coreVersion.split("\\."); + if (numbers.length < 2) { + return new VersionInfo(0, 0, 0, false); + } + + int major = Integer.parseInt(numbers[0]); + int minor = Integer.parseInt(numbers[1]); + int patch = numbers.length >= 3 ? Integer.parseInt(numbers[2]) : 0; + return new VersionInfo(major, minor, patch, true); + } catch (Exception ignored) { + return new VersionInfo(0, 0, 0, false); + } + } + + private static boolean isVersion(int major, int minor) { + return SERVER_VERSION.valid && SERVER_VERSION.major == major && SERVER_VERSION.minor == minor; + } + + private static boolean isVersion(int major, int minor, int patch) { + return SERVER_VERSION.valid && SERVER_VERSION.major == major && SERVER_VERSION.minor == minor + && SERVER_VERSION.patch == patch; + } + + private static boolean isAtLeast(int major, int minor) { + return isAtLeast(major, minor, 0); + } + + private static boolean isAtLeast(int major, int minor, int patch) { + // Fail-open for unknown version formats to preserve legacy behavior. + if (!SERVER_VERSION.valid) + return true; + + if (SERVER_VERSION.major != major) + return SERVER_VERSION.major > major; + if (SERVER_VERSION.minor != minor) + return SERVER_VERSION.minor > minor; + return SERVER_VERSION.patch >= patch; + } + // ******************************************************************* // Version detection // ******************************************************************* public static boolean isMC121() { - return Bukkit.getBukkitVersion().contains("1.21"); + return isVersion(1, 21); } public static boolean isMC120() { - return Bukkit.getBukkitVersion().contains("1.20"); + return isVersion(1, 20); } public static boolean isMC119() { - return Bukkit.getBukkitVersion().contains("1.19"); + return isVersion(1, 19); } public static boolean isMC118() { - return Bukkit.getBukkitVersion().contains("1.18"); + return isVersion(1, 18); } public static boolean isMC117() { - return Bukkit.getBukkitVersion().contains("1.17"); + return isVersion(1, 17); } public static boolean isMC1162() { - return Bukkit.getBukkitVersion().contains("1.16.2"); + return isVersion(1, 16, 2); } public static boolean isMC116() { - return Bukkit.getBukkitVersion().contains("1.16"); + return isVersion(1, 16); } public static boolean isMC115() { - return Bukkit.getBukkitVersion().contains("1.15"); + return isVersion(1, 15); } public static boolean isMC114() { - return Bukkit.getBukkitVersion().contains("1.14"); + return isVersion(1, 14); } public static boolean isMC113() { - return Bukkit.getBukkitVersion().contains("1.13"); + return isVersion(1, 13); } public static boolean isMC112() { - return Bukkit.getBukkitVersion().contains("1.12"); + return isVersion(1, 12); } public static boolean isMC111() { - return Bukkit.getBukkitVersion().contains("1.11"); + return isVersion(1, 11); } public static boolean isMC110() { - return Bukkit.getBukkitVersion().contains("1.10"); + return isVersion(1, 10); } public static boolean isMC19() { - return Bukkit.getBukkitVersion().matches("1\\.9[^0-9].*"); + return isVersion(1, 9); } public static boolean isMC18() { - return Bukkit.getBukkitVersion().matches("1\\.8[^0-9].*"); + return isVersion(1, 8); } public static boolean isMC121OrNewer() { - if (isMC121()) - return true; - else if (isMC120() || isMC119() || isMC118() || isMC117() || isMC1162() || isMC116() || isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 21); } public static boolean isMC120OrNewer() { - if (isMC120()) - return true; - else if (isMC119() || isMC118() || isMC117() || isMC1162() || isMC116() || isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 20); } public static boolean isMC119OrNewer() { - if (isMC119()) - return true; - else if (isMC118() || isMC117() || isMC1162() || isMC116() || isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 19); } public static boolean isMC118OrNewer() { - if (isMC118()) - return true; - else if (isMC117() || isMC1162() || isMC116() || isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 18); } public static boolean isMC117OrNewer() { - if (isMC117()) - return true; - else if (isMC1162() || isMC116() || isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 17); } public static boolean isMC1162OrNewer() { - if (isMC1162()) - return true; - else if (isMC116() || isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 16, 2); } public static boolean isMC116OrNewer() { - if (isMC116()) - return true; - else if (isMC115() || isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 16); } public static boolean isMC115OrNewer() { - if (isMC115()) - return true; - else if (isMC114() || isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 15); } public static boolean isMC114OrNewer() { - if (isMC114()) - return true; - else if (isMC113() || isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 14); } public static boolean isMC113OrNewer() { - if (isMC113()) - return true; - else if (isMC112() || isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 13); } public static boolean isMC112OrNewer() { - if (isMC112()) - return true; - else if (isMC111() || isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 12); } public static boolean isMC111OrNewer() { - if (isMC111()) - return true; - else if (isMC110() || isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 11); } public static boolean isMC110OrNewer() { - if (isMC110()) - return true; - else if (isMC19() || isMC18()) - return false; - return true; + return isAtLeast(1, 10); } public static boolean isMC19OrNewer() { - if (isMC19()) - return true; - else if (isMC18()) - return false; - return true; + return isAtLeast(1, 9); } // ******************************************************************* @@ -187,22 +190,58 @@ public static boolean isGlowstoneServer() { return Bukkit.getServer().getName().equalsIgnoreCase("Glowstone"); } - public static boolean isPaperServer() { - return Bukkit.getServer().getName().equalsIgnoreCase("Paper") - && Bukkit.getServer().getVersion().toLowerCase().contains("paper"); - } + private static String serverNameLower() { + return Bukkit.getServer().getName().toLowerCase(); + } - public static boolean isPurpurServer() { - return Bukkit.getServer().getName().equalsIgnoreCase("Purpur") - && Bukkit.getServer().getVersion().toLowerCase().contains("purpur"); - } + private static String serverVersionLower() { + return Bukkit.getServer().getVersion().toLowerCase(); + } + + private static boolean classExists(String className) { + try { + Class.forName(className, false, Bukkit.getServer().getClass().getClassLoader()); + return true; + } catch (Throwable ignored) { + return false; + } + } + + public static boolean isPaperServer() { + String name = serverNameLower(); + String version = serverVersionLower(); + + if (name.contains("paper") || version.contains("paper")) + return true; + + // Support both legacy and modern Paper package names. + return classExists("com.destroystokyo.paper.PaperConfig") + || classExists("io.papermc.paper.configuration.GlobalConfiguration"); + } + + public static boolean isPurpurServer() { + String name = serverNameLower(); + String version = serverVersionLower(); + + if (name.contains("purpur") || version.contains("purpur")) + return true; + + return classExists("org.purpurmc.purpur.PurpurConfig"); + } public static boolean isSpigotServer() { - return Bukkit.getServer().getName().equalsIgnoreCase("CraftBukkit") - && Bukkit.getServer().getVersion().toLowerCase().contains("spigot"); + if (isPaperServer() || isPurpurServer()) + return false; + + String name = serverNameLower(); + String version = serverVersionLower(); + return name.contains("spigot") || (name.contains("craftbukkit") && version.contains("spigot")); } public static boolean isCraftBukkitServer() { + if (isPaperServer() || isPurpurServer() || isSpigotServer()) + return false; + return Bukkit.getServer().getName().equalsIgnoreCase("CraftBukkit") && Bukkit.getServer().getVersion().toLowerCase().contains("bukkit"); } diff --git a/src/one/lindegaard/CustomItemsLib/storage/DataStoreManager.java b/src/one/lindegaard/CustomItemsLib/storage/DataStoreManager.java index be3b2be..22351b8 100644 --- a/src/one/lindegaard/CustomItemsLib/storage/DataStoreManager.java +++ b/src/one/lindegaard/CustomItemsLib/storage/DataStoreManager.java @@ -1,6 +1,7 @@ package one.lindegaard.CustomItemsLib.storage; import java.util.LinkedHashSet; +import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -92,14 +93,45 @@ public OfflinePlayer getPlayerByName(String name) { * @throws UserNotFoundException */ public int getPlayerId(OfflinePlayer offlinePlayer) throws UserNotFoundException { + if (offlinePlayer == null) + return 0; + + try { + int playerId = mStore.getPlayerId(offlinePlayer); + if (playerId > 0) + return playerId; + } catch (DataStoreException e) { + if (Core.getConfigManager().debug) + e.printStackTrace(); + } + try { - return mStore.getPlayerId(offlinePlayer); + PlayerSettings playerSettings; + try { + playerSettings = mStore.loadPlayerSettings(offlinePlayer); + } catch (UserNotFoundException e) { + String worldgroup = offlinePlayer.isOnline() + ? Core.getWorldGroupManager().getCurrentWorldGroup(offlinePlayer) + : Core.getWorldGroupManager().getDefaultWorldgroup(); + playerSettings = new PlayerSettings(offlinePlayer, worldgroup, Core.getConfigManager().learningMode, false, + null, null, System.currentTimeMillis(), System.currentTimeMillis()); + } + + Set playerDataSet = new LinkedHashSet(); + playerDataSet.add(playerSettings); + mStore.savePlayerSettings(playerDataSet, false); + + int recoveredPlayerId = mStore.getPlayerId(offlinePlayer); + if (recoveredPlayerId > 0) + return recoveredPlayerId; } catch (DataStoreException e) { if (Core.getConfigManager().debug) e.printStackTrace(); } + throw new UserNotFoundException( - Core.PREFIX + " User " + offlinePlayer.getName() + " is not present in Core database"); + Core.PREFIX + " User " + offlinePlayer.getName() + " (" + offlinePlayer.getUniqueId() + + ") is not present in Core database"); } public OfflinePlayer getPlayerByPlayerId(int playerId) throws UserNotFoundException {