Files
cell/scripts/core/engine.js
John Alanbrook c431f117e9
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m42s
Build and Deploy / build-windows (CLANG64) (push) Failing after 6m13s
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
remove atoms for multithreading
2025-03-11 10:13:15 -05:00

955 lines
24 KiB
JavaScript

(function engine() {
prosperon.DOC = Symbol('+documentation+') // Symbol for documentation references
prosperon.id = 'newguy';
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) {
var now = time.now()
return `[${prosperon.id.substring(0,6)}] [${time.text(now, "mb d yyyy h:nn:ss")}] ${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')
var time = use('time')
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 math = use('math')
var crypto = use('crypto')
var REPLY = Symbol()
var REPLYCC = Symbol()
var ACTORID = Symbol()
var PEER = Symbol()
var ar = 5 // seconds before reclamation
/*
When an actor object like $_ is passed to another machine or function, it appears like this
id: local to the machine. Currently, the local port it is listening on
address: the IP address of the portal that connects it, if set
port: the public port of the portal that connects it, if set
There is a map of actor.id -> peer, so messages can be sent.
*/
/*
Each actor has an id. When an actor object is received, that is essentially just an id. The method to reach the id is in routing tables local to the actor that has obtained the object. If actor.id === prosperon.id, it's localhost.
Currently, each actor has an enet peer, so we're focused on that. Eventually, there might also be a thread local mailbox.
*/
var $_ = {}
$_.random = crypto.random;
$_.random[prosperon.DOC] = "returns a number between 0 and 1. There is a 50% chance that the result is less than 0.5."
$_.clock = function(fn) { return os.now() }
$_.clock[prosperon.DOC] = "takes a function input value that will eventually be called with the current time in number form."
var underlings = new Set()
var overling = undefined
var host = enet.create_host({
address:"127.0.0.1",
port:0,
channels:0,
incoming_bandwidth:0,
outgoing_bandwidth:0
});
globalThis.$_ = $_
var receive_fn
var peers = {} // mapping of guids to peers
var greeters = {} // mapping of underling guids to their system callback functions
var peer2id = new WeakMap() // local bound peers to relevant id
$_.connection = function(callback, actor, config) {
var peer = peers[actor.id]
if (!peer) throw new Error(`Cannot get information from actor ${json.encode(actor)}`)
callback({
latency: peer.rtt,
bandwidth: {
incoming: peer.incoming_bandwidth,
outgoing: peer.outgoing_bandwidth
},
activity: {
last_sent: peer.last_send_time,
last_received: peer.last_receive_time,
},
mtu: peer.mtu,
data: {
incoming_total: peer.incoming_data_total,
outgoing_total: peer.outgoing_data_total,
reliable_in_transit: peer.reliable_data_in_transit
},
latency_variance: peer.rtt_variance,
packet_loss: peer.packet_loss,
state: peer.state
});
};
$_.connection[prosperon.DOC] = "takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated."
var portal = undefined
var pppfn
$_.portal = function(fn, port)
{
if (portal)
throw new Error(`Already started a portal listening on ${portal.port}`)
console.log(`starting a portal on port ${port}`)
if (!port)
throw new Error("Requires a valid port.");
portal = enet.create_host({
address: "any", // any can connect
port,
})
pppfn = fn
}
$_.portal[prosperon.DOC] = "starts apublic address that performs introduction services. It listens on a specified port for contacts by external actors that need to acquire an actor object. The function will receive the record containing the request. The record can have a reply sent through it. A portal can respond by beginning a new actor, or finding an existing actor, or by forwarding the contact message to another actor. This is how distributed Misty networks are bootstrapped."
function portal_fn(e)
{
switch (e.type) {
case "connect":
console.log('portal got connect')
break
case "disconnect":
console.log('portal got disconnect')
break
case "receive":
pppfn(e.data.data)
break
}
}
var peer2contact = new WeakMap() //
$_.contact = function(callback, record)
{
if (!callback) throw new Error('Contact requires a callback function')
console.log(`connecting to ${json.encode(record)}`)
var peer = host.connect(record.address, record.port)
peer2contact.set(peer, {
callback,
record
})
}
$_.contact[prosperon.DOC] = `The contact function sends a message to a portal on another machine to obtain an actor object.
The callback is a function with a actor input and a reason input. If successful, actor is bound to an actor object. If not successful, actor is null and reason may contain an explanation.`
$_.receiver = function(fn)
{
receive_fn = fn;
}
$_.receiver[prosperon.DOC] = `registers a function that will receive all messages sent to the actor except for delay events, reply messages (which are sent to the send callback), the unneeded message, and portal contact messages.`
$_.start = function(cb, prg, arg)
{
var id = util.guid()
greeters[id] = cb
var argv = [
"./prosperon",
"spawn",
"--overling", host.port,
"--id", id,
"--overlingid", prosperon.id
]
if (prg)
argv = argv.concat(['--program', prg])
if (arg)
argv = argv.concat(cmd.encode(arg))
underlings.add(id)
os.createprocess(argv)
}
$_.start[prosperon.DOC] = `The start function creates a new actor. The callback function receives messages about the new actor, starting with a message containing the new actor's address object.
The program text identifies the executable in the program shop that the new actor runs.
The arguments array contains up to four arguments of type logical, number, text, or actor address object.
The current actor is the overling of the new actor, and it is notified when the new actor stops. The new actor is an underling of the current actor.`
$_.stop = function(actor)
{
if (!actor)
destroyself()
if (!underlings.has(actor.id))
throw new Error('Can only call stop on an underling or self.')
actor_send(actor, {type:"stop", id: prosperon.id})
}
$_.stop[prosperon.DOC] = `The stop function stops an underling.`
var unneeded_fn = $_.stop
var unneeded_time = ar
$_.unneeded = function(fn, seconds = ar)
{
if (typeof fn !== 'function') throw new Error ('Must supply a function to unneeded.')
unneeded_fn = fn
unneeded_time = seconds
}
$_.unneeded[prosperon.DOC] = `registers a function that is called when the actor has not received a message in the recent seconds. The default for seconds is the ar timer. This likely means that the actor is no longer needed. The actor should finish its work and then @.stop().`
$_.delay = function(fn, seconds)
{
var id = os.addtimer(fn, seconds);
return function() { os.removetimer(id); }
}
$_.delay[prosperon.DOC] = `used to schedule the invocation of a function at a later time. Any value returned from the delayed invocation is ignored`
// Set of actor guids
var couplings = new Set()
$_.couple = function(actor)
{
console.log(`coupled to ${actor.id}`)
couplings.add(actor.id)
}
$_.couple[prosperon.DOC] = `causes this actor to stop when another actor stops.`
// Shuffsles the message to the actor with whatever means available
function actor_send(actor, message)
{
if (typeof message !== 'object')
throw new Error('Must send an object record.')
if (receive_fn && actor.id === prosperon.id) { // handle loopback case
receive_fn(message.data)
return
}
var peer = peers[actor.id]
if (peer) {
peer.send(message)
return
}
try {
os.mailbox_push(actor.id, message)
} catch(e) {
}
throw new Error (`Unable to send message to actor ${json.encode(actor)}`)
}
// Map of reply IDs to functions
var replies = {}
// Map of a message object to a peer for replying directly
var reply_cc = new WeakMap()
$_.send = function(actor, message, reply)
{
if (typeof message !== 'object')
throw new Error('Must send an object record.')
console.log(`sending to ${json.encode(actor)} ...`)
var send = {
type:"user",
data: message
}
if (actor[REPLYCC]) {
console.log(`replying to a message: ${json.encode(actor)}`)
console.log(`replycc and reply are ${actor[REPLYCC]} and ${actor[REPLY]}`)
actor.id = actor[REPLYCC]
send.return = actor[REPLY]
}
if (reply) {
var id = util.guid()
replies[id] = reply
send.reply = id
send.id = prosperon.id
}
actor_send(actor, send)
}
$_.send[prosperon.DOC] = `sends a message to another actor. The left expression must resolve to an actor address object or a message object that is expecting a reply because it was sent with a callback. The right expression is a record containing the outgoing message. The outgoing message record must not contain functions or patterns or cyclic structures.
If a callback function is included, then the callback function will receive the reply, not the receiver function.`
var cmd = use('cmd')
cmd.process(prosperon.argv)
if (!prosperon.args.id)
prosperon.id = util.guid()
else
prosperon.id = prosperon.args.id;
if (prosperon.args.overlingid)
overling = { id: prosperon.args.overlingid }
// now can start the mailbox
os.mailbox_start(prosperon.id);
if (prosperon.args.overling)
host.connect("localhost", prosperon.args.overling)
if (prosperon.args.program)
actor.spawn(prosperon.args.program)
var unneeded_timer = $_.delay($_.stop, ar)
function destroyself()
{
if (overling)
actor_send(overling, { type: "stopped" , id: prosperon.id})
host.flush()
os.exit(0)
}
function handle_actor_disconnect(id)
{
var greeter = greeters[id]
if (greeter) {
greeter({
type: "stopped"
})
// Greeter can now be removed
delete greeters[id]
}
console.log(`actor ${id} disconnected`)
if (couplings.has(id))
$_.stop()
delete peers[id]
}
/*
msg format is:
id: id of the actor that sent the message
reply: reply id for a response
*/
function handle_message(msg)
{
switch (msg.type) {
case "user":
if (msg.return) {
console.log(`Message has a return address.`)
var fn = replies[msg.return]
if (!fn)
throw new Error(`Could not find return function for message ${msg.return}`)
fn(msg)
delete replies[msg.return]
return
}
if (receive_fn) {
if (msg.reply) {
msg.data[REPLY] = msg.reply
msg.data[REPLYCC] = msg.id
}
receive_fn(msg.data)
}
break
case "stop":
if (msg.id !== overling.id)
throw new Error(`Got a message from an actor ${msg.id} to stop, that was not our overling (${overling.id}).`);
destroyself()
break
case "contact":
if (pppfn) pppfn(msg.data)
break
case "stopped":
handle_actor_disconnect(msg.id)
break
}
}
function handle_connect(e)
{
var contact = peer2contact.get(e.peer)
if (contact) {
// We have successfully made contact. now send the request.
e.peer.send({
type: "contact",
data: contact.record
})
return
}
// For a local greet
e.peer.send({ id: prosperon.id })
}
var hang = 0.001
while (1) {
host.service(e => {
unneeded_timer()
switch(e.type) {
case "connect":
handle_connect(e);
break;
case "receive":
if (e.data.id && !peers[e.data.id]) {
// we can hook them up
peers[e.data.id] = e.peer
peer2id.set(e.peer, e.data.id)
// first time they've been seen; if it has a system callback, run it now
var greeter = greeters[e.data.id]
if (greeter)
greeter({
type: "actor_started",
actor: { id: e.data.id }
})
}
handle_message(e.data)
break;
case "disconnect":
var id = peer2id.get(e.peer)
if (!id) throw new Error('A peer disconnected but we had no ID associated with it.')
handle_actor_disconnect(id);
break;
}
unneeded_timer = $_.delay(unneeded_fn, unneeded_time)
}, hang);
if (portal)
portal.service(portal_fn, hang)
os.mailbox_service(prosperon.id, handle_message)
}
})()