code wiki / _hdl_build / nx_netsync.nx

nx_netsync.nx source

↩ module page · 201 lines · 8473 B

1// nx_netsync.nx -- the SOVEREIGN MULTIPLAYER NETCODE PART. nx_gamebench gap-queue rank 1 2// (networking-multiplayer, PARTIAL, blocks Freeciv-class and Veloren-class titles): the existing 3// nx_lockstep / nx_rollback cores prove the MECHANISM on a synthetic toy stepper only -- nothing 4// composed netcode with the CERTIFIED GAME PARTS. This part is that composition: deterministic 5// lockstep sessions whose simulation IS nx_worldsim (the certified faction/economy world), whose 6// snapshots ARE nx_gamesave files, so every property those parts certified carries onto the wire. 7// 8// THE INVARIANTS THIS PART EXISTS TO HOLD (each a gate tooth in nx_netsync_gate): 9// 1. LOCKSTEP: only INPUTS cross the wire. Every peer runs the same deterministic sim; a session 10// advances tick T only when commands from ALL peers for T are present. It STALLS rather than 11// guesses -- advancing on partial input is the defining desync bug of lockstep netcode. 12// 2. ARRIVAL-ORDER IMMUNE: commands may arrive shuffled, duplicated, or late; duplicates are 13// idempotent, a CONFLICTING resend (same tick+peer, different command) is REFUSED loudly -- 14// a peer cannot rewrite its committed input (the cheat/corruption door stays shut). 15// 3. DESYNC DETECTABLE: ns_checksum walks the LIVE shared state independently of any save path 16// (the GX-9 hollow-verifier law), so two peers comparing checksums localize a divergence to 17// the exact tick it appears. 18// 4. ROLLBACK-READY: whole-session snapshot/copy primitives make the confirmed-state + throwaway 19// speculative-head pattern (fighting-game rollback) buildable on top without poisoning the 20// confirmed input table. 21// 5. DURABLE: the whole session arena is one flat i64 block -> gs_save/gs_load; a late joiner 22// loads a snapshot file and resumes bit-identically (join-in-progress). 23// Command wire format: one i64 per (tick,peer): 0 = no-op, else from*100000000 + to*1000000 + amt 24// = a trade order applied through ws_trade (which conserves, clamps, refuses self-dealing -- the 25// certified properties do the sanitizing; malformed decode fields die in ws_trade's guards). 26// LIB ONLY -- no main() by ecosystem convention. license_tier: ORIGINAL expect_exit: 0 27import "nx_syscalls.nx" 28import "nx_worldsim.nx" 29const NS_MAGIC_100000000: i64 = 100000000 30const NS_MAGIC_1000000: i64 = 1000000 31const NS_MAGIC_40961: i64 = 40961 32 33// ---- arena layout ---- 34// hdr: [0]=npeers [1]=self_id (per-peer identity, EXCLUDED from shared state) [2]=confirmed_tick 35// [3]=maxticks [4]=nfac [5]=applied_trades [6]=total_moved [7]=reserved 36// then: cmd table maxticks*npeers (command per (tick,peer)) 37// then: have table maxticks*npeers (0/1 received) 38// then: the embedded nx_worldsim arena, ws_words(nfac) 39const NS_HDR: i64 = 8 40 41// offer() return codes -- distinct, loud, never silent 42const NS_OK: i64 = 1 43const NS_DUP: i64 = 0 44const NS_E_TICK: i64 = 0-1 45const NS_E_PEER: i64 = 0-2 46const NS_E_CONFLICT: i64 = 0-3 47 48func ns_npeers(a: *i64) -> i64 { return a[0] } 49func ns_self(a: *i64) -> i64 { return a[1] } 50func ns_tick(a: *i64) -> i64 { return a[2] } 51func ns_maxticks(a: *i64) -> i64 { return a[3] } 52func ns_nfac(a: *i64) -> i64 { return a[4] } 53func ns_applied(a: *i64) -> i64 { return a[5] } 54func ns_moved(a: *i64) -> i64 { return a[6] } 55 56func ns_words(npeers: i64, maxticks: i64, nfac: i64) -> i64 { 57 return NS_HDR + 2*maxticks*npeers + ws_words(nfac) 58} 59func ns_bytes(npeers: i64, maxticks: i64, nfac: i64) -> i64 { 60 return ns_words(npeers, maxticks, nfac) * 8 61} 62func ns_o_cmd(a: *i64) -> i64 { return NS_HDR } 63func ns_o_have(a: *i64) -> i64 { return NS_HDR + ns_maxticks(a)*ns_npeers(a) } 64func ns_o_world(a: *i64) -> i64 { return NS_HDR + 2*ns_maxticks(a)*ns_npeers(a) } 65 66// the embedded world arena as a pointer (same cast shape as gs_cksum's payload pointer) 67func ns_world(a: *i64) -> *i64 { 68 return (a as i64 + ns_o_world(a)*8) as *i64 69} 70 71func ns_init(a: *i64, npeers: i64, self_id: i64, maxticks: i64, nfac: i64, seed: i64) -> i64 { 72 a[0] = npeers 73 a[1] = self_id 74 a[2] = 0 75 a[3] = maxticks 76 a[4] = nfac 77 a[5] = 0 78 a[6] = 0 79 a[7] = 0 80 var i: i64 = 0 81 let tbl: i64 = 2*maxticks*npeers 82 while i < tbl { a[NS_HDR + i] = 0; i = i + 1 } 83 ws_init(ns_world(a), nfac, seed) 84 return 0 85} 86 87func ns_cmd_at(a: *i64, t: i64, p: i64) -> i64 { return a[ns_o_cmd(a) + t*ns_npeers(a) + p] } 88func ns_have_at(a: *i64, t: i64, p: i64) -> i64 { return a[ns_o_have(a) + t*ns_npeers(a) + p] } 89 90// receive one command from the wire. Idempotent on exact duplicates; REFUSES a conflicting 91// resend; bounds-refuses out-of-range tick/peer. Arrival order is irrelevant by construction. 92func ns_offer(a: *i64, t: i64, p: i64, cmd: i64) -> i64 { 93 if t < 0 { return NS_E_TICK } 94 if t >= ns_maxticks(a) { return NS_E_TICK } 95 if p < 0 { return NS_E_PEER } 96 if p >= ns_npeers(a) { return NS_E_PEER } 97 let ci: i64 = ns_o_cmd(a) + t*ns_npeers(a) + p 98 let hi: i64 = ns_o_have(a) + t*ns_npeers(a) + p 99 if a[hi] == 1 { 100 if a[ci] == cmd { return NS_DUP } 101 return NS_E_CONFLICT 102 } 103 a[ci] = cmd 104 a[hi] = 1 105 return NS_OK 106} 107 108// the LOCKSTEP BARRIER: can the session advance its confirmed tick? 109func ns_can_advance(a: *i64) -> i64 { 110 let t: i64 = a[2] 111 if t >= ns_maxticks(a) { return 0 } 112 let n: i64 = ns_npeers(a) 113 var p: i64 = 0 114 while p < n { 115 if a[ns_o_have(a) + t*n + p] == 0 { return 0 } 116 p = p + 1 117 } 118 return 1 119} 120 121// decode + apply one command through the certified sim. Returns units actually moved. 122// The decode does no validation on purpose: ws_trade's own guards (bounds, overdraw, 123// self-dealing) are the certified sanitizer -- one definition of legality, not two. 124func ns_apply_cmd(w: *i64, cmd: i64) -> i64 { 125 if cmd <= 0 { return 0 } 126 let from: i64 = (cmd / NS_MAGIC_100000000) % 100 127 let to: i64 = (cmd / NS_MAGIC_1000000) % 100 128 let amt: i64 = cmd % NS_MAGIC_1000000 129 return ws_trade(w, from, to, amt) 130} 131 132// advance ONE confirmed tick if and only if every peer's command is present (else stall, return 0). 133// Application order is fixed (peer 0..n-1) so every peer applies identically. 134func ns_step(a: *i64, rate: i64) -> i64 { 135 if ns_can_advance(a) == 0 { return 0 } 136 let t: i64 = a[2] 137 let n: i64 = ns_npeers(a) 138 let w: *i64 = ns_world(a) 139 var p: i64 = 0 140 while p < n { 141 let cmd: i64 = a[ns_o_cmd(a) + t*n + p] 142 if cmd > 0 { 143 let mv: i64 = ns_apply_cmd(w, cmd) 144 if mv > 0 { 145 a[5] = a[5] + 1 146 a[6] = a[6] + mv 147 } 148 } 149 p = p + 1 150 } 151 ws_tick(w, rate) 152 a[2] = t + 1 153 return 1 154} 155 156// DESYNC DETECTOR: checksum of the SHARED live state (confirmed tick, applied counters, the 157// whole world arena). Walks the arena directly -- independent of any save/serialize path, so a 158// serializer that forgets a field cannot hide a divergence (the GX-9 law). self_id and the 159// input tables are per-peer/transient and deliberately excluded. Masked to 63 bits every step: 160// bounded, positive, machine-stable. 161func ns_checksum(a: *i64) -> i64 { 162 var h: i64 = NS_MAGIC_40961 163 h = ((h * 131) + a[0]) & 0x7FFFFFFFFFFFFFFF 164 h = ((h * 131) + a[2]) & 0x7FFFFFFFFFFFFFFF 165 h = ((h * 131) + a[5]) & 0x7FFFFFFFFFFFFFFF 166 h = ((h * 131) + a[6]) & 0x7FFFFFFFFFFFFFFF 167 let w: *i64 = ns_world(a) 168 let n: i64 = ws_words(ns_nfac(a)) 169 var i: i64 = 0 170 while i < n { 171 h = ((h * 131) + w[i]) & 0x7FFFFFFFFFFFFFFF 172 i = i + 1 173 } 174 return h 175} 176 177// word-exact shared-state equality (hdr minus self_id, plus the world). The bitwise truth the 178// checksum approximates -- gates compare with THIS, peers on a wire compare checksums. 179func ns_shared_eq(a: *i64, b: *i64) -> i64 { 180 if a[0] != b[0] { return 0 } 181 if a[2] != b[2] { return 0 } 182 if a[5] != b[5] { return 0 } 183 if a[6] != b[6] { return 0 } 184 let wa: *i64 = ns_world(a) 185 let wb: *i64 = ns_world(b) 186 let n: i64 = ws_words(ns_nfac(a)) 187 var i: i64 = 0 188 while i < n { 189 if wa[i] != wb[i] { return 0 } 190 i = i + 1 191 } 192 return 1 193} 194 195// whole-arena copy: the snapshot primitive for rollback's confirmed-state + speculative-head 196// pattern and for late-join staging. n = ns_words(...) of the source. 197func ns_copy(dst: *i64, src: *i64, n: i64) -> i64 { 198 var i: i64 = 0 199 while i < n { dst[i] = src[i]; i = i + 1 } 200 return 0 201}