From cd28f69a0e108f026f693e398c078b547751b3f0 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:39:14 -0500 Subject: [PATCH] Enhance ability validation: add method to check if an ability can be resolved without deep copy and prune invalid unlocked abilities during load --- .../data/ability/AbilityController.java | 31 +++++++++++++++++++ .../controllers/data/PlayerAbilityData.java | 23 ++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 43af10b7c..ae21a9598 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -340,6 +340,37 @@ public class AbilityController implements IAbilityHandler { return builtAbilities.containsKey(key) || customAbilities.containsKey(key); } + /** + * Check if a key can be resolved to a valid ability without creating a deep copy. + * Uses the same lookup chain as {@link #resolveAbility(String)}. + */ + public boolean canResolveAbility(String key) { + if (key == null || key.isEmpty()) return false; + + // Built-in: exact name + if (builtAbilities.containsKey(key)) return true; + + // Built-in: case-insensitive name + for (String name : builtAbilities.keySet()) { + if (name.equalsIgnoreCase(key)) return true; + } + + // Built-in: registry key / ID + for (Ability ability : builtAbilities.values()) { + if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true; + } + + // Custom: exact UUID + if (customAbilities.containsKey(key)) return true; + + // Custom: case-insensitive name + for (Ability ability : customAbilities.values()) { + if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true; + } + + return false; + } + // ═══════════════════════════════════════════════════════════════════════════ // EXTENSION POINTS // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 6ced6f68a..d7f543726 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -15,6 +15,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; import net.minecraft.util.DamageSource; +import noppes.npcs.LogWriter; import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; @@ -23,6 +24,7 @@ import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; /** @@ -860,9 +862,30 @@ public class PlayerAbilityData implements IPlayerAbilityData { } } selectedIndex = compound.getInteger("PlayerAbilitySelected"); + + // Prune abilities that no longer exist (deleted custom abilities or removed built-ins) + validateUnlockedAbilities(); + if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } playingAbilityAnimation = compound.getBoolean("AbilityAnimating"); } + + /** + * Remove any unlocked ability keys that can no longer be resolved. + * Called during load to clean up references to deleted abilities. + */ + private void validateUnlockedAbilities() { + if (AbilityController.Instance == null) return; + + Iterator it = unlockedAbilities.iterator(); + while (it.hasNext()) { + String key = it.next(); + if (!AbilityController.Instance.canResolveAbility(key)) { + it.remove(); + LogWriter.info("Removed invalid ability reference from player data: " + key); + } + } + } }