Skip to main content

wowlab_engine/cli/inspect/
mod.rs

1// #t(file: rust_alloc_in_loop) CLI display formatting in loops for terminal output, not performance-critical
2
3use wowlab_common::output;
4use wowlab_engine_application::introspect_spec_resolved;
5use wowlab_fs::path::Path;
6use wowlab_types::game::{DamageKind, PeriodicEffect, SpecId, SpecIntrospection, SpellInfo};
7
8use super::{CliError, args::OutputFormat, presentation};
9use crate::composition::EngineComposition;
10
11pub(crate) async fn run(
12    spec: SpecId,
13    format: OutputFormat,
14    id_filter: Option<u32>,
15    composition: EngineComposition,
16    workspace_root: &Path,
17) -> Result<(), CliError> {
18    let handle = super::resolver::create(workspace_root)?;
19    let introspection =
20        introspect_spec_resolved(composition.catalog(), spec, &handle.resolver).await?;
21
22    match format {
23        OutputFormat::Json => print_json(&introspection, id_filter),
24        OutputFormat::Text => print_text(&introspection, spec, id_filter),
25    }
26
27    Ok(())
28}
29
30fn print_json(intro: &SpecIntrospection, id_filter: Option<u32>) {
31    if let Some(id) = id_filter {
32        let spell = intro.spells.iter().find(|s| s.spell_id == id);
33        let aura = intro.auras.iter().find(|a| a.aura_id == id);
34        let auto = intro.auto_attacks.iter().find(|a| a.spell_id == id);
35
36        output::json(&serde_json::json!({ "spell": spell, "aura": aura, "auto_attack": auto }));
37    } else {
38        output::json(intro);
39    }
40}
41
42fn print_text(intro: &SpecIntrospection, spec: SpecId, id_filter: Option<u32>) {
43    output::banner(
44        &format!("{} Introspection", presentation::spec_display_name(spec)),
45        "",
46    );
47    print_resources(intro);
48    print_spells(intro, id_filter);
49    print_auras(intro, id_filter);
50    print_auto_attacks(intro, id_filter);
51    print_hero_talents(intro);
52}
53
54fn print_resources(intro: &SpecIntrospection) {
55    output::header("Primary Resource");
56    let r = &intro.resource_primary;
57
58    output::kv("Name", &r.name);
59    output::kv_fmt("Max", r.max);
60    output::kv_fmt("Regen", r.regen);
61    output::kv_fmt("Start", r.starts_at);
62    output::kv_fmt("Type ID", r.type_id);
63
64    if let Some(ref sec) = intro.resource_secondary {
65        output::blank();
66        output::header("Secondary Resource");
67        output::kv("Name", &sec.name);
68        output::kv_fmt("Max", sec.max);
69        output::kv_fmt("Regen", sec.regen);
70        output::kv_fmt("Start", sec.starts_at);
71        output::kv_fmt("Type ID", sec.type_id);
72    }
73
74    output::blank();
75}
76
77fn print_spells(intro: &SpecIntrospection, id_filter: Option<u32>) {
78    let spells: Vec<&SpellInfo> = match id_filter {
79        Some(id) => intro.spells.iter().filter(|s| s.spell_id == id).collect(),
80        None => intro.spells.iter().collect(),
81    };
82
83    output::header(&format!("Spells ({})", spells.len()));
84
85    for s in &spells {
86        output::detail(&format!(
87            "[{:>6}] {:<35} cast={:>5}ms  gcd={:>5}ms  cost={:<6.1}  gain={:<6.1}  off_gcd={}  pet={}",
88            s.spell_id,
89            s.name,
90            s.cast_time_ms,
91            s.gcd_ms,
92            s.resource_cost,
93            s.resource_gain,
94            s.off_gcd,
95            s.is_pet,
96        ));
97
98        if let Some(ref cd) = s.cooldown {
99            output::detail(&format!(
100                "         cd={:.1}s  charges={}  recharge={:.1}s",
101                cd.duration_secs, cd.max_charges, cd.recharge_secs,
102            ));
103        }
104
105        print_damage_kind(&s.damage.kind);
106
107        if let Some(aura_id) = s.applies_aura_id {
108            output::detail(&format!("         applies_aura={aura_id}"));
109        }
110    }
111
112    output::blank();
113}
114
115fn print_damage_kind(kind: &DamageKind) {
116    if !matches!(kind, DamageKind::None) {
117        output::detail(&format!("         {kind}"));
118    }
119}
120
121fn print_auras(intro: &SpecIntrospection, id_filter: Option<u32>) {
122    let auras: Vec<_> = match id_filter {
123        Some(id) => intro.auras.iter().filter(|a| a.aura_id == id).collect(),
124        None => intro.auras.iter().collect(),
125    };
126
127    output::header(&format!("Auras ({})", auras.len()));
128
129    for a in &auras {
130        output::detail(&format!(
131            "[{:>6}] {:<35} dur={:>6}ms  stacks={:<2}  on={:<7}  pandemic={}",
132            a.aura_id, a.name, a.base_duration_ms, a.max_stacks, a.on, a.pandemic,
133        ));
134
135        if let Some(ref p) = a.periodic {
136            print_periodic_effect(p.tick_ms, &p.effect);
137        }
138    }
139
140    output::blank();
141}
142
143fn print_periodic_effect(tick_ms: u32, effect: &PeriodicEffect) {
144    output::detail(&format!("         tick={tick_ms}ms  {effect}"));
145}
146
147fn print_auto_attacks(intro: &SpecIntrospection, id_filter: Option<u32>) {
148    if intro.auto_attacks.is_empty() {
149        return;
150    }
151
152    output::header(&format!("Auto Attacks ({})", intro.auto_attacks.len()));
153
154    for aa in &intro.auto_attacks {
155        if id_filter.is_some_and(|id| id != aa.spell_id) {
156            continue;
157        }
158
159        output::detail(&format!(
160            "[{:>6}] swing={}ms  ap_coef={:.4}  pet={}",
161            aa.spell_id, aa.swing_ms, aa.ap_coef, aa.is_pet,
162        ));
163    }
164
165    output::blank();
166}
167
168fn print_hero_talents(intro: &SpecIntrospection) {
169    if intro.hero_talents.is_empty() {
170        return;
171    }
172
173    output::header(&format!("Hero Talent Trees ({})", intro.hero_talents.len()));
174
175    for tree in &intro.hero_talents {
176        output::subheader(&tree.name);
177
178        for (name, id) in &tree.spells {
179            output::detail(&format!("spell: {name} ({id})"));
180        }
181
182        for (name, id) in &tree.auras {
183            output::detail(&format!("aura:  {name} ({id})"));
184        }
185    }
186
187    output::blank();
188}