Files
cell/scripts/core/engine.js
John Alanbrook 23d764c534
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m42s
Build and Deploy / build-windows (CLANG64) (push) Failing after 8m47s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
actor detection
2025-03-05 09:01:29 -06:00

631 lines
14 KiB
JavaScript

(function engine() {
prosperon.DOC = Symbol('+documentation+') // Symbol for documentation references
var listeners = new Map()
prosperon.on = function(type, callback) {
if (!listeners.has(type)) listeners.set(type, [])
listeners.get(type).push(callback)
return function() {
var arr = listeners.get(type)
if (!arr) return
var idx = arr.indexOf(callback)
if (idx >= 0) arr.splice(idx,1)
}
}
prosperon.dispatch = function(type, data) {
var arr = listeners.get(type)
if (!arr) return
for (var callback of arr) callback(data)
}
var os = use_embed('os')
var js = use_embed('js')
prosperon.on('SIGINT', function() {
os.exit(1)
})
prosperon.on('SIGABRT', function() {
console.error(new Error('SIGABRT'))
os.exit(1)
})
prosperon.on('SIGSEGV', function() {
console.error(new Error('SIGSEGV'))
os.exit(1)
})
var use_cache = {}
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
},
})
var io = use_embed('io')
var tracy = use_embed('tracy')
var RESPATH = 'scripts/modules/resources.js'
var canonical = io.realdir(RESPATH) + 'resources.js'
var content = io.slurp(RESPATH)
var resources = js.eval(RESPATH, `(function setup_resources(){${content}})`).call({})
use_cache[resources.canonical('resources.js')] = resources
function print_api(obj) {
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) continue
var val = obj[prop]
console.log(prop)
if (typeof val === 'function') {
var m = val.toString().match(/\(([^)]*)\)/)
if (m) console.log(' function: ' + prop + '(' + m[1].trim() + ')')
}
}
}
prosperon.PATH = [
"/",
"scripts/modules/",
"scripts/modules/ext/",
]
// path is the path of a module or script to resolve
var script_fn = function script_fn(path) {
var parsed = {}
var file = resources.find_script(path)
if (!file) {
parsed.module_ret = bare_load(path)
if (!parsed.module_ret) throw new Error(`Module ${path} could not be created`)
return parsed
}
var content = io.slurp(file)
var parsed = parse_file(content, file)
var module_name = file.name()
parsed.module_ret = bare_load(path)
parsed.module_ret ??= {}
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 = js.eval(file, mod_script)
parsed.module_ret = module_fn.call(parsed.module_ret)
if (parsed.module_ret === undefined || parsed.module_ret === null)
throw new Error(`Module ${module_name} must return a value`)
parsed.module_fn = module_fn
}
parsed.program ??= ""
var prog_script = `(function use_${module_name}() { var self = this; var $ = this.__proto__; ${parsed.program}})`
parsed.prog_fn = js.eval(file, prog_script)
return parsed
}.hashify()
function bare_load(file) {
try {
return use_embed(file)
} catch (e) { }
try {
return use_dyn(file + so_ext)
} catch(e) { }
return undefined
}
var res_cache = {}
function console_rec(category, priority, line, file, msg) {
return `${file}:${line}: [${category} ${priority}]: ${msg}\n`
}
io.mkdir('.prosperon')
var logfile = io.open('.prosperon/log.txt')
function pprint(msg, lvl = 0) {
if (!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)
console.print(fmt)
if (logfile)
logfile.write(fmt)
if (tracy) tracy.message(fmt)
}
console.spam = function(msg) {
pprint(msg, 0)
}
console.debug = function(msg) {
pprint(msg, 1)
}
console.info = function(msg) {
pprint(msg, 2)
}
console.warn = function(msg) {
pprint(msg, 3)
}
console.log = function(msg) {
pprint(msg, 2)
}
console.error = function(e) {
if (!e)
e = new Error()
if (e instanceof Error)
pprint(`${e.name} : ${e.message}
${e.stack}`, 4)
else
pprint(e,4)
}
console.panic = function(e) {
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[prosperon.DOC] = {
doc: "The console object provides various logging, debugging, and output methods.",
spam: "Output a spam-level message for very verbose logging.",
debug: "Output a debug-level message.",
info: "Output info level message.",
warn: "Output warn level message.",
error: "Output error level message, and print stacktrace.",
panic: "Output a panic-level message and exit the program.",
assert: "If the condition is false, print an error and panic.",
critical: "Output critical level message, and exit game immediately.",
write: "Write raw text to console.",
say: "Write raw text to console, plus a newline.",
log: "Output directly to in game console.",
level: "Set level to output logging to console.",
stack: "Output a stacktrace to console.",
clear: "Clear console."
}
var BASEPATH = 'scripts/core/base.js'
var script = io.slurp(BASEPATH)
var fnname = "base"
script = `(function ${fnname}() { ${script}; })`
js.eval(BASEPATH, script)()
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 DEAD = Symbol()
var GARBAGE = Symbol()
var FILE = Symbol()
var TIMERS = Symbol()
var REGGIES = Symbol()
var UNDERLINGS = Symbol()
var OVERLING = Symbol()
var actor = {}
globalThis.actor = actor
var so_ext
switch(os.platform()) {
case 'Windows':
so_ext = '.dll'
break
default:
so_ext = '.so'
break
}
globalThis.use = function use(file) {
if (use_cache[file]) return use_cache[file]
var mod = script_fn(file)
use_cache[file] = mod.module_ret
return use_cache[file]
}
globalThis.json = use('json')
function parse_file(content, file) {
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 (!/\breturn\b/.test(module))
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.`);
try {
new Function(module)()
} catch (e) {
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.\n` + e.message);
}
var pad = '\n'.repeat(module.split('\n').length+4)
return {
module,
program: pad+parts[1]
}
}
globalThis.Register = {
registries: [],
add_cb(name) {
var n = {}
var fns = []
n.register = function (fn, oname) {
if (typeof fn !== '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) {
fns.forEach(fn => {
fn(...args)
})
}
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
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)
}
}
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")
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 new 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()
Object.defineProperty(underling, 'overling', {
get() { return this[OVERLING] },
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'underlings', {
get() { return new Set(this[UNDERLINGS]) },
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'spawn', {
value: actor.spawn,
writable:false,
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'kill', {
value: actor.kill,
writable:false,
enumerable:true,
configurable:false
})
Object.defineProperty(underling, 'delay', {
value: actor.delay,
writable:false,
enumerable:true,
configurable:false
})
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) act.tag_add(underling.tag, underling)
underling[GARBAGE] = underling.garbage
return underling
}
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())
delete this[TIMERS]
input.do_uncontrol(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()
act.tag_clear_guid(this)
}
actor.kill.doc = `Remove this actor and all its underlings from existence.`
actor.delay = function(fn, seconds) {
if (this[DEAD]) return
add_timer(this, fn, seconds)
}
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`
var act = use('actor')
actor[UNDERLINGS] = new Set()
globalThis.mixin("color")
var DOCPATH = 'scripts/core/doc.js'
var script = io.slurp(DOCPATH)
var fnname = "doc"
script = `(function ${fnname}() { ${script}; })`
js.eval(DOCPATH, script)()
var enet = use('enet')
var util = use('util')
var $_ = {}
var host = enet.create_host()
$_.host = host
console.log(`made a host with port ${host.port()}`)
globalThis.$_ = $_
var portal = undefined
var receive_fn = undefined;
$_.contact = function(callback, record)
{
}
$_.connection = function(callback, actor, config)
{
}
$_.portal = function(fn, port)
{
}
$_.receiver = function(fn)
{
receive_fn = fn;
}
var underlings = {}
$_.start = function(cb, prg, arg)
{
var guid = util.guid()
underlings[guid] = cb
os.createprocess(["./prosperon", "spawn", "--program", prg, "--overling", $_.host.port(), "--guid", guid])
}
$_.stop = function(actor)
{
if (!actor)
os.exit(0)
actor.peer.send({
type:"stop",
})
}
$_.unneeded = function(fn, seconds)
{
}
$_.delay = function(fn, seconds)
{
var id = os.addtimer(fn, seconds);
return function() { os.removetimer(id); }
}
use('cmd')(prosperon.argv)
function handle_receive(e)
{
var data = e.data
switch(data.type) {
case "greet":
if (underlings[data.guid]) underlings[data.guid]({
type: "greet",
data: {peer:e.peer}
})
break
case "stop":
console.log("STOPPING!")
os.exit(0)
}
}
var hang = 0.016
while (1) {
os.waitevent(_ => {}, hang)
host.service(e => {
switch(e.type) {
case "connect":
console.log(`connected. sending greet with guid ${prosperon.guid} to peer ${e.peer}`)
e.peer.send({
type: "greet",
guid: prosperon.guid
});
break;
case "receive":
handle_receive(e);
break;
case "disconnect":
console.log(`this peer left: ${e.peer}`)
break
}
}, hang);
}
})()