Renderer/src/main/java/electrosphere/script/ScriptEngine.java
austin b892bea3d3
Some checks failed
studiorailgun/Renderer/pipeline/head There was a failure building this commit
Fix backing out to main menu
2024-11-20 19:12:18 -05:00

311 lines
9.9 KiB
Java

package electrosphere.script;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import electrosphere.client.script.ScriptClientVoxelUtils;
import electrosphere.client.ui.menu.script.ScriptLevelEditorUtils;
import electrosphere.client.ui.menu.script.ScriptMenuUtils;
import electrosphere.client.ui.menu.tutorial.TutorialMenus;
import electrosphere.engine.Globals;
import electrosphere.engine.Main;
import electrosphere.engine.signal.Signal;
import electrosphere.engine.signal.Signal.SignalType;
import electrosphere.engine.signal.SignalServiceImpl;
import electrosphere.logger.LoggerInterface;
import electrosphere.script.translation.JSServerUtils;
import electrosphere.script.utils.ScriptMathInterface;
import electrosphere.util.FileUtils;
import electrosphere.util.math.SpatialMathUtils;
/**
* Handles the actual file loading of script files
*/
public class ScriptEngine extends SignalServiceImpl {
/**
* The directory with all script source files
*/
public static final String TS_SOURCE_DIR = "./assets/Scripts";
/**
* The typescript cache dir
*/
public static final String TS_CACHE_DIR = "./.cache/tscache";
/**
* Directory that should contain all ts source dirs
*/
public static final String TS_SOURCE_CACHE_DIR = TS_CACHE_DIR + "/src";
/**
* The id for firing signals globally
*/
public static final int GLOBAL_SCENE = -1;
//the map of script filepaths to parsed, in-memory scripts
Map<String,Source> sourceMap;
/**
* Stores all loaded files' md5 checksums
*/
Map<String,String> fileChecksumMap = new HashMap<String,String>();
/**
* The script context
*/
ScriptContext scriptContext = new ScriptContext();
/**
* The file system object
*/
FileSystem fs;
/**
* The watch service
*/
WatchService watchService;
/**
* Tracks the initialization status of the script engine
*/
boolean initialized = false;
//The files that are loaded on init to bootstrap the script engine
public 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
*/
public 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
public static final Object[][] staticClasses = new Object[][]{
{"mathUtils",SpatialMathUtils.class},
{"simulation",Main.class},
{"tutorialUtils",TutorialMenus.class},
{"serverUtils",JSServerUtils.class},
{"menuUtils",ScriptMenuUtils.class},
{"voxelUtils",ScriptClientVoxelUtils.class},
{"levelEditorUtils",ScriptLevelEditorUtils.class},
{"math",ScriptMathInterface.class},
};
//singletons from the host that are provided to the javascript context
public static final Object[][] hostSingletops = new Object[][]{
{"timekeeper",Globals.timekeeper},
{"currentPlayer",Globals.clientPlayer},
{"loggerScripts",LoggerInterface.loggerScripts},
};
/**
* Constructor
*/
public ScriptEngine(){
super(
"ScriptEngine",
new SignalType[]{
SignalType.SCRIPT_RECOMPILE,
}
);
sourceMap = new HashMap<String,Source>();
this.fs = FileSystems.getDefault();
try {
this.watchService = fs.newWatchService();
} catch (IOException e) {
LoggerInterface.loggerFileIO.ERROR(e);
}
//register all source directories
try {
Files.walkFileTree(new File(TS_SOURCE_DIR).toPath(), new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
dir.register(
watchService,
new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE}
);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Initializes the engine
*/
public void initScripts(){
//init datastructures
initialized = false;
//init script context
scriptContext.init(this);
//read files from cache
boolean readCache = this.initCache();
//compile
if(!readCache){
scriptContext.compile();
}
//post init logic
scriptContext.postInit();
initialized = true;
}
/**
* Scans the scripts directory for updates
*/
public void scanScriptDir(){
WatchKey key = null;
while((key = watchService.poll()) != null){
List<WatchEvent<?>> events = key.pollEvents();
for(WatchEvent<?> event : events){
if(event.kind() == StandardWatchEventKinds.ENTRY_MODIFY){
if(event.context() instanceof Path){
// Path filePath = (Path)event.context();
// System.out.println(filePath);
}
} else if(event.kind() == StandardWatchEventKinds.ENTRY_CREATE){
throw new Error("Cannot handle create events yet");
} else if(event.kind() == StandardWatchEventKinds.ENTRY_DELETE){
throw new Error("Cannot handle delete events yet");
}
}
key.reset();
}
}
/**
* Gets the script context of the engine
* @return The script context
*/
public ScriptContext getScriptContext(){
return this.scriptContext;
}
/**
* Makes sure the cache folder exists
* @return true if files were read from cache, false otherwise
*/
private boolean initCache(){
File tsCache = new File(ScriptEngine.TS_SOURCE_CACHE_DIR);
if(!tsCache.exists()){
try {
Files.createDirectories(tsCache.toPath());
} catch (IOException e) {
LoggerInterface.loggerFileIO.ERROR(e);
}
return false;
} else {
Value fileMap = this.scriptContext.getTopLevelValue("COMPILER").getMember("fileMap");
this.recursivelyRegisterCachedFiles(tsCache,fileMap,tsCache);
return true;
}
}
/**
* Register files recursively from the current file
* @param tsCache The ts cache file
* @param fileMap The file map on script side
* @param currentDirectory The current directory
*/
private void recursivelyRegisterCachedFiles(File tsCache, Value fileMap, File currentDirectory){
for(File file : currentDirectory.listFiles()){
if(file.isDirectory()){
this.recursivelyRegisterCachedFiles(tsCache, fileMap, file);
} else if(file.getPath().endsWith(".ts") || file.getPath().endsWith(".js")){
try {
String relativePath = FileUtils.relativize(file, tsCache);
String normalizedPath = "/" + relativePath;
if(!this.fileChecksumMap.containsKey(normalizedPath)){
//read file
String fileContent = Files.readString(file.toPath());
//store checksum
try {
this.fileChecksumMap.put(normalizedPath,FileUtils.getChecksum(fileContent));
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//store on script side
LoggerInterface.loggerScripts.DEBUG("Preload: " + normalizedPath);
this.scriptContext.getTopLevelValue("COMPILER").invokeMember("preloadFile", normalizedPath, fileContent);
}
} catch (IOException e) {
LoggerInterface.loggerFileIO.ERROR(e);
}
}
}
}
/**
* Gets the initialization status of the script engine
* @return true if initialized, false otherwise
*/
public boolean isInitialized(){
return this.initialized;
}
@Override
public boolean handle(Signal signal){
boolean rVal = false;
switch(signal.getType()){
case SCRIPT_RECOMPILE: {
if(signal.getData() != null && signal.getData() instanceof Runnable){
scriptContext.recompile((Runnable)signal.getData());
} else {
scriptContext.recompile(null);
}
rVal = true;
} break;
default: {
} break;
}
return rVal;
}
}