nx_game_save.nx source
↩ module page · 90 lines · 3998 B
1// nx_game_save.nx -- SOVEREIGN SAVE / LOAD for game state. Built because the gamebench ruler ranks
2// save-load-persistence as the #1 blocker across the ingested corpus (11 of 12 real titles need it), i.e.
3// this is demand-ranked by measurement, not chosen by taste.
4// STATE IS AN i64 VECTOR, so this is game-agnostic: any game that can describe itself as integers can use it.
5// THE CONTRACT -- a save file is either loaded EXACTLY or REFUSED WITH A REASON. It is never partially
6// applied and never silently accepted when damaged, because a corrupted save that half-loads is worse than
7// one that fails: it strands the player in an impossible world state and looks like a game bug forever.
8// Layout (all i64, little-endian): [magic][version][count][payload...][checksum-of-everything-before].
9// license_tier: ORIGINAL
10import "nx_syscalls.nx"
11const GS_MAGIC_1469598103: i64 = 1469598103
12const GS_MAGIC_1000000007: i64 = 1000000007
13
14const GS_MAGIC: i64 = 5182089520394403 // distinctive, so a wrong file type is rejected on sight
15const GS_VERSION: i64 = 1
16const GS_HDR: i64 = 3 // magic, version, count
17
18// explicit refusal codes -- the caller can tell the player WHY, instead of guessing
19const GS_ERR_UNREADABLE: i64 = 0 - 1
20const GS_ERR_TRUNCATED: i64 = 0 - 2
21const GS_ERR_MAGIC: i64 = 0 - 3
22const GS_ERR_VERSION: i64 = 0 - 4
23const GS_ERR_CHECKSUM: i64 = 0 - 5
24const GS_ERR_TOOBIG: i64 = 0 - 6
25
26func gs_put64(b: *u8, at: i64, v: i64) -> i64 {
27 var i: i64 = 0
28 var x: i64 = v
29 while i < 8 { b[at + i] = (x % 256) as u8; x = x / 256; i = i + 1 }
30 return 0
31}
32func gs_get64(b: *u8, at: i64) -> i64 {
33 var i: i64 = 7
34 var v: i64 = 0
35 while i >= 0 { v = v * 256 + (b[at + i] as i64); i = i - 1 }
36 return v
37}
38
39// order-sensitive rolling checksum: a swapped pair of fields changes it, which a plain sum would not catch
40func gs_ck(words: *i64, n: i64) -> i64 {
41 var h: i64 = GS_MAGIC_1469598103
42 var i: i64 = 0
43 while i < n { h = (h * 31 + words[i] + i * 7) % GS_MAGIC_1000000007; i = i + 1 }
44 return h
45}
46
47// write `n` state words to `path`. Returns bytes written, or a negative code.
48func gs_save(path: *u8, words: *i64, n: i64) -> i64 {
49 if n < 0 { return GS_ERR_TOOBIG }
50 let total: i64 = GS_HDR + n + 1
51 let buf: *u8 = sys_mmap(total * 8 + 64)
52 let all: *i64 = sys_mmap((total + 4) * 8) as *i64
53 all[0] = GS_MAGIC
54 all[1] = GS_VERSION
55 all[2] = n
56 var i: i64 = 0
57 while i < n { all[GS_HDR + i] = words[i]; i = i + 1 }
58 all[GS_HDR + n] = gs_ck(all, GS_HDR + n) // checksum covers header AND payload
59 i = 0
60 while i < total { gs_put64(buf, i * 8, all[i]); i = i + 1 }
61 let fd: i64 = sys_openat_wr(path, 420)
62 if fd < 0 { return GS_ERR_UNREADABLE }
63 sys_write(fd, buf, total * 8)
64 sys_close(fd)
65 return total * 8
66}
67
68// read state from `path` into `out` (capacity `maxn`). Returns the word count, or a negative code.
69// Every failure path returns BEFORE touching `out`, so a refused load cannot leave a half-applied world.
70func gs_load(path: *u8, out: *i64, maxn: i64) -> i64 {
71 let szp: *i64 = sys_mmap(16) as *i64
72 let raw: *u8 = sys_read_file(path, szp)
73 if (raw as i64) == 0 { return GS_ERR_UNREADABLE }
74 let sz: i64 = szp[0]
75 if sz < (GS_HDR + 1) * 8 { return GS_ERR_TRUNCATED }
76 if gs_get64(raw, 0) != GS_MAGIC { return GS_ERR_MAGIC }
77 if gs_get64(raw, 8) != GS_VERSION { return GS_ERR_VERSION }
78 let n: i64 = gs_get64(raw, 16)
79 if n < 0 { return GS_ERR_TRUNCATED }
80 if n > maxn { return GS_ERR_TOOBIG }
81 let total: i64 = GS_HDR + n + 1
82 if sz < total * 8 { return GS_ERR_TRUNCATED }
83 let all: *i64 = sys_mmap((total + 4) * 8) as *i64
84 var i: i64 = 0
85 while i < total { all[i] = gs_get64(raw, i * 8); i = i + 1 }
86 if all[GS_HDR + n] != gs_ck(all, GS_HDR + n) { return GS_ERR_CHECKSUM }
87 i = 0
88 while i < n { out[i] = all[GS_HDR + i]; i = i + 1 } // only now is the caller's state touched
89 return n
90}