Renderer/src/main/java/electrosphere/script/ScriptContext.java
2025-05-15 16:42:06 -04:00

451 lines
15 KiB
Java

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);
}
}