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

3
.gitignore vendored
View File

@ -45,3 +45,6 @@
#imgui local layout
/imgui.ini
#script engine related
/assets/Scripts/compiler/typescript.js

View File

@ -4,5 +4,7 @@
"**/.git/objects/**": 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

@ -5,3 +5,4 @@
- @subpage largelocationideas
- @subpage macrolocationideas
- @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
- Sour spot, sweet spot for damage hitboxes and hurtboxes
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
Scene Message Service
- Can send arbitrary events and messages

View File

@ -406,6 +406,8 @@ Audio
- Sword Hit Metal
- Sword Hit Flesh
(06/02/2024)
better scaffolding for scripting engine with hooks for equipping items, spawning entities, pausing/resuming play, etc
# TODO
@ -413,7 +415,7 @@ Audio
BIG BIG BIG BIG IMMEDIATE TO DO:
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();
//script engine
scriptEngine = new ScriptEngine();
scriptEngine.init();
//ai manager
aiManager = new AIManager();
//realm & data cell manager

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package electrosphere.logger;
import org.graalvm.polyglot.HostAccess.Export;
/**
* 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)
* @param message The message to report
*/
@Export
public void INFO(String message){
if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO){
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)
* @param message The message to report
*/
@Export
public void WARNING(String message){
if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO || level == LogLevel.WARNING){
System.out.println(message);

View File

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

View File

@ -7,11 +7,14 @@ import java.util.Map;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.util.FileUtils;
import electrosphere.util.MathUtils;
/**
* Interface for executing scripts in the game engine
@ -25,7 +28,37 @@ public class ScriptEngine {
Map<String,Source> sourceMap;
//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
@ -36,11 +69,42 @@ public class ScriptEngine {
//create engine with flag to disable warning
Engine engine = Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build();
//create context
context = Context.newBuilder("js").engine(engine).build();
//read scripts into source map
readScriptsDirectory("/src/main/sql", FileUtils.getAssetFile("/src/main/sql"));
context = Context.newBuilder("js")
.allowNativeAccess(false)
.engine(engine)
.build();
//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
// try {
// String content = FileUtils.getAssetFileAsString("/Scripts/test.js");
@ -59,7 +123,7 @@ public class ScriptEngine {
* @param value The value that is stored at that variable
*/
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
*/
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 directory
*/
void readScriptsDirectory(String path, File 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()){
readScriptsDirectory(qualifiedName,childFile);
registerScriptDirectory(qualifiedName,childFile);
} else {
//add to source map
String content = FileUtils.readFileToString(childFile);
sourceMap.put(qualifiedName,Source.create("js",content));
registerFile(qualifiedName);
// String content = FileUtils.readFileToString(childFile);
// sourceMap.put(qualifiedName,Source.create("js",content));
}
}
}
@ -96,15 +170,17 @@ public class ScriptEngine {
* Loads a script from disk
* @param path The path to the script file
*/
public void loadScript(String path){
public void loadDependency(String path){
String content;
try {
content = FileUtils.getAssetFileAsString(path);
sourceMap.put(path,Source.create("js",content));
context.eval(sourceMap.get(path));
} 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();
LoggerInterface.loggerGameLogic.ERROR("FAILED TO LOAD SCRIPT", e);
}
}
@ -113,12 +189,96 @@ public class ScriptEngine {
* @param path The filepath of the script
*/
public void runScript(String path){
Source source = sourceMap.get(path);
if(source != null){
context.eval(source);
invokeFunction("COMPILER_runFile", path);
}
/**
* 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/*"
],
}
}
}