Renderer/src/main/java/electrosphere/script/ScriptEngine.java
2024-08-25 11:51:15 -04:00

415 lines
14 KiB
Java

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<String,Source> 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<String,Source>();
//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);
}
}