package electrosphere.script; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; 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.engine.Main; import electrosphere.logger.LoggerInterface; import electrosphere.menu.tutorial.TutorialMenus; import electrosphere.script.translation.JSServerUtils; import electrosphere.util.FileUtils; import electrosphere.util.math.MathUtils; /** * Interface for executing scripts in the game engine */ public class ScriptEngine { //the default namespaces for public static String SCRIPT_NAMESPACE_ENGINE = "engine"; //namespace for the engine functions exposed to the script engine public static String SCRIPT_NAMESPACE_SCRIPT = "script"; //namespace for the core typescript functionsw public static String SCRIPT_NAMESPACE_SCENE = "scene"; //namespace for the current scene /** * The id for firing signals globally */ public static final int GLOBAL_SCENE = -1; //the graal context Context context; //used to build source objects Builder builder; //the map of script filepaths to parsed, in-memory scripts Map sourceMap; //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 files that are loaded on init to bootstrap the script engine static final String[] filesToLoadOnInit = new String[]{ //polyfills "Scripts/compiler/require_polyfill.js", //main typescript engine "Scripts/compiler/typescript.js", //compiler and utilities "Scripts/compiler/file_resolution.js", "Scripts/compiler/compiler.js", "Scripts/compiler/host_access.js", }; /** * List of files that are ignored when registering new files */ static final String[] registerIgnores = new String[]{ "/Scripts/compiler/host_access.ts", }; //The classes that will be provided to the scripting engine //https://stackoverflow.com/a/65942034 static final Object[][] staticClasses = new Object[][]{ {"mathUtils",MathUtils.class}, {"simulation",Main.class}, {"tutorialUtils",TutorialMenus.class}, {"serverUtils",JSServerUtils.class}, }; //singletons from the host that are provided to the javascript context static final Object[][] hostSingletops = new Object[][]{ {"timekeeper",Globals.timekeeper}, {"currentPlayer",Globals.clientPlayer}, {"loggerScripts",LoggerInterface.loggerScripts}, }; /** * Initializes the engine */ public void init(){ //init datastructures sourceMap = new HashMap(); //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 putTopLevelValue("loggerScripts",LoggerInterface.loggerScripts); //load all files required to start the engine for(String fileToLoad : filesToLoadOnInit){ loadDependency(fileToLoad); } //register engine files registerFile("/Scripts/engine/engine-init.ts"); //compile compile(); //run script for engine init 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 defineHostMembers(); //init on script side invokeModuleFunction("/Scripts/engine/engine-init.ts","ENGINE_onInit"); //call the engine initialization function // invokeFunction("ENGINE_onInit"); //read scripts into source map // readScriptsDirectory("/src/main/sql", FileUtils.getAssetFile("/src/main/sql")); //create bindings // try { // String content = FileUtils.getAssetFileAsString("/Scripts/test.js"); // Source source = Source.create("js", content); // context.eval(source); // System.out.println("Evaluated"); // } catch (IOException e) { // // TODO Auto-generated catch block // e.printStackTrace(); // } } /** * 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); } @Deprecated /** * Reads the scripts directory * @param path The * @param directory */ void registerScriptDirectory(String path, File directory){ if(directory.exists() && directory.isDirectory()){ File[] children = directory.listFiles(); for(File childFile : children){ String qualifiedName = path + "/" + childFile.getName(); if(childFile.isDirectory()){ registerScriptDirectory(qualifiedName,childFile); } else { //add to source map registerFile(qualifiedName); // String content = FileUtils.readFileToString(childFile); // sourceMap.put(qualifiedName,Source.create("js",content)); } } } } /** * Loads a script from disk * @param path The path to the script file */ void loadDependency(String path){ String content; Source source = null; try { content = FileUtils.getAssetFileAsString(path); source = Source.create("js",content); sourceMap.put(path,source); 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 */ private boolean registerFile(String path){ String content; try { content = FileUtils.getAssetFileAsString(path); sourceMap.put(path,Source.create("js",content)); Value dependentFilesValue = 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 : registerIgnores){ if(ignorePath.equals(dependentFilePath)){ shouldRegister = false; } } if(shouldRegister){ LoggerInterface.loggerScripts.INFO("[HOST - Script Engine] Should register file " + dependentFilePath); 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 */ private void compile(){ invokeMemberFunction("COMPILER", "run"); } /** * 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){ invokeFunction("require", filePath); } /** * Defines host members within javascript context */ private void defineHostMembers(){ hostObject = topLevelValue.getMember("HOST_ACCESS"); //give guest access to static classes Value classes = engineObject.getMember("classes"); for(Object[] currentClass : 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); } }