script engine work

This commit is contained in:
austin 2024-07-02 16:14:05 -04:00
parent df4fe45dd5
commit e5a187ce19
21 changed files with 414 additions and 28 deletions

5
.gitignore vendored
View File

@ -44,4 +44,7 @@
/docs/DoxygenWarningLog.txt /docs/DoxygenWarningLog.txt
#imgui local layout #imgui local layout
/imgui.ini /imgui.ini
#script engine related
/assets/Scripts/compiler/typescript.js

View File

@ -4,5 +4,7 @@
"**/.git/objects/**": true, "**/.git/objects/**": true,
"**/node_modules/**": true "**/node_modules/**": true
}, },
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable" "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable",
"javascript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifier": "non-relative"
} }

View File

@ -0,0 +1,113 @@
/**
* The map of all source files to their content and compiled value
*/
let COMPILER_fileMap = { }
/**
* The compiled program
*/
let COMPILER_emitted_value = ''
/**
* Registers a file with the compiler
* @param {*} fileName The file's name
* @param {*} content The content of the file
*/
const COMPILER_registerFile = (fileName, content) => {
let normalizedFilePath = FILE_RESOLUTION_getFilePath(fileName,false)
loggerScripts.INFO('REGISTER FILE ' + normalizedFilePath)
COMPILER_fileMap[normalizedFilePath] = {
content: content,
compiled: ts.createSourceFile(
normalizedFilePath, content, ts.ScriptTarget.Latest
)
}
}
/**
* The callback invoked when the compiler host tries to read a file
* @param {*} fileName The name of the file
* @param {*} languageVersion The language version
* @returns The file if it exists, null otherwise
*/
const COMPILER_getSourceFile = (fileName, languageVersion) => {
if(!!COMPILER_fileMap[fileName]){
return COMPILER_fileMap[fileName].compiled
} else {
return null
}
}
/**
* Constructs the compiler host
* https://www.typescriptlang.org/tsconfig/#compilerOptions
*/
const COMPILER_customCompilerHost = {
getSourceFile: COMPILER_getSourceFile,
writeFile: (fileName, data) => {
let normalizedFilePath = FILE_RESOLUTION_getFilePath(fileName)
loggerScripts.INFO("EMIT FILE " + normalizedFilePath)
let finalData =
"let exports = { }\n" +
data + "\n" +
"return exports"
// COMPILER_emitted_value = data
COMPILER_fileMap[normalizedFilePath] = {
content: data, //to be eval'd from top level
moduleContent: finalData, //to be eval'd from require()
}
},
getDefaultLibFileName: () => "lib.d.ts",
useCaseSensitiveFileNames: () => false,
getCanonicalFileName: filename => filename,
getCurrentDirectory: () => "",
getNewLine: () => "\n",
getDirectories: () => [],
fileExists: () => true,
readFile: () => ""
}
/**
* Instructs Typescript to emit the final compiled value
*/
const COMPILER_run = () => {
loggerScripts.INFO('COMPILE ALL REGISTERED FILES')
const compilerOptions = { }
const COMPILER_program = ts.createProgram(
Object.keys(COMPILER_fileMap), compilerOptions, COMPILER_customCompilerHost
)
COMPILER_program.emit()
}
/**
* Loads a file
* @param {*} fileName The name of the file to load (preferably already has .ts at the end)
*/
const COMPILER_runFile = (fileName) => {
let normalizedFilePath = FILE_RESOLUTION_getFilePath(fileName)
if(!!COMPILER_fileMap[normalizedFilePath]){
loggerScripts.INFO('RUN FILE ' + normalizedFilePath)
eval(COMPILER_fileMap[normalizedFilePath].content)
} else {
loggerScripts.WARNING('FAILED TO RESOLVE FILE ' + normalizedFilePath)
}
}
/**
* Loads a file
* @param {*} fileName The name of the file to load (preferably already has .ts at the end)
*/
const COMPILER_printSource = (fileName) => {
let normalizedFilePath = FILE_RESOLUTION_getFilePath(fileName)
if(!!COMPILER_fileMap[normalizedFilePath]){
loggerScripts.INFO('FILE CONTENT ' + normalizedFilePath)
} else {
loggerScripts.WARNING('FAILED TO RESOLVE FILE ' + normalizedFilePath)
}
}

View File

@ -0,0 +1,23 @@
/**
* Normalizes a file path
* @param {*} rawFilePath The raw file path
* @returns The normalized file path
*/
const FILE_RESOLUTION_getFilePath = (rawFilePath, isJavascript = true) => {
let fileName = rawFilePath
if(isJavascript && fileName.includes('.ts')){
fileName = fileName.replace('.ts','.js')
}
if(fileName.startsWith('/Scripts')){
fileName = fileName.replace('/Scripts','')
}
if(fileName.startsWith('Scripts/')){
fileName = fileName.replace('Scripts/','/')
}
if(isJavascript && !fileName.endsWith(".js")){
fileName = fileName + ".js"
}
return fileName
}

View File

@ -0,0 +1 @@
wget -O typescript.js https://unpkg.com/typescript@latest/lib/typescript.js

View File

@ -0,0 +1,9 @@
/**
* The host context that contains the core engine functions
*/
export let HOST = {
classes: { }, //the classes available to the script engine
singletons: { }, //the singletons available to the script engine
}

View File

@ -0,0 +1,36 @@
/**
* Caches loaded modules
*/
let REQUIRE_CACHE = { }
/**
* Used if the module is directly executed instead of being require'd for some reason
*/
let exports = { }
/**
* Imports a module
* @param {*} path The path of the module
* @param {*} cwd The current working directory
*/
const require = (path) => {
let normalizedFilePath = FILE_RESOLUTION_getFilePath(path)
if(REQUIRE_CACHE[path]){
return REQUIRE_CACHE[normalizedFilePath].exports
} else if(!!COMPILER_fileMap[normalizedFilePath]?.content) {
const code = COMPILER_fileMap[normalizedFilePath].moduleContent
let exports = new Function(code)()
//create module object
const module = {
exports: exports,
exportedValues: Object.keys(exports),
}
REQUIRE_CACHE[normalizedFilePath] = module
loggerScripts.INFO("[require] CREATE MODULE " + normalizedFilePath)
return module.exports
} else {
loggerScripts.WARNING("FAILED TO REQUIRE FILE " + normalizedFilePath)
}
}

View File

@ -0,0 +1,7 @@
/**
* Called when the script engine first initializes
*/
export const ENGINE_onInit = () => {
console.log('Script Engine Init')
}

View File

@ -0,0 +1,9 @@
/**
* The host context that contains all core engine functions
*/
export interface Host {
classes: any, //the host's view of the scripting engine
singletons: any, //the singletons available to the script engine
}

View File

@ -1 +0,0 @@
console.log("test")

View File

@ -4,4 +4,5 @@
- @subpage biomeideas - @subpage biomeideas
- @subpage largelocationideas - @subpage largelocationideas
- @subpage macrolocationideas - @subpage macrolocationideas
- @subpage smalllocations - @subpage smalllocations
- @subpage minidungeons

View File

@ -0,0 +1,4 @@
@page minidungeons Mini Dungeons
In certain levels, you can find premade characters that can join your party.

View File

@ -21,7 +21,6 @@ Redo hitboxes to have capsules and also chaining between frames (but not between
- Introduce block hitbox (blockbox) type - Introduce block hitbox (blockbox) type
- Sour spot, sweet spot for damage hitboxes and hurtboxes - Sour spot, sweet spot for damage hitboxes and hurtboxes
Enemy AI Enemy AI
better scaffolding for scriptig engine with hooks for equipping items, spawning entities, pausing/resuming play, etc
Ability for private realms to have time start/stop based on the player's feedback <-- sync this up to tutorial ui via script Ability for private realms to have time start/stop based on the player's feedback <-- sync this up to tutorial ui via script
Scene Message Service Scene Message Service
- Can send arbitrary events and messages - Can send arbitrary events and messages

View File

@ -406,6 +406,8 @@ Audio
- Sword Hit Metal - Sword Hit Metal
- Sword Hit Flesh - Sword Hit Flesh
(06/02/2024)
better scaffolding for scripting engine with hooks for equipping items, spawning entities, pausing/resuming play, etc
# TODO # TODO
@ -413,7 +415,7 @@ Audio
BIG BIG BIG BIG IMMEDIATE TO DO: BIG BIG BIG BIG IMMEDIATE TO DO:
always enforce opengl interface across all opengl calls jesus christ the bone uniform bug was impossible always enforce opengl interface across all opengl calls jesus christ the bone uniform bug was impossible
Fix not all grass tiles update when updating a nearby voxel (ie it doesn't go into negative coordinates to scan for foliage updates)

View File

@ -444,7 +444,6 @@ public class Globals {
elementManager = new ElementManager(); elementManager = new ElementManager();
//script engine //script engine
scriptEngine = new ScriptEngine(); scriptEngine = new ScriptEngine();
scriptEngine.init();
//ai manager //ai manager
aiManager = new AIManager(); aiManager = new AIManager();
//realm & data cell manager //realm & data cell manager

View File

@ -76,6 +76,9 @@ public class Main {
//init global variables //init global variables
Globals.initGlobals(); Globals.initGlobals();
//init scripting engine
Globals.scriptEngine.init();
//controls //controls
if(Globals.RUN_CLIENT){ if(Globals.RUN_CLIENT){
initControlHandler(); initControlHandler();

View File

@ -56,10 +56,11 @@ public class SceneLoader {
} }
} }
//load scripts //load scripts
for(String scriptPath : file.getScriptPaths()){ //TODO: integrate scripts for client side of scenes
Globals.scriptEngine.loadScript(scriptPath); // for(String scriptPath : file.getScriptPaths()){
} // Globals.scriptEngine.loadScript(scriptPath);
Globals.scriptEngine.runScript(file.getInitScriptPath()); // }
// Globals.scriptEngine.runScript(file.getInitScriptPath());
return rVal; return rVal;
} }

View File

@ -1,5 +1,7 @@
package electrosphere.logger; package electrosphere.logger;
import org.graalvm.polyglot.HostAccess.Export;
/** /**
* A channel for logging messages * A channel for logging messages
*/ */
@ -56,6 +58,7 @@ public class Logger {
* This should be used for messages that would have interest to someone running a server (ie specific network messages, account creation, etc) * This should be used for messages that would have interest to someone running a server (ie specific network messages, account creation, etc)
* @param message The message to report * @param message The message to report
*/ */
@Export
public void INFO(String message){ public void INFO(String message){
if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO){ if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO){
System.out.println(message); System.out.println(message);
@ -68,6 +71,7 @@ public class Logger {
* This should be used for reporting events that happen in the engine that are concerning but don't mean the engine has failed to execute (ie a texture failed to load) * This should be used for reporting events that happen in the engine that are concerning but don't mean the engine has failed to execute (ie a texture failed to load)
* @param message The message to report * @param message The message to report
*/ */
@Export
public void WARNING(String message){ public void WARNING(String message){
if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO || level == LogLevel.WARNING){ if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO || level == LogLevel.WARNING){
System.out.println(message); System.out.println(message);

View File

@ -20,6 +20,7 @@ public class LoggerInterface {
public static Logger loggerDB; public static Logger loggerDB;
public static Logger loggerAudio; public static Logger loggerAudio;
public static Logger loggerUI; public static Logger loggerUI;
public static Logger loggerScripts;
/** /**
* Initializes all logic objects * Initializes all logic objects
@ -35,6 +36,7 @@ public class LoggerInterface {
loggerDB = new Logger(LogLevel.WARNING); loggerDB = new Logger(LogLevel.WARNING);
loggerAudio = new Logger(LogLevel.WARNING); loggerAudio = new Logger(LogLevel.WARNING);
loggerUI = new Logger(LogLevel.WARNING); loggerUI = new Logger(LogLevel.WARNING);
loggerScripts = new Logger(LogLevel.WARNING);
loggerStartup.INFO("Initialized loggers"); loggerStartup.INFO("Initialized loggers");
} }
} }

View File

@ -7,11 +7,14 @@ import java.util.Map;
import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value; import org.graalvm.polyglot.Value;
import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface; import electrosphere.logger.LoggerInterface;
import electrosphere.util.FileUtils; import electrosphere.util.FileUtils;
import electrosphere.util.MathUtils;
/** /**
* Interface for executing scripts in the game engine * Interface for executing scripts in the game engine
@ -25,7 +28,37 @@ public class ScriptEngine {
Map<String,Source> sourceMap; Map<String,Source> sourceMap;
//the javascript object that stores values //the javascript object that stores values
Value jsBindingsObject; Value topLevelValue;
//the object that contains all host values accessible to javascript land
Value hostObject;
//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",
};
//The classes that will be provided to the scripting engine
//https://stackoverflow.com/a/65942034
static final Object[][] staticClasses = new Object[][]{
{"mathUtils",MathUtils.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 * Initializes the engine
@ -36,11 +69,42 @@ public class ScriptEngine {
//create engine with flag to disable warning //create engine with flag to disable warning
Engine engine = Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build(); Engine engine = Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build();
//create context //create context
context = Context.newBuilder("js").engine(engine).build(); context = Context.newBuilder("js")
//read scripts into source map .allowNativeAccess(false)
readScriptsDirectory("/src/main/sql", FileUtils.getAssetFile("/src/main/sql")); .engine(engine)
.build();
//save the js bindings object //save the js bindings object
jsBindingsObject = context.getBindings("js"); 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);
}
//define host members
defineHostMembers();
//register engine files
registerScriptDirectory("Scripts/engine",FileUtils.getAssetFile("Scripts/engine"));
//compile
compile();
//run script for engine init
requireModule("/Scripts/engine/engine-init.ts");
invokeModuleFunction("/Scripts/engine/engine-init.ts","ENGINE_onInit");
//call the engine initialization function
// invokeFunction("ENGINE_onInit");
System.exit(0);
//read scripts into source map
// readScriptsDirectory("/src/main/sql", FileUtils.getAssetFile("/src/main/sql"));
//create bindings //create bindings
// try { // try {
// String content = FileUtils.getAssetFileAsString("/Scripts/test.js"); // String content = FileUtils.getAssetFileAsString("/Scripts/test.js");
@ -59,7 +123,7 @@ public class ScriptEngine {
* @param value The value that is stored at that variable * @param value The value that is stored at that variable
*/ */
public void putTopLevelValue(String valueName, Object value){ public void putTopLevelValue(String valueName, Object value){
jsBindingsObject.putMember(valueName, value); topLevelValue.putMember(valueName, value);
} }
/** /**
@ -68,7 +132,16 @@ public class ScriptEngine {
* @return The value of the variable * @return The value of the variable
*/ */
public Value getTopLevelValue(String valueName){ public Value getTopLevelValue(String valueName){
return jsBindingsObject.getMember(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);
} }
/** /**
@ -76,17 +149,18 @@ public class ScriptEngine {
* @param path The * @param path The
* @param directory * @param directory
*/ */
void readScriptsDirectory(String path, File directory){ void registerScriptDirectory(String path, File directory){
if(directory.exists() && directory.isDirectory()){ if(directory.exists() && directory.isDirectory()){
File[] children = directory.listFiles(); File[] children = directory.listFiles();
for(File childFile : children){ for(File childFile : children){
String qualifiedName = path + "/" + childFile.getName(); String qualifiedName = path + "/" + childFile.getName();
if(childFile.isDirectory()){ if(childFile.isDirectory()){
readScriptsDirectory(qualifiedName,childFile); registerScriptDirectory(qualifiedName,childFile);
} else { } else {
//add to source map //add to source map
String content = FileUtils.readFileToString(childFile); registerFile(qualifiedName);
sourceMap.put(qualifiedName,Source.create("js",content)); // String content = FileUtils.readFileToString(childFile);
// sourceMap.put(qualifiedName,Source.create("js",content));
} }
} }
} }
@ -96,15 +170,17 @@ public class ScriptEngine {
* Loads a script from disk * Loads a script from disk
* @param path The path to the script file * @param path The path to the script file
*/ */
public void loadScript(String path){ public void loadDependency(String path){
String content; String content;
try { try {
content = FileUtils.getAssetFileAsString(path); content = FileUtils.getAssetFileAsString(path);
sourceMap.put(path,Source.create("js",content)); sourceMap.put(path,Source.create("js",content));
context.eval(sourceMap.get(path));
} catch (IOException e) { } catch (IOException e) {
// TODO Auto-generated catch block LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e);
} catch (PolyglotException e){
LoggerInterface.loggerScripts.ERROR("Script error", e);
e.printStackTrace(); e.printStackTrace();
LoggerInterface.loggerGameLogic.ERROR("FAILED TO LOAD SCRIPT", e);
} }
} }
@ -113,12 +189,96 @@ public class ScriptEngine {
* @param path The filepath of the script * @param path The filepath of the script
*/ */
public void runScript(String path){ public void runScript(String path){
Source source = sourceMap.get(path); invokeFunction("COMPILER_runFile", path);
if(source != null){ }
context.eval(source);
/**
* Prints the content of a file
* @param path The filepath of the script
*/
public void printScriptSource(String path){
invokeFunction("COMPILER_printSource", path);
}
/**
* Registers a file with the scripting engine to be compiled into the full binary
* @param path The path to the script file
*/
private void registerFile(String path){
String content;
try {
content = FileUtils.getAssetFileAsString(path);
sourceMap.put(path,Source.create("js",content));
invokeFunction("COMPILER_registerFile",path,content);
} catch (IOException e) {
LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e);
} }
} }
/**
* Compiles the project
*/
private void compile(){
invokeFunction("COMPILER_run");
Value compiledCode = topLevelValue.getMember("COMPILER_emitted_value");
context.eval("js",compiledCode.asString());
}
/**
* 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){
Value function = topLevelValue.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(){
//remove top level members required for bootstrapping the engine
removeTopLevelValue("loggerScripts");
//give guest access to static classes
Value classes = hostObject.getMember("classes");
for(Object[] currentClass : staticClasses){
classes.putMember((String)currentClass[0], currentClass[1]);
}
}
} }

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"paths" : {
"/*" : [
"./assets/Scripts/*"
],
}
}
}