diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 7840621f7..5d3c19ebf 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -220,6 +220,7 @@ public class AbilityController implements IAbilityHandler { public void load() { customAbilities.clear(); customAbilitiesById.clear(); + abilityScriptHandlers.clear(); File dir = getDir(); File[] files = dir.exists() ? dir.listFiles() : null; @@ -470,6 +471,7 @@ public class AbilityController implements IAbilityHandler { private void loadChainedAbilities() { chainedAbilities.clear(); chainedAbilitiesById.clear(); + chainedAbilityScriptHandlers.clear(); File dir = getChainedDir(); File[] files = dir.exists() ? dir.listFiles() : null; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index dc0de02fa..fbad42cb3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -45,6 +45,7 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.Frame; import noppes.npcs.controllers.data.AbilityScript; @@ -237,6 +238,9 @@ public abstract class Ability implements IAbility, IAbilityAction { // Burst execution state protected transient int burstIndex = 0; protected transient List burstEntities = new ArrayList<>(); + + // Per-instance script (isolated per execution, not saved to NBT) + protected transient AbilityScript instanceScript; protected transient float damageMultiplier = 1.0f; // Ability-internal damage scaling (e.g., Slam height) // ═══════════════════════════════════════════════════════════════════ @@ -1335,6 +1339,7 @@ public abstract class Ability implements IAbility, IAbilityAction { telegraphInstances.clear(); previewMode = false; previewEntityHandler = null; + instanceScript = null; } /** @@ -1647,10 +1652,14 @@ public abstract class Ability implements IAbility, IAbilityAction { } // Script handler data (matching CustomEffect.readFromNBT pattern) + // Only create a new handler if one doesn't already exist in the global map, + // to avoid clobbering a live handler's script state during deepCopy/resolve. if (nbt.hasKey("ScriptData", 10)) { - AbilityScript handler = new AbilityScript(this.id); - handler.readFromNBT(nbt.getCompoundTag("ScriptData")); - setScriptHandler(handler); + if (getScriptHandler() == null) { + AbilityScript handler = new AbilityScript(this.id); + handler.readFromNBT(nbt.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } } } @@ -1753,7 +1762,7 @@ public abstract class Ability implements IAbility, IAbilityAction { } public Ability deepCopy() { - return AbilityController.Instance.fromNBT(this.writeNBT(true)); + return AbilityController.Instance.fromNBT(this.writeNBT(false)); } @Override @@ -2734,4 +2743,25 @@ public abstract class Ability implements IAbility, IAbilityAction { } return handler; } + + /** + * Get or create a per-instance script handler for this ability execution. + * Shares the template's engine but creates isolated Bindings for variable state. + * Returns null if no script is configured on the template. + */ + public AbilityScript getOrCreateInstanceScript() { + if (instanceScript != null) return instanceScript; + + AbilityScript template = getScriptHandler(); + if (template == null || !template.getEnabled() || template.container == null) return null; + + instanceScript = new AbilityScript(this.id); + instanceScript.setLanguage(template.getLanguage()); + instanceScript.setEnabled(true); + + ScriptContainer clone = ((ScriptContainer) template.container).createInstanceScope(instanceScript); + instanceScript.addScriptUnit(clone); + + return instanceScript; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java index cfc90d38f..ccceb7cd4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java @@ -10,6 +10,7 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import noppes.npcs.api.ability.IChainedAbility; +import noppes.npcs.controllers.ScriptContainer; import kamkeel.npcs.util.FileNameHelper; import java.util.ArrayList; @@ -75,6 +76,9 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { */ private NBTTagCompound customData = new NBTTagCompound(); + // Per-instance script (isolated per execution, not saved to NBT) + private transient ChainedAbilityScript instanceScript; + public ChainedAbility() { } @@ -371,7 +375,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { */ public ChainedAbility deepCopy() { ChainedAbility copy = new ChainedAbility(); - copy.readNBT(this.writeNBT(true)); + copy.readNBT(this.writeNBT(false)); return copy; } @@ -468,9 +472,11 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { // Script handler data (matching CustomEffect.readFromNBT pattern) if (nbt.hasKey("ScriptData", 10)) { - ChainedAbilityScript handler = new ChainedAbilityScript(this.id); - handler.readFromNBT(nbt.getCompoundTag("ScriptData")); - setScriptHandler(handler); + if (getScriptHandler() == null) { + ChainedAbilityScript handler = new ChainedAbilityScript(this.id); + handler.readFromNBT(nbt.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } } } @@ -494,4 +500,32 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { } return handler; } + + /** + * Get or create a per-instance script handler for this chain execution. + * Shares the template's engine but creates isolated Bindings for variable state. + * Returns null if no script is configured on the template. + */ + public ChainedAbilityScript getOrCreateInstanceScript() { + if (instanceScript != null) return instanceScript; + + ChainedAbilityScript template = getScriptHandler(); + if (template == null || !template.getEnabled() || template.container == null) return null; + + instanceScript = new ChainedAbilityScript(this.id); + instanceScript.setLanguage(template.getLanguage()); + instanceScript.setEnabled(true); + + ScriptContainer clone = ((ScriptContainer) template.container).createInstanceScope(instanceScript); + instanceScript.addScriptUnit(clone); + + return instanceScript; + } + + /** + * Clear the per-instance script handler (call on chain completion/reset). + */ + public void clearInstanceScript() { + this.instanceScript = null; + } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java index eceb04286..ada1989c3 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java @@ -88,9 +88,17 @@ public final class AbilityScriptPacket extends AbstractPacket { Ability ability = AbilityController.Instance.resolveAbility(id); if (ability == null) { - // Ability not yet saved — send default empty script data so the GUI is usable + // Ability not yet saved — store handler in global map so scripts persist + // until the ability is saved via CustomAbilitySavePacket + AbilityScript data = AbilityController.Instance.abilityScriptHandlers.get(id); + if (data == null) { + data = new AbilityScript(id); + AbilityController.Instance.abilityScriptHandlers.put(id, data); + } if (requestedAction == Action.GET) { - PacketUtil.getScripts((IScriptHandler) new AbilityScript(id), (EntityPlayerMP) player); + PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); + } else { + data.saveScript(in, player); } return; } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java index a8c455207..99142681d 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java @@ -88,9 +88,17 @@ public final class ChainedAbilityScriptPacket extends AbstractPacket { ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(id); if (chain == null) { - // Chain not yet saved — send default empty script data so the GUI is usable + // Chain not yet saved — store handler in global map so scripts persist + // until the chain is saved via ChainedAbilitySavePacket + ChainedAbilityScript data = AbilityController.Instance.chainedAbilityScriptHandlers.get(id); + if (data == null) { + data = new ChainedAbilityScript(id); + AbilityController.Instance.chainedAbilityScriptHandlers.put(id, data); + } if (requestedAction == Action.GET) { - PacketUtil.getScripts((IScriptHandler) new ChainedAbilityScript(id), (EntityPlayerMP) player); + PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); + } else { + data.saveScript(in, player); } return; } diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 3f041ecdc..635c4507f 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -1079,6 +1079,7 @@ public abstract class AbstractDataAbilities { rollChainCooldown(currentChain); interruptCooldownRolled = true; fireChainInterruptEvent(currentChain, chainEntryIndex, getTarget(), source, damage); + currentChain.clearInstanceScript(); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1; @@ -1138,6 +1139,7 @@ public abstract class AbstractDataAbilities { // Roll cooldown if (currentChain != null) { rollChainCooldown(currentChain); + currentChain.clearInstanceScript(); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1; @@ -1336,6 +1338,7 @@ public abstract class AbstractDataAbilities { if (currentChain != null) { rollChainCooldown(currentChain); fireChainCompleteEvent(currentChain, chainEntryIndex, getTarget()); + currentChain.clearInstanceScript(); } currentChain = null; chainEntryIndex = -1; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index fa6913455..a26f38544 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -299,6 +299,7 @@ public class DataAbilities extends AbstractDataAbilities { if (positionLocked) releaseLockedPosition(); } // Clear chain and concurrent state + if (currentChain != null) currentChain.clearInstanceScript(); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1; @@ -588,6 +589,7 @@ public class DataAbilities extends AbstractDataAbilities { lastTarget = null; } // Also clear chain state + if (currentChain != null) currentChain.clearInstanceScript(); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1; diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 7063b7653..b783f98e0 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -887,7 +887,8 @@ public class EventHooks { IAbilityEvent abilityEvent = (IAbilityEvent) event; // 1. Internal ability script (fires for ALL — both NPC and Player) - AbilityScript abScript = ability.getScriptHandler(); + // Use per-instance script so each execution has isolated variable state + AbilityScript abScript = ability.getOrCreateInstanceScript(); if (abScript != null) { abScript.callScript(abilityEvent.getHookName(), event); } @@ -916,7 +917,8 @@ public class EventHooks { IChainEvent chainEvent = (IChainEvent) event; // 1. Internal chain script (fires first) - ChainedAbilityScript chainScript = chain.getScriptHandler(); + // Use per-instance script so each execution has isolated variable state + ChainedAbilityScript chainScript = chain.getOrCreateInstanceScript(); if (chainScript != null) { chainScript.callScript(chainEvent.getHookName(), event); } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 521c0cb02..53dbfb239 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -794,11 +794,10 @@ public class NoppesUtilServer { HashMap map = new HashMap(); AbilityController controller = AbilityController.Instance; if (controller != null) { - int index = 0; for (String name : controller.getAbilityNames()) { Ability ability = controller.getAbility(name); if (ability != null) { - map.put(ability.getName(), index++); + map.put(ability.getName(), ability.getAllowedBy().ordinal()); } } } @@ -809,11 +808,10 @@ public class NoppesUtilServer { HashMap map = new HashMap(); AbilityController controller = AbilityController.Instance; if (controller != null) { - int index = 0; for (String name : controller.getCustomAbilityNames()) { Ability ability = controller.getCustomAbility(name); - if (ability == null || !ability.getAllowedBy().allowsNpc()) continue; - map.put(name, index++); + if (ability == null) continue; + map.put(name, ability.getAllowedBy().ordinal()); } } sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index dda5ee8e6..568f60883 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; @@ -21,6 +22,7 @@ import noppes.npcs.constants.EnumScrollData; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Vector; /** @@ -98,14 +100,18 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL } private List getFilteredList() { - if (search.isEmpty()) { - return new ArrayList<>(rawData.keySet()); - } List list = new ArrayList<>(); - for (String name : rawData.keySet()) { - if (name.toLowerCase().contains(search.toLowerCase())) { - list.add(name); + for (Map.Entry entry : rawData.entrySet()) { + String name = entry.getKey(); + + // Only show NPC-valid abilities (NPC_ONLY or BOTH) + UserType ut = UserType.fromOrdinal(entry.getValue()); + if (!ut.allowsNpc()) continue; + + if (!search.isEmpty() && !name.toLowerCase().contains(search.toLowerCase())) { + continue; } + list.add(name); } return list; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java index 70faa9059..8cab092a2 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.advanced; +import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; import net.minecraft.client.gui.GuiButton; @@ -15,6 +16,7 @@ import noppes.npcs.constants.EnumScrollData; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Vector; /** @@ -93,14 +95,18 @@ public class SubGuiAbilitySelect extends SubGuiInterface implements ICustomScrol merged.putAll(builtInData); } - if (search.isEmpty()) { - return new ArrayList<>(merged.keySet()); - } List list = new ArrayList<>(); - for (String name : merged.keySet()) { - if (name.toLowerCase().contains(search.toLowerCase())) { - list.add(name); + for (Map.Entry entry : merged.entrySet()) { + String name = entry.getKey(); + + // Only show NPC-valid abilities (NPC_ONLY or BOTH) + UserType ut = UserType.fromOrdinal(entry.getValue()); + if (!ut.allowsNpc()) continue; + + if (!search.isEmpty() && !name.toLowerCase().contains(search.toLowerCase())) { + continue; } + list.add(name); } return list; } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index cac2cccd5..5511eeb4f 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityVariant; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.network.PacketClient; import java.util.UUID; @@ -48,6 +49,7 @@ import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Vector; /** @@ -72,6 +74,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private static final int BTN_PREVIEW_PLAY = 91; private static final int BTN_PREVIEW_PAUSE = 92; private static final int BTN_PREVIEW_STOP = 93; + private static final int BTN_USER_TYPE_FILTER = 11; // ── TextField / Label IDs ───────────────────────────────────────────────── private static final int TF_SEARCH = 55; @@ -99,6 +102,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private boolean showingChained = false; private boolean currentIsBuiltIn = false; + // ==================== USER TYPE FILTER ==================== + // 0 = Both, 1 = NPC Only, 2 = Player Only + private int userTypeFilter = 0; + private static final String[] USER_TYPE_FILTER_LABELS = {"gui.both", "gui.npcs", "gui.players"}; + // ==================== CHAINED DATA ==================== private HashMap chainedData = new HashMap<>(); private ChainedAbility selectedChain = null; @@ -176,6 +184,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Search bar addTextField(new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); + // User type filter button (only for Custom and Built-in views) + if (!showingChained) { + addButton(new GuiNpcButton(BTN_USER_TYPE_FILTER, guiLeft + 368, guiTop + 192, 45, 20, USER_TYPE_FILTER_LABELS[userTypeFilter])); + } + // Check if we have something to preview if (showingChained) { if (selectedChain == null || selected == null || selectedChain.getEntries().isEmpty()) { @@ -213,14 +226,23 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private List getSearchList() { HashMap data = getCurrentData(); - if (search.isEmpty()) { - return new ArrayList<>(data.keySet()); - } List list = new ArrayList<>(); - for (String name : data.keySet()) { - if (name.toLowerCase().contains(search.toLowerCase())) { - list.add(name); + for (Map.Entry entry : data.entrySet()) { + String name = entry.getKey(); + + // Apply search filter + if (!search.isEmpty() && !name.toLowerCase().contains(search.toLowerCase())) { + continue; } + + // Apply UserType filter (only for Custom and Built-in views) + if (!showingChained && userTypeFilter != 0) { + UserType ut = UserType.fromOrdinal(entry.getValue()); + if (userTypeFilter == 1 && !ut.allowsNpc()) continue; + if (userTypeFilter == 2 && !ut.allowsPlayer()) continue; + } + + list.add(name); } return list; } @@ -278,6 +300,16 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface int id = guibutton.id; + // User type filter: Both → NPC → Player → Both + if (id == BTN_USER_TYPE_FILTER) { + userTypeFilter = (userTypeFilter + 1) % 3; + if (scroll != null) { + scroll.setList(getSearchList()); + } + initGui(); + return; + } + // Toggle: Custom → Built-in → Chained → Custom if (id == BTN_TOGGLE_VIEW) { if (!showingBuiltIn && !showingChained) { diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index c090b2a75..e8dbe22b1 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -11,6 +11,7 @@ import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; +import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptContext; @@ -52,10 +53,39 @@ public class ScriptContainer implements IScriptUnit { */ private String language = null; + /** + * Per-instance bindings for variable isolation. When non-null, this container + * is an instance scope sharing a template's engine. All eval/lookup uses these + * bindings instead of the engine's default ENGINE_SCOPE. + */ + private Bindings instanceBindings = null; + public ScriptContainer(IScriptHandler handler) { this.handler = handler; } + /** + * Create a lightweight instance scope that shares this container's engine + * but has its own isolated Bindings for variable state. + * The template's engine is initialized if needed. + * + * @param instanceHandler the script handler for the instance + */ + public ScriptContainer createInstanceScope(IScriptHandler instanceHandler) { + if (this.engine == null) { + this.setEngine(this.getLanguage()); + } + ScriptContainer instance = new ScriptContainer(instanceHandler); + instance.script = this.script; + instance.scripts = new ArrayList<>(this.scripts); + instance.console = this.console; + instance.language = this.language; + instance.engine = this.engine; + instance.currentScriptLanguage = this.currentScriptLanguage; + instance.instanceBindings = this.engine.createBindings(); + return instance; + } + public void readFromNBT(NBTTagCompound compound) { String prevScript = this.script; this.script = compound.getString("Script"); @@ -205,20 +235,33 @@ public class ScriptContainer implements IScriptUnit { engine.getContext().setWriter(pw); engine.getContext().setErrorWriter(pw); - engine.put("API", NpcAPI.Instance()); - HashMap engineEntries = new HashMap<>(NpcAPI.engineObjects); - for (Map.Entry objectEntry : engineEntries.entrySet()) { - engine.put(objectEntry.getKey(), objectEntry.getValue()); + // Populate bindings with API objects + if (instanceBindings != null) { + instanceBindings.put("API", NpcAPI.Instance()); + HashMap engineEntries = new HashMap<>(NpcAPI.engineObjects); + for (Map.Entry objectEntry : engineEntries.entrySet()) { + instanceBindings.put(objectEntry.getKey(), objectEntry.getValue()); + } + } else { + engine.put("API", NpcAPI.Instance()); + HashMap engineEntries = new HashMap<>(NpcAPI.engineObjects); + for (Map.Entry objectEntry : engineEntries.entrySet()) { + engine.put(objectEntry.getKey(), objectEntry.getValue()); + } } try { if (!evaluated) { this.cachedFunctions.clear(); - engine.eval(getFullCode()); + if (instanceBindings != null) { + engine.eval(getFullCode(), instanceBindings); + } else { + engine.eval(getFullCode()); + } evaluated = true; } if (engine.getFactory().getLanguageName().equals("lua")) { - Object ob = engine.get(type); + Object ob = instanceBindings != null ? instanceBindings.get(type) : engine.get(type); if (ob != null) { if (luaCoerce == null) { luaCoerce = Class.forName("org.luaj.vm2.lib.jse.CoerceJavaToLua").getMethod("coerce", Object.class); @@ -230,8 +273,14 @@ public class ScriptContainer implements IScriptUnit { } } else { if (!this.cachedFunctions.containsKey(type)) { - ScriptObjectMirror global = (ScriptObjectMirror) engine.getBindings(ScriptContext.ENGINE_SCOPE); - ScriptObjectMirror func = (ScriptObjectMirror) global.get(type); + ScriptObjectMirror func; + if (instanceBindings != null) { + Object val = instanceBindings.get(type); + func = val instanceof ScriptObjectMirror ? (ScriptObjectMirror) val : null; + } else { + ScriptObjectMirror global = (ScriptObjectMirror) engine.getBindings(ScriptContext.ENGINE_SCOPE); + func = (ScriptObjectMirror) global.get(type); + } this.cachedFunctions.put(type, func); } ScriptObjectMirror func = this.cachedFunctions.get(type); @@ -280,6 +329,8 @@ public class ScriptContainer implements IScriptUnit { public void setEngine(String scriptLanguage) { if (!Objects.equals(scriptLanguage, this.currentScriptLanguage)) { + // Instance scopes share the template's engine — never create a new one + if (instanceBindings != null) return; this.currentScriptLanguage = scriptLanguage; if (ConfigScript.ScriptingECMA6 && scriptLanguage.equals("ECMAScript")) { System.setProperty("nashorn.args", "--language=es6"); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 21e989644..1330b34fa 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -696,6 +696,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } // Clear chain and concurrent state + if (currentChain != null) currentChain.clearInstanceScript(); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1; @@ -778,6 +779,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } // Clear chain and concurrent state + if (currentChain != null) currentChain.clearInstanceScript(); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1;