nx_kkfacts_lib.nx source
↩ module page · 81 lines · 3701 B
1// nx_kkfacts_lib.nx -- KOIKATSU CARD FACT EXTRACTION CORE (the Honey Select / Koikatsu card
2// study's first organ; benchmark card KK_396262 measured 2026-08-04).
3//
4// A Koikatsu card is a PNG portrait with the CHARACTER DEFINITION appended after IEND: the
5// KoiKatuChara marker, then MessagePack block structures (Custom = face/body/hair, Coordinate =
6// outfits, Parameter, Status, KKEx = modded extended data). This core reads STRUCTURE ONLY:
7// portrait bounds + dims, payload size, marker, block-name census. Slider values and names stay
8// unread in v1 -- the deep (msgpack) rung comes with the k>=8 card study, and per-card creative
9// content never enters the published plane (one card = one creator; the k-wall exists for this).
10//
11// ★THE CARD FORMAT CARRIES NO MOTION: coordinates are OUTFITS, not animations -- motion lives in
12// the game engine, not the artifact. A fact this lane's compare page states outright.
13// Slots: [0] is_card [1] portrait_bytes [2] portrait_w [3] portrait_h [4] payload_bytes
14// [5] custom [6] coordinate [7] parameter [8] status [9] kkex (-1 = not measured)
15// license_tier: ORIGINAL No hw writes (Rule 26).
16import "nx_syscalls.nx"
17import "nx_img_dims.nx"
18const KK_MAGIC_4096: i64 = 4096
19
20const KK_N_SLOTS: i64 = 10
21
22func kk_b(buf: *u8, o: i64) -> i64 { return buf[o] as i64 & 0xff }
23func kk_find(buf: *u8, start: i64, end: i64, needle: *u8) -> i64 {
24 var m: i64 = 0
25 while needle[m] != (0 as u8) { m = m + 1 }
26 var i: i64 = start
27 while i + m <= end {
28 var k: i64 = 0
29 while k < m { if (buf[i+k] as i64) != (needle[k] as i64) { k = m + 9 } else { k = k + 1 } }
30 if k == m { return i }
31 i = i + 1
32 }
33 return 0 - 1
34}
35func kk_count(buf: *u8, start: i64, end: i64, needle: *u8) -> i64 {
36 var m: i64 = 0
37 while needle[m] != (0 as u8) { m = m + 1 }
38 var c: i64 = 0
39 var i: i64 = start
40 while i + m <= end {
41 var k: i64 = 0
42 while k < m { if (buf[i+k] as i64) != (needle[k] as i64) { k = m + 9 } else { k = k + 1 } }
43 if k == m { c = c + 1; i = i + m } else { i = i + 1 }
44 }
45 return c
46}
47
48// probe: buf holds the first n bytes; file_size = true size (truncation honesty).
49func kk_probe(buf: *u8, n: i64, file_size: i64, facts: *i64) -> i64 {
50 var i: i64 = 0
51 while i < KK_N_SLOTS { facts[i] = 0 - 1; i = i + 1 }
52 facts[0] = 0
53 if n < 64 { return 0 }
54 if kk_b(buf,0) != 0x89 { return 0 }
55 if kk_b(buf,1) != 0x50 { return 0 }
56 if kk_b(buf,2) != 0x4e { return 0 }
57 if kk_b(buf,3) != 0x47 { return 0 }
58 // first IEND = the portrait's end; the chara payload begins right after IEND's CRC (+8)
59 let iend: i64 = kk_find(buf, 8, n, "IEND" as *u8)
60 if iend < 0 { return 0 }
61 let pend: i64 = iend + 8
62 if pend >= file_size { return 0 } // nothing after the portrait: a plain PNG
63 // the marker sits in the first KB of the payload
64 var scan_end: i64 = pend + KK_MAGIC_4096
65 if scan_end > n { scan_end = n }
66 if kk_find(buf, pend, scan_end, "KoiKatuChara" as *u8) < 0 { return 0 }
67 facts[0] = 1
68 facts[1] = pend
69 let wh: *i64 = sys_mmap(16) as *i64
70 if nx_img_dims(buf, n, wh) == 1 { facts[2] = wh[0]; facts[3] = wh[1] }
71 facts[4] = file_size - pend
72 // block census only over a COMPLETE payload -- a truncated count is an undercount
73 if file_size <= n {
74 facts[5] = kk_count(buf, pend, n, "Custom" as *u8)
75 facts[6] = kk_count(buf, pend, n, "Coordinate" as *u8)
76 facts[7] = kk_count(buf, pend, n, "Parameter" as *u8)
77 facts[8] = kk_count(buf, pend, n, "Status" as *u8)
78 facts[9] = kk_count(buf, pend, n, "KKEx" as *u8)
79 }
80 return 0
81}