nx_kkfacts_lib.nx source
↩ module page · 169 lines · 8653 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 core: buf holds the first n bytes; file_size = true size (truncation honesty). The DIALECT
49// is the marker parameter -- one ruler for every Illusion card family, each public probe a thin
50// wrapper naming its measured marker (rule 3: the second dialect arrived 2026-08-18 and a copied
51// body would already have drifted).
52func kk_probe_marker(buf: *u8, n: i64, file_size: i64, facts: *i64, marker: *u8) -> i64 {
53 var i: i64 = 0
54 while i < KK_N_SLOTS { facts[i] = 0 - 1; i = i + 1 }
55 facts[0] = 0
56 if n < 64 { return 0 }
57 if kk_b(buf,0) != 0x89 { return 0 }
58 if kk_b(buf,1) != 0x50 { return 0 }
59 if kk_b(buf,2) != 0x4e { return 0 }
60 if kk_b(buf,3) != 0x47 { return 0 }
61 // first IEND = the portrait's end; the chara payload begins right after IEND's CRC (+8)
62 let iend: i64 = kk_find(buf, 8, n, "IEND" as *u8)
63 if iend < 0 { return 0 }
64 let pend: i64 = iend + 8
65 if pend >= file_size { return 0 } // nothing after the portrait: a plain PNG
66 // the marker sits in the first KB of the payload
67 var scan_end: i64 = pend + KK_MAGIC_4096
68 if scan_end > n { scan_end = n }
69 if kk_find(buf, pend, scan_end, marker) < 0 { return 0 }
70 facts[0] = 1
71 facts[1] = pend
72 let wh: *i64 = sys_mmap(16) as *i64
73 if nx_img_dims(buf, n, wh) == 1 { facts[2] = wh[0]; facts[3] = wh[1] }
74 facts[4] = file_size - pend
75 // block census only over a COMPLETE payload -- a truncated count is an undercount
76 if file_size <= n {
77 facts[5] = kk_count(buf, pend, n, "Custom" as *u8)
78 facts[6] = kk_count(buf, pend, n, "Coordinate" as *u8)
79 facts[7] = kk_count(buf, pend, n, "Parameter" as *u8)
80 facts[8] = kk_count(buf, pend, n, "Status" as *u8)
81 facts[9] = kk_count(buf, pend, n, "KKEx" as *u8)
82 }
83 return 0
84}
85func kk_probe(buf: *u8, n: i64, file_size: i64, facts: *i64) -> i64 {
86 return kk_probe_marker(buf, n, file_size, facts, "KoiKatuChara" as *u8)
87}
88// AI SHOUJO / HONEY SELECT 2 (the third Illusion family; watch contract kk_probe_aishoujo on
89// /compare/koikatsu). GROUNDED FROM THE BANKED FIXTURE, never guessed: knowledge/library/bepis/
90// AI_248895.png (sha f0511393..., byte-equal to the BepisDB API's declared content pin) carries
91// after IEND+8: length bytes, then the CJK-bracketed marker U+3010 AIS_Chara U+3011, version
92// "1.0.0", two GUIDs, then a msgpack lstInfo naming KKEx/Custom/Coordinate/Parameter blocks --
93// the same msgpack machinery the slider decoder below speaks. The needle is the ASCII core
94// "AIS_Chara" (the brackets are multi-byte UTF-8; the core measured at IEND+13 on the fixture,
95// unique in the payload head). Status may honestly census 0 in this family.
96func kk_probe_aishoujo(buf: *u8, n: i64, file_size: i64, facts: *i64) -> i64 {
97 return kk_probe_marker(buf, n, file_size, facts, "AIS_Chara" as *u8)
98}
99
100// ---- SLIDER VALUE DECODE (2026-08-18, operator: "i have cards but they do nothing" -- the
101// kk_decode_sliders watch contract published on /compare/koikatsu). The Custom block serializes its
102// shape sliders as MessagePack float32 arrays under keys like shapeValueFace / shapeValueBody: the
103// key is a fixstr whose bytes sit verbatim in the payload, and the VALUE follows immediately after
104// the key bytes -- a fixarray (0x90+n) or array16 (0xdc + BE16 count) of float32 elements
105// (0xca + 4 BE bytes). Each element converts to PERMIL by integer mantissa/exponent shift
106// arithmetic (the g2_f32mm idiom from nx_gltf2mesh -- no float unit in the path).
107// FAIL-CLOSED + HONEST-PARTIAL: missing key -> meta[0]=-1, parsed=0; no array header -> meta[0]=-2;
108// a non-float element STOPS the parse so parsed<declared is visible, never papered over.
109// K-WALL HOLDS: decoded values are for the caller's stdout/library use -- per-card creative
110// content never enters a published plane.
111const KK_MP_FIXARRAY_BASE: i64 = 0x90 // msgpack fixarray tag; low nibble carries the count
112const KK_MP_TAG_HI_MASK: i64 = 0xf0
113const KK_MP_TAG_LO_MASK: i64 = 0x0f
114const KK_MP_ARRAY16: i64 = 0xdc // BE16 count follows
115const KK_MP_F32: i64 = 0xca // 4 BE bytes follow
116const KK_MP_F32_SPAN: i64 = 5 // tag + 4 payload bytes
117const KK_F32_EXP_BIAS: i64 = 127
118const KK_F32_EXP_MASK: i64 = 255
119const KK_F32_MANT_BITS: i64 = 23
120const KK_F32_MANT_MASK: i64 = 8388607
121const KK_F32_MANT_ONE: i64 = 8388608
122const KK_PERMIL: i64 = 1000
123const KK_SHIFT_SAFE_HI: i64 = 30 // beyond this a left-shift of mant*1000 overflows i64 headroom
124const KK_SHIFT_SAFE_LO: i64 = 62 // beyond this a right-shift underflows to 0 anyway
125
126func kk_f32_permil(w: i64) -> i64 {
127 let sign: i64 = (w >> 31) & 1
128 let expo: i64 = (w >> KK_F32_MANT_BITS) & KK_F32_EXP_MASK
129 if expo == 0 { return 0 }
130 let mant: i64 = (w & KK_F32_MANT_MASK) | KK_F32_MANT_ONE
131 let sh: i64 = expo - KK_F32_EXP_BIAS
132 var v: i64 = 0
133 if sh >= KK_F32_MANT_BITS { if sh - KK_F32_MANT_BITS > KK_SHIFT_SAFE_HI { return 0 } }
134 if sh >= KK_F32_MANT_BITS { v = mant * KK_PERMIL * (1 << (sh - KK_F32_MANT_BITS)) }
135 if sh < KK_F32_MANT_BITS { if KK_F32_MANT_BITS - sh > KK_SHIFT_SAFE_LO { return 0 } }
136 if sh < KK_F32_MANT_BITS { v = (mant * KK_PERMIL) >> (KK_F32_MANT_BITS - sh) }
137 if sign == 1 { return 0 - v }
138 return v
139}
140
141// find <key> in [start,end); parse the msgpack float32 array that follows into out (permil).
142// returns parsed count; meta[0] = declared count, or -1 key absent, -2 no array header after key.
143func kk_decode_sliders(buf: *u8, start: i64, end: i64, key: *u8, out: *i64, outcap: i64, meta: *i64) -> i64 {
144 meta[0] = 0 - 1
145 let at: i64 = kk_find(buf, start, end, key)
146 if at < 0 { return 0 }
147 var m: i64 = 0
148 while key[m] != (0 as u8) { m = m + 1 }
149 var p: i64 = at + m
150 if p >= end { meta[0] = 0 - 2; return 0 }
151 var n: i64 = 0 - 1
152 let tag: i64 = kk_b(buf, p)
153 if (tag & KK_MP_TAG_HI_MASK) == KK_MP_FIXARRAY_BASE { n = tag & KK_MP_TAG_LO_MASK; p = p + 1 }
154 if tag == KK_MP_ARRAY16 { if p + 3 <= end { n = kk_b(buf,p+1)*256 + kk_b(buf,p+2); p = p + 3 } }
155 if n < 0 { meta[0] = 0 - 2; return 0 }
156 meta[0] = n
157 var k: i64 = 0
158 while k < n {
159 if p + KK_MP_F32_SPAN > end { return k }
160 if kk_b(buf, p) != KK_MP_F32 { return k }
161 if k < outcap {
162 let w: i64 = (kk_b(buf,p+1) << 24) | (kk_b(buf,p+2) << 16) | (kk_b(buf,p+3) << 8) | kk_b(buf,p+4)
163 out[k] = kk_f32_permil(w)
164 }
165 p = p + KK_MP_F32_SPAN
166 k = k + 1
167 }
168 return k
169}