415 lines
14 KiB
Java
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);
|
|
}
|
|
|
|
|
|
}
|