nx_genp_allow_lib.nx source
↩ module page · 103 lines · 4942 B
1// nx_genp_allow_lib.nx -- THE ONE GEN-TIME RECIPE ALLOWLIST (2026-09-04).
2//
3// WHY THIS FILE EXISTS. Three organs each carried their own copy of "which recipe indices cross into the engine
4// BEFORE genworld reads the spec table": nx_wasm_craft (init_impl_v's gok9 chain), nx_world_snap (its g8 chain)
5// and nx_game_page_emit (the NXGP count pass and write pass). The three drifted three ways. MEASURED 2026-09-04:
6// the engine admitted 0..6, 59..64, 66..68; the snap was blind to 62..68 until 2026-09-01 (debt 1788294505);
7// the page emitter still admitted only 0..6, 59, 60, 61, 64 -- so a recipe's 66, 67 and 68 pairs VALIDATED (the
8// validator had been widened on 2026-08-28) and then reached nothing. WITNESS: sites/nishifamily/world/ridge.html
9// line 108 served const NXGP=[[64,0]] against a recipe of 67:12,68:32 -- the STAGED-PIPELINE world had served
10// legacy two-octave terrain since it shipped, while the capture instrument drew the staged one.
11// A LAW APPLIED IN ONE ORGAN AND NOT ITS SIBLINGS IS HALF A LAW, AND WITH THREE COPIES THE HALF LEFT UNDONE IS
12// THE ONE THAT SHIPS. This lib is the single ruler all three compose: an index added here reaches the engine, the
13// instrument and the page in ONE edit, and nx_wasm_craft_gate pins every engine P_* slot to it so the names and
14// the numbers cannot part.
15// WASM-SAFE BY CONSTRUCTION: pure arithmetic, no allocation, no syscalls (nx_wasm_craft compiles it to wasm).
16// The STYLE lane (7..58, poked post-boot) is deliberately NOT here: gen-time and style are different channels,
17// and blurring them is the silent no-op the engine's whitelist exists to prevent.
18// license_tier: ORIGINAL No hw writes (Rule 26).
19
20// The gen-time slots, named for what they are. The engine's P_* table declares the same numbers under its own
21// names; the gate asserts each pair agrees, so this file cannot silently mean a different slot than the engine.
22const GA_TERRAIN_FIRST: i64 = 0 // P_TBASE .. P_CAVETH: the terrain-shape block
23const GA_TERRAIN_LAST: i64 = 6
24const GA_TREEMAX: i64 = 59
25const GA_HISTAGE: i64 = 60
26const GA_TREESPEC: i64 = 61
27const GA_BLDG: i64 = 62
28const GA_BLDGSPEC: i64 = 63
29const GA_ERODE: i64 = 64
30const GA_OCEAN: i64 = 66
31const GA_WPIPE: i64 = 67
32const GA_WPSCALE: i64 = 68
33// a scan bound for the census, not a limit: past the widest spec table the engine owns (WC_SPECN = 80)
34const GA_CENSUS_SPAN: i64 = 128
35// the number of admitted indices, stated ONCE so the census below can be asserted against it: 7 terrain + 9 named
36const GA_DUNE: i64 = 79
37const GA_ADMITTED: i64 = 16
38
39// 1 = idx crosses into the engine BEFORE genworld reads the spec table; 0 = it does not (style lane or unowned).
40func genp_gen_ok(idx: i64) -> i64 {
41 if idx >= GA_TERRAIN_FIRST { if idx <= GA_TERRAIN_LAST { return 1 } }
42 if idx == GA_TREEMAX { return 1 }
43 if idx == GA_HISTAGE { return 1 }
44 if idx == GA_TREESPEC { return 1 }
45 if idx == GA_BLDG { return 1 }
46 if idx == GA_BLDGSPEC { return 1 }
47 if idx == GA_ERODE { return 1 }
48 if idx == GA_OCEAN { return 1 }
49 if idx == GA_WPIPE { return 1 }
50 if idx == GA_WPSCALE { return 1 }
51 return 0
52}
53// the census: how many indices the allowlist admits. A gate asserts it equals GA_ADMITTED, so an index added
54// above without its count (or a count moved without an index) is caught as a number that disagrees with itself.
55func genp_gen_count() -> i64 {
56 var n: i64 = 0
57 var i: i64 = 0
58 while i < GA_CENSUS_SPAN {
59 if genp_gen_ok(i) == 1 { n = n + 1 }
60 i = i + 1
61 }
62 return n
63}
64
65const GA_DUNE_AMP_MASK: i64 = 4095
66const GA_DUNE_WAVE_MASK: i64 = 255
67const GA_DUNE_WAVE_SHIFT: i64 = 12
68const GA_DUNE_HEADING_SHIFT: i64 = 20
69const GA_DUNE_PACK_MAX: i64 = 268435455
70const GA_DUNE_MIN_WAVE: i64 = 4
71func genp_dune_valid(packed: i64) -> i64 {
72 if packed < 0 { return 0 }
73 if packed > GA_DUNE_PACK_MAX { return 0 }
74 if packed == 0 { return 1 }
75 if (packed & GA_DUNE_AMP_MASK) == 0 { return 0 }
76 if ((packed >> GA_DUNE_WAVE_SHIFT) & GA_DUNE_WAVE_MASK) < GA_DUNE_MIN_WAVE { return 0 }
77 return 1
78}
79// The text door consumes a bounded decimal field without multiplying an overflow.
80// Return -1 for empty, malformed or out-of-range input; delimiter remains unread.
81func genp_dune_decimal(text: *u8, pos: *i64) -> i64 {
82 var p: i64 = pos[0]
83 var value: i64 = 0
84 var digits: i64 = 0
85 while text[p] != (0 as u8) {
86 let c: i64 = text[p] as i64
87 if c == 44 { break }
88 if c < 48 || c > 57 { return 0 - 1 }
89 let digit: i64 = c - 48
90 if value > (GA_DUNE_PACK_MAX - digit)/10 { return 0 - 1 }
91 value = value*10 + digit
92 digits = digits + 1; p = p + 1
93 }
94 if digits == 0 { return 0 - 1 }
95 pos[0] = p
96 return value
97}
98
99// Opt-in predicate: incumbent engines keep their original sixteen indices.
100func genp_dune_gen_ok(idx: i64) -> i64 {
101 if idx == GA_DUNE { return 1 }
102 return genp_gen_ok(idx)
103}