vimp_engine_core/client/
divergence.rs1use std::collections::VecDeque;
16
17use serde_json::{Value, json};
18
19use crate::config::{DivergenceConfig, PLAYER_STATE_LEN};
20
21#[derive(Clone, Copy, PartialEq, Eq)]
23pub enum Source {
24 Camera,
26 State,
28}
29
30impl Source {
31 fn as_str(self) -> &'static str {
32 match self {
33 Source::Camera => "camera",
34 Source::State => "state",
35 }
36 }
37}
38
39pub struct Observation<'a> {
41 pub source: Source,
42 pub predicted: &'a [f32],
45 pub authoritative: &'a [f32; PLAYER_STATE_LEN],
46 pub server_time: f64,
47 pub local_now: f64,
48 pub offset: f64,
49 pub input_seq: u32,
50 pub replayed: Option<(f64, f64, usize)>,
53}
54
55pub struct DivergenceTracker {
56 cfg: DivergenceConfig,
57 records: VecDeque<Value>,
58 samples: u64,
59 violations: u64,
60 dropped: u64,
61 max_delta: [f32; PLAYER_STATE_LEN],
62}
63
64impl DivergenceTracker {
65 pub fn new(cfg: DivergenceConfig) -> Self {
66 Self {
67 cfg,
68 records: VecDeque::new(),
69 samples: 0,
70 violations: 0,
71 dropped: 0,
72 max_delta: [0.0; PLAYER_STATE_LEN],
73 }
74 }
75
76 pub fn observe(&mut self, obs: Observation) {
80 self.samples += 1;
81
82 let width = obs.predicted.len().min(PLAYER_STATE_LEN);
83 let mut deltas = Vec::with_capacity(width);
84 let mut exceeded = Vec::new();
85
86 for index in 0..width {
87 let delta = obs.predicted[index] - obs.authoritative[index];
88
89 deltas.push(delta);
90
91 if delta.abs() > self.max_delta[index] {
92 self.max_delta[index] = delta.abs();
93 }
94
95 if delta.abs() > self.cfg.threshold(index) {
96 exceeded.push(index);
97 }
98 }
99
100 if exceeded.is_empty() {
101 return;
102 }
103
104 self.violations += 1;
105
106 if self.records.len() >= self.cfg.capacity {
107 self.records.pop_front();
108 self.dropped += 1;
109 }
110
111 let thresholds: Vec<Value> = (0..width)
112 .map(|index| json!(round4(self.cfg.threshold(index))))
113 .collect();
114
115 self.records.push_back(json!({
116 "source": obs.source.as_str(),
117 "serverTime": obs.server_time,
118 "localNow": obs.local_now,
119 "offset": obs.offset,
120 "inputSeq": obs.input_seq,
121 "replayed": obs.replayed.map(|(from, to, count)| json!({
122 "from": from,
123 "to": to,
124 "count": count,
125 })),
126 "predicted": floats(&obs.predicted[..width]),
127 "authoritative": floats(&obs.authoritative[..width]),
128 "delta": floats(&deltas),
129 "exceeded": exceeded,
130 "thresholds": thresholds,
131 }));
132 }
133
134 pub fn take_json(&mut self) -> String {
138 let records: Vec<Value> = self.records.drain(..).collect();
139
140 json!({
141 "samples": self.samples,
142 "violations": self.violations,
143 "dropped": self.dropped,
144 "maxDelta": floats(&self.max_delta),
145 "records": records,
146 })
147 .to_string()
148 }
149}
150
151fn floats(values: &[f32]) -> Vec<Value> {
152 values.iter().map(|v| json!(round4(*v))).collect()
153}
154
155fn round4(value: f32) -> f64 {
157 ((value as f64) * 10_000.0).round() / 10_000.0
158}