Fix migrated gem voucher redemption

This commit is contained in:
Coldsmiles
2026-10-20 02:10:54 +08:00
parent 5cd6ae121f
commit 778ba65a65
16 changed files with 217 additions and 51 deletions
@@ -96,10 +96,10 @@ public class MagicGemPlugin extends JavaPlugin {
} }
private void logIntegrationStatus() { private void logIntegrationStatus() {
logOptionalPlugin("PlaceholderAPI", "api=" + (placeholders.available() ? "available" : "unavailable")); logOptionalPlugin("PlaceholderAPI", "接口=" + (placeholders.available() ? "可用" : "不可用"));
logOptionalPlugin("Vault", "economy=" + (economy.available() ? "available" : "unavailable")); logOptionalPlugin("Vault", "经济接口=" + (economy.available() ? "可用" : "不可用"));
logOptionalPlugin("PlayerPoints", "api=" + (points.available() ? "available" : "unavailable")); logOptionalPlugin("PlayerPoints", "接口=" + (points.available() ? "可用" : "不可用"));
logOptionalPlugin("MythicMobs", "adapter=" + mythic.status()); logOptionalPlugin("MythicMobs", "适配器=" + mythic.status());
logOptionalPlugin("MMOItems", null); logOptionalPlugin("MMOItems", null);
logOptionalPlugin("MythicArtifacts", null); logOptionalPlugin("MythicArtifacts", null);
logOptionalPlugin("MythicCrucible", null); logOptionalPlugin("MythicCrucible", null);
@@ -112,16 +112,16 @@ public class MagicGemPlugin extends JavaPlugin {
private void logOptionalPlugin(String name, String adapterStatus) { private void logOptionalPlugin(String name, String adapterStatus) {
Plugin dependency = getServer().getPluginManager().getPlugin(name); Plugin dependency = getServer().getPluginManager().getPlugin(name);
if (dependency == null) { if (dependency == null) {
getLogger().info("Optional integration " + name + ": absent"); getLogger().info("可选插件 " + name + ":未安装");
return; return;
} }
String version = dependency.getDescription().getVersion(); String version = dependency.getDescription().getVersion();
if (!dependency.isEnabled()) { if (!dependency.isEnabled()) {
getLogger().warning("Optional integration " + name + ": present but disabled (version " + version + ")"); getLogger().warning("可选插件 " + name + ":已安装但未启用(版本 " + version + ")");
return; return;
} }
String suffix = adapterStatus == null ? "" : ", " + adapterStatus; String suffix = adapterStatus == null ? "" : "," + adapterStatus;
getLogger().info("Optional integration " + name + ": enabled (version " + version + suffix + ")"); getLogger().info("可选插件 " + name + ":已启用(版本 " + version + suffix + ")");
} }
public void reloadAll() { public void reloadAll() {
@@ -152,7 +152,13 @@ public final class GemRegistry {
public Optional<GemDefinition> find(String name) { public Optional<GemDefinition> find(String name) {
if (name == null) return Optional.empty(); if (name == null) return Optional.empty();
return Optional.ofNullable(gems.get(name.toLowerCase(Locale.ROOT))); String normalized = name.trim().toLowerCase(Locale.ROOT);
GemDefinition direct = gems.get(normalized);
if (direct != null) return Optional.of(direct);
// Legacy MagicGem items may store the YAML section id instead of Name.
return gems.values().stream()
.filter(gem -> gem.id().equalsIgnoreCase(name.trim()))
.findFirst();
} }
public Collection<GemDefinition> all() { public Collection<GemDefinition> all() {
@@ -147,8 +147,11 @@ public final class GemService {
return new GemUseResult(true, successful, target, defaultMessage(successful ? gem.successTip() : gem.failTip(), return new GemUseResult(true, successful, target, defaultMessage(successful ? gem.successTip() : gem.failTip(),
languages.text(successful ? "gem.tip.success" : "gem.tip.fail", languages.text(successful ? "gem.tip.success" : "gem.tip.fail",
successful ? "&a宝石使用成功" : "&c宝石使用失败"))); successful ? "&a宝石使用成功" : "&c宝石使用失败")));
} catch (RewardException exception) { } catch (RewardException | RuntimeException exception) {
return GemUseResult.rejected(target, exception.getMessage()); String message = exception.getMessage() == null || exception.getMessage().isBlank()
? "兑换奖励执行失败,请查看服务器日志" : exception.getMessage();
Bukkit.getLogger().warning("MagicGem-Reforged 兑换奖励执行失败:" + message);
return GemUseResult.rejected(target, message);
} }
} }
@@ -5,6 +5,7 @@ import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
public final class EconomyBridge { public final class EconomyBridge {
private final JavaPlugin plugin; private final JavaPlugin plugin;
@@ -20,6 +21,9 @@ public final class EconomyBridge {
public void refresh() { public void refresh() {
provider = null; provider = null;
getBalance = null;
deposit = null;
withdraw = null;
try { try {
Class<?> economy = Class.forName("net.milkbowl.vault.economy.Economy"); Class<?> economy = Class.forName("net.milkbowl.vault.economy.Economy");
RegisteredServiceProvider<?> registration = plugin.getServer().getServicesManager().getRegistration(economy); RegisteredServiceProvider<?> registration = plugin.getServer().getServicesManager().getRegistration(economy);
@@ -28,7 +32,8 @@ public final class EconomyBridge {
getBalance = economy.getMethod("getBalance", OfflinePlayer.class); getBalance = economy.getMethod("getBalance", OfflinePlayer.class);
deposit = economy.getMethod("depositPlayer", OfflinePlayer.class, double.class); deposit = economy.getMethod("depositPlayer", OfflinePlayer.class, double.class);
withdraw = economy.getMethod("withdrawPlayer", OfflinePlayer.class, double.class); withdraw = economy.getMethod("withdrawPlayer", OfflinePlayer.class, double.class);
} catch (ReflectiveOperationException ignored) { } catch (ReflectiveOperationException | LinkageError exception) {
plugin.getLogger().warning("Vault 经济接口加载失败:" + detail(exception));
} }
} }
@@ -41,20 +46,47 @@ public final class EconomyBridge {
} }
public double balance(OfflinePlayer player) { public double balance(OfflinePlayer player) {
if (!available()) refresh();
if (!available()) return 0; if (!available()) return 0;
try { return ((Number) getBalance.invoke(provider, player)).doubleValue(); } try { return ((Number) getBalance.invoke(provider, player)).doubleValue(); }
catch (ReflectiveOperationException exception) { return 0; } catch (ReflectiveOperationException | RuntimeException exception) {
plugin.getLogger().warning("读取玩家金币失败:" + detail(exception));
return 0;
}
} }
public boolean change(OfflinePlayer player, double amount) { public boolean change(OfflinePlayer player, double amount) {
if (!Double.isFinite(amount) || Math.abs(amount) < 1.0E-9) return true;
if (!available()) refresh();
if (!available()) return false; if (!available()) return false;
if (amount < 0 && balance(player) + amount < 0) return false; if (amount < 0 && balance(player) + amount < 0) return false;
try { try {
Object response = (amount >= 0 ? deposit : withdraw).invoke(provider, player, Math.abs(amount)); Object response = (amount >= 0 ? deposit : withdraw).invoke(provider, player, Math.abs(amount));
Method successful = response.getClass().getMethod("transactionSuccess"); Method successful = response.getClass().getMethod("transactionSuccess");
return (boolean) successful.invoke(response); boolean result = (boolean) successful.invoke(response);
} catch (ReflectiveOperationException exception) { if (!result) {
plugin.getLogger().warning("Vault 经济插件拒绝交易:玩家=" + player.getName()
+ ",金额=" + amount + ",原因=" + responseError(response));
}
return result;
} catch (ReflectiveOperationException | RuntimeException exception) {
plugin.getLogger().warning("修改玩家金币失败:" + detail(exception));
return false; return false;
} }
} }
private String detail(Throwable exception) {
Throwable cause = exception instanceof InvocationTargetException target && target.getCause() != null
? target.getCause() : exception;
return cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage();
}
private String responseError(Object response) {
try {
Object message = response.getClass().getField("errorMessage").get(response);
return message == null || message.toString().isBlank() ? "未提供原因" : message.toString();
} catch (ReflectiveOperationException ignored) {
return "未提供原因";
}
}
} }
@@ -71,8 +71,9 @@ public final class MythicBridge {
} }
public String status() { public String status() {
if (!available()) return "unavailable"; if (!available()) return "不可用";
return "items=" + itemManager.getClass().getName() + ", skillApi=" + skillApiAvailable(); return "物品接口=" + itemManager.getClass().getSimpleName() + ",技能接口="
+ (skillApiAvailable() ? "可用" : "不可用");
} }
public Optional<ItemStack> item(String name) { public Optional<ItemStack> item(String name) {
@@ -5,6 +5,7 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID; import java.util.UUID;
public final class PlayerPointsBridge { public final class PlayerPointsBridge {
@@ -21,16 +22,23 @@ public final class PlayerPointsBridge {
public void refresh() { public void refresh() {
api = null; api = null;
look = null;
give = null;
take = null;
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("PlayerPoints"); Plugin dependency = plugin.getServer().getPluginManager().getPlugin("PlayerPoints");
if (dependency == null || !dependency.isEnabled()) return; if (dependency == null || !dependency.isEnabled()) return;
try { try {
api = dependency.getClass().getMethod("getAPI").invoke(dependency); Method getApi = findMethod(dependency.getClass(), "getAPI");
look = api.getClass().getMethod("look", UUID.class); if (getApi == null) throw new NoSuchMethodException("PlayerPoints.getAPI()");
give = api.getClass().getMethod("give", UUID.class, int.class); api = getApi.invoke(dependency);
take = api.getClass().getMethod("take", UUID.class, int.class); look = findMethod(api.getClass(), "look", UUID.class);
} catch (ReflectiveOperationException exception) { give = findMethod(api.getClass(), "give", UUID.class, int.class);
take = findMethod(api.getClass(), "take", UUID.class, int.class);
if (api == null || look == null || give == null || take == null)
throw new NoSuchMethodException("PlayerPoints API 缺少 look/give/take 方法");
} catch (ReflectiveOperationException | RuntimeException exception) {
api = null; api = null;
plugin.getLogger().warning("PlayerPoints 存在但接口不可用: " + exception.getMessage()); plugin.getLogger().warning("PlayerPoints 接口加载失败:" + detail(exception));
} }
} }
@@ -43,23 +51,47 @@ public final class PlayerPointsBridge {
} }
public boolean change(Player player, int amount) { public boolean change(Player player, int amount) {
if (amount == 0) return true;
if (!available()) refresh();
if (!available()) return false; if (!available()) return false;
try { try {
int balance = ((Number) look.invoke(api, player.getUniqueId())).intValue(); int balance = ((Number) look.invoke(api, player.getUniqueId())).intValue();
if (amount < 0 && balance + amount < 0) return false; if (amount < 0 && balance + amount < 0) return false;
Object result = (amount >= 0 ? give : take).invoke(api, player.getUniqueId(), Math.abs(amount)); Object result = (amount >= 0 ? give : take).invoke(api, player.getUniqueId(), Math.abs(amount));
return !(result instanceof Boolean value) || value; if (result instanceof Boolean value && !value) {
} catch (ReflectiveOperationException exception) { plugin.getLogger().warning("PlayerPoints 拒绝交易:玩家=" + player.getName()
+ ",金额=" + amount + ",当前点券=" + balance);
return false;
}
return true;
} catch (ReflectiveOperationException | RuntimeException exception) {
plugin.getLogger().warning("修改玩家点券失败:" + detail(exception));
return false; return false;
} }
} }
public int balance(Player player) { public int balance(Player player) {
if (!available()) refresh();
if (!available()) return 0; if (!available()) return 0;
try { try {
return ((Number) look.invoke(api, player.getUniqueId())).intValue(); return ((Number) look.invoke(api, player.getUniqueId())).intValue();
} catch (ReflectiveOperationException exception) { } catch (ReflectiveOperationException | RuntimeException exception) {
plugin.getLogger().warning("读取玩家点券失败:" + detail(exception));
return 0; return 0;
} }
} }
private Method findMethod(Class<?> type, String name, Class<?>... parameters) {
for (Class<?> current = type; current != null; current = current.getSuperclass()) {
try { return current.getMethod(name, parameters); }
catch (NoSuchMethodException ignored) { }
}
return null;
}
private String detail(Throwable exception) {
Throwable cause = exception instanceof InvocationTargetException target && target.getCause() != null
? target.getCause() : exception;
return cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage();
}
} }
@@ -42,8 +42,10 @@ public final class ItemCodec {
private final NamespacedKey embeddedKey; private final NamespacedKey embeddedKey;
private final NamespacedKey skillsKey; private final NamespacedKey skillsKey;
private final LegacyItemMigrator legacyMigrator; private final LegacyItemMigrator legacyMigrator;
private final JavaPlugin plugin;
public ItemCodec(JavaPlugin plugin, GemRegistry registry) { public ItemCodec(JavaPlugin plugin, GemRegistry registry) {
this.plugin = plugin;
this.registry = registry; this.registry = registry;
this.gemNameKey = new NamespacedKey(plugin, "gem_name"); this.gemNameKey = new NamespacedKey(plugin, "gem_name");
this.dataVersionKey = new NamespacedKey(plugin, "data_version"); this.dataVersionKey = new NamespacedKey(plugin, "data_version");
@@ -147,6 +149,8 @@ public final class ItemCodec {
return migrated; return migrated;
} }
} }
Optional<GemDefinition> fallback = displayFallback(item, meta);
if (fallback.isPresent()) return fallback;
if (!allowLore || !meta.hasLore()) return Optional.empty(); if (!allowLore || !meta.hasLore()) return Optional.empty();
List<String> lore = meta.getLore(); List<String> lore = meta.getLore();
if (lore == null) return Optional.empty(); if (lore == null) return Optional.empty();
@@ -158,6 +162,30 @@ public final class ItemCodec {
return Optional.empty(); return Optional.empty();
} }
/**
* Old MagicGem items can lose their custom NBT when copied through a server
* version converter. The display name and material are still stable for the
* bundled vouchers, so use them as a last-resort migration identity.
*/
private Optional<GemDefinition> displayFallback(ItemStack item, ItemMeta meta) {
String display = meta.getDisplayName();
if (display == null || display.isBlank()) return Optional.empty();
String normalizedDisplay = strip(display);
for (GemDefinition gem : registry.all()) {
if (gem.display() == null || gem.display().isBlank() || gem.material() != item.getType()) continue;
if (!strip(gem.display()).equals(normalizedDisplay)) continue;
plugin.getLogger().info("检测到缺少旧版标识的宝石物品,已按显示名称迁移:" + gem.name());
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 Optional.of(gem);
}
return Optional.empty();
}
public Map<String, Integer> embedded(ItemStack item) { public Map<String, Integer> embedded(ItemStack item) {
if (item == null || !item.hasItemMeta()) return Map.of(); if (item == null || !item.hasItemMeta()) return Map.of();
PersistentDataContainer data = item.getItemMeta().getPersistentDataContainer(); PersistentDataContainer data = item.getItemMeta().getPersistentDataContainer();
@@ -49,6 +49,19 @@ public final class ItemMatcher {
return parts.length == 1 || nbt.getString(parts[0]).equals(parts[1]); return parts.length == 1 || nbt.getString(parts[0]).equals(parts[1]);
}); });
} }
// The legacy configuration uses a leading underscore for suffix matches,
// for example _AXE means every material whose name ends with _AXE.
return matchesMaterial(material, requirement);
}
boolean matchesMaterial(Material material, String rawRequirement) {
if (material == null || rawRequirement == null) return false;
String requirement = rawRequirement.toUpperCase(Locale.ROOT);
if (requirement.startsWith("_") && requirement.length() > 1)
return material.name().endsWith(requirement);
// The original checker treated AXE as a name suffix without requiring
// a separator, so PICKAXE also matched the AXE requirement.
if (requirement.equals("AXE") && material.name().endsWith("AXE")) return true;
if (material.name().equals(requirement) || material.name().endsWith('_' + requirement)) return true; if (material.name().equals(requirement) || material.name().endsWith('_' + requirement)) return true;
return switch (requirement) { return switch (requirement) {
case "ARMOR" -> material.name().endsWith("_HELMET") || material.name().endsWith("_CHESTPLATE") case "ARMOR" -> material.name().endsWith("_HELMET") || material.name().endsWith("_CHESTPLATE")
@@ -78,7 +78,9 @@ public final class GemUseListener implements Listener {
ItemStack target = targetSlot == null ? null : get(event.getPlayer().getInventory(), targetSlot); ItemStack target = targetSlot == null ? null : get(event.getPlayer().getInventory(), targetSlot);
GemUseResult result = gemService.use(event.getPlayer(), gem, target); GemUseResult result = gemService.use(event.getPlayer(), gem, target);
if (result.consumed() && used != null) { if (result.consumed() && used != null) {
used.setAmount(used.getAmount() - 1); EquipmentSlot hand = event.getHand() == null ? EquipmentSlot.HAND : event.getHand();
ItemStack held = get(event.getPlayer().getInventory(), hand);
if (held != null && !held.getType().isAir()) held.setAmount(Math.max(0, held.getAmount() - 1));
if (result.target() != null && targetSlot != null) set(event.getPlayer().getInventory(), targetSlot, result.target()); if (result.target() != null && targetSlot != null) set(event.getPlayer().getInventory(), targetSlot, result.target());
} }
messages.send(event.getPlayer(), result.message()); messages.send(event.getPlayer(), result.message());
@@ -37,18 +37,18 @@ public final class LegacyDirectoryImporter {
public ImportResult importIfPresent() { public ImportResult importIfPresent() {
Path sourceDirectory = pluginsDirectory.resolve(LEGACY_PLUGIN_DIRECTORY).normalize(); Path sourceDirectory = pluginsDirectory.resolve(LEGACY_PLUGIN_DIRECTORY).normalize();
if (targetDirectory.equals(sourceDirectory)) { if (targetDirectory.equals(sourceDirectory)) {
logger.info("MagicGem migration skipped because the legacy source and target directories are identical: " logger.info("MagicGem 迁移已跳过:旧版源目录与当前插件目录相同:"
+ sourceDirectory); + sourceDirectory);
return ImportResult.none(); return ImportResult.none();
} }
if (!Files.isDirectory(sourceDirectory)) { if (!Files.isDirectory(sourceDirectory)) {
logger.info("MagicGem migration source not found at " + sourceDirectory + "; no legacy import is needed."); logger.info("未找到 MagicGem 旧版数据目录:" + sourceDirectory + ",无需迁移。");
return ImportResult.none(); return ImportResult.none();
} }
Path marker = targetDirectory.resolve(MARKER); Path marker = targetDirectory.resolve(MARKER);
if (Files.exists(marker)) { if (Files.exists(marker)) {
logger.info("MagicGem migration already completed; skipping legacy import (marker: " + marker + ")."); logger.info("MagicGem 迁移已完成,跳过重复导入(标记文件:" + marker + ")。");
return new ImportResult(0, 0, true); return new ImportResult(0, 0, true);
} }
@@ -56,7 +56,7 @@ public final class LegacyDirectoryImporter {
int conflictFiles = 0; int conflictFiles = 0;
List<Conflict> conflicts = new ArrayList<>(); List<Conflict> conflicts = new ArrayList<>();
try { try {
logger.info("Starting MagicGem migration: source=" + sourceDirectory + ", target=" + targetDirectory); logger.info("开始迁移 MagicGem 数据:源目录=" + sourceDirectory + ",目标目录=" + targetDirectory);
Files.createDirectories(targetDirectory); Files.createDirectories(targetDirectory);
for (String directory : DATA_DIRECTORIES) { for (String directory : DATA_DIRECTORIES) {
ImportCounts counts = importDirectory(sourceDirectory.resolve(directory), ImportCounts counts = importDirectory(sourceDirectory.resolve(directory),
@@ -76,16 +76,14 @@ public final class LegacyDirectoryImporter {
writeConflictBackups(sourceDirectory, conflicts); writeConflictBackups(sourceDirectory, conflicts);
Files.writeString(marker, "Imported from plugins/MagicGem. Source directory remains unchanged.\n"); Files.writeString(marker, "Imported from plugins/MagicGem. Source directory remains unchanged.\n");
for (Conflict conflict : conflicts) { for (Conflict conflict : conflicts) {
logger.warning("MagicGem Reforged kept the target file for " + conflict.relativePath() logger.warning("MagicGem-Reforged 保留了目标文件 " + conflict.relativePath()
+ "; the original file was saved under migration-backup/MagicGem."); + ";旧版文件已备份到 migration-backup/MagicGem。");
} }
logger.info("Imported original plugins/MagicGem into " + targetDirectory.getFileName() logger.info("已将旧版 plugins/MagicGem 导入 " + targetDirectory.getFileName()
+ ": " + copiedFiles + " files copied, " + conflictFiles + " conflicts. " + ":复制 " + copiedFiles + " 个文件,发现 " + conflictFiles + " 个冲突;旧版目录未被修改。");
+ "The original directory was left untouched.");
return new ImportResult(copiedFiles, conflictFiles, false); return new ImportResult(copiedFiles, conflictFiles, false);
} catch (IOException exception) { } catch (IOException exception) {
logger.log(Level.SEVERE, "Could not finish importing original plugins/MagicGem. " logger.log(Level.SEVERE, "导入旧版 plugins/MagicGem 未完成;旧版目录未被删除,插件将在下次启动时重试迁移。", exception);
+ "The original directory was not deleted; migration will be retried on the next startup.", exception);
return new ImportResult(copiedFiles, conflictFiles, false); return new ImportResult(copiedFiles, conflictFiles, false);
} }
} }
@@ -39,28 +39,36 @@ public final class LegacyItemMigrator {
} }
private void readExact(ReadableNBT nbt, Collector collector) { private void readExact(ReadableNBT nbt, Collector collector) {
if (nbt.hasTag(GEM_NAME_KEY) && nbt.getType(GEM_NAME_KEY) == NBTType.NBTTagString) { String identityKey = findKey(nbt, GEM_NAME_KEY);
registry.find(nbt.getString(GEM_NAME_KEY)).ifPresent(gem -> collector.identity = gem.name()); if (identityKey != null && nbt.getType(identityKey) == NBTType.NBTTagString) {
registry.find(nbt.getString(identityKey)).ifPresent(gem -> collector.identity = gem.name());
} }
if (nbt.hasTag(EMBEDDED_GEMS_KEY) && nbt.getType(EMBEDDED_GEMS_KEY) == NBTType.NBTTagCompound) { String embeddedKey = findKey(nbt, EMBEDDED_GEMS_KEY);
ReadableNBT embedded = nbt.getCompound(EMBEDDED_GEMS_KEY); if (embeddedKey != null && nbt.getType(embeddedKey) == NBTType.NBTTagCompound) {
ReadableNBT embedded = nbt.getCompound(embeddedKey);
if (embedded != null) for (String name : embedded.getKeys()) { if (embedded != null) for (String name : embedded.getKeys()) {
NBTType type = embedded.getType(name); NBTType type = embedded.getType(name);
if (!numeric(type)) continue; if (!numeric(type)) continue;
registry.find(name).ifPresent(gem -> collector.embedded.put(gem.name(), number(embedded, name, type))); registry.find(name).ifPresent(gem -> collector.embedded.put(gem.name(), number(embedded, name, type)));
} }
} }
if (nbt.hasTag(SKILLS_KEY) && nbt.getType(SKILLS_KEY) == NBTType.NBTTagList) { String skillsKey = findKey(nbt, SKILLS_KEY);
var skills = nbt.getStringList(SKILLS_KEY); if (skillsKey != null && nbt.getType(skillsKey) == NBTType.NBTTagList) {
var skills = nbt.getStringList(skillsKey);
for (int index = 0; index < skills.size(); index++) collector.skills.add(skills.get(index)); for (int index = 0; index < skills.size(); index++) collector.skills.add(skills.get(index));
} }
} }
private String findKey(ReadableNBT nbt, String expected) {
if (nbt == null) return null;
for (String key : nbt.getKeys()) if (key.equalsIgnoreCase(expected)) return key;
return null;
}
private void scan(ReadableNBT nbt, Collector collector, int depth) { private void scan(ReadableNBT nbt, Collector collector, int depth) {
if (nbt == null || depth > 12) return; if (nbt == null || depth > 12) return;
for (String key : nbt.getKeys()) { for (String key : nbt.getKeys()) {
if (key.equalsIgnoreCase(GEM_NAME_KEY) || key.equalsIgnoreCase(EMBEDDED_GEMS_KEY) if (key.equalsIgnoreCase(EMBEDDED_GEMS_KEY) || key.equalsIgnoreCase(SKILLS_KEY)) continue;
|| key.equalsIgnoreCase(SKILLS_KEY)) continue;
NBTType type = nbt.getType(key); NBTType type = nbt.getType(key);
try { try {
if (type == NBTType.NBTTagString) { if (type == NBTType.NBTTagString) {
@@ -153,6 +153,10 @@ public final class RewardEngine {
private void validatePlayerRewards(RewardContext context, List<Directive> directives) throws RewardException { private void validatePlayerRewards(RewardContext context, List<Directive> directives) throws RewardException {
Player player = context.player(); Player player = context.player();
// Vault/PlayerPoints may register their services after this plugin enables.
// Refresh before validation so a late service registration cannot reject a valid exchange.
if (!economy.available()) economy.refresh();
if (!points.available()) points.refresh();
double moneyBalance = economy.available() ? economy.balance(player) : 0; double moneyBalance = economy.available() ? economy.balance(player) : 0;
int pointBalance = points.available() ? points.balance(player) : 0; int pointBalance = points.available() ? points.balance(player) : 0;
int levels = player.getLevel(); int levels = player.getLevel();
@@ -1090,13 +1094,11 @@ public final class RewardEngine {
private void money(RewardContext context, Directive directive) throws RewardException { private void money(RewardContext context, Directive directive) throws RewardException {
double amount = amount(context, directive); double amount = amount(context, directive);
if (!economy.available()) throw new RewardException("Vault 经济服务不可用");
if (!economy.change(context.player(), amount)) throw new RewardException("金币不足或经济操作失败"); if (!economy.change(context.player(), amount)) throw new RewardException("金币不足或经济操作失败");
} }
private void point(RewardContext context, Directive directive) throws RewardException { private void point(RewardContext context, Directive directive) throws RewardException {
int amount = (int) Math.round(amount(context, directive)); int amount = (int) Math.round(amount(context, directive));
if (!points.available()) throw new RewardException("PlayerPoints 不可用");
if (!points.change(context.player(), amount)) throw new RewardException("点券不足或操作失败"); if (!points.change(context.player(), amount)) throw new RewardException("点券不足或操作失败");
} }
+1 -1
View File
@@ -63,6 +63,6 @@ public class MagicGem extends MagicGemPlugin {
/** Network license activation belonged to the retired vendor service. */ /** Network license activation belonged to the retired vendor service. */
public void license(CommandSender sender) { public void license(CommandSender sender) {
sender.sendMessage("MagicGem license service is not configured in this build."); sender.sendMessage("MagicGem-Reforged 未配置授权服务;当前版本无需授权。");
} }
} }
-1
View File
@@ -67,7 +67,6 @@ Version: 1.05
Name: 精准时运切换宝石 Name: 精准时运切换宝石
Require: Require:
- AXE - AXE
- PICKAXE
- SHOVEL - SHOVEL
Display: "&8&l&k||&a&l精准/时运切换宝石&8&l&k||" Display: "&8&l&k||&a&l精准/时运切换宝石&8&l&k||"
Tips: Tips:
@@ -0,0 +1,33 @@
package cn.infstar.magicgem.item;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ItemMatcherTest {
private final ItemMatcher matcher = new ItemMatcher(null);
@Test
void supportsLegacySuffixRequirements() {
assertTrue(matcher.matchesMaterial(Material.DIAMOND_AXE, "_AXE"));
assertTrue(matcher.matchesMaterial(Material.NETHERITE_AXE, "_AXE"));
assertFalse(matcher.matchesMaterial(Material.DIAMOND_SWORD, "_AXE"));
}
@Test
void preservesOriginalAxeAliasForPickaxes() {
assertTrue(matcher.matchesMaterial(Material.DIAMOND_AXE, "AXE"));
assertTrue(matcher.matchesMaterial(Material.DIAMOND_PICKAXE, "AXE"));
assertFalse(matcher.matchesMaterial(Material.DIAMOND_SHOVEL, "AXE"));
}
@Test
void supportsExplicitMaterialRequirementsUsedByBundledGems() {
assertTrue(matcher.matchesMaterial(Material.SHIELD, "SHIELD"));
assertTrue(matcher.matchesMaterial(Material.FLINT_AND_STEEL, "FLINT_AND_STEEL"));
assertTrue(matcher.matchesMaterial(Material.DIAMOND_PICKAXE, "PICKAXE"));
assertFalse(matcher.matchesMaterial(Material.DIAMOND_PICKAXE, "TRIDENT"));
}
}
@@ -131,6 +131,15 @@ class BundledConfigurationTest {
void precisionFortuneSwitchGemAcceptsPickaxes() { void precisionFortuneSwitchGemAcceptsPickaxes() {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration( YamlConfiguration yaml = YamlConfiguration.loadConfiguration(
new File("src/main/resources/gems/FunctionGem.yml")); new File("src/main/resources/gems/FunctionGem.yml"));
assertTrue(yaml.getStringList("内置-精准时运切换宝石.Require").contains("PICKAXE")); assertTrue(yaml.getStringList("内置-精准时运切换宝石.Require").contains("AXE"));
assertTrue(yaml.getStringList("内置-精准时运切换宝石.Require").contains("SHOVEL"));
}
@Test
void efficiencyGemsAcceptPickaxes() {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(
new File("src/main/resources/gems/EnchantGem.yml"));
assertTrue(yaml.getStringList("内置-效率宝石.Require").contains("AXE"));
assertTrue(yaml.getStringList("内置-真效率宝石.Require").contains("AXE"));
} }
} }