Skip to main content

wowlab_types/
proto.rs

1//! Protobuf-generated types for binary result encoding.
2
3mod 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    /// Stable lower-case slug for a timeline marker kind, used by view-model DTOs.
32    #[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    /// Mean DPS in real units.
46    #[must_use]
47    pub fn mean_dps(&self) -> f64 {
48        f64::from(self.mean_dps_x10) / PROTO_DPS_SCALE
49    }
50
51    /// Minimum per-iteration DPS in real units.
52    #[must_use]
53    pub fn min_dps(&self) -> f64 {
54        f64::from(self.min_dps_x10) / PROTO_DPS_SCALE
55    }
56
57    /// Maximum per-iteration DPS in real units.
58    #[must_use]
59    pub fn max_dps(&self) -> f64 {
60        f64::from(self.max_dps_x10) / PROTO_DPS_SCALE
61    }
62
63    /// Standard deviation of per-iteration DPS, derived from the streaming M2 accumulator.
64    #[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    /// Total damage attributed to this action in real units.
79    #[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    /// Total resource gained in real units.
91    #[must_use]
92    #[expect(
93        clippy::cast_precision_loss,
94        // #t(rust_duplicate_strings) Rust attributes require a literal reason at the declaration they govern.
95        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    /// Total resource spent in real units.
102    #[must_use]
103    #[expect(
104        clippy::cast_precision_loss,
105        // #t(rust_duplicate_strings) Rust attributes require a literal reason at the declaration they govern.
106        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    /// Total resource wasted (overcapped) in real units.
113    #[must_use]
114    #[expect(
115        clippy::cast_precision_loss,
116        // #t(rust_duplicate_strings) Rust attributes require a literal reason at the declaration they govern.
117        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}