Files
CustomNPC-Plus/src/main/java/noppes/npcs/controllers/ScriptContainer.java
T

417 lines
15 KiB
Java
Raw Normal View History

2018-09-13 12:03:28 -04:00
package noppes.npcs.controllers;
import cpw.mods.fml.common.eventhandler.Event;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
2018-09-13 12:03:28 -04:00
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.CustomNpcs;
2018-09-13 12:03:28 -04:00
import noppes.npcs.NBTTags;
2023-01-07 00:21:08 -05:00
import noppes.npcs.config.ConfigScript;
import noppes.npcs.constants.EnumScriptType;
2023-01-12 21:33:06 -05:00
import noppes.npcs.controllers.data.IScriptHandler;
import noppes.npcs.controllers.data.IScriptUnit;
import noppes.npcs.scripted.NpcAPI;
2018-09-13 12:03:28 -04:00
2026-03-03 22:15:49 -05:00
import javax.script.Bindings;
2025-07-23 14:22:52 -04:00
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
2023-01-12 21:33:06 -05:00
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
2025-07-23 14:22:52 -04:00
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
2023-01-12 21:33:06 -05:00
public class ScriptContainer implements IScriptUnit {
private static final String lock = "lock";
public static ScriptContainer Current;
private static String CurrentType;
public String fullscript = "";
public String script = "";
public TreeMap<Long, String> console = new TreeMap<>();
public boolean errored = false;
public List<String> scripts = new ArrayList<>();
private HashSet<String> unknownFunctions = new HashSet<>();
public long lastCreated = 0L;
private String currentScriptLanguage = null;
public ScriptEngine engine = null;
private IScriptHandler handler = null;
2023-05-25 22:18:54 -05:00
private boolean evaluated = false;
private static Method luaCoerce;
private static Method luaCall;
private CompiledScript compScript = null;
private final HashMap<String, ScriptObjectMirror> cachedFunctions = new HashMap<>();
2026-03-02 16:38:35 -05:00
/**
* Persisted per-container language. Null means inherit from handler default.
* Separate from currentScriptLanguage which is the engine cache.
*/
private String language = null;
2026-03-03 22:15:49 -05:00
/**
* 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;
}
2026-03-03 22:15:49 -05:00
/**
* 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) {
2023-05-25 22:18:54 -05:00
String prevScript = this.script;
this.script = compound.getString("Script");
2023-01-07 00:21:08 -05:00
for (int i = 0; i < ConfigScript.ExpandedScriptLimit; i++) {
2025-03-13 17:14:45 -04:00
if (compound.hasKey("ExpandedScript" + i)) {
this.script += compound.getString("ExpandedScript" + i);
} else {
break;
}
}
2023-05-25 22:18:54 -05:00
if (!this.script.equals(prevScript)) {
this.evaluated = false;
}
this.console = NBTTags.GetLongStringMap(compound.getTagList("Console", 10));
this.scripts = NBTTags.getStringList(compound.getTagList("ScriptList", 10));
this.lastCreated = 0L;
2026-03-02 16:38:35 -05:00
// Read per-container language (empty string or missing = inherit from handler)
if (compound.hasKey("ContainerLanguage")) {
String lang = compound.getString("ContainerLanguage");
this.language = lang.isEmpty() ? null : lang;
}
}
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_ECMASCRIPT);
if (this.script.length() < 65535) {
compound.setString("Script", this.script);
} else {
2023-01-07 00:21:08 -05:00
if (ConfigScript.ExpandedScriptLimit > 0) {
int i = 0;
int length = this.script.length();
2023-01-07 00:21:08 -05:00
while (length > 0 && i <= ConfigScript.ExpandedScriptLimit) {
String str = "";
if (i == 0) {
compound.setString("Script", this.script.substring(0, 65535));
str = this.script.substring(0, 65535);
} else {
int end = (length - 65535) >= 0 ? 65535 * (i + 1) : 65535 * i + length;
str = this.script.substring(65535 * i, end);
compound.setString("ExpandedScript" + (i - 1), str);
}
i++;
length -= str.length();
}
} else {
compound.setString("Script", this.script.substring(0, 65535));
}
}
//compound.setString("Type", this.type);
compound.setTag("Console", NBTTags.NBTLongStringMap(this.console));
compound.setTag("ScriptList", NBTTags.nbtStringList(this.scripts));
2026-03-02 16:38:35 -05:00
// Persist per-container language if explicitly set
if (this.language != null) {
compound.setString("ContainerLanguage", this.language);
}
return compound;
}
private String getFullCode() {
2025-03-13 17:14:45 -04:00
if (!this.evaluated) {
2025-06-11 17:37:17 -05:00
// build includes first depending on config setting
2025-05-29 20:42:36 -04:00
StringBuilder sb = new StringBuilder();
if (CustomNpcs.proxy.isRunLoadedScriptsFirst()) {
this.appendExternalScripts(sb);
}
2025-06-11 17:37:17 -05:00
// then your per‐hook main script
2025-05-29 20:42:36 -04:00
if (this.script != null && !this.script.isEmpty()) {
sb.append(this.script).append("\n");
}
2025-06-11 17:37:17 -05:00
// build includes last if not built first
if (!CustomNpcs.proxy.isRunLoadedScriptsFirst()) {
this.appendExternalScripts(sb);
}
2025-05-29 20:42:36 -04:00
this.fullscript = sb.toString();
this.unknownFunctions = new HashSet<>();
}
return this.fullscript;
}
private void appendExternalScripts(StringBuilder sb) {
for (String loc : this.scripts) {
String code = ScriptController.Instance.scripts.get(loc);
if (code != null && !code.isEmpty()) {
sb.append(code).append("\n");
}
}
}
2025-03-13 17:14:45 -04:00
public void run(ScriptEngine engine) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
this.engine.getContext().setWriter(pw);
this.engine.getContext().setErrorWriter(pw);
try {
2025-03-13 17:14:45 -04:00
if (compScript == null && engine instanceof Compilable)
compScript = ((Compilable) engine).compile(getFullCode());
2025-03-13 17:14:45 -04:00
if (compScript != null) {
compScript.eval(engine.getContext());
} else {
engine.eval(getFullCode());
}
} catch (Throwable var14) {
this.errored = true;
var14.printStackTrace(pw);
} finally {
String errorString = sw.getBuffer().toString().trim();
this.appendConsole(errorString);
pw.close();
}
}
public void run(EnumScriptType type, Event event) {
if (!CustomNpcs.proxy.isScriptingEnabled())
return;
2025-03-13 17:14:45 -04:00
this.run((String) type.function, (Object) event);
}
public void run(String type, Object event) {
if (!CustomNpcs.proxy.isScriptingEnabled() || errored || !hasCode() || unknownFunctions.contains(type))
return;
2026-03-02 16:38:35 -05:00
this.setEngine(this.getLanguage());
2025-03-13 17:14:45 -04:00
if (engine == null)
return;
2023-06-02 17:17:00 -05:00
2025-03-13 17:14:45 -04:00
if (ScriptController.Instance.lastLoaded > this.lastCreated) {
this.lastCreated = ScriptController.Instance.lastLoaded;
2023-05-25 22:18:54 -05:00
evaluated = false;
}
synchronized (lock) {
Current = this;
CurrentType = type;
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
engine.getContext().setWriter(pw);
engine.getContext().setErrorWriter(pw);
2026-03-03 22:15:49 -05:00
// Populate bindings with API objects
if (instanceBindings != null) {
instanceBindings.put("API", NpcAPI.Instance());
HashMap<String, Object> engineEntries = new HashMap<>(NpcAPI.engineObjects);
for (Map.Entry<String, Object> objectEntry : engineEntries.entrySet()) {
instanceBindings.put(objectEntry.getKey(), objectEntry.getValue());
}
} else {
engine.put("API", NpcAPI.Instance());
HashMap<String, Object> engineEntries = new HashMap<>(NpcAPI.engineObjects);
for (Map.Entry<String, Object> objectEntry : engineEntries.entrySet()) {
engine.put(objectEntry.getKey(), objectEntry.getValue());
}
}
try {
2025-03-13 17:14:45 -04:00
if (!evaluated) {
this.cachedFunctions.clear();
2026-03-03 22:15:49 -05:00
if (instanceBindings != null) {
engine.eval(getFullCode(), instanceBindings);
} else {
engine.eval(getFullCode());
}
2023-05-25 22:18:54 -05:00
evaluated = true;
}
2025-03-13 17:14:45 -04:00
if (engine.getFactory().getLanguageName().equals("lua")) {
2026-03-03 22:15:49 -05:00
Object ob = instanceBindings != null ? instanceBindings.get(type) : engine.get(type);
2025-03-13 17:14:45 -04:00
if (ob != null) {
if (luaCoerce == null) {
luaCoerce = Class.forName("org.luaj.vm2.lib.jse.CoerceJavaToLua").getMethod("coerce", Object.class);
luaCall = ob.getClass().getMethod("call", Class.forName("org.luaj.vm2.LuaValue"));
}
luaCall.invoke(ob, luaCoerce.invoke(null, event));
2025-03-13 17:14:45 -04:00
} else {
unknownFunctions.add(type);
}
2025-03-13 17:14:45 -04:00
} else {
if (!this.cachedFunctions.containsKey(type)) {
ScriptObjectMirror func;
if (instanceBindings != null) {
2026-03-03 22:15:49 -05:00
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);
if (func != null) {
func.call(null, event);
}
}
} catch (NoSuchMethodException e) {
unknownFunctions.add(type);
} catch (Throwable e) {
errored = true;
e.printStackTrace(pw);
2025-03-13 17:14:45 -04:00
} finally {
appendConsole(sw.getBuffer().toString().trim());
pw.close();
Current = null;
}
}
}
public void appendConsole(String message) {
if (message != null && !message.isEmpty()) {
long time = System.currentTimeMillis();
if (this.console.containsKey(time)) {
2025-03-13 17:14:45 -04:00
message = (String) this.console.get(time) + "\n" + message;
}
this.console.put(time, message);
2025-03-13 17:14:45 -04:00
while (this.console.size() > 40) {
this.console.remove(this.console.firstKey());
}
}
}
2023-03-16 02:51:40 -05:00
public boolean isValid() {
2023-05-25 22:18:54 -05:00
return evaluated && !errored;
2023-03-16 02:51:40 -05:00
}
@Override
public boolean hasCode() {
2025-03-13 17:14:45 -04:00
if (!scripts.isEmpty())
return true;
return !this.getFullCode().isEmpty();
}
public void setEngine(String scriptLanguage) {
2023-06-02 17:17:00 -05:00
if (!Objects.equals(scriptLanguage, this.currentScriptLanguage)) {
2026-03-03 22:15:49 -05:00
// Instance scopes share the template's engine — never create a new one
if (instanceBindings != null) return;
2023-06-02 17:17:00 -05:00
this.currentScriptLanguage = scriptLanguage;
if (ConfigScript.ScriptingECMA6 && scriptLanguage.equals("ECMAScript")) {
System.setProperty("nashorn.args", "--language=es6");
}
this.engine = ScriptController.Instance.getEngineByName(scriptLanguage.toLowerCase());
this.evaluated = false;
2022-11-25 20:53:03 -06:00
}
}
// ==================== IScriptUnit IMPLEMENTATION ====================
@Override
public String getScript() {
return this.script;
}
@Override
public void setScript(String script) {
this.script = script;
this.evaluated = false;
}
@Override
public List<String> getExternalScripts() {
return this.scripts;
}
@Override
public void setExternalScripts(List<String> scripts) {
this.scripts = scripts;
this.evaluated = false;
}
@Override
public TreeMap<Long, String> getConsole() {
return this.console;
}
@Override
public void clearConsole() {
this.console.clear();
}
@Override
public String getLanguage() {
2026-03-02 16:38:35 -05:00
// Return persisted per-container language, defaulting to ECMAScript.
// This is separate from currentScriptLanguage which is the engine cache.
return this.language != null ? this.language : "ECMAScript";
}
@Override
public void setLanguage(String language) {
2026-03-02 16:38:35 -05:00
if (!Objects.equals(this.language, language)) {
this.language = language;
this.evaluated = false;
}
}
@Override
public String generateHookStub(String hookName, Object hookData) {
// ECMAScript function template
return "function " + hookName + "(event) {\n \n}\n";
}
@Override
public void ensureCompiled() {
}
@Override
public boolean hasErrored() {
return this.errored;
}
@Override
public void setErrored(boolean errored) {
this.errored = errored;
}
2026-02-03 03:19:03 +01:00
@Override
public boolean isJanino() {
return false;
}
2018-09-13 12:03:28 -04:00
}