|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2025-2026 hyperpolymath |
| 3 | +// |
| 4 | +// VMMessageBus -- the message-routing co-processor, the pure-integer core |
| 5 | +// extracted from idaptik src/app/multiplayer/VMMessageBus.res (the bridge between |
| 6 | +// VM I/O ports and game/co-op events). Per the DESIGN-VISION ("AffineScript is |
| 7 | +// the brain, JS/Pixi the senses; only primitives cross the wasm boundary"), the |
| 8 | +// host keeps every STRING and every mutable buffer: the Dict<portName,[Int]> |
| 9 | +// output buffer (writePortOutput/readPortOutput, res:53-82), the coopOutbox |
| 10 | +// array, the inbound handler refs, and -- decisively -- ALL the string work in |
| 11 | +// classifyPort (res:96-114: the `port == "console"` literal compares and the |
| 12 | +// `String.startsWith(port, "covert:")` prefix test). String.startsWith on the |
| 13 | +// topic prefix is a host SENSE. The host parses the port string into an integer |
| 14 | +// message-kind code and the kernel does the integer dispatch over that code. |
| 15 | +// Every export is i32 -> i32 (scalar ABI). |
| 16 | +// |
| 17 | +//## Why this split, not a port of classifyPort / flushCoopOutbox |
| 18 | +// classifyPort is end-to-end string work (literal `==` and prefix `startsWith`) |
| 19 | +// -- exactly what AffineScript's canonical face cannot do soundly (broken string |
| 20 | +// subscript / string ==). So the host runs that parse and emits the matched |
| 21 | +// message-target ORDINAL. What survives as the integer brain are the routing |
| 22 | +// DECISIONS the message-target consumers and flushCoopOutbox make over the |
| 23 | +// closed enum bands: "is this target relayed to the co-op partner over the |
| 24 | +// WebSocket?", "is it a co-op channel / a covert channel?", and the per-coop- |
| 25 | +// event-kind "does this event call the multiplayer client?" dispatch |
| 26 | +// (flushCoopOutbox res:136-161, whose only non-sending arm is PortData). Those |
| 27 | +// are integer truth tables over the two enums; the string parse that produces |
| 28 | +// the ordinal is the host sense feeding them. |
| 29 | +// |
| 30 | +//## messageTarget encoding (the header contract for the JS host) |
| 31 | +// The host parses a port string to exactly one of these ordinals, in the |
| 32 | +// declaration order of the `messageTarget` variant (res:86-94), and hands the |
| 33 | +// kernel the ordinal -- never the string: |
| 34 | +// code target port form (classifyPort branch) |
| 35 | +// 0 Console "console" |
| 36 | +// 1 Display "display" |
| 37 | +// 2 Firewall "firewall" |
| 38 | +// 3 CoopSync "coop:sync" |
| 39 | +// 4 CoopChat "coop:chat" |
| 40 | +// 5 CoopItem "coop:item" |
| 41 | +// 6 CovertLinkChannel "covert:<id>" (String.startsWith host sense) |
| 42 | +// 7 DevicePort any other port (the else arm) |
| 43 | +// The encoding is LOSSLESS over its closed 0..7 band. A target ordinal outside |
| 44 | +// 0..7 is not a message target: is_valid_target reports 0 and clamp_target |
| 45 | +// returns the OUT-OF-BAND SENTINEL -1 -- never an in-band code, so out-of-band |
| 46 | +// input cannot be confused with a real target (sentinel, not a clamp; assail |
| 47 | +// PA-AFF-001 stays clean). |
| 48 | +// |
| 49 | +//## coopEvent encoding (the header contract for the JS host) |
| 50 | +// The host tags each outgoing co-op event with one of these ordinals, in the |
| 51 | +// declaration order of the `coopEvent` variant (res:120-127): |
| 52 | +// code event payload kind |
| 53 | +// 0 VMExecuted {deviceId, instruction, args} |
| 54 | +// 1 VMUndone {deviceId} |
| 55 | +// 2 StateSync {deviceId, registers} |
| 56 | +// 3 PortData {port, values} -- routed via VM state sync, NOT sent |
| 57 | +// 4 CovertLinkFound {connectionId} |
| 58 | +// 5 CovertLinkActivated {connectionId} |
| 59 | +// 6 ChatSent {message} |
| 60 | +// flushCoopOutbox sends every kind to MultiplayerClient EXCEPT PortData (res:148: |
| 61 | +// `| PortData(_) => ()` -- "Port data goes through VM state sync"). That single |
| 62 | +// exception is the only non-trivial integer decision in the drain loop. |
| 63 | + |
| 64 | +// The number of message-target classes in the routing taxonomy. |
| 65 | +pub fn message_target_count() -> Int { 8 } |
| 66 | + |
| 67 | +//## Whether a host integer names a defined message target. 1 = valid, 0 = out of band. |
| 68 | +pub fn is_valid_target(code: Int) -> Int { |
| 69 | + if code < 0 { return 0; } |
| 70 | + if code > 7 { return 0; } |
| 71 | + 1 |
| 72 | +} |
| 73 | + |
| 74 | +//## Canonicalise a host target ordinal: identity on a valid 0..7 code, the |
| 75 | +// out-of-band SENTINEL -1 otherwise. -1 is not an in-band code, so an |
| 76 | +// unrecognised target can never be confused with a real one (sentinel, not a |
| 77 | +// clamp). Mirrors the closed `messageTarget` band of classifyPort. |
| 78 | +pub fn clamp_target(code: Int) -> Int { |
| 79 | + if is_valid_target(code) == 1 { code } else { -1 } |
| 80 | +} |
| 81 | + |
| 82 | +//## Is this message target relayed to the co-op partner over the WebSocket? |
| 83 | +// The routing decision the co-op relay makes: only the three co-op channels -- |
| 84 | +// CoopSync(3), CoopChat(4), CoopItem(5) -- cross to the partner via the |
| 85 | +// multiplayer client; Console/Display/Firewall (local game systems) and |
| 86 | +// CovertLinkChannel/DevicePort (handled by the in-VM covert link / device port |
| 87 | +// plumbing) stay local. Returns 1 = relay, 0 = local. Out-of-band target -> 0. |
| 88 | +pub fn relays_to_partner(target: Int) -> Int { |
| 89 | + if target == 3 { return 1; } // CoopSync |
| 90 | + if target == 4 { return 1; } // CoopChat |
| 91 | + if target == 5 { return 1; } // CoopItem |
| 92 | + 0 |
| 93 | +} |
| 94 | + |
| 95 | +//## Is this target one of the co-op channels (coop:sync / coop:chat / coop:item)? |
| 96 | +// The closed CoopSync..CoopItem sub-band (3..5) of messageTarget. Lets the host |
| 97 | +// distinguish a co-op channel from the covert/device/local targets without a |
| 98 | +// second string parse. Returns 1 = co-op channel, 0 = not. Out-of-band -> 0. |
| 99 | +pub fn is_coop_channel(target: Int) -> Int { |
| 100 | + if target < 3 { return 0; } |
| 101 | + if target > 5 { return 0; } |
| 102 | + 1 |
| 103 | +} |
| 104 | + |
| 105 | +//## Is this target the covert-link data channel ("covert:<id>")? |
| 106 | +// CovertLinkChannel(6) alone. The `String.startsWith(port, "covert:")` branch of |
| 107 | +// classifyPort, read as its ordinal. Returns 1 = covert channel, 0 = not. |
| 108 | +pub fn is_covert_channel(target: Int) -> Int { |
| 109 | + if target == 6 { 1 } else { 0 } |
| 110 | +} |
| 111 | + |
| 112 | +// The number of co-op event kinds in the outbox taxonomy. |
| 113 | +pub fn coop_event_count() -> Int { 7 } |
| 114 | + |
| 115 | +//## Whether a host integer names a defined co-op event kind. 1 = valid, 0 = out of band. |
| 116 | +pub fn is_valid_event(kind: Int) -> Int { |
| 117 | + if kind < 0 { return 0; } |
| 118 | + if kind > 6 { return 0; } |
| 119 | + 1 |
| 120 | +} |
| 121 | + |
| 122 | +//## Does this co-op event kind get sent to the multiplayer client on outbox drain? |
| 123 | +// THE flushCoopOutbox decision (res:136-161): the drain loop forwards every |
| 124 | +// event kind to MultiplayerClient EXCEPT PortData(3), whose data is routed via |
| 125 | +// VM state sync instead (res:148). So the predicate is "valid kind AND not |
| 126 | +// PortData". Returns 1 = send to client, 0 = do not. An out-of-band kind is not |
| 127 | +// a defined event and returns 0 (fails safe to "do not send"). |
| 128 | +pub fn event_sends_to_client(kind: Int) -> Int { |
| 129 | + if is_valid_event(kind) == 0 { return 0; } // not a defined event kind |
| 130 | + if kind == 3 { return 0; } // PortData: routed via VM state sync, not sent |
| 131 | + 1 |
| 132 | +} |
0 commit comments