nx_moment_resolve.nx source
↩ module page · 227 lines · 10008 B
1// nx_moment_resolve.nx -- resolve an arc stage into a populated DirectorsNote.
2//
3// This is the bridge between nx_arc (the data: arcs + stages + pools)
4// and nx_directors_note (the envelope: the typed packet that flows
5// through the entire render pipeline).
6//
7// Per cardinal feedback-arc-and-moment-are-king-cinematographer-pattern:
8// the director (arc + moment_generator) authors the moment. This
9// primitive IS that authoring step on the substrate side. It picks
10// one ID from each of the stage's pools (location, outfit, pose,
11// expression=affect, camera) using deterministic prng + the stage's
12// arousal range, and produces a *DirectorsNote that downstream
13// services consume unchanged.
14//
15// === Composition ====================================================
16//
17// nx_arc.nx -- Arc + ArcStage structure (data)
18// nx_directors_note.nx -- DirectorsNote (envelope)
19// nx_prng.nx -- deterministic sampling (Park-Miller LCG)
20// nx_tier.nx -- nx_int alias
21//
22// === Three-tier API ==================================================
23//
24// nx_moment_resolve(arc, stage_idx, prev_seq, prng, dn_out)
25// -- the canonical resolver; 5 args (well under <=8 codegen ceiling).
26// Returns NX_DN_UNDERSPEC_NONE on success, else a code naming
27// which DirectorsNote field couldn't be filled.
28//
29// nx_moment_resolve_companion(arc, stage_idx, character_id,
30// domain, affect, prng, dn_out)
31// -- companion-authored variant where Elara chose the domain +
32// affect axis herself; resolver fills the rest from the arc.
33// 7 args.
34//
35// Both call _pool_pick + _pick_arousal internally.
36//
37// genealogy_id: lindenmayer_systems + propp_morphology +
38// bridson_2007_uniform_sampling
39// lineage_id: nx_moment_resolve_v1
40
41// nx_safety_envelope:
42// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
43// sil_target: SIL1
44// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
45// verdict: NOT_YET_EVALUATED
46
47import "nx_syscalls.nx"
48import "nx_tier.nx"
49import "nx_prng.nx"
50import "nx_arc.nx"
51import "nx_directors_note.nx"
52
53// ===== Internal: pick a single ID uniformly from a stage's pool ====
54//
55// Returns the picked ID, or 0 if the pool is empty.
56
57func _pool_pick(arc: *Arc, stage_idx: nx_int, pool_kind: nx_int,
58 prng_state: *i64) -> nx_int {
59 let off_buf: *i64 = (sys_mmap(8)) as *i64
60 let len_buf: *i64 = (sys_mmap(8)) as *i64
61 let rc: nx_int = nx_arc_stage_get_pool(arc, stage_idx, pool_kind, off_buf, len_buf)
62 if rc != 0 { return 0 }
63 let pool_off: nx_int = off_buf[0]
64 let pool_len: nx_int = len_buf[0]
65 if pool_len <= 0 { return 0 }
66 let pick_idx: nx_int = nx_prng_range(prng_state, pool_len)
67 return nx_arc_pool_get(arc, pool_off + pick_idx)
68}
69
70// ===== Internal: pick an arousal value in [min, max] (Q10) =========
71//
72// Uniform in [min_q10, max_q10]. If min > max, returns min (safe).
73
74func _pick_arousal(arc: *Arc, stage_idx: nx_int, prng_state: *i64) -> nx_int {
75 let min_q10: nx_int = nx_arc_stage_get(arc, stage_idx, NX_AST_F_MIN_AROUSAL_Q10)
76 let max_q10: nx_int = nx_arc_stage_get(arc, stage_idx, NX_AST_F_MAX_AROUSAL_Q10)
77 if max_q10 <= min_q10 { return min_q10 }
78 let span: nx_int = max_q10 - min_q10
79 let delta: nx_int = nx_prng_range(prng_state, span + 1)
80 return min_q10 + delta
81}
82
83// ===== Internal: map EmotionalTone (stage) -> AffectAxis (moment) ==
84//
85// Used as fallback when the stage has no expression_pool, so the
86// director's intent is still conveyed via the stage-level tone.
87
88func _tone_to_affect(tone: nx_int) -> nx_int {
89 if tone == NX_ET_NEUTRAL { return NX_AF_NEUTRAL }
90 if tone == NX_ET_TENDER { return NX_AF_VULNERABLE }
91 if tone == NX_ET_PLAYFUL { return NX_AF_PLAYFUL }
92 if tone == NX_ET_INTENSE { return NX_AF_DOMINANT }
93 if tone == NX_ET_LONGING { return NX_AF_MELANCHOLIC }
94 if tone == NX_ET_TRIUMPHANT { return NX_AF_JOYFUL }
95 if tone == NX_ET_REVERENT { return NX_AF_REVERENT }
96 if tone == NX_ET_DOMESTIC { return NX_AF_FOCUSED }
97 if tone == NX_ET_ANTICIPATORY { return NX_AF_PLAYFUL }
98 if tone == NX_ET_AFTERGLOW { return NX_AF_INTIMATE }
99 return NX_AF_NEUTRAL
100}
101
102// ===== Internal: map Awareness (stage) -> Perspective (moment) ====
103//
104// Awareness modulates the cinematography: an UNAWARE character is
105// rendered VOYEUR; AWARE goes to CANDID; EYE_CONTACT goes to POV;
106// DIRECT_ENGAGEMENT goes to POV_BOYFRIEND (intimate).
107
108func _awareness_to_perspective(aw: nx_int) -> nx_int {
109 if aw == NX_AW_UNAWARE { return NX_PERSP_VOYEUR }
110 if aw == NX_AW_PEEKING { return NX_PERSP_CANDID }
111 if aw == NX_AW_AWARE { return NX_PERSP_CANDID }
112 if aw == NX_AW_EYE_CONTACT { return NX_PERSP_POV }
113 if aw == NX_AW_DIRECT_ENGAGEMENT { return NX_PERSP_POV_BOYFRIEND }
114 return NX_PERSP_THIRD
115}
116
117// ===== Public: resolve a moment from arc + stage ===================
118//
119// Canonical resolver. Fills dn_out with picks from the stage's pools
120// and the stage's typed fields. Returns the underspec code from
121// nx_dn_validate_for_render so the caller knows if anything's missing.
122
123func nx_moment_resolve(arc: *Arc, stage_idx: nx_int,
124 prev_seq: nx_int,
125 prng_state: *i64,
126 dn_out: *DirectorsNote) -> nx_int {
127 // Identity
128 dn_out.arc_id = arc.arc_id
129 dn_out.arc_stage = nx_arc_stage_get(arc, stage_idx, NX_AST_F_STAGE_NUMBER)
130 dn_out.moment_seq = prev_seq + 1
131 dn_out.prev_moment_seq = prev_seq
132
133 // Provenance: arc-driven external-system director
134 dn_out.director_role = NX_DR_EXTERNAL_SYSTEM
135 dn_out.source_tag = NX_ST_STAGE_POOL
136 dn_out.content_domain = arc.default_domain
137
138 // Scene picks from pools
139 let loc_id: nx_int = _pool_pick(arc, stage_idx, NX_ARC_POOL_LOCATION, prng_state)
140 let outfit_id: nx_int = _pool_pick(arc, stage_idx, NX_ARC_POOL_OUTFIT, prng_state)
141 let pose_id: nx_int = _pool_pick(arc, stage_idx, NX_ARC_POOL_POSE, prng_state)
142 let cam_id: nx_int = _pool_pick(arc, stage_idx, NX_ARC_POOL_CAMERA, prng_state)
143
144 dn_out.location_id = loc_id
145 dn_out.outfit_key_id = outfit_id
146 dn_out.pose_id = pose_id
147 dn_out.camera_motivation_id = cam_id
148
149 // Affect: expression_pool picks are AffectAxis values. Special-
150 // case: NX_AF_NEUTRAL is the enum's zero, indistinguishable from
151 // "pool was empty / unset," so we must check pool LEN directly
152 // rather than test the picked value. If pool present, pick;
153 // else derive from stage emotional_tone.
154 let tone: nx_int = nx_arc_stage_get(arc, stage_idx, NX_AST_F_EMOTIONAL_TONE)
155 var affect: nx_int = _tone_to_affect(tone)
156 let expr_off_buf: *i64 = (sys_mmap(8)) as *i64
157 let expr_len_buf: *i64 = (sys_mmap(8)) as *i64
158 nx_arc_stage_get_pool(arc, stage_idx, NX_ARC_POOL_EXPRESSION,
159 expr_off_buf, expr_len_buf)
160 if expr_len_buf[0] > 0 {
161 let expr_pick: nx_int = _pool_pick(arc, stage_idx, NX_ARC_POOL_EXPRESSION, prng_state)
162 if nx_af_axis_is_valid(expr_pick) == 1 { affect = expr_pick }
163 }
164 dn_out.affect_axis = affect
165
166 // Arousal: uniform in stage's [min, max] Q10 range
167 dn_out.arousal_q10 = _pick_arousal(arc, stage_idx, prng_state)
168
169 // Camera + cinematography from stage awareness
170 let aw: nx_int = nx_arc_stage_get(arc, stage_idx, NX_AST_F_AWARENESS)
171 dn_out.perspective = _awareness_to_perspective(aw)
172 // shot_size defaults to MS unless explicitly set; caller can override
173 dn_out.shot_size = NX_SHOT_MS
174
175 // Activity defaults: not yet typed at arc-stage level in v1.
176 // For full validation we set a sentinel non-zero so the underspec
177 // code reports a meaningful field if caller really needs activity.
178 // (v2: add activity_pool to ArcStage.)
179 if dn_out.activity_id == 0 { dn_out.activity_id = pose_id }
180
181 // Character: caller is responsible for setting before resolve.
182 // If unset, validate_for_render reports it.
183
184 // Body archetype: caller-set; if unset, validate_for_render reports.
185
186 // Quality starts UNVERIFIED
187 dn_out.award_class = NX_DN_AWARD_UNVERIFIED
188 dn_out.preflight_tier = NX_PFT_GOOD
189 dn_out.refine_iteration = 0
190 dn_out.underspec_code = NX_DN_UNDERSPEC_NONE
191
192 let usc: nx_int = nx_dn_validate_for_render(dn_out)
193 dn_out.underspec_code = usc
194 return usc
195}
196
197// ===== Companion-authored resolve ==================================
198//
199// When the companion (e.g., Elara) authors a moment from her own
200// ontological decision (per the cardinal: "external director or one
201// of the companions, elara in this case making ontological or whatever
202// type decisions"), this variant:
203// - sets director_role = COMPANION + source_tag = COMPANION_DECISION
204// - takes the companion's chosen content_domain + affect_axis
205// - still draws location/outfit/pose/camera from the arc's pools
206//
207// 7 args. Under the <=8 ceiling.
208
209func nx_moment_resolve_companion(arc: *Arc, stage_idx: nx_int,
210 character_id: nx_int,
211 domain: nx_int, affect: nx_int,
212 prng_state: *i64,
213 dn_out: *DirectorsNote) -> nx_int {
214 // Same base path as canonical resolve, then override authority
215 let rc: nx_int = nx_moment_resolve(arc, stage_idx,
216 dn_out.prev_moment_seq,
217 prng_state, dn_out)
218 dn_out.director_role = NX_DR_COMPANION
219 dn_out.source_tag = NX_ST_COMPANION_DECISION
220 dn_out.character_id = character_id
221 dn_out.content_domain = domain
222 dn_out.affect_axis = affect
223 // Re-validate after companion overrides
224 let usc: nx_int = nx_dn_validate_for_render(dn_out)
225 dn_out.underspec_code = usc
226 return usc
227}