Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-linux (push) Failing after 1m33s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
1234 lines
33 KiB
JavaScript
1234 lines
33 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 tracy = use_embed('tracy')
|
||
|
||
os.trace = true;
|
||
|
||
if (os.trace)
|
||
tracy.level(1);
|
||
|
||
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)
|
||
})
|
||
|
||
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 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({})
|
||
|
||
var use_cache = {}
|
||
|
||
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()
|
||
|
||
var id = prosperon.name ? prosperon.name : prosperon.id
|
||
id = id.substring(0,6)
|
||
|
||
return `[${id}] [${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
|
||
|
||
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)
|
||
}
|
||
|
||
function format_args(...args) {
|
||
return args.map(arg => {
|
||
if (typeof arg === 'object' && arg !== null) {
|
||
try {
|
||
return json.encode(arg)
|
||
} catch (e) {
|
||
return String(arg)
|
||
}
|
||
}
|
||
return String(arg)
|
||
}).join(' ')
|
||
}
|
||
|
||
console.spam = function spam(...args) {
|
||
pprint(format_args(...args), 0)
|
||
}
|
||
console.debug = function debug(...args) {
|
||
pprint(format_args(...args), 1)
|
||
}
|
||
console.info = function info(...args) {
|
||
pprint(format_args(...args), 2)
|
||
}
|
||
console.warn = function warn(...args) {
|
||
pprint(format_args(...args), 3)
|
||
}
|
||
console.log = function log(...args) {
|
||
pprint(format_args(...args), 2)
|
||
}
|
||
console.error = function error(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 panic(e) {
|
||
pprint(e, 5)
|
||
os.quit()
|
||
}
|
||
console.assert = function assert(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
|
||
}
|
||
|
||
var use_cache = {}
|
||
var inProgress = {}
|
||
var loadingStack = []
|
||
|
||
globalThis.use = function use(file) {
|
||
// If we've already begun loading this file in this chain, show the cycle
|
||
if (loadingStack.includes(file)) {
|
||
// Find where in the stack this file first appeared
|
||
let cycleIndex = loadingStack.indexOf(file)
|
||
// Extract just the modules in the cycle
|
||
let cyclePath = loadingStack.slice(cycleIndex).concat(file)
|
||
|
||
throw new Error(
|
||
`Circular dependency detected while loading "${file}".\n` +
|
||
`Module chain: ${loadingStack.join(" -> ")}\n` +
|
||
`Cycle specifically: ${cyclePath.join(" -> ")}`
|
||
)
|
||
}
|
||
|
||
// Already fully loaded? Return it
|
||
if (use_cache[file]) {
|
||
return use_cache[file]
|
||
}
|
||
|
||
// If it's loading (but not on the stack), mark it as a new chain entry
|
||
// (This is optional if you just rely on loadingStack.
|
||
// But if you'd like a simple “already loading” check, keep 'inProgress'.)
|
||
if (inProgress[file]) {
|
||
throw new Error(`Circular dependency detected while loading "${file}"`)
|
||
}
|
||
inProgress[file] = true
|
||
|
||
// Push onto loading stack for chain tracking
|
||
loadingStack.push(file)
|
||
|
||
// Actually load the module
|
||
var mod = script_fn(file)
|
||
|
||
// Done loading, remove from the chain and mark as loaded
|
||
loadingStack.pop()
|
||
delete inProgress[file]
|
||
|
||
// Cache and return
|
||
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 {}
|
||
if (!/^\s*---\s*$/m.test(content)) {
|
||
var part = content.trim()
|
||
if (part.match(/return\s+[^;]+;?\s*$/)) {
|
||
return { module: part }
|
||
}
|
||
return { program: part }
|
||
}
|
||
var parts = content.split(/\n\s*---\s*\n/)
|
||
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) {
|
||
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
|
||
})
|
||
|
||
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)()
|
||
|
||
/*
|
||
When handling a message, the message appears like this:
|
||
{
|
||
type: type of message
|
||
- contact: used for contact messages
|
||
- stop: used to issue stop command
|
||
- etc
|
||
reply: ID this message will respond to (callback saved on the actor)
|
||
replycc: the actor that is waiting for the reply
|
||
target: ID of the actor that's supposed to receive the message. Only added to non direct sends (out of portals)
|
||
return: reply ID so the replycc actor can know what callback to send the message to
|
||
|
||
data: the actual content of the message
|
||
}
|
||
|
||
actors look like
|
||
{
|
||
id: the GUID of this actor
|
||
address: the IP this actor can be found at
|
||
port: the port of the IP the actor can be found at
|
||
}
|
||
*/
|
||
|
||
var enet = use('enet')
|
||
var util = use('util')
|
||
var math = use('math')
|
||
var crypto = use('crypto')
|
||
var nota = use('nota')
|
||
|
||
var dying = false
|
||
|
||
var HEADER = Symbol()
|
||
var ACTORDATA = Symbol()
|
||
|
||
function create_actor(id = util.guid()) {
|
||
return {id}
|
||
}
|
||
|
||
var $_ = create_actor()
|
||
|
||
$_.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 root = undefined
|
||
|
||
globalThis.$_ = $_
|
||
|
||
var receive_fn = undefined
|
||
var greeters = {}
|
||
|
||
$_.is_actor = function(actor) {
|
||
return "id" in actor || ACTORDATA in actor
|
||
}
|
||
|
||
function peer_connection(peer) {
|
||
return {
|
||
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 = function(callback, actor, config) {
|
||
var peer = peers[actor.id]
|
||
if (peer) {
|
||
callback(peer_connection(peer))
|
||
return
|
||
}
|
||
if (os.mailbox_exist(actor.id)) {
|
||
callback({type:"local"})
|
||
return
|
||
}
|
||
throw new Error(`Could not get connection information for ${actor}`)
|
||
}
|
||
$_.connection[prosperon.DOC] = "takes a callback function, an actor object, and a configuration record..."
|
||
|
||
// 1) id → peer (live ENet connection)
|
||
const peer_by_id = Object.create(null)
|
||
// 2) id → {address,port} (last seen endpoint)
|
||
const id_address = Object.create(null)
|
||
// 3) address:port → peer (fast lookup for incoming events)
|
||
const peers = Object.create(null)
|
||
|
||
var peer_queue = new WeakMap()
|
||
var portal, portal_fn
|
||
var local_address, local_port
|
||
|
||
var service_delay = 0.01 // how often to ping enet
|
||
|
||
function route_set(id, peer){ peer_by_id[id] = peer }
|
||
function route_hint(id, address, port){ id_address[id] = {address,port} }
|
||
function route_peerString(peer){ return `${peer.address}:${peer.port}` }
|
||
|
||
$_.portal = function(fn, port) {
|
||
if (portal) throw new Error(`Already started a portal listening on ${portal.port}`)
|
||
if (!port) throw new Error("Requires a valid port.")
|
||
console.log(`starting a portal on port ${port}`)
|
||
portal = enet.create_host({address: "any", port})
|
||
local_address = 'localhost'
|
||
local_port = port
|
||
portal_fn = fn
|
||
console.log(`Portal initialized with actor ID: ${$_.id}`)
|
||
}
|
||
$_.portal[prosperon.DOC] = "starts a public address that performs introduction services..."
|
||
|
||
function handle_host(e) {
|
||
switch (e.type) {
|
||
case "connect":
|
||
// Store peer information for future routing
|
||
var key = route_peerString(e.peer)
|
||
peers[key] = e.peer
|
||
console.log(`Connected to peer: ${e.peer.address}:${e.peer.port}`)
|
||
|
||
// Check if we have queued messages for this peer
|
||
var queue = peer_queue.get(e.peer)
|
||
if (queue && queue.length > 0) {
|
||
console.log(`Sending ${queue.length} queued messages to newly connected peer`)
|
||
|
||
for (var msg of queue) {
|
||
try {
|
||
e.peer.send(nota.encode(msg))
|
||
console.log(`Sent queued message: ${json.encode(msg)}`)
|
||
} catch (err) {
|
||
console.error(`Failed to send queued message: ${err.message}`)
|
||
}
|
||
}
|
||
|
||
// Clear the queue after sending
|
||
peer_queue.delete(e.peer)
|
||
}
|
||
break
|
||
|
||
case "disconnect":
|
||
// Clean up peer references
|
||
var key = route_peerString(e.peer)
|
||
console.log(`Peer disconnected: ${key}`)
|
||
|
||
// Remove from peers map
|
||
delete peers[key]
|
||
|
||
// Remove from queue
|
||
peer_queue.delete(e.peer)
|
||
|
||
// Remove from any ID-based routing
|
||
for (var id in peer_by_id) {
|
||
if (peer_by_id[id] === e.peer) {
|
||
console.log(`Removing route for actor ${id}`)
|
||
delete peer_by_id[id]
|
||
}
|
||
}
|
||
break
|
||
|
||
case "receive":
|
||
// Decode the message
|
||
try {
|
||
var msg = nota.decode(e.data)
|
||
console.log(`Received message from ${e.peer.address}:${e.peer.port}:`, json.encode(msg))
|
||
|
||
// Update routing information based on message contents
|
||
function touch(obj){
|
||
if (!obj || typeof obj !== 'object') return
|
||
|
||
if (typeof obj.id === 'string'){
|
||
// Set up routing for this actor ID
|
||
route_set(obj.id, e.peer)
|
||
|
||
// Add address hint if we don't have one
|
||
if (!id_address[obj.id]) {
|
||
route_hint(obj.id, e.peer.address, e.peer.port)
|
||
console.log(`Added route hint for ${obj.id}: ${e.peer.address}:${e.peer.port}`)
|
||
}
|
||
}
|
||
|
||
// Recursively touch all properties
|
||
for (const k in obj) {
|
||
if (obj.hasOwnProperty(k)) touch(obj[k])
|
||
}
|
||
}
|
||
|
||
// Extract routing information
|
||
touch(msg)
|
||
|
||
// Process the message
|
||
handle_message(msg)
|
||
} catch (err) {
|
||
console.error(`Failed to decode or process received message: ${err.message}`)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
var contactor = undefined
|
||
$_.contact = function(callback, record) {
|
||
console.log("Contact function called with:", json.encode(record));
|
||
|
||
// Create a pseudo-actor for the initial contact
|
||
var sendto = {}
|
||
sendto[ACTORDATA] = record
|
||
|
||
// Ensure we send a properly formatted contact message
|
||
var contactMsg = {
|
||
type: "contact",
|
||
data: record
|
||
};
|
||
|
||
// Wrap the original callback to handle the actor reference properly
|
||
function wrappedCallback(response) {
|
||
console.log("Contact wrapped callback received:", json.encode(response));
|
||
|
||
// First param should be the actor, second param is reason/error
|
||
if (response && typeof response === 'object') {
|
||
if ('id' in response) {
|
||
// This is an actor reference, pass it directly
|
||
console.log("Identified actor reference in response");
|
||
callback(response, null);
|
||
} else if (response.data && typeof response.data === 'object' && 'id' in response.data) {
|
||
// The actor reference is in the data field
|
||
console.log("Identified actor reference in response.data");
|
||
callback(response.data, null);
|
||
} else {
|
||
// No actor reference found, must be an error
|
||
console.log("No actor reference found in response");
|
||
callback(null, response);
|
||
}
|
||
} else {
|
||
// Error case or unexpected response format
|
||
console.log("Unexpected response format");
|
||
callback(null, response);
|
||
}
|
||
}
|
||
|
||
// Send the contact message with callback for response
|
||
$_.send(sendto, contactMsg, wrappedCallback);
|
||
}
|
||
|
||
$_.contact[prosperon.DOC] = "The contact function sends a message to a portal..."
|
||
|
||
$_.receiver = function(fn) {
|
||
receive_fn = fn
|
||
}
|
||
$_.receiver[prosperon.DOC] = "registers a function that will receive all messages..."
|
||
|
||
$_.start = function(cb, prg, arg) {
|
||
if (dying) {
|
||
console.warn(`Cannot start an underling in the same turn as we're stopping`)
|
||
return
|
||
}
|
||
var id = util.guid()
|
||
greeters[id] = cb
|
||
var argv = ["./prosperon", "spawn", "--id", id, "--overling", prosperon.id, "--root", root]
|
||
if (prg) argv = argv.concat(['--program', prg])
|
||
if (arg) argv = argv.concat(cmd.encode(arg))
|
||
underlings.add(id)
|
||
os.createactor(argv)
|
||
}
|
||
$_.start[prosperon.DOC] = "The start function creates a new actor..."
|
||
|
||
$_.stop = function(actor) {
|
||
if (!actor) {
|
||
destroyself()
|
||
return
|
||
}
|
||
if (!$_.is_actor(actor))
|
||
throw new Error('Can only call stop on an actor.')
|
||
if (!underlings.has(actor.id))
|
||
throw new Error('Can only call stop on an underling or self.')
|
||
|
||
actor_prep(actor, {type:"stop", id: prosperon.id})
|
||
}
|
||
$_.stop[prosperon.DOC] = "The stop function stops an underling."
|
||
|
||
$_.unneeded = function(fn, seconds) {
|
||
os.unneeded(fn, seconds)
|
||
}
|
||
$_.unneeded[prosperon.DOC] = "registers a function that is called when the actor..."
|
||
|
||
$_.delay = function(fn, seconds) {
|
||
var id = os.delay(fn, seconds)
|
||
return function() { os.removetimer(id) }
|
||
}
|
||
$_.delay[prosperon.DOC] = "used to schedule the invocation of a function..."
|
||
|
||
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."
|
||
|
||
function actor_prep(actor, send) {
|
||
message_queue.push({actor,send});
|
||
}
|
||
|
||
function ensure_route(actor){
|
||
// Extract the actor ID
|
||
const id = actor.id
|
||
|
||
// First check if we already have a peer for this actor
|
||
if (peer_by_id[id]) {
|
||
console.log(`Found existing peer for actor ${id}`)
|
||
return peer_by_id[id]
|
||
}
|
||
|
||
// Check if we have a hint for this actor's address
|
||
const hint = id_address[id]
|
||
if (hint) {
|
||
console.log(`Found address hint for ${id}: ${hint.address}:${hint.port}`)
|
||
|
||
// Create host if needed
|
||
if (!contactor && !portal) {
|
||
console.log("Creating new contactor host")
|
||
contactor = enet.create_host()
|
||
}
|
||
|
||
// Connect to the peer
|
||
const host = contactor || portal
|
||
console.log(`Connecting to ${hint.address}:${hint.port} via ${contactor ? 'contactor' : 'portal'}`)
|
||
const p = host.connect(hint.address, hint.port)
|
||
|
||
// Initialize queue for this peer
|
||
peer_queue.set(p, [])
|
||
return p
|
||
}
|
||
|
||
// Check if we have connection data for this actor
|
||
var cnn = actor[ACTORDATA]
|
||
if (cnn) {
|
||
console.log(`Using ACTORDATA for connection: ${cnn.address}:${cnn.port}`)
|
||
|
||
// Create host if needed
|
||
if (!contactor && !portal) {
|
||
console.log("Creating new contactor host")
|
||
contactor = enet.create_host()
|
||
}
|
||
|
||
// Connect to the peer
|
||
const host = contactor || portal
|
||
console.log(`Connecting to ${cnn.address}:${cnn.port} via ${contactor ? 'contactor' : 'portal'}`)
|
||
var p = host.connect(cnn.address, cnn.port)
|
||
|
||
// Initialize queue for this peer
|
||
peer_queue.set(p, [])
|
||
return p
|
||
}
|
||
|
||
console.log(`No route found for actor ${id}`)
|
||
return null
|
||
}
|
||
|
||
function actor_send(actor, send){
|
||
if (!$_.is_actor(actor)) throw Error('bad actor: ' + json.encode(actor))
|
||
if (actor.id===prosperon.id) { // message to self
|
||
console.log("actor_send: message to self")
|
||
if(receive_fn)
|
||
receive_fn(send.data);
|
||
return
|
||
}
|
||
|
||
if (os.mailbox_exist(actor.id)){ // message to local mailbox
|
||
os.mailbox_push(actor.id, send);
|
||
return
|
||
}
|
||
|
||
const peer = ensure_route(actor)
|
||
if (peer){
|
||
console.log("actor_send: sending via peer route", peer.address + ":" + peer.port)
|
||
peer_queue.get(peer)?.push(send) || peer.send(nota.encode(send))
|
||
return
|
||
}
|
||
|
||
// fallback – forward via parent header if present
|
||
if (actor[HEADER] && actor[HEADER].replycc){
|
||
console.log("actor_send: forwarding via parent header")
|
||
const fwd = {type:'forward', forward_to:actor.id, payload:send}
|
||
actor_send(actor[HEADER].replycc, fwd); return
|
||
}
|
||
console.error("actor_send: no route to actor", actor.id)
|
||
throw Error(`no route to actor ${actor.id}`)
|
||
}
|
||
|
||
// Holds all messages queued during the current turn.
|
||
var message_queue = []
|
||
|
||
function send_messages() {
|
||
// Attempt to flush the queued messages. If one fails, keep going anyway.
|
||
var errors = []
|
||
|
||
// Process all queued messages
|
||
while (message_queue.length > 0) {
|
||
var {actor,send} = message_queue.shift()
|
||
|
||
// console.log("Processing queued message:", json.encode(send))
|
||
|
||
try {
|
||
actor_send(actor, send)
|
||
// console.log("Message sent successfully")
|
||
} catch (err) {
|
||
console.error("Failed to send message:", err.message)
|
||
errors.push(err)
|
||
}
|
||
}
|
||
|
||
// Report any send errors
|
||
if (errors.length) {
|
||
console.error(`${errors.length} messages failed to send`)
|
||
for (var i = 0; i < Math.min(errors.length, 3); i++) {
|
||
console.error(`Error ${i+1}: ${errors[i].message}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
var replies = {}
|
||
|
||
$_.send = function(actor, message, reply) {
|
||
if (typeof message !== 'object')
|
||
throw new Error('Message must be an object')
|
||
|
||
// If message already has type, respect it, otherwise wrap it as user message
|
||
var send = message.type ? message : {type:"user", data: message}
|
||
|
||
// Make sure contact messages have 'data' property
|
||
if (message.type === 'contact' && !send.data) {
|
||
console.log("Fixing contact message structure")
|
||
send.data = message.data || {}
|
||
}
|
||
|
||
if (actor[HEADER] && actor[HEADER].replycc) { // in this case, it's not a true actor, but a message we're responding to
|
||
console.log("Send: responding to message with header", json.encode(actor[HEADER]))
|
||
var header = actor[HEADER]
|
||
if (!header.replycc || !$_.is_actor(header.replycc))
|
||
throw new Error(`Supplied actor had a return, but it's not a valid actor! ${json.encode(actor[HEADER])}`)
|
||
|
||
actor = header.replycc
|
||
send.return = header.reply
|
||
|
||
// When replying to a contact message, ensure proper data structure
|
||
if (header.type === "contact" && send.type === "user") {
|
||
// Make sure we're sending the actor ID in a proper format for contact responses
|
||
send.data = send.data || $_;
|
||
}
|
||
}
|
||
|
||
if (reply) {
|
||
var id = util.guid()
|
||
replies[id] = reply
|
||
send.reply = id
|
||
send.replycc = $_
|
||
console.log("Send: added reply callback with id", id)
|
||
}
|
||
|
||
// Instead of sending immediately, queue it
|
||
// console.log("Send: queuing message", json.encode(send))
|
||
actor_prep(actor, send);
|
||
}
|
||
$_.send[prosperon.DOC] = "sends a message to another actor..."
|
||
|
||
$_.blast = $_.send;
|
||
|
||
var cmd = use('cmd')
|
||
cmd.process(prosperon.argv.slice())
|
||
|
||
if (!prosperon.args.id) prosperon.id = util.guid()
|
||
else prosperon.id = prosperon.args.id
|
||
|
||
os.register_actor(prosperon.id, function(msg) {
|
||
try {
|
||
handle_message(msg)
|
||
send_messages()
|
||
} catch (err) {
|
||
message_queue = []
|
||
throw err
|
||
}
|
||
}, prosperon.args.main)
|
||
|
||
$_.id = prosperon.id
|
||
|
||
if (prosperon.args.overling) overling = create_actor(prosperon.args.overling)
|
||
|
||
if (prosperon.args.root) root = json.decode(prosperon.args.root)
|
||
else root = $_
|
||
|
||
if (overling) actor_prep(overling, {type:'greet', id: prosperon.id})
|
||
|
||
if (!prosperon.args.program)
|
||
os.exit(1)
|
||
|
||
if (typeof prosperon.args.program !== 'string')
|
||
prosperon.args.program = 'main.js';
|
||
|
||
console.log(`running ${prosperon.args.program}`)
|
||
|
||
actor.spawn(prosperon.args.program)
|
||
|
||
function destroyself() {
|
||
console.log(`Got the message to destroy self.`)
|
||
dying = true
|
||
for (var i of underlings)
|
||
$_.stop(create_actor(id));
|
||
os.destroy()
|
||
}
|
||
|
||
function handle_actor_disconnect(id) {
|
||
var greeter = greeters[id]
|
||
if (greeter) {
|
||
greeter({type: "stopped", id})
|
||
delete greeters[id]
|
||
}
|
||
console.log(`actor ${id} disconnected`)
|
||
if (couplings.has(id)) $_.stop()
|
||
delete peers[id]
|
||
}
|
||
|
||
function handle_message(msg) {
|
||
// console.log("Handling message:", json.encode(msg));
|
||
|
||
if (msg.target) {
|
||
if (msg.target !== prosperon.id) {
|
||
console.log(`Forwarding message to ${msg.target}`);
|
||
os.mailbox_push(msg.target, msg);
|
||
return;
|
||
}
|
||
}
|
||
|
||
switch (msg.type) {
|
||
case "user":
|
||
var letter = msg.data;
|
||
delete msg.data;
|
||
letter[HEADER] = msg;
|
||
|
||
if (msg.return) {
|
||
console.log(`Received a message for the return id ${msg.return}`);
|
||
var fn = replies[msg.return];
|
||
if (!fn) {
|
||
console.error(`Could not find return function for message ${msg.return}`);
|
||
return;
|
||
}
|
||
|
||
// Handle contact response specially - first parameter is the actor reference
|
||
if (letter && letter[HEADER] && letter[HEADER].type === "user" && letter[HEADER].return) {
|
||
// For contact responses, we need to extract the actor reference
|
||
// letter.data should contain the actor object from the portal
|
||
if (letter.data && typeof letter.data === 'object' && letter.data.id) {
|
||
console.log("Processing contact response with actor data:", json.encode(letter.data));
|
||
fn(letter.data); // This is the actor reference
|
||
} else {
|
||
console.log("Processing contact response with letter as actor:", json.encode(letter));
|
||
fn(letter); // Fallback to the whole message
|
||
}
|
||
} else {
|
||
fn(letter);
|
||
}
|
||
|
||
delete replies[msg.return];
|
||
return;
|
||
}
|
||
|
||
if (receive_fn) receive_fn(letter);
|
||
break;
|
||
|
||
case "stop":
|
||
if (overling && msg.id !== overling.id)
|
||
throw new Error(`Got a message from an actor ${msg.id} to stop...`);
|
||
destroyself();
|
||
break;
|
||
|
||
case "contact":
|
||
if (portal_fn) {
|
||
console.log("Portal received contact message");
|
||
var letter2 = msg.data;
|
||
letter2[HEADER] = msg;
|
||
delete msg.data;
|
||
console.log("Portal handling contact message:", json.encode(letter2));
|
||
portal_fn(letter2);
|
||
} else {
|
||
console.error('Got a contact message, but no portal is established.');
|
||
}
|
||
break;
|
||
|
||
case "stopped":
|
||
handle_actor_disconnect(msg.id);
|
||
break;
|
||
|
||
case "greet":
|
||
var greeter = greeters[msg.id];
|
||
if (greeter) {
|
||
console.log("Greeting actor with id:", msg.id);
|
||
greeter({type: "actor_started", actor: create_actor(msg)});
|
||
}
|
||
break;
|
||
|
||
case "ping":
|
||
// Keep-alive ping, no action needed
|
||
break;
|
||
|
||
default:
|
||
// console.log("Default message handler for type:", msg.type);
|
||
if (receive_fn) receive_fn(msg);
|
||
break;
|
||
}
|
||
};
|
||
|
||
function enet_check()
|
||
{
|
||
if (portal) portal.service(handle_host)
|
||
if (contactor) contactor.service(handle_host)
|
||
send_messages();
|
||
|
||
// Send keep-alive ping to all active peers to prevent timeout
|
||
for (var peerKey in peers) {
|
||
var peer = peers[peerKey]
|
||
if (peer && peer.state === 1) { // ENET_PEER_STATE_CONNECTED
|
||
try {
|
||
peer.send(nota.encode({type: 'ping'}))
|
||
} catch (e) {
|
||
console.debug(`Failed to send ping to ${peerKey}:`, e)
|
||
}
|
||
}
|
||
}
|
||
|
||
$_.delay(enet_check, service_delay);
|
||
}
|
||
|
||
send_messages();
|
||
enet_check();
|
||
|
||
})()
|