package electrosphere.script; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.HostAccess; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Source.Builder; import org.graalvm.polyglot.Value; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; import electrosphere.util.FileUtils; /** * A context for executing scripts */ public class ScriptContext { /** * namespace for the engine functions exposed to the script engine */ public static final String SCRIPT_NAMESPACE_ENGINE = "engine"; /** * namespace for the core typescript functions */ public static final String SCRIPT_NAMESPACE_SCRIPT = "script"; /** * namespace for the current scene */ public static final String SCRIPT_NAMESPACE_SCENE = "scene"; /** * the graal context */ Context context; /** * used to build source objects */ Builder builder; /** * the javascript object that stores values */ Value topLevelValue; /** * the object that contains all host values accessible to javascript land */ Value hostObject; /** * the engine object */ Value engineObject; /** * The hook manager */ Value hookManager; /** * The parent script engine */ ScriptEngine parent; /** * Locks the script engine to enforce synchronization */ ReentrantLock lock = new ReentrantLock(); /** * Initializes the context * @param engine */ public void init(ScriptEngine scriptEngine){ //register parent this.parent = scriptEngine; //create engine with flag to disable warning Engine engine = Engine.newBuilder() .option("engine.WarnInterpreterOnly", "false") .build(); //Create the rules for guest accessing the host environment HostAccess accessRules = HostAccess.newBuilder(HostAccess.EXPLICIT) .allowArrayAccess(true) .build(); //create context context = Context.newBuilder("js") .allowNativeAccess(false) .allowHostAccess(accessRules) .engine(engine) .build(); //save the js bindings object topLevelValue = context.getBindings("js"); //put host members into environment this.putTopLevelValue("loggerScripts",LoggerInterface.loggerScripts); //load all files required to start the engine for(String fileToLoad : ScriptEngine.filesToLoadOnInit){ this.loadDependency(fileToLoad); } //register engine files this.registerFile("/Scripts/engine/engine-init.ts"); } /** * Logic to run after initializing */ public void postInit(){ //run script for engine init this.requireModule("/Scripts/engine/engine-init.ts"); //get the engine object engineObject = topLevelValue.getMember("REQUIRE_CACHE").getMember("/Scripts/engine/engine-init.js").getMember("exports").getMember("engine"); hookManager = engineObject.getMember("hookManager"); //define host members this.defineHostMembers(); //init on script side this.invokeModuleFunction("/Scripts/engine/engine-init.ts","ENGINE_onInit"); } /** * Stores a variable at the top level of the js bindings * @param valueName The name of the variable (ie the name of the variable) * @param value The value that is stored at that variable */ public void putTopLevelValue(String valueName, Object value){ topLevelValue.putMember(valueName, value); } /** * Gets a top level value from the script engine * @param valueName The name of the variable * @return The value of the variable */ public Value getTopLevelValue(String valueName){ return topLevelValue.getMember(valueName); } /** * Removes a top level member from the javascript context * @param valueName The name of the top level member * @return true if successfully removed, false otherwise */ public boolean removeTopLevelValue(String valueName){ return topLevelValue.removeMember(valueName); } /** * Loads a script from disk * @param path The path to the script file */ private void loadDependency(String path){ String content; Source source = null; try { content = FileUtils.getAssetFileAsString(path); source = Source.create("js",content); context.eval(source); } catch (IOException e) { LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e); } catch (PolyglotException e){ if(source != null){ LoggerInterface.loggerScripts.WARNING("Source language: " + source.getLanguage()); } LoggerInterface.loggerScripts.ERROR("Script error", e); e.printStackTrace(); } } /** * Prints the content of a file * @param path The filepath of the script */ public void printScriptSource(String path){ invokeMemberFunction("COMPILER", "printSource", path); } /** * Gets the contents of a file in the virtual filepath * @param path The virtual filepath * @return The contents of that file if it exists, null otherwise */ public String getVirtualFileContent(String path){ String rVal = null; Value compiler = this.topLevelValue.getMember("COMPILER"); Value fileMap = compiler.getMember("fileMap"); Value virtualFile = fileMap.getMember(path); rVal = virtualFile.getMember("content").asString(); return rVal; } /** * Registers a file with the scripting engine to be compiled into the full binary * @param path The path to the script file */ protected boolean registerFile(String path){ String content; try { content = FileUtils.getAssetFileAsString(path); Value dependentFilesValue = this.invokeMemberFunction("COMPILER", "registerFile", path, content); // //register dependent files if necessary long dependentFilesCount = dependentFilesValue.getArraySize(); if(dependentFilesCount > 0){ for(int i = 0; i < dependentFilesCount; i++){ String dependentFilePath = dependentFilesValue.getArrayElement(i).asString(); boolean shouldRegister = true; for(String ignorePath : ScriptEngine.registerIgnores){ if(ignorePath.equals(dependentFilePath)){ shouldRegister = false; } } if(shouldRegister){ LoggerInterface.loggerScripts.INFO("[HOST - Script Engine] Should register file " + dependentFilePath); this.registerFile(dependentFilePath); } else { LoggerInterface.loggerScripts.DEBUG("[HOST - Script Engine] Skipping ignorepath file " + dependentFilePath); } } } } catch (IOException e) { LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e); return false; } return true; } /** * Compiles the project */ protected void compile(){ ScriptFileChecksumMap checksumMap = this.parent.getChecksumMap(); //actually compile this.invokeMemberFunction("COMPILER", "run"); Value fileMap = this.topLevelValue.getMember("COMPILER").getMember("fileMap"); //register new files, update cache where appropriate for(String key : fileMap.getMemberKeys()){ Value fileData = fileMap.getMember(key); String content = fileData.getMember("content").asString(); String cacheFilePath = ScriptEngine.TS_SOURCE_CACHE_DIR + key; File toWriteFile = new File(cacheFilePath); //make sure all containing folders exist try { Files.createDirectories(toWriteFile.getParentFile().toPath()); } catch (IOException e) { LoggerInterface.loggerFileIO.ERROR(e); } //update cached timestamp { String pathRaw = toWriteFile.toPath() + ""; pathRaw = pathRaw.replace(".\\.cache\\tscache\\src\\", "./assets/"); File correspondingFile = new File(pathRaw.replace(".\\.cache\\tscache\\src\\", "./assets/")); String cacheKey = pathRaw.replace("./assets", "").replace("\\", "/"); checksumMap.getFileLastModifyMap().put(cacheKey, correspondingFile.lastModified() + ""); } //write the actual file try { Files.writeString(toWriteFile.toPath(), content); } catch (IOException e) { LoggerInterface.loggerFileIO.ERROR(e); } } //write out cache map file this.parent.writeChecksumMap(); } /** * Recompiles the scripting engine */ protected void recompile(Runnable onCompletion){ Thread recompileThread = new Thread(() -> { Globals.engineState.scriptEngine.getScriptContext().executeSynchronously(() -> { Globals.engineState.scriptEngine.initScripts(); }); if(onCompletion != null){ onCompletion.run(); } }); recompileThread.setName("Recompile Script Engine"); recompileThread.start(); } /** * Initializes a scene script * @param scenePath The scene's init script path * @return The id assigned to the scene instance from the script-side */ public int initScene(String scenePath){ //add files to virtual filesystem in script engine registerFile(scenePath); //load scene from javascript side Value sceneLoader = this.engineObject.getMember("sceneLoader"); Value loadFunc = sceneLoader.getMember("loadScene"); Value result = loadFunc.execute(scenePath); return result.asInt(); } /** * Calls a function defined in the global scope with the arguments provided * @param functionName The function name * @param args The arguments */ public Value invokeFunction(String functionName, Object... args){ LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName); Value function = topLevelValue.getMember(functionName); if(function != null){ return function.execute(args); } else { LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); } return null; } /** * Calls a function on a child of the top level member * @param memberName The name of the child * @param functionName The name of the function * @param args The arguments for the function * @return The value from the function call */ public Value invokeMemberFunction(String memberName, String functionName, Object ... args){ LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName); Value childMember = topLevelValue.getMember(memberName); Value function = childMember.getMember(functionName); if(function != null){ return function.execute(args); } else { LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); } return null; } /** * Invokes a function defined in a file * @param filePath The file the function is defined in * @param functionName The function's name * @param args The args to pass into the function */ public void invokeModuleFunction(String filePath, String functionName, Object ... args){ Value filePathRaw = invokeFunction("FILE_RESOLUTION_getFilePath",filePath); Value requireCache = topLevelValue.getMember("REQUIRE_CACHE"); Value module = requireCache.getMember(filePathRaw.asString()); Value exports = module.getMember("exports"); Value function = exports.getMember(functionName); if(function != null && function.canExecute()){ function.execute(args); } else { LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); } } /** * Requires a module into the global space * @param filePath The filepath of the module */ public void requireModule(String filePath){ this.invokeFunction("require", filePath); } /** * Invokes a function on a member of arbitrary depth on the engine object * @param memberName The member name * @param functionName The function's name * @param className The class of the expected return value * @param args The args to pass to the function * @return The results of the invocation or null if there was no result */ public Value invokeEngineMember(String memberName, String functionName, Object ... args){ Value member = this.engineObject.getMember(memberName); if(member == null){ throw new Error("Member is null!"); } Value function = member.getMember(functionName); if(function == null || !function.canExecute()){ throw new Error("Function is not executable! " + function); } Value executionResult = function.execute(args); if(executionResult == null){ return null; } return executionResult; } /** * Executes some code synchronously that requires script engine access * @param function The function */ public void executeSynchronously(Runnable function){ boolean success = false; try { success = lock.tryLock(1, TimeUnit.MICROSECONDS); } catch (InterruptedException e) { LoggerInterface.loggerScripts.ERROR(e); } if(!success){ throw new Error("Failed to acquire lock!"); } function.run(); lock.unlock(); } /** * Defines host members within javascript context */ protected void defineHostMembers(){ hostObject = topLevelValue.getMember("HOST_ACCESS"); //give guest access to static classes Value classes = engineObject.getMember("classes"); for(Object[] currentClass : ScriptEngine.staticClasses){ classes.putMember((String)currentClass[0], currentClass[1]); } //give access to script engine instance hostObject.putMember("scriptEngine", this); } /** * Fires a signal on a given scene * @param signal The signal name * @param sceneInstanceId The script-side instanceid of the scene * @param args The arguments to accompany the signal invocation */ public void fireSignal(String signal, int sceneInstanceId, Object ... args){ Value fireSignal = this.hookManager.getMember("fireSignal"); fireSignal.execute(sceneInstanceId,signal,args); } }