1mod generated {
4 #![expect(
5 clippy::doc_markdown,
6 clippy::must_use_candidate,
7 clippy::struct_excessive_bools,
8 reason = "prost-build owns these generated declarations and methods"
9 )]
10
11 include!(concat!(env!("OUT_DIR"), "/wowlab.sim.rs"));
12}
13
14use crate::constants::{PROTO_DPS_SCALE, PROTO_RESOURCE_SCALE};
15
16#[rustfmt::skip]
17pub use generated::{
18 ActionRow, ActionSource, ActionTelemetry, ActorKind, AuraIntervalProto, AuraRow, AuraTelemetry,
19 AuraWindow, BatchChunkCompletion, BatchWorkResult, CandidateItem, ChunkTelemetry, CooldownRow,
20 CooldownTelemetry, CooldownWindow, DamageByTarget, DamageProfileData, DictionaryView,
21 DistributionTelemetry, EncounterEventKind, EncounterTimelineEvent, ExecutionData,
22 HistogramData, InstrumentationCoverage, JobResult, JobTimeline, MarkerKind, Percentiles,
23 PermutationSummary, PhaseMarker, RepresentativeMeta, ResourceRow, ResourceSampleProto,
24 ResourceSourceRow, ResourceTelemetry, ResultCoreV1, ResultFidelity, ResultViewV1,
25 RunningAggregateStateV1, SingleResult, SingleTimeline, SlotCandidates, SlotDiff, SlotItemEntry,
26 SlotRanking, TargetEntry, TimelineMarker, TimelineSnapshot, TimelineViewV1, TournamentPayload,
27 TournamentResult, TournamentStats, TournamentTimeline, UnitEntry, job_result, job_timeline,
28};
29
30impl MarkerKind {
31 #[must_use]
33 pub fn slug(self) -> &'static str {
34 match self {
35 MarkerKind::Damage => "damage",
36 MarkerKind::Cast => "cast",
37 MarkerKind::Resource => "resource",
38 MarkerKind::Proc => "proc",
39 MarkerKind::Unspecified => "unknown",
40 }
41 }
42}
43
44impl ChunkTelemetry {
45 #[must_use]
47 pub fn mean_dps(&self) -> f64 {
48 f64::from(self.mean_dps_x10) / PROTO_DPS_SCALE
49 }
50
51 #[must_use]
53 pub fn min_dps(&self) -> f64 {
54 f64::from(self.min_dps_x10) / PROTO_DPS_SCALE
55 }
56
57 #[must_use]
59 pub fn max_dps(&self) -> f64 {
60 f64::from(self.max_dps_x10) / PROTO_DPS_SCALE
61 }
62
63 #[must_use]
65 pub fn std_dps(&self) -> f64 {
66 let m2 = f64::from_bits(self.m2_dps_bits);
67 let n = f64::from(self.iterations);
68
69 if n > 0.0 {
70 (m2 / n).max(0.0).sqrt()
71 } else {
72 0.0
73 }
74 }
75}
76
77impl ActionRow {
78 #[must_use]
80 #[expect(
81 clippy::cast_precision_loss,
82 reason = "fixed-point telemetry is intentionally decoded into f64 display units"
83 )]
84 pub fn total_damage(&self) -> f64 {
85 self.total_damage_x10 as f64 / PROTO_DPS_SCALE
86 }
87}
88
89impl ResourceRow {
90 #[must_use]
92 #[expect(
93 clippy::cast_precision_loss,
94 reason = "fixed-point telemetry is intentionally decoded into f64 display units"
96 )]
97 pub fn gained(&self) -> f64 {
98 self.gained_x100 as f64 / PROTO_RESOURCE_SCALE
99 }
100
101 #[must_use]
103 #[expect(
104 clippy::cast_precision_loss,
105 reason = "fixed-point telemetry is intentionally decoded into f64 display units"
107 )]
108 pub fn spent(&self) -> f64 {
109 self.spent_x100 as f64 / PROTO_RESOURCE_SCALE
110 }
111
112 #[must_use]
114 #[expect(
115 clippy::cast_precision_loss,
116 reason = "fixed-point telemetry is intentionally decoded into f64 display units"
118 )]
119 pub fn wasted(&self) -> f64 {
120 self.wasted_x100 as f64 / PROTO_RESOURCE_SCALE
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use googletest::prelude::*;
127 use prost::Message;
128 use rstest::rstest;
129
130 use super::*;
131
132 #[derive(Clone, Message, PartialEq)]
133 struct LegacyActionRow {
134 #[prost(uint32, tag = "1")]
135 spell_id: u32,
136 #[prost(uint32, tag = "2")]
137 target_id: u32,
138 #[prost(enumeration = "ActionSource", tag = "3")]
139 source: i32,
140 #[prost(uint64, tag = "10")]
141 casts: u64,
142 #[prost(uint64, tag = "20")]
143 total_damage_x10: u64,
144 }
145
146 #[gtest]
147 #[rstest]
148 #[case::damage(MarkerKind::Damage, "damage")]
149 #[case::cast(MarkerKind::Cast, "cast")]
150 #[case::resource(MarkerKind::Resource, "resource")]
151 #[case::proc(MarkerKind::Proc, "proc")]
152 #[case::unspecified(MarkerKind::Unspecified, "unknown")]
153 fn marker_kind_slug(#[case] kind: MarkerKind, #[case] expected: &str) -> Result<()> {
154 verify_that!(kind.slug(), eq(expected))
155 }
156
157 #[gtest]
158 fn chunk_telemetry_scaled_dps() -> Result<()> {
159 let ct = ChunkTelemetry {
160 mean_dps_x10: 12345,
161 min_dps_x10: 12345,
162 max_dps_x10: 12345,
163 ..Default::default()
164 };
165
166 verify_that!(ct.mean_dps(), near(1234.5, 1e-9))?;
167 verify_that!(ct.min_dps(), near(1234.5, 1e-9))?;
168
169 verify_that!(ct.max_dps(), near(1234.5, 1e-9))
170 }
171
172 #[gtest]
173 #[rstest]
174 #[case::zero_iters(0, 16.0f64.to_bits(), 0.0)]
175 #[case::known(4, 16.0f64.to_bits(), 2.0)]
176 #[case::neg_m2_clamped(1, (-1.0f64).to_bits(), 0.0)]
177 fn chunk_telemetry_std_dps(
178 #[case] iterations: u32,
179 #[case] m2_dps_bits: u64,
180 #[case] expected: f64,
181 ) -> Result<()> {
182 let ct = ChunkTelemetry {
183 iterations,
184 m2_dps_bits,
185 ..Default::default()
186 };
187
188 verify_that!(ct.std_dps(), near(expected, 1e-9))
189 }
190
191 #[gtest]
192 fn action_row_total_damage() -> Result<()> {
193 let row = ActionRow {
194 total_damage_x10: 500,
195 ..Default::default()
196 };
197
198 verify_that!(row.total_damage(), near(50.0, 1e-9))
199 }
200
201 #[gtest]
202 fn additive_action_identity_fields_decode_legacy_wire_bytes() -> Result<()> {
203 let legacy = LegacyActionRow {
204 spell_id: 42,
205 target_id: 7,
206 source: ActionSource::Pet as i32,
207 casts: 3,
208 total_damage_x10: 125,
209 };
210 let decoded = ActionRow::decode(legacy.encode_to_vec().as_slice()).or_fail()?;
211
212 verify_that!(decoded.spell_id, eq(42))?;
213 verify_that!(decoded.target_id, eq(7))?;
214 verify_that!(decoded.source, eq(ActionSource::Pet as i32))?;
215 verify_that!(decoded.casts, eq(3))?;
216 verify_that!(decoded.total_damage_x10, eq(125))?;
217 verify_that!(decoded.source_kind, eq(ActorKind::Unspecified as i32))?;
218 verify_that!(decoded.source_id, eq(0))?;
219 verify_that!(decoded.pull_id, none())?;
220
221 verify_that!(decoded.group_id, none())
222 }
223
224 #[gtest]
225 #[rstest]
226 #[case::gained(250, 0, 0, 2.5, 0.0, 0.0)]
227 #[case::spent(0, 250, 0, 0.0, 2.5, 0.0)]
228 #[case::wasted(0, 0, 250, 0.0, 0.0, 2.5)]
229 fn resource_row_scaled(
230 #[case] gained_x100: u64,
231 #[case] spent_x100: u64,
232 #[case] wasted_x100: u64,
233 #[case] gained: f64,
234 #[case] spent: f64,
235 #[case] wasted: f64,
236 ) -> Result<()> {
237 let row = ResourceRow {
238 gained_x100,
239 spent_x100,
240 wasted_x100,
241 ..Default::default()
242 };
243
244 verify_that!(row.gained(), near(gained, 1e-9))?;
245 verify_that!(row.spent(), near(spent, 1e-9))?;
246
247 verify_that!(row.wasted(), near(wasted, 1e-9))
248 }
249}