(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(); })()