1use wowlab_types::sim::{ActorId, EnemyIdx, GroupId, PullId, SpatialTransform, TargetIdx};
4
5#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub struct TelemetryScope {
8 pub source: ActorId,
9 pub target: EnemyIdx,
10 pub pull: PullId,
11 pub group: GroupId,
12}
13
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
16pub struct AuraTelemetryScope {
17 pub source: ActorId,
18 pub affected: ActorId,
19 pub pull: PullId,
20 pub group: Option<GroupId>,
21}
22
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub struct DamageEventFlags {
26 pub is_crit: bool,
27 pub is_periodic: bool,
28 pub is_pet: bool,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct DamageObservation {
34 pub spell_id: u32,
35 pub amount: f64,
36 pub flags: DamageEventFlags,
37}
38
39impl DamageEventFlags {
40 #[must_use]
41 pub const fn with_crit(mut self, is_crit: bool) -> Self {
42 self.is_crit = is_crit;
43
44 self
45 }
46
47 #[must_use]
48 pub const fn with_periodic(mut self, is_periodic: bool) -> Self {
49 self.is_periodic = is_periodic;
50
51 self
52 }
53
54 #[must_use]
55 pub const fn with_pet(mut self, is_pet: bool) -> Self {
56 self.is_pet = is_pet;
57
58 self
59 }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct TargetMetadata {
65 pub id: TargetIdx,
66 pub npc_id: Option<u32>,
67 pub display_name: String,
68 pub group: GroupId,
69 pub enemy_tags: Vec<Box<str>>,
70 pub group_tags: Vec<Box<str>>,
71}
72
73#[derive(Clone, Debug)]
74#[non_exhaustive]
75pub struct DamageEvent {
76 pub spell_id: u32,
77 pub amount: f64,
78 pub is_crit: bool,
79 pub is_periodic: bool,
80 pub is_pet: bool,
81 pub time_ms: u32,
82 pub scope: TelemetryScope,
83}
84
85impl DamageEvent {
86 #[must_use]
87 pub fn new(observation: DamageObservation, time_ms: u32, scope: TelemetryScope) -> Self {
88 Self {
89 spell_id: observation.spell_id,
90 amount: observation.amount,
91 is_crit: observation.flags.is_crit,
92 is_periodic: observation.flags.is_periodic,
93 is_pet: observation.flags.is_pet,
94 time_ms,
95 scope,
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug)]
102pub struct AuraObservation {
103 pub aura_id: u32,
104 pub kind: AuraEventKind,
105}
106
107#[derive(Clone, Debug)]
108#[non_exhaustive]
109pub struct AuraEvent {
110 pub aura_id: u32,
111 pub kind: AuraEventKind,
112 pub time_ms: u32,
113 pub source: ActorId,
114 pub affected: ActorId,
115 pub pull: PullId,
116 pub group: Option<GroupId>,
117}
118
119impl AuraEvent {
120 #[must_use]
121 pub const fn new(
122 observation: AuraObservation,
123 time_ms: u32,
124 scope: AuraTelemetryScope,
125 ) -> Self {
126 Self {
127 aura_id: observation.aura_id,
128 kind: observation.kind,
129 time_ms,
130 source: scope.source,
131 affected: scope.affected,
132 pull: scope.pull,
133 group: scope.group,
134 }
135 }
136}
137
138#[derive(Clone, Copy, Debug, PartialEq)]
140pub struct ResourceValues {
141 pub amount: f64,
142 pub current: f64,
143 pub maximum: f64,
144}
145
146#[derive(Clone, Copy, Debug)]
148pub struct ResourceObservation {
149 pub kind: ResourceEventKind,
150 pub resource_type: u8,
151 pub values: ResourceValues,
152}
153
154#[derive(Clone, Copy, Debug)]
155pub enum AuraEventKind {
157 Apply { stacks: u8 },
158 Refresh { stacks: u8 },
159 Expire,
160}
161
162pub const RESOURCE_SOURCE_UNATTRIBUTED: u32 = 0;
164
165pub const RESOURCE_SOURCE_AUTO_ATTACK: u32 = 1;
167
168pub const RESOURCE_SOURCE_PASSIVE_REGEN: u32 = u32::MAX;
170
171#[derive(Clone, Debug)]
172#[non_exhaustive]
173pub struct ResourceEvent {
174 pub kind: ResourceEventKind,
175 pub resource_type: u8,
176 pub amount: f64,
177 pub current: f64,
178 pub max: f64,
179 pub time_ms: u32,
180 pub source_spell_id: u32,
182}
183
184impl ResourceEvent {
185 #[must_use]
186 pub fn new(observation: ResourceObservation, time_ms: u32) -> Self {
187 Self {
188 kind: observation.kind,
189 resource_type: observation.resource_type,
190 amount: observation.values.amount,
191 current: observation.values.current,
192 max: observation.values.maximum,
193 time_ms,
194 source_spell_id: RESOURCE_SOURCE_UNATTRIBUTED,
195 }
196 }
197
198 #[must_use]
200 pub const fn with_source(mut self, source_spell_id: u32) -> Self {
201 self.source_spell_id = source_spell_id;
202
203 self
204 }
205}
206
207#[derive(Clone, Copy, Debug)]
208pub enum ResourceEventKind {
210 Spend,
211 Gain { wasted: f64 },
212}
213
214#[derive(Clone, Debug)]
215#[non_exhaustive]
216pub struct CooldownEvent {
217 pub spell_id: u32,
218 pub duration_ms: u32,
219 pub time_ms: u32,
220}
221
222#[derive(Clone, Debug)]
223#[non_exhaustive]
224pub struct CastEvent {
225 pub spell_id: u32,
226 pub time_ms: u32,
227 pub gcd_ms: u32,
228 pub scope: TelemetryScope,
229}
230
231#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub enum EncounterTelemetryEventKind {
235 Spawn,
236 Movement,
237 Death,
238 Despawn,
239 TargetChange,
240 PullTransition,
241}
242
243#[derive(Clone, Debug, PartialEq)]
245pub struct EncounterTelemetryEvent {
246 pub kind: EncounterTelemetryEventKind,
247 pub time_ms: u32,
248 pub actor: ActorId,
249 pub previous_target: Option<EnemyIdx>,
250 pub target: Option<EnemyIdx>,
251 pub pull: PullId,
252 pub group: Option<GroupId>,
253 pub transform: Option<SpatialTransform>,
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258#[non_exhaustive]
259pub enum TimelineEventIndex {
260 Cast(usize),
261 Damage(usize),
262 Encounter(usize),
263}
264
265#[derive(Debug)]
267pub struct TelemetrySink {
269 pub damage: Vec<DamageEvent>,
270 pub auras: Vec<AuraEvent>,
271 pub resources: Vec<ResourceEvent>,
272 pub cooldowns: Vec<CooldownEvent>,
273 pub casts: Vec<CastEvent>,
274 pub encounter: Vec<EncounterTelemetryEvent>,
275 pub targets: Vec<TargetMetadata>,
276 timeline_order: Vec<TimelineEventIndex>,
277}
278impl TelemetrySink {
281 const DAMAGE_CAPACITY: usize = 1024;
282 const AURA_CAPACITY: usize = 256;
283 const RESOURCE_CAPACITY: usize = 512;
284 const COOLDOWN_CAPACITY: usize = 128;
285 const CAST_CAPACITY: usize = 512;
286 const ENCOUNTER_CAPACITY: usize = 128;
287
288 #[must_use]
289 pub fn new() -> Self {
290 Self {
291 damage: Vec::with_capacity(Self::DAMAGE_CAPACITY),
292 auras: Vec::with_capacity(Self::AURA_CAPACITY),
293 resources: Vec::with_capacity(Self::RESOURCE_CAPACITY),
294 cooldowns: Vec::with_capacity(Self::COOLDOWN_CAPACITY),
295 casts: Vec::with_capacity(Self::CAST_CAPACITY),
296 encounter: Vec::with_capacity(Self::ENCOUNTER_CAPACITY),
297 targets: Vec::new(),
298 timeline_order: Vec::with_capacity(
299 Self::CAST_CAPACITY + Self::DAMAGE_CAPACITY + Self::ENCOUNTER_CAPACITY,
300 ),
301 }
302 }
303
304 pub fn clear(&mut self) {
305 self.damage.clear();
306 self.auras.clear();
307 self.resources.clear();
308 self.cooldowns.clear();
309 self.casts.clear();
310 self.encounter.clear();
311 self.targets.clear();
312 self.timeline_order.clear();
313 }
314
315 pub fn emit_damage(&mut self, event: &DamageEvent) {
316 self.timeline_order
317 .push(TimelineEventIndex::Damage(self.damage.len()));
318 self.damage.push(event.clone());
319 }
320
321 pub fn emit_aura_apply(
322 &mut self,
323 aura_id: u32,
324 stacks: u8,
325 time_ms: u32,
326 scope: AuraTelemetryScope,
327 ) {
328 self.emit_aura(AuraEvent::new(
329 AuraObservation {
330 aura_id,
331 kind: AuraEventKind::Apply { stacks },
332 },
333 time_ms,
334 scope,
335 ));
336 }
337
338 pub fn emit_aura_refresh(
339 &mut self,
340 aura_id: u32,
341 stacks: u8,
342 time_ms: u32,
343 scope: AuraTelemetryScope,
344 ) {
345 self.emit_aura(AuraEvent::new(
346 AuraObservation {
347 aura_id,
348 kind: AuraEventKind::Refresh { stacks },
349 },
350 time_ms,
351 scope,
352 ));
353 }
354
355 pub fn emit_aura_expire(&mut self, aura_id: u32, time_ms: u32, scope: AuraTelemetryScope) {
356 self.emit_aura(AuraEvent::new(
357 AuraObservation {
358 aura_id,
359 kind: AuraEventKind::Expire,
360 },
361 time_ms,
362 scope,
363 ));
364 }
365
366 pub fn emit_resource(&mut self, event: &ResourceEvent) {
367 self.resources.push(event.clone());
368 }
369
370 pub fn emit_cooldown_start(&mut self, spell_id: u32, duration_ms: u32, time_ms: u32) {
371 self.cooldowns.push(CooldownEvent {
372 spell_id,
373 duration_ms,
374 time_ms,
375 });
376 }
377
378 pub fn emit_cast(&mut self, spell_id: u32, time_ms: u32, gcd_ms: u32, scope: TelemetryScope) {
379 self.timeline_order
380 .push(TimelineEventIndex::Cast(self.casts.len()));
381 self.casts.push(CastEvent {
382 spell_id,
383 time_ms,
384 gcd_ms,
385 scope,
386 });
387 }
388
389 pub fn emit_encounter(&mut self, event: EncounterTelemetryEvent) {
390 self.timeline_order
391 .push(TimelineEventIndex::Encounter(self.encounter.len()));
392 self.encounter.push(event);
393 }
394
395 #[must_use]
397 pub fn timeline_order(&self) -> &[TimelineEventIndex] {
398 &self.timeline_order
399 }
400
401 pub fn set_targets(&mut self, targets: Vec<TargetMetadata>) {
403 self.targets = targets;
404 }
405
406 fn emit_aura(&mut self, event: AuraEvent) {
407 self.auras.push(event);
408 }
409}
410
411impl Default for TelemetrySink {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417#[cfg(test)]
418mod tests;