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}