nmarshall commited on
Commit
0d8ecb5
·
1 Parent(s): ec2de4d

gameplay: resolve defuse score and counter feedback

Browse files
crates/urt-gameplay/src/bin/urt-effect-feedback.rs CHANGED
@@ -27,6 +27,7 @@ fn main() {
27
  );
28
  }
29
  "radio" => println!("{}", radio(&w)),
 
30
  _ => panic!("unknown contract"),
31
  }
32
  }
@@ -81,3 +82,187 @@ fn radio(w: &[u32]) -> String {
81
  list(&m)
82
  )
83
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  );
28
  }
29
  "radio" => println!("{}", radio(&w)),
30
+ "finish" => println!("{}", fragment(&w, DefusePhase::Finish)),
31
  _ => panic!("unknown contract"),
32
  }
33
  }
 
82
  list(&m)
83
  )
84
  }
85
+ fn decode_defuse(
86
+ w: &[u32],
87
+ ) -> (
88
+ urt_gameplay::defuse::DefuseState,
89
+ urt_gameplay::defuse::DefuseInput,
90
+ ) {
91
+ use urt_gameplay::defuse::*;
92
+ (
93
+ DefuseState {
94
+ flags: w[0],
95
+ flags104: w[1],
96
+ busy: w[2],
97
+ clock: w[3],
98
+ complete: w[4],
99
+ animation: w[5],
100
+ weapon: w[6],
101
+ event: [7, 0x80000001],
102
+ },
103
+ DefuseInput {
104
+ tick: w[7],
105
+ duration: w[8],
106
+ index: w[9],
107
+ slot: 2,
108
+ },
109
+ )
110
+ }
111
+ fn feedback(marker: bool) -> FeedbackState {
112
+ let mut counters = (100..115).collect::<Vec<_>>();
113
+ counters[5] = u32::MAX;
114
+ FeedbackState {
115
+ score: urt_gameplay::score::State {
116
+ player: -1,
117
+ teams: [100, 101, 102, 103],
118
+ },
119
+ counters,
120
+ marker: if marker {
121
+ Some(RadioMarker {
122
+ deadline: 17,
123
+ active: 18,
124
+ })
125
+ } else {
126
+ None
127
+ },
128
+ event_record: None,
129
+ }
130
+ }
131
+ fn effect_events(v: &[ResolvedEffect]) -> String {
132
+ v.iter()
133
+ .map(|e| match e {
134
+ ResolvedEffect::Log { format, slot } => {
135
+ format!(r#"{{"op":"log","format":"{}","slot":{slot}}}"#, hex(format))
136
+ }
137
+ ResolvedEffect::Allocate { event } => format!(r#"{{"op":"allocate","event":{event}}}"#),
138
+ ResolvedEffect::Link => r#"{"op":"link"}"#.into(),
139
+ ResolvedEffect::Score { delta } => format!(r#"{{"op":"score","delta":{delta}}}"#),
140
+ ResolvedEffect::Rank => r#"{"op":"rank"}"#.into(),
141
+ ResolvedEffect::Counter { index } => format!(r#"{{"op":"counter","index":{index}}}"#),
142
+ ResolvedEffect::Radio { code } => format!(r#"{{"op":"radio","code":{code}}}"#),
143
+ })
144
+ .collect::<Vec<_>>()
145
+ .join(",")
146
+ }
147
+ fn event_record(record: &Option<urt_gameplay::transition::EventRecord>) -> String {
148
+ let Some(e) = record else {
149
+ return "null".into();
150
+ };
151
+ let keys = [
152
+ 4, 12, 16, 20, 24, 28, 32, 36, 40, 44, 84, 140, 400, 424, 488, 492, 496, 536, 540, 568, 572,
153
+ ];
154
+ let values = [
155
+ e.kind,
156
+ e.trajectory[0],
157
+ e.trajectory[1],
158
+ e.trajectory[2],
159
+ e.origin[0],
160
+ e.origin[1],
161
+ e.origin[2],
162
+ e.velocity[0],
163
+ e.velocity[1],
164
+ e.velocity[2],
165
+ e.caller_time,
166
+ e.owner,
167
+ e.field400,
168
+ e.flags,
169
+ e.current_origin[0],
170
+ e.current_origin[1],
171
+ e.current_origin[2],
172
+ e.class_token,
173
+ e.class_hash,
174
+ e.event_time as u32,
175
+ e.free_after_event,
176
+ ];
177
+ format!(
178
+ "{{{}}}",
179
+ keys.iter()
180
+ .zip(values)
181
+ .map(|(k, v)| format!("\"{k}\":{v}"))
182
+ .collect::<Vec<_>>()
183
+ .join(",")
184
+ )
185
+ }
186
+ fn feedback_fields(
187
+ s: urt_gameplay::defuse::DefuseState,
188
+ f: &FeedbackState,
189
+ o: &ResolvedOutput,
190
+ ) -> String {
191
+ let marker = f.marker.map(|v| [v.deadline, v.active]).unwrap_or([17, 18]);
192
+ let radio = o
193
+ .radio
194
+ .iter()
195
+ .map(|r| {
196
+ format!(
197
+ r#"{{"commands":[{}],"getter_calls":{},"marker":{}}}"#,
198
+ commands(&r.commands),
199
+ r.getter_calls,
200
+ list(&marker)
201
+ )
202
+ })
203
+ .collect::<Vec<_>>()
204
+ .join(",");
205
+ format!(
206
+ r#""flags":{},"flags104":{},"busy":{},"clock":{},"complete":{},"animation":{},"weapon":{},"event":{},"guards":[2864434397,287454020],"events":[{}],"score":{},"teams":{},"counters":{},"radio":[{}],"marker":{},"event_record":{}"#,
207
+ s.flags,
208
+ s.flags104,
209
+ s.busy,
210
+ s.clock,
211
+ s.complete,
212
+ s.animation,
213
+ s.weapon,
214
+ list(&s.event),
215
+ effect_events(&o.effects),
216
+ f.score.player as u32,
217
+ list(&f.score.teams.map(|v| v as u32)),
218
+ list(&f.counters),
219
+ radio,
220
+ list(&marker),
221
+ event_record(&f.event_record)
222
+ )
223
+ }
224
+ fn fragment(w: &[u32], phase: DefusePhase) -> String {
225
+ assert!(w.len() >= 18);
226
+ let n = w[17] as usize;
227
+ assert_eq!(w.len(), 18 + n);
228
+ let (mut s, i) = decode_defuse(w);
229
+ let mut f = feedback(w[12] != 0);
230
+ let c = EffectContext {
231
+ paused: w[10] != 0,
232
+ constructed: w[11] != 0,
233
+ tick: w[7],
234
+ origin: w[14..17].try_into().unwrap(),
235
+ radio: RadioInput {
236
+ follow: 0,
237
+ health: 1,
238
+ team: 1,
239
+ slot: 2,
240
+ mode: 8,
241
+ count: 4,
242
+ tick: w[7],
243
+ code: 7,
244
+ success: w[13] != 0,
245
+ payload: w[18..].iter().map(|v| *v as u8).collect(),
246
+ },
247
+ clients: vec![
248
+ RadioClient {
249
+ connection: 2,
250
+ team: 1,
251
+ },
252
+ RadioClient {
253
+ connection: 2,
254
+ team: 2,
255
+ },
256
+ RadioClient {
257
+ connection: 2,
258
+ team: 1,
259
+ },
260
+ RadioClient {
261
+ connection: 1,
262
+ team: 2,
263
+ },
264
+ ],
265
+ };
266
+ let o = resolve_defuse_fragment(&mut s, &mut f, i, &c, phase).unwrap();
267
+ format!("{{{}}}", feedback_fields(s, &f, &o))
268
+ }
crates/urt-gameplay/src/effect_feedback.rs CHANGED
@@ -181,3 +181,235 @@ mod radio_tests {
181
  assert_eq!(m, old);
182
  }
183
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  assert_eq!(m, old);
182
  }
183
  }
184
+
185
+ #[derive(Clone, Debug, PartialEq, Eq)]
186
+ pub struct FeedbackState {
187
+ pub score: crate::score::State,
188
+ pub counters: Vec<u32>,
189
+ pub marker: Option<RadioMarker>,
190
+ pub event_record: Option<crate::transition::EventRecord>,
191
+ }
192
+ #[derive(Clone, Debug, PartialEq, Eq)]
193
+ pub struct EffectContext {
194
+ pub paused: bool,
195
+ pub constructed: bool,
196
+ pub tick: u32,
197
+ pub origin: [u32; 3],
198
+ pub radio: RadioInput,
199
+ pub clients: Vec<RadioClient>,
200
+ }
201
+ #[derive(Clone, Debug, PartialEq, Eq)]
202
+ pub enum ResolvedEffect {
203
+ Log { format: Vec<u8>, slot: i32 },
204
+ Allocate { event: u32 },
205
+ Link,
206
+ Score { delta: i32 },
207
+ Rank,
208
+ Counter { index: usize },
209
+ Radio { code: u32 },
210
+ }
211
+ #[derive(Clone, Debug, Default, PartialEq, Eq)]
212
+ pub struct ResolvedOutput {
213
+ pub effects: Vec<ResolvedEffect>,
214
+ pub radio: Vec<RadioOutcome>,
215
+ }
216
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
217
+ pub enum DefusePhase {
218
+ Begin,
219
+ Finish,
220
+ }
221
+ fn allocated_event() -> crate::transition::EventRecord {
222
+ let v = 0x13579bdf;
223
+ crate::transition::EventRecord {
224
+ kind: v,
225
+ trajectory: [v; 3],
226
+ origin: [v; 3],
227
+ velocity: [v; 3],
228
+ caller_time: v,
229
+ owner: v,
230
+ field400: v,
231
+ flags: 0x80000001,
232
+ current_origin: [v; 3],
233
+ class_token: v,
234
+ class_hash: v,
235
+ event_time: v as i32,
236
+ free_after_event: v,
237
+ }
238
+ }
239
+ fn resolve_effects(
240
+ state: &crate::defuse::DefuseState,
241
+ feedback: &mut FeedbackState,
242
+ effects: Vec<crate::defuse::DefuseEffect>,
243
+ context: &EffectContext,
244
+ ) -> Result<ResolvedOutput, Error> {
245
+ use crate::defuse::DefuseEffect;
246
+ let mut out = ResolvedOutput::default();
247
+ for effect in effects {
248
+ match effect {
249
+ DefuseEffect::Log { format, slot } => {
250
+ out.effects.push(ResolvedEffect::Log { format, slot })
251
+ }
252
+ DefuseEffect::Allocate { event } => {
253
+ out.effects.push(ResolvedEffect::Allocate { event });
254
+ if context.constructed {
255
+ let mut record = feedback
256
+ .event_record
257
+ .clone()
258
+ .unwrap_or_else(allocated_event);
259
+ crate::transition::temporary_event(
260
+ &mut record,
261
+ context.origin,
262
+ event,
263
+ context.tick as i32,
264
+ )
265
+ .map_err(|_| Error::InvalidInput)?;
266
+ feedback.event_record = Some(record);
267
+ out.effects.push(ResolvedEffect::Link);
268
+ }
269
+ }
270
+ // Compatibility tag Teleport is numeric205260: score addition, origin unused.
271
+ DefuseEffect::Teleport { .. } => {
272
+ out.effects.push(ResolvedEffect::Score { delta: 1 });
273
+ let (score, rank) =
274
+ crate::score::add(true, context.paused, 8, false, 1, 1, feedback.score)
275
+ .map_err(|_| Error::InvalidInput)?;
276
+ feedback.score = score;
277
+ if rank {
278
+ out.effects.push(ResolvedEffect::Rank);
279
+ }
280
+ }
281
+ // Compatibility tag Award is numeric283463: increment a counter index once.
282
+ DefuseEffect::Award { value, .. } => {
283
+ increment_objective_counter(&mut feedback.counters, value as usize)?;
284
+ out.effects.push(ResolvedEffect::Counter {
285
+ index: value as usize,
286
+ });
287
+ }
288
+ DefuseEffect::Sound { slot, sound } => {
289
+ out.effects.push(ResolvedEffect::Radio { code: sound });
290
+ let mut input = context.radio.clone();
291
+ input.slot = slot;
292
+ input.code = sound;
293
+ out.radio
294
+ .push(team_radio(&mut feedback.marker, &context.clients, &input)?);
295
+ }
296
+ }
297
+ }
298
+ if let Some(record) = &mut feedback.event_record {
299
+ record.flags = state.event[1];
300
+ }
301
+ Ok(out)
302
+ }
303
+ /// Defuse fragments with actual score/counter/radio/event-constructor policies.
304
+ /// Ranking, native sends/linking and base event allocation remain caller work.
305
+ pub fn resolve_defuse_fragment(
306
+ state: &mut crate::defuse::DefuseState,
307
+ feedback: &mut FeedbackState,
308
+ input: crate::defuse::DefuseInput,
309
+ context: &EffectContext,
310
+ phase: DefusePhase,
311
+ ) -> Result<ResolvedOutput, Error> {
312
+ let mut next = *state;
313
+ let mut fb = feedback.clone();
314
+ let effects = match phase {
315
+ DefusePhase::Begin => crate::defuse::begin_defuse(&mut next, input)?,
316
+ DefusePhase::Finish => crate::defuse::finish_defuse(&mut next, input)?,
317
+ };
318
+ let out = resolve_effects(&next, &mut fb, effects, context)?;
319
+ *state = next;
320
+ *feedback = fb;
321
+ Ok(out)
322
+ }
323
+ #[cfg(test)]
324
+ mod resolved_finish_tests {
325
+ use super::*;
326
+ pub(super) fn state() -> crate::defuse::DefuseState {
327
+ crate::defuse::DefuseState {
328
+ flags: 0x80008004,
329
+ flags104: 16,
330
+ busy: 1,
331
+ clock: 0,
332
+ complete: 0,
333
+ animation: 99,
334
+ weapon: 11,
335
+ event: [7, 0x80000001],
336
+ }
337
+ }
338
+ pub(super) fn feedback() -> FeedbackState {
339
+ let mut counters = (100..115).collect::<Vec<_>>();
340
+ counters[5] = u32::MAX;
341
+ FeedbackState {
342
+ score: crate::score::State {
343
+ player: -1,
344
+ teams: [100, 101, 102, 103],
345
+ },
346
+ counters,
347
+ marker: Some(RadioMarker {
348
+ deadline: 17,
349
+ active: 18,
350
+ }),
351
+ event_record: None,
352
+ }
353
+ }
354
+ pub(super) fn context() -> EffectContext {
355
+ EffectContext {
356
+ paused: false,
357
+ constructed: false,
358
+ tick: 10000,
359
+ origin: [0; 3],
360
+ radio: RadioInput {
361
+ follow: 0,
362
+ health: 1,
363
+ team: 1,
364
+ slot: 2,
365
+ mode: 8,
366
+ count: 3,
367
+ tick: 10000,
368
+ code: 7,
369
+ success: true,
370
+ payload: b"Bomb site".to_vec(),
371
+ },
372
+ clients: vec![
373
+ RadioClient {
374
+ connection: 2,
375
+ team: 1
376
+ };
377
+ 3
378
+ ],
379
+ }
380
+ }
381
+ pub(super) fn input() -> crate::defuse::DefuseInput {
382
+ crate::defuse::DefuseInput {
383
+ tick: 10000,
384
+ duration: 5,
385
+ index: 3,
386
+ slot: 2,
387
+ }
388
+ }
389
+ #[test]
390
+ fn paused_completion_increments_counter_but_not_score() {
391
+ let mut s = state();
392
+ let mut f = feedback();
393
+ let c = EffectContext {
394
+ paused: true,
395
+ ..context()
396
+ };
397
+ let o = resolve_defuse_fragment(&mut s, &mut f, input(), &c, DefusePhase::Finish).unwrap();
398
+ assert_eq!(f.score.player, -1);
399
+ assert_eq!(f.counters[5], 0);
400
+ assert!(!o.effects.contains(&ResolvedEffect::Rank));
401
+ assert_eq!(s.complete, 1);
402
+ }
403
+ #[test]
404
+ fn unsupported_counter_rolls_back_prior_completion_and_score() {
405
+ let mut s = state();
406
+ let mut f = feedback();
407
+ f.counters.clear();
408
+ let old = (s, f.clone());
409
+ assert!(
410
+ resolve_defuse_fragment(&mut s, &mut f, input(), &context(), DefusePhase::Finish)
411
+ .is_err()
412
+ );
413
+ assert_eq!((s, f), old);
414
+ }
415
+ }
docs/FINDINGS.md CHANGED
@@ -1997,3 +1997,5 @@ RE-1026: Thirty-four original-QVM scenarios recover193620 with actual mode class
1997
  RE-1027: Rust matches34 source team-radio scenarios, including the mode9 exception, mode11/signed exclusions, failed location text, raw quote preservation and64-recipient tables. Only actual sends update an existing marker; empty routing leaves it unchanged. Tests cover connected same-team routing, wrapped marker deadlines and invalid payload atomicity. Text location and native network sends remain boundaries. [Evidence](research/RUST_EFFECT_RADIO.md).
1998
 
1999
  RE-1028: Sixty-four source probes execute defuse completion with actual205260 and283463. Ready completion adds one player score point and requests ranking unless paused, then increments counter index5 by1 regardless of pause. Both score and counter wrap independently. Waiting does neither. Corrected ordered requests are log, allocate, score, optional rank, counter; allocation/log/ranking remain boundaries. This replaces the earlier misleading teleport/five-point labels with actual state feedback. [Evidence](research/QVM_EFFECT_DEFUSE_FINISH.md).
 
 
 
1997
  RE-1027: Rust matches34 source team-radio scenarios, including the mode9 exception, mode11/signed exclusions, failed location text, raw quote preservation and64-recipient tables. Only actual sends update an existing marker; empty routing leaves it unchanged. Tests cover connected same-team routing, wrapped marker deadlines and invalid payload atomicity. Text location and native network sends remain boundaries. [Evidence](research/RUST_EFFECT_RADIO.md).
1998
 
1999
  RE-1028: Sixty-four source probes execute defuse completion with actual205260 and283463. Ready completion adds one player score point and requests ranking unless paused, then increments counter index5 by1 regardless of pause. Both score and counter wrap independently. Waiting does neither. Corrected ordered requests are log, allocate, score, optional rank, counter; allocation/log/ranking remain boundaries. This replaces the earlier misleading teleport/five-point labels with actual state feedback. [Evidence](research/QVM_EFFECT_DEFUSE_FINISH.md).
2000
+
2001
+ RE-1029: Rust executes the existing score policy and indexed counter update for defuse completion across64 source cases. Paused score remains unchanged without a ranking request, while counter index5 wraps independently. Correct semantic trace tags replace historical aliases in these new reports. Whole completion and feedback state stage atomically; unit tests cover paused behavior and late counter failure after a planned score update. Ranking/log/allocation remain boundaries. [Evidence](research/RUST_EFFECT_DEFUSE_FINISH.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -2057,3 +2057,5 @@ RE-974–983 completed routing, command rewrite, inactivity, movement-parameter
2057
  - [x] RE-1027 Port team radio command planning and marker feedback ([evidence](research/RUST_EFFECT_RADIO.md)).
2058
 
2059
  - [x] RE-1028 Resolve defuse completion score and objective counter ([evidence](research/QVM_EFFECT_DEFUSE_FINISH.md)).
 
 
 
2057
  - [x] RE-1027 Port team radio command planning and marker feedback ([evidence](research/RUST_EFFECT_RADIO.md)).
2058
 
2059
  - [x] RE-1028 Resolve defuse completion score and objective counter ([evidence](research/QVM_EFFECT_DEFUSE_FINISH.md)).
2060
+
2061
+ - [x] RE-1029 Port resolved defuse score and counter feedback ([evidence](research/RUST_EFFECT_DEFUSE_FINISH.md)).
docs/RESEARCH_LEDGER.md CHANGED
@@ -2787,3 +2787,7 @@ Rust matches34 source team-radio scenarios, including the mode9 exception, mode1
2787
  ## 2026-10-29 — RE-1028 Resolve defuse completion score and objective counter
2788
 
2789
  Sixty-four source probes execute defuse completion with actual205260 and283463. Ready completion adds one player score point and requests ranking unless paused, then increments counter index5 by1 regardless of pause. Both score and counter wrap independently. Waiting does neither. Corrected ordered requests are log, allocate, score, optional rank, counter; allocation/log/ranking remain boundaries. This replaces the earlier misleading teleport/five-point labels with actual state feedback. Focused `cargo xtask test-group qvm-effect-defuse-finish` passes with deterministic report regeneration and selected regressions. Full suite deferred to task ten. Original assets unchanged; Python uses the flake-declared Nix runtime.
 
 
 
 
 
2787
  ## 2026-10-29 — RE-1028 Resolve defuse completion score and objective counter
2788
 
2789
  Sixty-four source probes execute defuse completion with actual205260 and283463. Ready completion adds one player score point and requests ranking unless paused, then increments counter index5 by1 regardless of pause. Both score and counter wrap independently. Waiting does neither. Corrected ordered requests are log, allocate, score, optional rank, counter; allocation/log/ranking remain boundaries. This replaces the earlier misleading teleport/five-point labels with actual state feedback. Focused `cargo xtask test-group qvm-effect-defuse-finish` passes with deterministic report regeneration and selected regressions. Full suite deferred to task ten. Original assets unchanged; Python uses the flake-declared Nix runtime.
2790
+
2791
+ ## 2026-10-29 — RE-1029 Port resolved defuse score and counter feedback
2792
+
2793
+ Rust executes the existing score policy and indexed counter update for defuse completion across64 source cases. Paused score remains unchanged without a ranking request, while counter index5 wraps independently. Correct semantic trace tags replace historical aliases in these new reports. Whole completion and feedback state stage atomically; unit tests cover paused behavior and late counter failure after a planned score update. Ranking/log/allocation remain boundaries. Focused `cargo xtask test-group rust-effect-defuse-finish` passes with deterministic report regeneration and selected regressions. Full suite deferred to task ten. Original assets unchanged; Python uses the flake-declared Nix runtime.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -2094,3 +2094,5 @@ RE-974–983 recover routing, command rewriting, inactivity and composed setup t
2094
  - [x] RE-1027 Port team radio command planning and marker feedback ([evidence](research/RUST_EFFECT_RADIO.md)).
2095
 
2096
  - [x] RE-1028 Resolve defuse completion score and objective counter ([evidence](research/QVM_EFFECT_DEFUSE_FINISH.md)).
 
 
 
2094
  - [x] RE-1027 Port team radio command planning and marker feedback ([evidence](research/RUST_EFFECT_RADIO.md)).
2095
 
2096
  - [x] RE-1028 Resolve defuse completion score and objective counter ([evidence](research/QVM_EFFECT_DEFUSE_FINISH.md)).
2097
+
2098
+ - [x] RE-1029 Port resolved defuse score and counter feedback ([evidence](research/RUST_EFFECT_DEFUSE_FINISH.md)).
docs/research/README.md CHANGED
@@ -1625,3 +1625,5 @@ The extension collector adds five focused tests for native tail calls, command-r
1625
  - [RE-1027: Port team radio command planning and marker feedback](RUST_EFFECT_RADIO.md).
1626
 
1627
  - [RE-1028: Resolve defuse completion score and objective counter](QVM_EFFECT_DEFUSE_FINISH.md).
 
 
 
1625
  - [RE-1027: Port team radio command planning and marker feedback](RUST_EFFECT_RADIO.md).
1626
 
1627
  - [RE-1028: Resolve defuse completion score and objective counter](QVM_EFFECT_DEFUSE_FINISH.md).
1628
+
1629
+ - [RE-1029: Port resolved defuse score and counter feedback](RUST_EFFECT_DEFUSE_FINISH.md).
docs/research/RUST_EFFECT_DEFUSE_FINISH.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Port resolved defuse score and counter feedback
2
+
3
+ RE-1029. Focused validation: `cargo xtask test-group rust-effect-defuse-finish`. [Machine report](rust-effect-defuse-finish.json).
4
+
5
+ Rust executes the existing score policy and indexed counter update for defuse completion across64 source cases. Paused score remains unchanged without a ranking request, while counter index5 wraps independently. Correct semantic trace tags replace historical aliases in these new reports. Whole completion and feedback state stage atomically; unit tests cover paused behavior and late counter failure after a planned score update. Ranking/log/allocation remain boundaries.
docs/research/rust-effect-defuse-finish.json ADDED
The diff for this file is too large to render. See raw diff
 
docs/research/rust-health-impulse.json CHANGED
@@ -2985,7 +2985,7 @@
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
- "rust_unit_tests": 283,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
 
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
+ "rust_unit_tests": 285,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
scripts/analyze_rust_effect_defuse_finish.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compare recovered Rust policy with original QVM fixtures."""
2
+ import hashlib,json,os,subprocess
3
+ from pathlib import Path
4
+ from analyze_qvm import write_json
5
+ from rust_lifecycle_support import result
6
+
7
+ def main():
8
+ subprocess.run(['cargo','build','--offline','--package','urt-gameplay','--bin','urt-effect-feedback'],check=True,capture_output=True)
9
+ binary=Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-effect-feedback'
10
+ path=Path('docs/research/qvm-effect-defuse-finish.json');data=path.read_bytes();probes=[]
11
+ for p in json.loads(data)['probes']:
12
+ probes.append(dict(source={k:p[k] for k in ['flags', 'flags104', 'busy', 'clock', 'complete', 'animation', 'weapon', 'event', 'guards', 'events', 'score', 'teams', 'counters', 'radio', 'marker', 'event_record']},rust=result(binary,'finish',encode(p['input']))))
13
+ write_json(Path('docs/research/rust-effect-defuse-finish.json'),dict(source_report=str(path),source_report_sha256=hashlib.sha256(data).hexdigest(),probes=probes,limits='Finite source/Rust contract parity; native time/file/network replies remain supplied or captured.'))
14
+
15
+ from analyze_rust_defuse_begin import encode as base_encode
16
+ def encode(a):
17
+ from analyze_qvm_acceleration import word
18
+ payload=bytes.fromhex(a['payload'])
19
+ return base_encode(a)+[a['paused'],a['constructed'],a['marker'],a['success'],*[word(v) for v in a['origin']],len(payload),*payload]
20
+ if __name__=='__main__':main()
scripts/test_rust_effect_defuse_finish.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ class PortTests(unittest.TestCase):
4
+ def test_original_parity(self):
5
+ r=json.loads(Path('docs/research/rust-effect-defuse-finish.json').read_text());self.assertGreaterEqual(len(r['probes']),12)
6
+ for p in r['probes']:self.assertEqual(p['source'],p['rust'])
7
+ def test_identity(self):
8
+ r=json.loads(Path('docs/research/rust-effect-defuse-finish.json').read_text());self.assertEqual(hashlib.sha256(Path(r['source_report']).read_bytes()).hexdigest(),r['source_report_sha256'])
9
+ if __name__=='__main__':unittest.main()
xtask/checks.tsv CHANGED
@@ -4400,3 +4400,40 @@ report scripts/analyze_qvm_effect_defuse_finish.py docs/research/qvm-effect-defu
4400
  edge docs/research/qvm-effect-defuse-finish.json scripts/test_qvm_effect_defuse_finish.py
4401
  group qvm-effect-defuse-finish scripts/analyze_qvm_effect_defuse_finish.py
4402
  edge scripts/analyze_qvm_effect_defuse_finish.py scripts/analyze_qvm_resumer_audit.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4400
  edge docs/research/qvm-effect-defuse-finish.json scripts/test_qvm_effect_defuse_finish.py
4401
  group qvm-effect-defuse-finish scripts/analyze_qvm_effect_defuse_finish.py
4402
  edge scripts/analyze_qvm_effect_defuse_finish.py scripts/analyze_qvm_resumer_audit.py
4403
+ report scripts/analyze_rust_effect_defuse_finish.py docs/research/rust-effect-defuse-finish.json
4404
+ edge docs/research/rust-effect-defuse-finish.json scripts/test_rust_effect_defuse_finish.py
4405
+ group rust-effect-defuse-finish scripts/analyze_rust_effect_defuse_finish.py
4406
+ edge scripts/analyze_rust_effect_defuse_finish.py scripts/analyze_qvm_resumer_audit.py
4407
+ edge Cargo.toml scripts/analyze_rust_effect_defuse_finish.py
4408
+ edge Cargo.lock scripts/analyze_rust_effect_defuse_finish.py
4409
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_effect_defuse_finish.py
4410
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_effect_defuse_finish.py
4411
+ edge crates/urt-gameplay/src/filter_loading.rs scripts/analyze_rust_effect_defuse_finish.py
4412
+ edge crates/urt-gameplay/src/filter_lifecycle.rs scripts/analyze_rust_effect_defuse_finish.py
4413
+ edge crates/urt-gameplay/src/native_policy.rs scripts/analyze_rust_effect_defuse_finish.py
4414
+ edge crates/urt-gameplay/src/bin/urt-filter-loading.rs scripts/analyze_rust_effect_defuse_finish.py
4415
+ edge crates/urt-gameplay/src/qvm_format.rs scripts/analyze_rust_effect_defuse_finish.py
4416
+ edge docs/research/qvm-effect-defuse-finish.json scripts/analyze_rust_effect_defuse_finish.py
4417
+ group rust-effect-defuse-finish scripts/analyze_rust_health_impulse.py
4418
+ group gameplay scripts/analyze_rust_effect_defuse_finish.py
4419
+ edge crates/urt-gameplay/src/filter_commands.rs scripts/analyze_rust_effect_defuse_finish.py
4420
+ edge crates/urt-gameplay/src/bin/urt-filter-commands.rs scripts/analyze_rust_effect_defuse_finish.py
4421
+ edge crates/urt-gameplay/src/client_timer.rs scripts/analyze_rust_effect_defuse_finish.py
4422
+ edge crates/urt-gameplay/src/bin/urt-client-timer.rs scripts/analyze_rust_effect_defuse_finish.py
4423
+ edge crates/urt-gameplay/src/client_processing.rs scripts/analyze_rust_effect_defuse_finish.py
4424
+ edge crates/urt-gameplay/src/bin/urt-client-processing.rs scripts/analyze_rust_effect_defuse_finish.py
4425
+ edge crates/urt-gameplay/src/client_move_context.rs scripts/analyze_rust_effect_defuse_finish.py
4426
+ edge crates/urt-gameplay/src/bin/urt-client-move-context.rs scripts/analyze_rust_effect_defuse_finish.py
4427
+ edge crates/urt-gameplay/src/move_callbacks.rs scripts/analyze_rust_effect_defuse_finish.py
4428
+ edge crates/urt-gameplay/src/bin/urt-move-callbacks.rs scripts/analyze_rust_effect_defuse_finish.py
4429
+ edge crates/urt-gameplay/src/assistance.rs scripts/analyze_rust_effect_defuse_finish.py
4430
+ edge crates/urt-gameplay/src/bin/urt-assistance.rs scripts/analyze_rust_effect_defuse_finish.py
4431
+ edge crates/urt-gameplay/src/move_callbacks.rs scripts/analyze_rust_effect_defuse_finish.py
4432
+ edge crates/urt-gameplay/src/defuse.rs scripts/analyze_rust_effect_defuse_finish.py
4433
+ edge crates/urt-gameplay/src/bin/urt-defuse.rs scripts/analyze_rust_effect_defuse_finish.py
4434
+ edge crates/urt-gameplay/src/move_callbacks.rs scripts/analyze_rust_effect_defuse_finish.py
4435
+ edge crates/urt-gameplay/src/effect_feedback.rs scripts/analyze_rust_effect_defuse_finish.py
4436
+ edge crates/urt-gameplay/src/bin/urt-effect-feedback.rs scripts/analyze_rust_effect_defuse_finish.py
4437
+ edge crates/urt-gameplay/src/score.rs scripts/analyze_rust_effect_defuse_finish.py
4438
+ edge crates/urt-gameplay/src/transition.rs scripts/analyze_rust_effect_defuse_finish.py
4439
+ edge crates/urt-gameplay/src/defuse.rs scripts/analyze_rust_effect_defuse_finish.py
xtask/src/lib.rs CHANGED
@@ -1168,6 +1168,9 @@ mod integration_tests {
1168
  ("effect_feedback", "effect_radio"),
1169
  ("score", "effect_radio"),
1170
  ("transition", "effect_radio"),
 
 
 
1171
  ("client_move_context", "client_move_scratch"),
1172
  ("client_processing", "client_routing"),
1173
  ("client_timer", "client_routing"),
@@ -1403,6 +1406,7 @@ mod integration_tests {
1403
  ("qvm-defuse-sequence", "defuse_sequence"),
1404
  ("qvm-effect-counter", "effect_counter"),
1405
  ("qvm-effect-radio", "effect_radio"),
 
1406
  ("qvm-client-move-scratch", "client_move_scratch"),
1407
  ("qvm-client-routing", "client_routing"),
1408
  ("qvm-client-command-rewrite", "client_command_rewrite"),
 
1168
  ("effect_feedback", "effect_radio"),
1169
  ("score", "effect_radio"),
1170
  ("transition", "effect_radio"),
1171
+ ("effect_feedback", "effect_defuse_finish"),
1172
+ ("score", "effect_defuse_finish"),
1173
+ ("transition", "effect_defuse_finish"),
1174
  ("client_move_context", "client_move_scratch"),
1175
  ("client_processing", "client_routing"),
1176
  ("client_timer", "client_routing"),
 
1406
  ("qvm-defuse-sequence", "defuse_sequence"),
1407
  ("qvm-effect-counter", "effect_counter"),
1408
  ("qvm-effect-radio", "effect_radio"),
1409
+ ("qvm-effect-defuse-finish", "effect_defuse_finish"),
1410
  ("qvm-client-move-scratch", "client_move_scratch"),
1411
  ("qvm-client-routing", "client_routing"),
1412
  ("qvm-client-command-rewrite", "client_command_rewrite"),