package cn.infstar.magicgem.item; import cn.infstar.magicgem.gem.GemDefinition; import cn.infstar.magicgem.gem.GemRegistry; import cn.infstar.magicgem.migration.LegacyItemMigrator; import cn.infstar.magicgem.util.PotionTypes; import cn.infstar.magicgem.util.MaterialResolver; import de.tr7zw.changeme.nbtapi.NBT; import org.bukkit.ChatColor; import org.bukkit.Bukkit; import org.bukkit.Color; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import com.destroystokyo.paper.profile.ProfileProperty; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; public final class ItemCodec { private final GemRegistry registry; private final NamespacedKey gemNameKey; private final NamespacedKey dataVersionKey; private final NamespacedKey embeddedKey; private final NamespacedKey skillsKey; private final LegacyItemMigrator legacyMigrator; public ItemCodec(JavaPlugin plugin, GemRegistry registry) { this.registry = registry; this.gemNameKey = new NamespacedKey(plugin, "gem_name"); this.dataVersionKey = new NamespacedKey(plugin, "data_version"); this.embeddedKey = new NamespacedKey(plugin, "embedded_gems"); this.skillsKey = new NamespacedKey(plugin, "skills"); this.legacyMigrator = new LegacyItemMigrator(registry); } public ItemStack createGem(GemDefinition gem, int amount) { ItemStack item = createItem(new ItemAppearance(gem.material().name(), amount, gem.display(), gem.tips(), gem.texture(), gem.color(), gem.effects(), gem.customModelData(), gem.glow())); ItemMeta meta = item.getItemMeta(); meta.getPersistentDataContainer().set(gemNameKey, PersistentDataType.STRING, gem.name()); meta.getPersistentDataContainer().set(dataVersionKey, PersistentDataType.INTEGER, 1); item.setItemMeta(meta); NBT.modify(item, nbt -> { nbt.setString(LegacyItemMigrator.GEM_NAME_KEY, gem.name()); }); return item; } public ItemStack createItem(ItemAppearance appearance) { String materialName = appearance.material() == null || appearance.material().isBlank() ? appearance.texture() == null || appearance.texture().isBlank() ? "STONE" : "PLAYER_HEAD" : appearance.material(); Material material = MaterialResolver.resolve(materialName); if (material == null) throw new IllegalArgumentException("未知材料: " + materialName); int amount = Math.max(1, Math.min(appearance.amount(), material.getMaxStackSize())); ItemStack item = new ItemStack(material, amount); ItemMeta meta = item.getItemMeta(); if (appearance.display() != null && !appearance.display().isBlank()) meta.setDisplayName(color(appearance.display())); if (appearance.lore() != null && !appearance.lore().isEmpty()) meta.setLore(appearance.lore().stream().map(this::color).toList()); if (appearance.customModelData() != null) meta.setCustomModelData(appearance.customModelData()); if (meta instanceof SkullMeta skull && appearance.texture() != null && !appearance.texture().isBlank()) { var profile = Bukkit.createProfile(UUID.randomUUID()); profile.setProperty(new ProfileProperty("textures", appearance.texture())); skull.setPlayerProfile(profile); } if (meta instanceof PotionMeta potion) { if (appearance.color() != null) potion.setColor(Color.fromRGB(appearance.color() & 0xFFFFFF)); addPotionEffects(potion, appearance.effects()); } if (appearance.glow()) { meta.addEnchant(Enchantment.UNBREAKING, 1, true); meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); } item.setItemMeta(meta); return item; } private void addPotionEffects(PotionMeta potion, String effects) { if (effects == null || effects.isBlank()) return; for (String encoded : effects.split(";")) { String[] parts = encoded.trim().split(":"); if (parts.length == 0) continue; PotionEffectType type = PotionTypes.resolve(parts[0]); if (type == null) continue; int duration = parts.length > 1 ? parseInt(parts[1], 1) * 20 : 20; int level = parts.length > 2 ? parseInt(parts[2], 1) : 1; potion.addCustomEffect(new PotionEffect(type, duration, Math.max(0, level - 1)), true); } } public static Integer parseColor(Object value) { if (value instanceof Number number) return number.intValue(); if (value == null || value.toString().isBlank()) return null; return switch (value.toString().toUpperCase()) { case "WHITE" -> 0xFFFFFF; case "SILVER" -> 0xC0C0C0; case "GRAY" -> 0x808080; case "BLACK" -> 0x000000; case "RED" -> 0xFF0000; case "MAROON" -> 0x800000; case "YELLOW" -> 0xFFFF00; case "OLIVE" -> 0x808000; case "LIME" -> 0x00FF00; case "GREEN" -> 0x008000; case "AQUA" -> 0x00FFFF; case "TEAL" -> 0x008080; case "BLUE" -> 0x0000FF; case "NAVY" -> 0x000080; case "FUCHSIA" -> 0xFF00FF; case "PURPLE" -> 0x800080; case "ORANGE" -> 0xFFA500; case "GOLD" -> 0xFFD700; default -> { try { yield Integer.decode(value.toString()); } catch (NumberFormatException ignored) { yield null; } } }; } public record ItemAppearance(String material, int amount, String display, List lore, String texture, Integer color, String effects, Integer customModelData, boolean glow) { } private int parseInt(String value, int fallback) { try { return Integer.parseInt(value); } catch (NumberFormatException ignored) { return fallback; } } public Optional readGem(ItemStack item, boolean allowLore) { if (item == null || item.getType().isAir() || !item.hasItemMeta()) return Optional.empty(); ItemMeta meta = item.getItemMeta(); String name = meta.getPersistentDataContainer().get(gemNameKey, PersistentDataType.STRING); Optional direct = registry.find(name); if (direct.isPresent()) return direct; LegacyItemMigrator.LegacyData legacy = legacyMigrator.inspect(item); if (legacy.identity() != null) { Optional migrated = registry.find(legacy.identity()); if (migrated.isPresent()) { meta.getPersistentDataContainer().set(gemNameKey, PersistentDataType.STRING, migrated.get().name()); meta.getPersistentDataContainer().set(dataVersionKey, PersistentDataType.INTEGER, 1); item.setItemMeta(meta); return migrated; } } if (!allowLore || !meta.hasLore()) return Optional.empty(); List lore = meta.getLore(); if (lore == null) return Optional.empty(); for (GemDefinition gem : registry.all()) { if (gem.loreIdentity() == null) continue; String expected = strip(gem.loreIdentity()); if (lore.stream().map(this::strip).anyMatch(line -> line.contains(expected))) return Optional.of(gem); } return Optional.empty(); } public Map embedded(ItemStack item) { if (item == null || !item.hasItemMeta()) return Map.of(); PersistentDataContainer data = item.getItemMeta().getPersistentDataContainer(); String encoded = data.get(embeddedKey, PersistentDataType.STRING); Map result = new LinkedHashMap<>(); if (encoded == null || encoded.isBlank()) { LegacyItemMigrator.LegacyData legacy = legacyMigrator.inspect(item); if (!legacy.embedded().isEmpty()) { writeEmbedded(item, legacy.embedded()); return new LinkedHashMap<>(legacy.embedded()); } return result; } for (String line : encoded.split("\\n")) { int separator = line.lastIndexOf('='); if (separator < 0) continue; try { result.put(URLDecoder.decode(line.substring(0, separator), StandardCharsets.UTF_8), Integer.parseInt(line.substring(separator + 1))); } catch (IllegalArgumentException ignored) { } } return result; } public ItemStack setEmbedded(ItemStack item, Map gems) { ItemStack result = item.clone(); writeEmbedded(result, gems); return result; } private void writeEmbedded(ItemStack item, Map gems) { ItemMeta meta = item.getItemMeta(); StringBuilder encoded = new StringBuilder(); gems.forEach((name, count) -> encoded.append(URLEncoder.encode(name, StandardCharsets.UTF_8)) .append('=').append(count).append('\n')); meta.getPersistentDataContainer().set(embeddedKey, PersistentDataType.STRING, encoded.toString()); meta.getPersistentDataContainer().set(dataVersionKey, PersistentDataType.INTEGER, 1); item.setItemMeta(meta); NBT.modify(item, nbt -> { String legacyKey = LegacyItemMigrator.EMBEDDED_GEMS_KEY; if (gems.isEmpty()) { nbt.removeKey(legacyKey); return; } var legacy = nbt.getOrCreateCompound(legacyKey); for (String name : legacy.getKeys()) legacy.removeKey(name); gems.forEach((name, count) -> { if (count != null && count > 0) legacy.setInteger(name, count); }); if (legacy.getKeys().isEmpty()) nbt.removeKey(legacyKey); }); } public List skills(ItemStack item) { if (item == null || !item.hasItemMeta()) return new ArrayList<>(); PersistentDataContainer data = item.getItemMeta().getPersistentDataContainer(); String value = data.get(skillsKey, PersistentDataType.STRING); if (value == null || value.isBlank()) { List legacy = new ArrayList<>(legacyMigrator.inspect(item).skills()); if (!legacy.isEmpty()) writeSkills(item, legacy); return legacy; } return new ArrayList<>(List.of(value.split("\\n"))); } public ItemStack setSkills(ItemStack item, List skills) { ItemStack result = item.clone(); writeSkills(result, skills); return result; } private void writeSkills(ItemStack item, List skills) { ItemMeta meta = item.getItemMeta(); PersistentDataContainer data = meta.getPersistentDataContainer(); data.set(skillsKey, PersistentDataType.STRING, String.join("\n", skills)); item.setItemMeta(meta); NBT.modify(item, nbt -> { String legacyKey = LegacyItemMigrator.SKILLS_KEY; nbt.removeKey(legacyKey); if (!skills.isEmpty()) { var legacy = nbt.getStringList(legacyKey); skills.forEach(legacy::add); } }); } public String color(String value) { return ChatColor.translateAlternateColorCodes('&', value == null ? "" : value); } public String strip(String value) { return ChatColor.stripColor(color(value)); } }