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/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/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;