nx_softbind.nx source
↩ module page · 285 lines · 18187 B
1// nx_softbind.nx -- LIB: BINDS nx_softdyn to character geometry (roadmap rung A4, 2026-08-01).
2// The solver was proven 7/7 in isolation but bound to NOTHING -- no body part consumed it, so
3// nothing in the game visibly jiggled. This lib is the missing joint-anchor contract: each mob
4// owns SB_PTS spring points (hair / chest / skirt class), each seated at a fixed local offset
5// from the mob ROOT, stepped once per sim tick from root motion ONLY (organ truth -- the same
6// source as the mobdx/mobdz/mobdy doors), yielding signed Q8 offsets a renderer ADDS to its
7// designated joints on top of the animated pose (spring-decomposed-skinning pattern: helper
8// offsets over the animation, never replacing it). Pure function of the root trajectory =>
9// bit-deterministic, so replay / netsync / save-transparency hold by construction.
10// license_tier: ORIGINAL No hw writes (Rule 26).
11import "nx_syscalls.nx"
12import "nx_softdyn.nx"
13const SB_MAGIC_5381: i64 = 5381
14const SB_MAGIC_1000000007: i64 = 1000000007
15
16const SB_PTS: i64 = 6
17const SB_HAIR: i64 = 0
18const SB_CHEST: i64 = 1
19const SB_SKIRT: i64 = 2
20// R7 regions (2026-08-26): the asset now DECLARES these bones (nx_nxa_dyna bones=6); the engine
21// ticks them on the same solver. fn: belly/glutes at the tissue band's LOW edge (softer mass),
22// thigh at midband -- all from the pinned conf rows, never tasted.
23const SB_BELLY: i64 = 3
24const SB_GLUTE: i64 = 4
25const SB_THIGH: i64 = 5
26// per-kind spring params (per-1024). ALL underdamped -- discrete envelope: underdamped while
27// C < 64*sqrt(K) (zeta = C / (2*sqrt(1024*K))), so every row below VISIBLY oscillates:
28// hair K=200 -> bound ~905, C=60
29// skirt K=300 -> bound ~1108, C=80
30// CHEST IS NOT A ROW HERE. It is derived from the oracle bands below, because it is the only one
31// of the three with published human free-vibration, damping, asymmetry and anisotropy to derive
32// FROM. hair and skirt keep their cited rows (hair_k_q10 120..420) until the same exists for them.
33// ===== HAIR: THE SAME TWO INTEGERS, NOW SAYING WHAT THEY MEAN (2026-08-25) =====
34// SB_K_HAIR and SB_C_HAIR were hand-picked: two per-1024 integers sitting inside hair_k_q10's
35// banked band by inspection, never derived from any frequency, and driving a SINGLE point. They
36// are UNCHANGED here -- 200 and 60, byte-for-byte the same plant -- because there is no citable
37// hair band to move them toward: knowledge/gamefeel_oracle.conf's hair_k_q10 source slug
38// "secondary-motion-hair-2to5hz" resolves to NOTHING (proven with complete coverage; the
39// correction record sits beside that row), and swapping one uncited number for another would be
40// the same defect wearing a newer date.
41// WHAT CHANGES IS THAT THE PLANT CAN NOW BE READ. The two rows below express the SAME plant in
42// physical units -- a natural frequency and a damping ratio -- so that a future citation has
43// something to bind to, and so a reader can see what the game actually does instead of two opaque
44// integers. They are EXACT, not approximate: nx_hairdyn_gate asserts sb_k_sym(SB_HAIR_FN_MHZ) ==
45// SB_K_HAIR and sb_c_for_zeta(SB_K_HAIR, SB_HAIR_ZETA_PERMIL) == SB_C_HAIR, so if anyone edits
46// either side the gate goes RED instead of the two drifting apart in silence.
47// MEASURED CONSEQUENCE, and note the ratio below is the REQUESTED one, not the DELIVERED one.
48// nx_hairdyn_gate ran the ringdown on this exact plant and read back fn 4285 mhz, zeta 22 permil
49// -- a THIRD of the 67 the coefficient implies. That is not an error in either number: sd_step's
50// damping term is (v*C)/SD_G and it TRUNCATES, so at C=60 no damping is applied at all until |v|
51// reaches 17 q8 per tick, and the shortfall grows as the swing decays into that dead zone. In this
52// solver a requested zeta is an UPPER BOUND on the delivered one, never an equality.
53// So shipped hair has a quality factor of about 23 and rings for tens of periods -- far longer
54// than the chest, whose MEASURED ratio is 480 permil. Whether that is right for hair is exactly
55// the question no citation in this estate can currently answer.
56const SB_HAIR_FN_MHZ: i64 = 4221 // == sb_k_sym -> SB_K_HAIR exactly (gate-pinned)
57const SB_HAIR_ZETA_PERMIL: i64 = 67 // == sb_c_for_zeta -> SB_C_HAIR exactly (gate-pinned)
58const SB_K_HAIR: i64 = 200
59const SB_C_HAIR: i64 = 60
60const SB_K_SKIRT: i64 = 300
61const SB_C_SKIRT: i64 = 80
62
63// ===== CHEST TISSUE: DERIVED, NEVER TUNED ==================================================
64// The four hand-picked rows that used to live here (K 60/85/110/135, C 100..148) were chosen to
65// hit a self-declared "2.3-3.4Hz, zeta 0.17-0.20" band. MEASUREMENT retired them on two counts,
66// and both are recorded because the second one is the defect nobody could see:
67// 1. OUT OF BAND. knowledge/gamefeel_oracle.conf tissue_fn_mhz is 4000..5200 (free-vibration
68// modes) and tissue_zeta_permil is 350..600 (haake-scurr-2010, zeta 0.475 +- 0.062). The
69// tuned plant sat BELOW both -- roughly half the cited damping.
70// 2. TUNED INTO ITS OWN DRIVE. tissue_drive_mhz records our own beach corpus at 2000..3100
71// mhz (n=5). The tuned plant's 2310..3470 mhz sits INSIDE that, so the tissue was driven AT
72// RESONANCE with quality factor Q = 1/(2*zeta) ~ 2.5. A plant tuned onto its own drive
73// cannot read as tissue: it wobbles at walking frequency with amplified, slow-decaying
74// gain instead of taking one footfall thump and dying. Real tissue at 4000..5200 mhz is
75// driven well BELOW resonance, which is what makes it look like flesh and not like jelly.
76// AUTHORITY FOR THE NUMBERS BELOW. The conf forbids comparing a solver tunable to a subject row
77// until the ringdown probe reports ("UNTIL THAT PROBE REPORTS, NO TUNABLE MAY BE COMPARED TO A
78// SUBJECT ROW"). nx_dyna_bind_skin ringdown was built and run for the first time on 2026-08-25
79// and our XPBD plant measured fn=4137 mhz zeta=480 permil at the linear limit -- INSIDE both
80// human bands. So the reduced-order spring below is identified FROM a reference solver that is
81// itself validated against the literature, which is the whole reason it may carry these numbers.
82const SB_FN_LO_MHZ: i64 = 4000 // conf tissue_fn_mhz lo
83const SB_FN_HI_MHZ: i64 = 5200 // conf tissue_fn_mhz hi
84const SB_ZETA_PERMIL: i64 = 480 // MEASURED: our XPBD plant at the linear limit
85const SB_KUP_REF: i64 = 74 // conf tissue_k_up_n_per_m midband (cai2018 k_a 73.5)
86const SB_KDN_REF: i64 = 660 // conf tissue_k_down_n_per_m midband (cai2018 k_b 658)
87const SB_ANISO_AP_PERMIL: i64 = 610 // conf tissue_aniso_ap_permil (mills2025 AP/SI)
88const SB_ANISO_ML_PERMIL: i64 = 600 // conf tissue_aniso_ml_permil (mills2025 ML/SI)
89const SB_FIRM_MAX: i64 = 3 // genome bits 16-17 span four bred firmness levels
90// 1024*4*pi^2 and 1024*pi^2 -- the two pure constants that turn a frequency into a per-1024
91// stiffness, and (1000 mhz-per-hz * 60 ticks-per-second)^2 which carries the units.
92const SB_K4PI2_Q10: i64 = 40426
93const SB_KPI2_Q10: i64 = 10106
94const SB_MHZ_TICK_SQ: i64 = 3600000000
95
96// firmness 0..3 spans the cited free-vibration band. Firmness is BRED -- genome bits 16-17,
97// field-crossed at birth like every other trait: a population of bodies, not a preset list.
98func sb_fn_mhz(firm: i64) -> i64 {
99 return SB_FN_LO_MHZ + firm*(SB_FN_HI_MHZ - SB_FN_LO_MHZ)/SB_FIRM_MAX
100}
101// symmetric-equivalent stiffness for a target natural frequency: K = 1024*(2*pi*f/rate)^2
102func sb_k_sym(fn_mhz: i64) -> i64 { return SB_K4PI2_Q10*fn_mhz*fn_mhz/SB_MHZ_TICK_SQ }
103// A piecewise oscillator's free-vibration PERIOD is the sum of its two half-periods, so the soft
104// branch is scaled by (1 + 1/sqrt(R)) to put the MEASURED frequency on target. Using the mean of
105// the two stiffnesses instead would miss the frequency the probe actually reads.
106func sb_branch_permil() -> i64 {
107 let r100: i64 = sd_isqrt(SB_KDN_REF*10000/SB_KUP_REF)
108 if r100 < 1 { return 1000 }
109 return 1000 + 100000/r100
110}
111func sb_k_up(fn_mhz: i64) -> i64 {
112 let p: i64 = sb_branch_permil()
113 var v: i64 = SB_KPI2_Q10*fn_mhz*fn_mhz
114 v = v*p/1000
115 v = v*p/1000
116 return v/SB_MHZ_TICK_SQ
117}
118func sb_k_dn(fn_mhz: i64) -> i64 { return sb_k_up(fn_mhz)*SB_KDN_REF/SB_KUP_REF }
119// horizontal axes are sign-symmetric (cai2018's asymmetry is about the VERTICAL static
120// equilibrium) but they are NOT as compliant as the vertical: amplitude ratio = compliance
121// ratio, so a smaller cited excursion means a proportionally STIFFER axis.
122func sb_k_lat(fn_mhz: i64) -> i64 { return sb_k_sym(fn_mhz)*1000/SB_ANISO_ML_PERMIL }
123func sb_k_ant(fn_mhz: i64) -> i64 { return sb_k_sym(fn_mhz)*1000/SB_ANISO_AP_PERMIL }
124// damping coefficient for the target ratio: zeta = C/(2*sqrt(1024*K)) => C = zeta*2*sqrt(1024*K)
125// ZETA-GENERAL FORM. The chest path fixes zeta at the MEASURED tissue value, but hair (nx_hairdyn)
126// needs the SAME arithmetic at a different ratio, and a second copy of it in the hair lane would be
127// exactly the duplicate-ruler defect this file already retired once for the chest. sb_c_for is this
128// function at SB_ZETA_PERMIL, so every existing chest caller is arithmetically unchanged BY
129// CONSTRUCTION -- there is one damping ruler in the estate and both consumers read it.
130func sb_c_for_zeta(K: i64, zeta_permil: i64) -> i64 { return zeta_permil*2*sd_isqrt(SD_G*K)/1000 }
131func sb_c_for(K: i64) -> i64 { return sb_c_for_zeta(K, SB_ZETA_PERMIL) }
132
133// ===== EVERY GIRL HER OWN BODY =============================================================
134// The cast used to tick on TWO GENOME BITS -- four firmness levels shared across sixteen girls,
135// so four bodies moved and the rest were copies of them. The estate already had the cure and it
136// was simply out of reach: nx_softtissue's st_profile_seeded draws a whole body from a seed on a
137// single FULLNESS axis, and states why it must be a single axis -- fields drawn independently
138// produce implausible combinations (a tiny body with maximal fat compliance is not a person).
139// The hash below mirrors st_hash EXACTLY -- same constants, same mixing, SAME SALT 101 -- so a
140// girl's fullness here and her tissue profile there are ONE DRAW, not two families that happen
141// to resemble each other. Fullness moves size and compliance together, so a fuller body sits
142// LOWER in the cited free-vibration band (more mass on a more compliant suspension) and a
143// smaller, firmer one sits higher. That is 1001 distinct plants where there were 4.
144const SB_MAGIC_2654435761: i64 = 2654435761
145const SB_MAGIC_1013904223: i64 = 1013904223
146const SB_MAGIC_1442695040888963407: i64 = 1442695040888963407
147const SB_MAGIC_2246822519: i64 = 2246822519
148const SB_MAGIC_288230376151711743: i64 = 288230376151711743
149const SB_FULL_N: i64 = 1001 // fullness axis 0..1000, the axis st_profile_seeded uses
150const SB_FULL_SALT: i64 = 101 // st_profile_seeded's own fullness salt -- do not diverge
151
152func sb_hash(seed: i64, salt: i64) -> i64 {
153 var h: i64 = seed * SB_MAGIC_2654435761 + salt * SB_MAGIC_1013904223 + SB_MAGIC_1442695040888963407
154 h = h ^ (h >> 15)
155 h = h * SB_MAGIC_2246822519
156 h = h ^ (h >> 13)
157 h = h & SB_MAGIC_288230376151711743
158 if h < 0 { h = 0 - h }
159 return h
160}
161func sb_full_from_gene(gene: i64) -> i64 { return sb_hash(gene, SB_FULL_SALT) % SB_FULL_N }
162func sb_fn_mhz_gene(gene: i64) -> i64 {
163 return SB_FN_HI_MHZ - sb_full_from_gene(gene)*(SB_FN_HI_MHZ - SB_FN_LO_MHZ)/(SB_FULL_N - 1)
164}
165// hard displacement clamps (model units) -- tissue can never leave the body
166const SB_MAXD_HAIR: i64 = 6
167const SB_MAXD_CHEST: i64 = 2 // +-3.1cm: tissue-vs-bone p2p <= 6.2cm, the cited band
168const SB_MAXD_SKIRT: i64 = 5
169// local anchor heights above the mob root (model units): hair tip / chest / skirt hem class
170const SB_OFFY_HAIR: i64 = 17
171const SB_OFFY_CHEST: i64 = 12
172const SB_OFFY_SKIRT: i64 = 7
173
174// chest rows are DERIVED at the softest bred firmness so there is exactly one chest ruler: these
175// accessors and the per-body path below cannot disagree, because both read the same derivation.
176func sb_k(kind: i64) -> i64 { if kind == SB_HAIR { return SB_K_HAIR } if kind == SB_CHEST { return sb_k_up(sb_fn_mhz(0)) } return SB_K_SKIRT }
177func sb_c(kind: i64) -> i64 { if kind == SB_HAIR { return SB_C_HAIR } if kind == SB_CHEST { return sb_c_for(sb_k_up(sb_fn_mhz(0))) } return SB_C_SKIRT }
178func sb_maxd(kind: i64) -> i64 { if kind == SB_HAIR { return SB_MAXD_HAIR } if kind == SB_CHEST { return SB_MAXD_CHEST } return SB_MAXD_SKIRT }
179// R7: belly/glute/thigh reuse the hip-height offset (they are hip-adjacent); a mesh-derived
180// per-region offset is the refinement, and reusing a named anchor beats inventing one.
181func sb_offy(kind: i64) -> i64 { if kind == SB_HAIR { return SB_OFFY_HAIR } if kind == SB_CHEST { return SB_OFFY_CHEST } return SB_OFFY_SKIRT }
182
183// state for n mobs (n*SB_PTS softdyn points, softdyn owns the layout)
184func sb_alloc(nmobs: i64) -> *i64 { return sd_alloc(nmobs*SB_PTS) }
185
186// seat every point of one mob exactly on its anchor (no startup transient)
187func sb_seat_mob(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64) -> i64 {
188 var k: i64 = 0
189 while k < SB_PTS { sd_seat(st, mobi*SB_PTS + k, rx, ry + sb_offy(k), rz); k = k + 1 }
190 return 0
191}
192
193// one sim tick for one mob against its CURRENT root position (organ truth)
194// The un-bred tick is the bred one at the softest firmness -- delegated, not duplicated, so a
195// caller that does not carry a genome still gets the asymmetric chest rather than a second model.
196func sb_tick_mob(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64) -> i64 {
197 return sb_tick_mob_firm(st, mobi, rx, ry, rz, 0)
198}
199// single point with explicit params (the per-body range's primitive)
200func sb_tick_pt(st: *i64, mobi: i64, kind: i64, rx: i64, ry: i64, rz: i64, K: i64, C: i64, maxd: i64) -> i64 {
201 sd_step(st, mobi*SB_PTS + kind, rx, ry + sb_offy(kind), rz, K, C, maxd)
202 return 0
203}
204// one tick with a BRED body: firmness 0..3 selects the chest band; hair/skirt ride the defaults
205func sb_tick_mob_firm(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64, firm: i64) -> i64 {
206 var f: i64 = firm
207 if f < 0 { f = 0 }
208 if f > SB_FIRM_MAX { f = SB_FIRM_MAX }
209 return sb_tick_fn(st, mobi, rx, ry, rz, sb_fn_mhz(f))
210}
211// one tick with HER OWN body: the whole genome picks her plant, not two bits of it.
212func sb_tick_mob_gene(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64, gene: i64) -> i64 {
213 return sb_tick_fn(st, mobi, rx, ry, rz, sb_fn_mhz_gene(gene))
214}
215// THE one chest path. Both callers above land here, so a bred body and a genome body can never
216// be two different models -- there is only one, taking a natural frequency.
217func sb_tick_fn(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64, fn_mhz: i64) -> i64 {
218 sb_tick_pt(st, mobi, SB_HAIR, rx, ry, rz, SB_K_HAIR, SB_C_HAIR, SB_MAXD_HAIR)
219 // chest rides the anisotropic + sign-asymmetric integrator: vertical carries the cited
220 // 9x up/down split, the two horizontal axes carry the cited AP/ML compliance ratios.
221 let kup: i64 = sb_k_up(fn_mhz)
222 let kdn: i64 = sb_k_dn(fn_mhz)
223 let klat: i64 = sb_k_lat(fn_mhz)
224 let kant: i64 = sb_k_ant(fn_mhz)
225 sd_step_ax(st, mobi*SB_PTS + SB_CHEST, rx, ry + sb_offy(SB_CHEST), rz,
226 klat, kup, kdn, kant,
227 sb_c_for(klat), sb_c_for(kup), sb_c_for(kdn), sb_c_for(kant), SB_MAXD_CHEST)
228 sb_tick_pt(st, mobi, SB_SKIRT, rx, ry, rz, SB_K_SKIRT, SB_C_SKIRT, SB_MAXD_SKIRT)
229 // R7: region points. dyna_bind_skin -- the binding the charsim/graphics watch declares:
230 // asset-declared tissue bones driven by the one solver, consumed by the renderer's fields.
231 dyna_bind_skin(st, mobi, rx, ry, rz)
232 return 0
233}
234
235// dyna_bind_skin: tick the asset-declared soft-tissue regions (belly, glutes, thigh) on the one
236// solver. Named for the R7/GR13 watch contract: this IS the binding the boards declare.
237// K from the pinned tissue band via the same frequency->stiffness ruler as the chest.
238func dyna_bind_skin(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64) -> i64 {
239 let klo: i64 = sb_k_sym(SB_FN_LO_MHZ)
240 let kmid: i64 = sb_k_sym((SB_FN_LO_MHZ + SB_FN_HI_MHZ)/2)
241 sb_tick_pt(st, mobi, SB_BELLY, rx, ry, rz, klo, sb_c_for(klo), SB_MAXD_CHEST)
242 sb_tick_pt(st, mobi, SB_GLUTE, rx, ry, rz, klo, sb_c_for(klo), SB_MAXD_CHEST)
243 sb_tick_pt(st, mobi, SB_THIGH, rx, ry, rz, kmid, sb_c_for(kmid), SB_MAXD_CHEST)
244 return 0
245}
246
247// a hit / footfall / landing kicks every point of the mob (recoil inheritance, same solver as firearms)
248func sb_impulse_mob(st: *i64, mobi: i64, ix: i64, iy: i64, iz: i64) -> i64 {
249 var k: i64 = 0
250 while k < SB_PTS { sd_impulse(st, mobi*SB_PTS + k, ix, iy, iz); k = k + 1 }
251 return 0
252}
253
254// ---- renderer-facing accessors: signed Q8 offsets FROM the anchor, per axis --------------------
255// The consumer adds these to its designated joint translations. Q8 sub-unit resolution is
256// deliberate: jiggle amplitude is often a fraction of a model unit (softdyn gate T2 lesson).
257func sb_off_x_q8(st: *i64, mobi: i64, kind: i64, rx: i64) -> i64 {
258 return st[(mobi*SB_PTS + kind)*SD_STRIDE] - rx*SD_Q8
259}
260func sb_off_y_q8(st: *i64, mobi: i64, kind: i64, ry: i64) -> i64 {
261 return st[(mobi*SB_PTS + kind)*SD_STRIDE + 1] - (ry + sb_offy(kind))*SD_Q8
262}
263func sb_off_z_q8(st: *i64, mobi: i64, kind: i64, rz: i64) -> i64 {
264 return st[(mobi*SB_PTS + kind)*SD_STRIDE + 2] - rz*SD_Q8
265}
266// magnitude of displacement from anchor in Q8 (clamp verification)
267func sb_disp_q8(st: *i64, mobi: i64, kind: i64, rx: i64, ry: i64, rz: i64) -> i64 {
268 return sd_disp_q8(st, mobi*SB_PTS + kind, rx, ry + sb_offy(kind), rz)
269}
270// kinetic energy proxy of one point (decay proof)
271func sb_energy(st: *i64, mobi: i64, kind: i64) -> i64 { return sd_energy(st, mobi*SB_PTS + kind) }
272
273// deterministic rolling checksum over all state words of n mobs (replay / netsync proof)
274func sb_ck(st: *i64, nmobs: i64) -> i64 {
275 var h: i64 = SB_MAGIC_5381
276 var i: i64 = 0
277 let n: i64 = nmobs*SB_PTS*SD_STRIDE
278 while i < n {
279 var w: i64 = st[i]
280 if w < 0 { w = (0 - w)*2 + 1 } else { w = w*2 }
281 h = (h*33 + w) % SB_MAGIC_1000000007
282 i = i + 1
283 }
284 return h
285}