nx_nif_lib.nx source
↩ module page · 401 lines · 19011 B
1// nx_nif_lib.nx -- NETIMMERSE/GAMEBRYO NIF STRUCTURAL FACTS CORE (the Bethesda mesh container;
2// /compare/koikatsu KK12 contract, legs 1+2 2026-08-30). SCHEMA GROUND TRUTH is the mirrored
3// niftools nif.xml (GPL-3.0, read as an ORACLE) plus REAL fixtures from the nifly test corpus,
4// hex-measured the same day: magic line then u32 version 0x14020007, u8 endian, u32 user version,
5// u32 num blocks, BSStreamHeader, u16 num block types, SizedString type table, u16 type index per
6// block, u32 size per block, string table, groups.
7// BSStreamHeader export-string rule, VERCONDS FROM nif.xml AND CONFIRMED ON REAL BYTES (SE bsver
8// 100 and FO76 bsver 155): Author always; one u32 when BS > 130; Process Script when BS < 131;
9// Export Script always; Max Filepath when BS >= 103.
10// THREE-STATE HONESTY: is_nif = 0 not a NIF; 1 parsed (modern layout); 2 RECOGNIZED-UNSUPPORTED --
11// the magic line is real but the version or a bound refuses, so every count ABSTAINS at -1 rather
12// than misparse into confident garbage. A bounds failure is a named abstention, never a guess.
13// MEASURED COVERAGE: Skyrim SE (100), Skyrim LE (83) and FO76 (155) headers all parse.
14// GEOMETRY LEG (nif_geom): per-TriShape vertex/triangle counts extracted at the SSE BSTriShape
15// tuple (desc u64, tri u16, vert u16, datasize u32 behind the 100+4*extra NiAVObject prefix) and
16// VERIFIED BY THE SCHEMA'S OWN ARITHMETIC -- nif.xml declares DataSize = (desc AND 0xF) * verts *
17// 4 + tris * 6, so a tuple that satisfies it is self-proven and one that does not is counted
18// UNVERIFIED by name (LE NiTriShape and FO76 layouts land there deliberately). Full vertex DATA
19// extraction to .nxmesh remains the contract's declared final half.
20// SCOPE DECLARED: the census counts blocks whose TYPE NAME contains TriShape; NiTriStrips is not
21// counted -- stated imprecision, widen deliberately.
22// Slots: [0] is_nif [1] version [2] bs_version [3] num_blocks [4] num_block_types
23// [5] trishape_blocks [6] total_block_bytes [7] num_strings [8] header_bytes [9] endian
24// license_tier: ORIGINAL No hw writes (Rule 26).
25import "nx_syscalls.nx"
26import "nx_kkfacts_lib.nx"
27
28const NF_N_SLOTS: i64 = 10
29const NF_MAGIC_SCAN: i64 = 64 // the magic line ends within the first 64 bytes on every known dialect
30const NF_VER_SSE: i64 = 335675399 // 0x14020007 = 20.2.0.7, the modern Bethesda layout this lib parses
31const NF_MAX_TYPES: i64 = 2048 // a type table beyond this refuses as UNSUPPORTED (bound, not a guess)
32const NF_MAX_TYPENAME: i64 = 64
33const NF_MAX_STRINGS: i64 = 65536
34const NF_MAX_STRLEN: i64 = 4096
35const NF_MAX_GROUPS: i64 = 65536
36const NF_BS_EXTRAINT_GT: i64 = 130 // BS Version above which the header carries one extra u32 (nif.xml)
37const NF_BS_PROCSCRIPT_LT: i64 = 131 // BS Version below which Process Script exists (nif.xml)
38const NF_BS_MAXPATH_GTE: i64 = 103 // BS Version at/above which Max Filepath exists (nif.xml)
39const NF_BS_SSE: i64 = 100 // the one BS version whose BSTriShape tuple the geometry leg verifies
40const NF_UV_BS: i64 = 12 // user version at/above which the BSStreamHeader exists
41const NF_CH_NL: i64 = 10
42const NF_GEOM_PREFIX_BASE: i64 = 100 // SSE NiAVObject fixed part 72 + NiBound 16 + skin/shader/alpha refs 12
43const NF_GEOM_TUPLE: i64 = 16 // desc u64 + tri u16 + vert u16 + datasize u32
44const NF_MAX_EXTRA: i64 = 64 // an extra-data list beyond this reads UNVERIFIED, never trusted
45const NF_DESC_LOW_MASK: i64 = 0xF // vertex-record size in dwords lives in the desc's low nibble
46
47func nf_u8(buf: *u8, o: i64) -> i64 { return buf[o] as i64 & 0xff }
48func nf_u16(buf: *u8, o: i64) -> i64 { return nf_u8(buf, o) | (nf_u8(buf, o + 1) << 8) }
49func nf_u32(buf: *u8, o: i64) -> i64 { return nf_u8(buf, o) | (nf_u8(buf, o + 1) << 8) | (nf_u8(buf, o + 2) << 16) | (nf_u8(buf, o + 3) << 24) }
50
51// does buf[at..] start with the nul-str prefix (bounded)?
52func nf_starts(buf: *u8, at: i64, end: i64, pre: *u8) -> i64 {
53 var i: i64 = 0
54 while pre[i] != (0 as u8) {
55 if at + i >= end { return 0 }
56 if buf[at + i] != pre[i] { return 0 }
57 i = i + 1
58 }
59 return 1
60}
61// does the span [off,off+len) contain the nul-str needle?
62func nf_span_has(buf: *u8, off: i64, len: i64, needle: *u8) -> i64 {
63 let hit: i64 = kk_find(buf, off, off + len, needle)
64 if hit < 0 { return 0 }
65 return 1
66}
67// one ExportString: length byte (counting the terminating nul) + bytes. Returns new offset or -1.
68func nf_export(buf: *u8, p: i64, n: i64) -> i64 {
69 if p >= n { return 0 - 1 }
70 let elen: i64 = nf_u8(buf, p)
71 let q: i64 = p + 1 + elen
72 if q > n { return 0 - 1 }
73 return q
74}
75
76// walk the modern header. Returns the offset PAST the parsed header, or -1 on any bounds refusal.
77// Writes the census into facts and the type table into toffs/tlens/tcounts (up to maxtypes).
78// bidx (when non-zero cap bcap) receives each block's type index; bsz its declared size.
79func nf_walk(buf: *u8, n: i64, facts: *i64, toffs: *i64, tlens: *i64, tcounts: *i64, maxtypes: i64, bidx: *i64, bsz: *i64, bcap: i64) -> i64 {
80 let nl: i64 = kk_find(buf, 0, NF_MAGIC_SCAN, "\n" as *u8)
81 if nl < 0 { return 0 - 1 }
82 var p: i64 = nl + 1
83 if p + 13 > n { return 0 - 1 }
84 let ver: i64 = nf_u32(buf, p)
85 facts[1] = ver
86 p = p + 4
87 let endian: i64 = nf_u8(buf, p)
88 facts[9] = endian
89 p = p + 1
90 if ver != NF_VER_SSE { return 0 - 1 }
91 if endian != 1 { return 0 - 1 }
92 let userver: i64 = nf_u32(buf, p)
93 p = p + 4
94 let nblocks: i64 = nf_u32(buf, p)
95 p = p + 4
96 facts[3] = nblocks
97 if userver >= NF_UV_BS {
98 if p + 4 > n { return 0 - 1 }
99 let bsver: i64 = nf_u32(buf, p)
100 facts[2] = bsver
101 p = p + 4
102 // BSStreamHeader per nif.xml verconds, confirmed on SE (100) and FO76 (155) real bytes
103 p = nf_export(buf, p, n) // Author, always
104 if p < 0 { return 0 - 1 }
105 if bsver > NF_BS_EXTRAINT_GT { if p + 4 > n { return 0 - 1 } p = p + 4 }
106 if bsver < NF_BS_PROCSCRIPT_LT { p = nf_export(buf, p, n); if p < 0 { return 0 - 1 } }
107 p = nf_export(buf, p, n) // Export Script, always
108 if p < 0 { return 0 - 1 }
109 if bsver >= NF_BS_MAXPATH_GTE { p = nf_export(buf, p, n); if p < 0 { return 0 - 1 } }
110 }
111 if p + 2 > n { return 0 - 1 }
112 let ntypes: i64 = nf_u16(buf, p)
113 p = p + 2
114 facts[4] = ntypes
115 if ntypes < 1 { return 0 - 1 }
116 if ntypes > NF_MAX_TYPES { return 0 - 1 }
117 var t: i64 = 0
118 while t < ntypes {
119 if p + 4 > n { return 0 - 1 }
120 let slen: i64 = nf_u32(buf, p)
121 if slen < 1 { return 0 - 1 }
122 if slen > NF_MAX_TYPENAME { return 0 - 1 }
123 p = p + 4
124 if p + slen > n { return 0 - 1 }
125 if t < maxtypes { toffs[t] = p; tlens[t] = slen; tcounts[t] = 0 }
126 p = p + slen
127 t = t + 1
128 }
129 // per-block type index; high bit masked (historic phantom flag)
130 var b: i64 = 0
131 var trishapes: i64 = 0
132 while b < nblocks {
133 if p + 2 > n { return 0 - 1 }
134 let raw: i64 = nf_u16(buf, p)
135 let ti: i64 = raw & 0x7fff
136 p = p + 2
137 if ti >= ntypes { return 0 - 1 }
138 if ti < maxtypes {
139 tcounts[ti] = tcounts[ti] + 1
140 if nf_span_has(buf, toffs[ti], tlens[ti], "TriShape" as *u8) == 1 { trishapes = trishapes + 1 }
141 }
142 if bcap > 0 { if b < bcap { bidx[b] = ti } }
143 b = b + 1
144 }
145 facts[5] = trishapes
146 // per-block sizes
147 var total: i64 = 0
148 b = 0
149 while b < nblocks {
150 if p + 4 > n { return 0 - 1 }
151 let sz: i64 = nf_u32(buf, p)
152 total = total + sz
153 if bcap > 0 { if b < bcap { bsz[b] = sz } }
154 p = p + 4
155 b = b + 1
156 }
157 facts[6] = total
158 // string table
159 if p + 8 > n { return 0 - 1 }
160 let nstr: i64 = nf_u32(buf, p)
161 p = p + 8
162 facts[7] = nstr
163 if nstr > NF_MAX_STRINGS { return 0 - 1 }
164 var s: i64 = 0
165 while s < nstr {
166 if p + 4 > n { return 0 - 1 }
167 let sl: i64 = nf_u32(buf, p)
168 if sl > NF_MAX_STRLEN { return 0 - 1 }
169 p = p + 4 + sl
170 if p > n { return 0 - 1 }
171 s = s + 1
172 }
173 // groups
174 if p + 4 > n { return 0 - 1 }
175 let ngroups: i64 = nf_u32(buf, p)
176 p = p + 4
177 if ngroups > NF_MAX_GROUPS { return 0 - 1 }
178 p = p + ngroups * 4
179 if p > n { return 0 - 1 }
180 facts[8] = p
181 return p
182}
183
184// probe core: buf holds the first n bytes; file_size = true size. A partial read of a recognized
185// NIF is state 2 with every count abstained -- a truncated header cannot be censused honestly.
186func nif_probe(buf: *u8, n: i64, file_size: i64, facts: *i64) -> i64 {
187 var i: i64 = 0
188 while i < NF_N_SLOTS { facts[i] = 0 - 1; i = i + 1 }
189 facts[0] = 0
190 if n < 40 { return 0 }
191 var magic: i64 = 0
192 if nf_starts(buf, 0, n, "Gamebryo File Format, Version " as *u8) == 1 { magic = 1 }
193 if nf_starts(buf, 0, n, "NetImmerse File Format, Version " as *u8) == 1 { magic = 1 }
194 if magic == 0 { return 0 }
195 facts[0] = 2
196 if file_size > n { return 0 }
197 let toffs: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
198 let tlens: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
199 let tcounts: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
200 let none: *i64 = sys_mmap(8) as *i64
201 let endp: i64 = nf_walk(buf, n, facts, toffs, tlens, tcounts, NF_MAX_TYPES, none, none, 0)
202 if endp < 0 {
203 // recognized but refused: keep version/endian if read, abstain the rest
204 facts[3] = 0 - 1
205 facts[4] = 0 - 1
206 facts[5] = 0 - 1
207 facts[6] = 0 - 1
208 facts[7] = 0 - 1
209 facts[8] = 0 - 1
210 return 0
211 }
212 facts[0] = 1
213 return 0
214}
215
216// type-table census for the blocks verb: fills name spans + per-type block counts.
217// meta[0] = types written (bounded by maxtypes); returns is_nif state as nif_probe would.
218func nif_types(buf: *u8, n: i64, file_size: i64, toffs: *i64, tlens: *i64, tcounts: *i64, maxtypes: i64, meta: *i64) -> i64 {
219 meta[0] = 0
220 let facts: *i64 = sys_mmap(NF_N_SLOTS * 8) as *i64
221 var i: i64 = 0
222 while i < NF_N_SLOTS { facts[i] = 0 - 1; i = i + 1 }
223 facts[0] = 0
224 if n < 40 { return 0 }
225 var magic: i64 = 0
226 if nf_starts(buf, 0, n, "Gamebryo File Format, Version " as *u8) == 1 { magic = 1 }
227 if nf_starts(buf, 0, n, "NetImmerse File Format, Version " as *u8) == 1 { magic = 1 }
228 if magic == 0 { return 0 }
229 if file_size > n { return 2 }
230 let none: *i64 = sys_mmap(8) as *i64
231 let endp: i64 = nf_walk(buf, n, facts, toffs, tlens, tcounts, maxtypes, none, none, 0)
232 if endp < 0 { return 2 }
233 var written: i64 = facts[4]
234 if written > maxtypes { written = maxtypes }
235 meta[0] = written
236 return 1
237}
238
239const NF_GEOM_MAX_BLOCKS: i64 = 65536
240
241// GEOMETRY FACTS LEG: per-TriShape vertex/triangle counts, each tuple VERIFIED by the schema's own
242// arithmetic (DataSize = (desc AND 0xF) * verts * 4 + tris * 6, from nif.xml). vouts/touts/vflags
243// receive counts + a per-shape verified flag; a tuple that fails the formula (LE NiTriShape, FO76
244// layouts, or a lying file) is UNVERIFIED with zeros -- counted, never trusted.
245// meta[0] = verified shapes; meta[1] = total verts (verified only); meta[2] = total tris (verified).
246// Returns TriShape blocks examined, or -1 when the header itself does not parse (state != 1).
247func nif_geom(buf: *u8, n: i64, file_size: i64, vouts: *i64, touts: *i64, vflags: *i64, cap: i64, meta: *i64) -> i64 {
248 meta[0] = 0
249 meta[1] = 0
250 meta[2] = 0
251 let facts: *i64 = sys_mmap(NF_N_SLOTS * 8) as *i64
252 var i: i64 = 0
253 while i < NF_N_SLOTS { facts[i] = 0 - 1; i = i + 1 }
254 if n < 40 { return 0 - 1 }
255 var magic: i64 = 0
256 if nf_starts(buf, 0, n, "Gamebryo File Format, Version " as *u8) == 1 { magic = 1 }
257 if nf_starts(buf, 0, n, "NetImmerse File Format, Version " as *u8) == 1 { magic = 1 }
258 if magic == 0 { return 0 - 1 }
259 if file_size > n { return 0 - 1 }
260 let toffs: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
261 let tlens: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
262 let tcounts: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
263 let bidx: *i64 = sys_mmap(NF_GEOM_MAX_BLOCKS * 8) as *i64
264 let bsz: *i64 = sys_mmap(NF_GEOM_MAX_BLOCKS * 8) as *i64
265 let endp: i64 = nf_walk(buf, n, facts, toffs, tlens, tcounts, NF_MAX_TYPES, bidx, bsz, NF_GEOM_MAX_BLOCKS)
266 if endp < 0 { return 0 - 1 }
267 let nblocks: i64 = facts[3]
268 if nblocks > NF_GEOM_MAX_BLOCKS { return 0 - 1 }
269 var off: i64 = facts[8]
270 var shapes: i64 = 0
271 var b: i64 = 0
272 while b < nblocks {
273 let ti: i64 = bidx[b]
274 let sz: i64 = bsz[b]
275 if nf_span_has(buf, toffs[ti], tlens[ti], "TriShape" as *u8) == 1 {
276 var vok: i64 = 0
277 var nverts: i64 = 0
278 var ntris: i64 = 0
279 let nextra: i64 = nf_u32(buf, off + 4)
280 if nextra >= 0 { if nextra <= NF_MAX_EXTRA {
281 let doff: i64 = off + NF_GEOM_PREFIX_BASE + nextra * 4
282 if doff + NF_GEOM_TUPLE <= off + sz { if doff + NF_GEOM_TUPLE <= n {
283 let desc_lo: i64 = nf_u32(buf, doff) & NF_DESC_LOW_MASK
284 let tri: i64 = nf_u16(buf, doff + 8)
285 let vrt: i64 = nf_u16(buf, doff + 10)
286 let dsz: i64 = nf_u32(buf, doff + 12)
287 let calc: i64 = desc_lo * vrt * 4 + tri * 6
288 if dsz > 0 { if calc == dsz {
289 vok = 1
290 nverts = vrt
291 ntris = tri
292 meta[0] = meta[0] + 1
293 meta[1] = meta[1] + vrt
294 meta[2] = meta[2] + tri
295 } }
296 } }
297 } }
298 if shapes < cap {
299 vouts[shapes] = nverts
300 touts[shapes] = ntris
301 vflags[shapes] = vok
302 }
303 shapes = shapes + 1
304 }
305 off = off + sz
306 b = b + 1
307 }
308 return shapes
309}
310
311const NF_BOUND_OFF: i64 = 72 // NiBound (center xyz f32 + radius f32) after the 72-byte NiAVObject fixed part
312const NF_INSIDE_SLACK_NUM: i64 = 1051 // 5.1 percent radius slack for f32-to-permil rounding -- named imprecision
313const NF_INSIDE_SLACK_DEN: i64 = 1000
314
315// f32 at a byte offset to PERMIL, composing the sibling lib's mantissa decoder -- one float ruler
316func nf_f32p(buf: *u8, o: i64) -> i64 { return kk_f32_permil(nf_u32(buf, o)) }
317
318// VERTEX POSITIONS LEG: decode the shape_sel-th VERIFIED TriShape's vertex positions to PERMIL and
319// test every one against the block's OWN declared NiBound sphere -- the container carries its own
320// oracle, so the decode is self-proving: a wrong stride or offset scatters points outside the
321// bound. SSE stores positions as full f32 x,y,z at each record start (nif.xml BSVertexDataSSE).
322// meta[0] = verts decoded; meta[1] = verts inside the declared bound (with the named slack);
323// meta[2] = bound radius in permil. Returns verts, or 0 when the selected verified shape is absent.
324func nif_verts(buf: *u8, n: i64, file_size: i64, shape_sel: i64, pxs: *i64, pys: *i64, pzs: *i64, cap: i64, meta: *i64) -> i64 {
325 meta[0] = 0
326 meta[1] = 0
327 meta[2] = 0
328 let facts: *i64 = sys_mmap(NF_N_SLOTS * 8) as *i64
329 var i: i64 = 0
330 while i < NF_N_SLOTS { facts[i] = 0 - 1; i = i + 1 }
331 if n < 40 { return 0 }
332 var magic: i64 = 0
333 if nf_starts(buf, 0, n, "Gamebryo File Format, Version " as *u8) == 1 { magic = 1 }
334 if nf_starts(buf, 0, n, "NetImmerse File Format, Version " as *u8) == 1 { magic = 1 }
335 if magic == 0 { return 0 }
336 if file_size > n { return 0 }
337 let toffs: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
338 let tlens: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
339 let tcounts: *i64 = sys_mmap(NF_MAX_TYPES * 8) as *i64
340 let bidx: *i64 = sys_mmap(NF_GEOM_MAX_BLOCKS * 8) as *i64
341 let bsz: *i64 = sys_mmap(NF_GEOM_MAX_BLOCKS * 8) as *i64
342 let endp: i64 = nf_walk(buf, n, facts, toffs, tlens, tcounts, NF_MAX_TYPES, bidx, bsz, NF_GEOM_MAX_BLOCKS)
343 if endp < 0 { return 0 }
344 let nblocks: i64 = facts[3]
345 if nblocks > NF_GEOM_MAX_BLOCKS { return 0 }
346 var off: i64 = facts[8]
347 var seen: i64 = 0
348 var b: i64 = 0
349 while b < nblocks {
350 let ti: i64 = bidx[b]
351 let sz: i64 = bsz[b]
352 if nf_span_has(buf, toffs[ti], tlens[ti], "TriShape" as *u8) == 1 {
353 let nextra: i64 = nf_u32(buf, off + 4)
354 if nextra >= 0 { if nextra <= NF_MAX_EXTRA {
355 let doff: i64 = off + NF_GEOM_PREFIX_BASE + nextra * 4
356 if doff + NF_GEOM_TUPLE <= off + sz { if doff + NF_GEOM_TUPLE <= n {
357 let desc_lo: i64 = nf_u32(buf, doff) & NF_DESC_LOW_MASK
358 let tri: i64 = nf_u16(buf, doff + 8)
359 let vrt: i64 = nf_u16(buf, doff + 10)
360 let dsz: i64 = nf_u32(buf, doff + 12)
361 let calc: i64 = desc_lo * vrt * 4 + tri * 6
362 if dsz > 0 { if calc == dsz {
363 if seen == shape_sel {
364 // the block's own oracle: NiBound at the fixed NiAVObject offset
365 let boff: i64 = off + nextra * 4 + NF_BOUND_OFF
366 let cx: i64 = nf_f32p(buf, boff)
367 let cy: i64 = nf_f32p(buf, boff + 4)
368 let cz: i64 = nf_f32p(buf, boff + 8)
369 let rp: i64 = nf_f32p(buf, boff + 12)
370 meta[2] = rp
371 let rr: i64 = (rp * NF_INSIDE_SLACK_NUM) / NF_INSIDE_SLACK_DEN
372 let stride: i64 = desc_lo * 4
373 let vdata: i64 = doff + NF_GEOM_TUPLE
374 var v: i64 = 0
375 while v < vrt {
376 let ro: i64 = vdata + v * stride
377 if ro + 12 > n { v = vrt } else {
378 let px: i64 = nf_f32p(buf, ro)
379 let py: i64 = nf_f32p(buf, ro + 4)
380 let pz: i64 = nf_f32p(buf, ro + 8)
381 if v < cap { pxs[v] = px; pys[v] = py; pzs[v] = pz }
382 meta[0] = meta[0] + 1
383 let dx: i64 = px - cx
384 let dy: i64 = py - cy
385 let dz: i64 = pz - cz
386 if dx * dx + dy * dy + dz * dz <= rr * rr { meta[1] = meta[1] + 1 }
387 v = v + 1
388 }
389 }
390 return meta[0]
391 }
392 seen = seen + 1
393 } }
394 } }
395 } }
396 }
397 off = off + sz
398 b = b + 1
399 }
400 return 0
401}