Files
cell/scripts/engine.js
2025-01-24 00:04:55 -06:00

786 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

globalThis.prosperon = {}
var io = os.use('io')
os.mem_limit.doc = "Set the memory limit of the runtime in bytes.";
os.gc_threshold.doc = "Set the threshold before a GC pass is triggered in bytes. This is set to malloc_size + malloc_size>>1 after a GC pass.";
os.max_stacksize.doc = "Set the max stack size in bytes.";
globalThis.Resources = {};
prosperon.SIGINT = function() {
os.exit();
}
Resources.rm_fn = function rm_fn(fn, text) {
var reg = new RegExp(fn.source + "\\s*\\(");
var match;
while ((match = text.match(reg))) {
var last = match.index + match[0].length;
var par = 1;
while (par !== 0) {
if (text[last] === "(") par++;
if (text[last] === ")") par--;
last++;
}
text = text.rm(match.index, last);
}
return text;
};
Resources.rm_fn.doc = "Remove calls to a given function from a given text script.";
// Normalizes paths for use in prosperon
Resources.replpath = function replpath(str, path) {
if (!str) return str;
if (str[0] === "/") return str.rm(0);
if (!path) return str;
var stem = path.dir();
while (stem) {
var tr = stem + "/" + str;
if (io.exists(tr)) return tr;
stem = stem.updir();
}
return str;
};
// Given a script path, loads it, and replaces certain function calls to conform to environment
Resources.replstrs = function replstrs(path) {
if (!path) return;
var script = io.slurp(path);
if (!script) return;
var regexp = /"[^"\s]*?\.[^"\s]+?"/g;
var stem = path.dir();
// if (!console.enabled) script = Resources.rm_fn(/console\.(spam|info|warn|error)/, script);
/* if (!profile.enabled) script = Resources.rm_fn(/profile\.(cache|frame|endcache|endframe)/, script);
if (!debug.enabled) {
script = Resources.rm_fn(/assert/, script);
script = Resources.rm_fn(/debug\.(build|fn_break)/, script);
}
*/
script = script.replace(regexp, function (str) {
var newstr = Resources.replpath(os.trimchr(str,'"'), path);
return `"${newstr}"`;
});
return script;
}
Resources.is_sound = function (path) {
var ext = path.ext();
return Resources.sounds.any(x => x === ext);
};
Resources.is_animation = function (path) {
if (path.ext() === "gif" && Resources.gif.frames(path) > 1) return true;
if (path.ext() === "ase") return true;
return false;
};
Resources.is_path = function (str) {
return !/[\\\/:*?"<>|]/.test(str);
};
globalThis.json = {};
json.encode = function json_encode(value, replacer, space = 1) {
return JSON.stringify(value, replacer, space);
};
json.decode = function json_decode(text, reviver) {
if (!text) return undefined;
return JSON.parse(text, reviver);
};
json.readout = function (obj) {
var j = {};
for (var k in obj)
if (typeof obj[k] === "function") j[k] = "function " + obj[k].toString();
else j[k] = obj[k];
return json.encode(j);
};
json.doc = {
doc: "json implementation.",
encode: "Encode a value to json.",
decode: "Decode a json string to a value.",
readout: "Encode an object fully, including function definitions.",
};
Resources.scripts = ["jsoc", "jsc", "jso", "js"];
Resources.images = ["qoi", "png", "gif", "jpg", "jpeg", "ase", "aseprite"];
Resources.sounds = ["wav", "flac", "mp3", "qoa"];
Resources.fonts = ["ttf"];
Resources.is_image = function (path) {
var ext = path.ext();
return Resources.images.some(x => x === ext);
};
Resources.shaders = ["hlsl", "glsl", "cg"]
Resources.is_shader = function(path) {
var ext = path.ext();
return Resources.shaders.some(x => x === ext)
}
var res_cache = {};
// ext is a list of extensions to search
function find_ext(file, ext) {
if (!file) return;
var file_ext = file.ext();
var has_ext = file_ext.length > 0;
for (var e of ext) {
var nf = `${file}.${e}`;
if (io.exists(nf)) return nf;
}
var glob_pat = has_ext ? `**/${file}` : `**/${file}.*`;
var all_files = io.glob(glob_pat);
var find = undefined;
for (var e of ext) {
var finds = all_files.filter(x => x.ext() === e);
if (finds.length > 1) {
console.warn(`Found conflicting files when searching for '${file}': ${json.encode(finds)}. Returning the topmost one.`);
finds.sort((a,b) => a.length-b.length);
return finds[0];
}
if (finds.length === 1) return finds[0];
}
return find;
}
Object.defineProperty(Function.prototype, "hashify", {
value: function () {
var hash = new Map();
var fn = this;
function hashified(...args) {
var key = args[0];
if (!hash.has(key)) hash.set(key, fn(...args));
return hash.get(key);
}
return hashified;
},
});
Resources.find_image = function (file, root = "") {
return find_ext(file, Resources.images, root);
}.hashify();
Resources.find_sound = function (file, root = "") {
return find_ext(file, Resources.sounds, root);
}.hashify();
Resources.find_script = function (file, root = "") {
return find_ext(file, Resources.scripts, root);
}.hashify();
Resources.find_font = function(file, root = "") {
return find_ext(file, Resources.fonts, root);
}.hashify();
var tmpslurp = io.slurp;
io.slurp = function slurp(path)
{
var findpath = Resources.replpath(path);
var ret = tmpslurp(findpath, true); //|| core_db.slurp(findpath, true);
return ret;
}
io.slurpbytes = function(path)
{
path = Resources.replpath(path);
var ret = tmpslurp(path);// || core_db.slurp(path);
if (!ret) throw new Error(`Could not find file ${path} anywhere`);
return ret;
}
var ignore = io.slurp('.prosperonignore').split('\n');
var allpaths;
io.glob = function glob(pat) {
if (!allpaths)
allpaths = io.globfs(ignore);
return allpaths.filter(str => io.match(pat,str)).sort();
}
io.invalidate = function()
{
allpaths = undefined;
}
console.transcript = "";
console.say = function (msg) {
console.print(msg);
if (debug.termout) console.term_print(msg);
console.transcript += msg;
};
console.rec = function(category, priority, line, file, msg)
{
return `${file}:${line}: [${category} ${priority}]: ${msg}` + "\n";
}
var logfile = io.open('.prosperon/log.txt')
//logfile.buffer(1024*1024) // 1MB buffer
console.stdout_lvl = 0;
function pprint(msg, lvl = 0) {
if (lvl < console.stdout_lvl && !logfile) return;
if (typeof msg === "object") msg = JSON.stringify(msg, null, 2);
var file = "nofile";
var line = 0;
var caller = new Error().stack.split("\n")[2];
if (caller) {
var md = caller.match(/\((.*)\:/);
var m = md ? md[1] : "SCRIPT";
if (m) file = m;
md = caller.match(/\:(\d*)\)/);
m = md ? md[1] : 0;
if (m) line = m;
}
var fmt = console.rec("script", lvl, line,file, msg);
if (lvl >= console.stdout_lvl)
console.print(fmt)
if (logfile)
logfile.write(fmt)
if (tracy) tracy.message(fmt);
};
console.spam = function spam(msg) {
pprint(msg, 0);
};
console.debug = function debug(msg) {
pprint(msg, 1);
};
console.info = function info(msg) {
pprint(msg, 2);
};
console.warn = function warn(msg) {
pprint(msg, 3);
};
console.log = function(msg)
{
pprint(msg, 2)
}
console.error = function(e) {
console.print(e.message)
if (!e)
e = new Error();
pprint(`${e.name} : ${e.message}
${e.stack}`, 4)
};
console.panic = function (e) {
console.pprint(e , 5)
os.quit();
};
console.assert = function (op, str = `assertion failed [value '${op}']`) {
if (!op) console.panic(str);
};
os.on('uncaught_exception', function(e) { console.error(e); });
console.doc = {
log: "Output directly to in game console.",
level: "Set level to output logging to console.",
info: "Output info level message.",
warn: "Output warn level message.",
error: "Output error level message, and print stacktrace.",
critical: "Output critical level message, and exit game immediately.",
write: "Write raw text to console.",
say: "Write raw text to console, plus a newline.",
stack: "Output a stacktrace to console.",
clear: "Clear console.",
};
var script = io.slurp("core/scripts/base.js")
var fnname = "base"
script = `(function ${fnname}() { ${script}; })`
os.eval('core/scripts/base.js', script)()
prosperon.SIGABRT = function()
{
console.error(new Error('SIGABRT'));
os.exit(1);
}
prosperon.SIGSEGV = function()
{
console.error(new Error('SIGSEGV'));
os.exit(1);
}
function add_timer(obj, fn, seconds)
{
var timers = obj.timers;
var stop = function () {
if (!timer) return
timers.delete(stop);
timer.fn = undefined;
timer = undefined;
};
function execute() {
if (fn)
timer.remain = fn(stop.seconds);
if (!timer) return
if (!timer.remain)
stop();
else
stop.seconds = timer.remain;
}
var timer = os.make_timer(execute);
timer.remain = seconds;
stop.remain = seconds;
stop.seconds = seconds;
timers.push(stop);
return stop;
}
var actor = {};
var so_ext;
switch(os.sys()) {
case 'Windows':
so_ext = '.dll';
break;
default:
so_ext = '.so';
break;
}
var use_cache = {}
function load_mod(file)
{
try {
var par = script_fn(file)
if (par?.module_ret) {
use_cache[file] = par.module_
return par.module_ret
}
} catch(e) {
console.error(e)
}
try {
return os.use('./lib' + file + so_ext)
} catch(e) { console.error(e) }
return os.use(file)
}
var use = function use(file) {
if (use_cache[file]) return use_cache[file];
use_cache[file] = load_mod(file)
return use_cache[file]
}
use.hotreload = function(file)
{
console.log(`hot reloading ${file}`)
var oldval = use_cache[file]
var newval = load_mod(file)
if (!oldval) {
use_cache[file] = newval;
return newval;
}
if (typeof oldval !== 'object' || typeof newval !== 'object' || !oldval || !newval) {
use_cache[file] = newval;
return newval;
}
use_patch(oldval, newval);
return oldval;
}
function use_patch(target, source)
{
// First remove properties that arent in source at all
for (let key of Object.keys(target)) {
if (!(key in source)) {
delete target[key];
}
}
// Then shallow-copy the sources own properties
for (let key of Object.keys(source)) {
target[key] = source[key];
}
// Update the prototype if needed so that new or changed methods come along
let oldProto = Object.getPrototypeOf(target);
let newProto = Object.getPrototypeOf(source);
if (oldProto !== newProto) {
Object.setPrototypeOf(target, newProto);
}
}
var script_fn = function script_fn(path) {
var file = Resources.find_script(path)
if (!file) throw new Error(`File ${path} could not be found`)
var content = Resources.replstrs(file);
var parsed = parse_file(content)
var module_name = file.name()
if (parsed.module) {
var mod_script = `(function setup_${module_name}_module(){ var self = this; var $ = this; var exports = {}; var module = {exports:exports}; var define = undefined; ${parsed.module}})`;
var module_fn = os.eval(file, mod_script)
var module_ret = module_fn.call();
if (module_ret === undefined || module_ret === null)
throw new Error(`Module ${module_name} must return a value`);
parsed.module_fn = module_fn;
parsed.module_ret = module_ret;
} else
parsed.module_ret = {}
if (parsed.program) {
var prog_script = `(function use_${module_name}() { var self = this; var $ = this.__proto__; ${parsed.program}})`;
parsed.prog_fn = os.eval(file, prog_script);
}
return parsed;
}.hashify()
function parse_file(content) {
if (!content) return {}
var parts = content.split(/\n\s*---\s*\n/)
if (parts.length === 1) {
var part = parts[0]
if (part.match(/return\s+[^;]+;?\s*$/))
return { module: part }
return { program: part }
}
var module = parts[0]
if (!module.match(/return\s+[^;]+;?\s*$/))
throw new Error("Module section must end with a return statement")
var pad = '\n'.repeat(module.split('\n').length+2) // add 2 from the split search
return {
module,
program: pad+parts[1]
}
}
actor.__stats = function () {
var total = 0;
var stats = {};
search.all_objects(obj => {
if (!actor_spawns[obj._file]) return;
stats[obj._file] ??= 0;
stats[obj._file]++;
total++;
});
/* for (var i in actor_spawns) {
stats[i] = actor_spawns[i].length;
total += stats[i];
}*/
// stats.total = total;
return stats;
};
actor.hotreload = function hotreload(file) {
var script = Resources.replstrs(file);
script = `(function() { var self = this; var $ = this.__proto__;${script};})`;
var fn = os.eval(file, script);
/*
for (var obj of actor_spawns[file]) {
var a = obj;
a.timers.forEachRight(t=>t());
a.timers = [];
var save = json.decode(json.encode(a));
fn.call(a);
Object.merge(a, save);
Register.check_registers(a);
}
*/
};
//////////
/// EVENT
/////////
var Event = {
events: {},
observe(name, obj, fn) {
this.events[name] ??= [];
this.events[name].push([obj, fn]);
},
unobserve(name, obj) {
this.events[name] = this.events[name].filter(x => x[0] !== obj);
},
rm_obj(obj) {
Object.keys(this.events).forEach(name => Event.unobserve(name, obj));
},
notify(name, ...args) {
if (!this.events[name]) return;
this.events[name].forEach(function (x) {
x[1].call(x[0], ...args);
});
},
};
//////////////////
///////REGISTRANT
/////////////////
/*
Factory for creating registries. Register one with 'X.register',
which returns a function that, when invoked, cancels the registry.
*/
var Register = {
registries: [],
add_cb(name) {
var n = {};
var fns = [];
n.register = function (fn, oname) {
if (!(fn instanceof Function)) return;
var dofn = function (...args) {
fn(...args);
};
Object.defineProperty(dofn, 'name', {value:`do_${oname}`});
var left = 0;
var right = fns.length - 1;
dofn.layer = fn.layer;
dofn.layer ??= 0;
while (left <= right) {
var mid = Math.floor((left + right) / 2);
if (fns[mid] === dofn.layer) {
left = mid;
break;
} else if (fns[mid].layer < dofn.layer) left = mid + 1;
else right = mid - 1;
}
fns.splice(left, 0, dofn);
return function () {
fns.delete(dofn)
};
};
prosperon[name] = function (...args) {
// tracy.fiber_enter(vector.fib);
fns.forEach(fn => {
fn(...args)
});
// tracy.fiber_leave(vector.fib);
};
Object.defineProperty(prosperon[name], 'name', {value:name});
prosperon[name].fns = fns;
n.clear = function () {
fns = [];
};
Register[name] = n;
Register.registries[name] = n;
return n;
},
};
Register.pull_registers = function pull_registers(obj)
{
var reggies = [];
for (var reg in Register.registries) {
if (typeof obj[reg] === "function")
reggies.push(reg);
}
return reggies;
}
Register.register_obj = function register_obj(obj, reg)
{
var fn = obj[reg].bind(obj);
fn.layer = obj[reg].layer;
var name = obj.ur ? obj.ur.name : obj.toString();
obj.timers.push(Register.registries[reg].register(fn, name));
if (!obj[reg].name) Object.defineProperty(obj[reg], 'name', {value:`${obj._file}_${reg}`});
}
Register.check_registers = function check_registers(obj) {
if (obj.__reggies) {
if (obj.__reggies.length == 0) return;
// fast path
for (var reg of obj.__reggies)
Register.register_obj(obj,reg);
return;
}
for (var reg in Register.registries) {
if (typeof obj[reg] === "function")
Register.register_obj(obj,reg);
}
/* for (var k in obj) {
if (!k.startsWith("on_")) continue;
var signal = k.fromfirst("on_");
Event.observe(signal, obj, obj[k]);
}*/
};
Register.add_cb("appupdate");
Register.add_cb("update").doc = "Called once per frame.";
Register.add_cb("physupdate");
Register.add_cb("gui");
Register.add_cb("hud");
Register.add_cb("draw");
Register.add_cb("imgui");
Register.add_cb("app");
Register.add_cb("prerender");
function cant_kill()
{
throw Error("Can't kill an object in its spawning code. Move the kill command to awake.");
}
actor.toString = function() { return this.__file }
actor.spawn = function spawn(script, config, callback) {
if (this.__dead__) throw Error("Attempting to spawn on a dead actor")
var prog
if (!script) {
prog = {}
prog.module_ret = {}
prog.prog_fn = function() {}
} else {
prog = script_fn(script);
if (!prog.prog_fn) throw new Error(`Script ${script} is not an actor script or has no actor component`)
}
var underling;
prog.module_ret.__proto__ = actor;
underling = Object.create(prog.module_ret);
underling.overling = this;
underling.__file = script
underling.timers = []
underling.underlings = new Set()
if (callback) callback(underling, {
message:"created"
})
try{
prog.prog_fn.call(underling)
} catch(e) {throw e}
if (underling.__dead__)
return undefined
if (typeof config === 'object') Object.assign(underling,config);
if (!underling.__reggies)
underling.__proto__.__reggies = Register.pull_registers(underling)
Register.check_registers(underling);
if (underling.awake) underling.awake();
this.underlings.add(underling);
if (underling.tag)
search.tag_add(underling.tag, underling)
Object.defineProperty(underling, 'garbage', {
configurable: false,
writable: false,
value: underling.garbage
})
return underling;
};
actor.spawn.doc = `Create a new actor, using this actor as the overling, initializing it with 'script' and with data (as a JSON or Nota file) from 'config'.`;
actor.clear = function actor_clear()
{
this.underlings.forEach(p => {
p.kill();
})
this.underlings.clear()
}
var input = use('input')
actor.kill = function kill() {
if (this.__dead__) return;
this.__dead__ = true;
this.timers.slice().forEach(t => t()) // slice in case something is removed from timers while running
delete this.timers
input.do_uncontrol(this);
Event.rm_obj(this);
this.clear()
this.overling.underlings.delete(this)
delete this.underlings
if (typeof this.garbage === "function") this.garbage();
if (typeof this.then === "function") this.then();
search.tag_clear_guid(this)
};
actor.kill.doc = `Remove this actor and all its underlings from existence.`;
actor.delay = function (fn, seconds) { add_timer(this, fn, seconds) }
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`;
actor.interval = function interval(fn, seconds) {
var self = this;
var stop;
var usefn = function () {
fn();
stop = self.delay(usefn, seconds);
};
stop = self.delay(usefn, seconds);
return stop;
};
var search = use('search')
actor.underlings = new Set()
globalThis.mixin("color");
globalThis.mixin("std")