wowlab_engine_sim/telemetry/
representative.rs1use wowlab_engine_telemetry::{
4 EncounterTelemetryEvent, ResourceEventKind, TelemetrySink, TimelineEventIndex,
5};
6use wowlab_types::{
7 numeric::f64_to_u32_saturating_round,
8 proto,
9 sim::{ActorId, ChunkAuraWindow, ChunkCooldownWindow, ChunkTimelineMarker, GroupId, PullId},
10};
11
12use super::{TelemetryAccumulator, collect_aura_intervals, telemetry_preview_f32};
13
14const RESOURCE_SAMPLE_BUCKET_MS: u32 = 100;
15
16#[derive(Clone, Debug)]
17pub(crate) struct RepresentativeTimeline {
18 pub(crate) markers: Vec<ChunkTimelineMarker>,
19 pub(crate) aura_windows: Vec<ChunkAuraWindow>,
20 pub(crate) cooldown_windows: Vec<ChunkCooldownWindow>,
21 pub(crate) resource_samples: Vec<RepresentativeResourceSample>,
22 pub(crate) aura_intervals: Vec<RepresentativeAuraInterval>,
23 pub(crate) encounter_events: Vec<RepresentativeEncounterEvent>,
24}
25
26#[derive(Clone, Debug)]
27pub(crate) struct RepresentativeEncounterEvent {
28 pub(crate) sequence: u64,
29 pub(crate) event: EncounterTelemetryEvent,
30}
31
32#[derive(Clone, Debug)]
33pub(crate) struct RepresentativeResourceSample {
34 pub(crate) time_ms: u32,
35 pub(crate) resource_type: u8,
36 pub(crate) current: f32,
37 pub(crate) max: f32,
38 pub(crate) gain: f32,
39 pub(crate) loss: f32,
40}
41
42#[derive(Clone, Debug)]
43pub(crate) struct RepresentativeAuraInterval {
44 pub(crate) start_ms: u32,
45 pub(crate) end_ms: u32,
46 pub(crate) open_ended: bool,
47 pub(crate) aura_id: u32,
48 pub(crate) source: ActorId,
49 pub(crate) affected: ActorId,
50 pub(crate) pull: PullId,
51 pub(crate) group: Option<GroupId>,
52}
53
54#[derive(Default)]
55struct ResourceBucket {
57 current: f32,
58 max: f32,
59 gain: f32,
60 loss: f32,
61}
62
63fn bucket_resource_samples(
64 events: &[wowlab_engine_telemetry::ResourceEvent],
65) -> Vec<RepresentativeResourceSample> {
66 let mut buckets: std::collections::BTreeMap<(u32, u8), ResourceBucket> =
67 std::collections::BTreeMap::new();
68
69 for ev in events {
70 let bucket = ev.time_ms / RESOURCE_SAMPLE_BUCKET_MS;
71 let slot = buckets.entry((bucket, ev.resource_type)).or_default();
72
73 slot.current = telemetry_preview_f32(ev.current);
74 slot.max = telemetry_preview_f32(ev.max);
75
76 match ev.kind {
77 ResourceEventKind::Gain { wasted } => {
78 slot.gain += telemetry_preview_f32(ev.amount - wasted);
79 }
80 ResourceEventKind::Spend => {
81 slot.loss += telemetry_preview_f32(ev.amount);
82 }
83 }
84 }
85
86 buckets
87 .into_iter()
88 .map(|((bucket, rt), slot)| RepresentativeResourceSample {
89 time_ms: bucket * RESOURCE_SAMPLE_BUCKET_MS,
90 resource_type: rt,
91 current: slot.current,
92 max: slot.max,
93 gain: slot.gain,
94 loss: slot.loss,
95 })
96 .collect()
97}
98
99fn collect_aura_intervals_with_open_ended(
100 events: &[wowlab_engine_telemetry::AuraEvent],
101 encounter_end_ms: u32,
102) -> Vec<RepresentativeAuraInterval> {
103 let mut out: Vec<RepresentativeAuraInterval> = Vec::new();
104
105 collect_aura_intervals(events, |event, start, end| {
106 out.push(RepresentativeAuraInterval {
107 start_ms: start,
108 end_ms: end.unwrap_or(encounter_end_ms),
109 open_ended: end.is_none(),
110 aura_id: event.aura_id,
111 source: event.source,
112 affected: event.affected,
113 pull: event.pull,
114 group: event.group,
115 });
116 });
117 out.sort_by_key(|w| {
118 (
119 w.start_ms,
120 w.aura_id,
121 super::actor_sort_key(w.affected),
122 super::actor_sort_key(w.source),
123 )
124 });
125
126 out
127}
128
129impl TelemetryAccumulator {
130 pub(crate) fn capture_representative(&mut self, sink: &TelemetrySink, duration_ms: u32) {
131 let mut markers = Vec::with_capacity(sink.casts.len() + sink.damage.len());
132 let mut encounter_events = Vec::with_capacity(sink.encounter.len());
133
134 for (sequence, event_index) in sink.timeline_order().iter().enumerate() {
135 let sequence = u64::try_from(sequence).expect("timeline event count fits u64");
136
137 match *event_index {
138 TimelineEventIndex::Cast(index) => {
139 let Some(event) = sink.casts.get(index) else {
140 continue;
141 };
142
143 markers.push(ChunkTimelineMarker {
144 time_ms: event.time_ms,
145 sequence,
146 kind: proto::MarkerKind::Cast,
147 spell_or_aura_id: event.spell_id,
148 target: event.scope.target.0.into(),
149 amount: 0,
150 is_crit: false,
151 source: Some(event.scope.source),
152 pull: Some(event.scope.pull),
153 group: Some(event.scope.group),
154 });
155 }
156 TimelineEventIndex::Damage(index) => {
157 let Some(event) = sink.damage.get(index) else {
158 continue;
159 };
160
161 markers.push(ChunkTimelineMarker {
162 time_ms: event.time_ms,
163 sequence,
164 kind: proto::MarkerKind::Damage,
165 spell_or_aura_id: event.spell_id,
166 target: event.scope.target.0.into(),
167 amount: f64_to_u32_saturating_round(event.amount.max(0.0)),
168 is_crit: event.is_crit,
169 source: Some(event.scope.source),
170 pull: Some(event.scope.pull),
171 group: Some(event.scope.group),
172 });
173 }
174 TimelineEventIndex::Encounter(index) => {
175 let Some(event) = sink.encounter.get(index) else {
176 continue;
177 };
178
179 encounter_events.push(RepresentativeEncounterEvent {
180 sequence,
181 event: event.clone(),
183 });
184 }
185 _ => {}
186 }
187 }
188
189 markers.sort_by_key(|m| (m.time_ms, m.sequence));
190
191 let mut aura_windows = Vec::new();
192
193 collect_aura_intervals(&sink.auras, |event, start, end| {
194 aura_windows.push(ChunkAuraWindow {
195 aura_id: event.aura_id,
196 target: match event.affected {
197 ActorId::Enemy(enemy) => u32::from(enemy.0),
198 ActorId::Player | ActorId::External | ActorId::Pet(_) => 0,
199 },
200 start_ms: start,
201 end_ms: end.unwrap_or(duration_ms),
202 source: Some(event.source),
203 affected: Some(event.affected),
204 pull: Some(event.pull),
205 group: event.group,
206 });
207 });
208
209 let cooldown_windows: Vec<ChunkCooldownWindow> = sink
210 .cooldowns
211 .iter()
212 .map(|event| ChunkCooldownWindow {
213 spell_id: event.spell_id,
214 start_ms: event.time_ms,
215 duration_ms: event.duration_ms,
216 })
217 .collect();
218
219 let (resource_samples, aura_intervals) = if self.trace_extras_enabled {
220 (
221 bucket_resource_samples(&sink.resources),
222 collect_aura_intervals_with_open_ended(&sink.auras, duration_ms),
223 )
224 } else {
225 (Vec::new(), Vec::new())
226 };
227
228 self.representative_sink = Some(RepresentativeTimeline {
229 markers,
230 aura_windows,
231 cooldown_windows,
232 resource_samples,
233 aura_intervals,
234 encounter_events,
235 });
236 }
237}
238
239#[cfg(test)]
240mod tests;