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 sourceMap; /** * Stores all loaded files' md5 checksums */ Map fileChecksumMap = new HashMap(); /** * 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(); 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(){ @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> 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; } }