Skip to main content

wowlab_engine/cli/sim/
mod.rs

1mod output;
2mod progress;
3
4use progress::ConsoleProgress;
5use prost::Message;
6use wowlab_common::{
7    output as out,
8    output::fmt_integer,
9    sim::{
10        defaults::DEFAULT_SEED,
11        intent::{SimConfigIntent, serialize_sim_config},
12        synthetic::CLI_ROTATION_ID,
13    },
14    time::Instant,
15};
16use wowlab_engine_adapter_data::OverlayResolver;
17use wowlab_engine_application::{
18    ParallelRun, assisted_rotation_id, resolve_assisted_rotation_script, simulate_intent_parallel,
19};
20use wowlab_engine_ports::DynDataResolver;
21use wowlab_fs::{file, path::Path};
22use wowlab_types::{game::SpecId, proto};
23
24use super::{
25    CliError,
26    args::{Args, Command, OutputFormat},
27    resolver::ResolverHandle,
28};
29use crate::composition::EngineComposition;
30
31struct SimSetup {
32    resolver: Box<DynDataResolver<'static>>,
33    sim_config_toml: String,
34}
35
36pub(crate) async fn run(
37    args: &Args,
38    composition: EngineComposition,
39    workspace_root: &Path,
40) -> Result<(), CliError> {
41    let Command::Sim {
42        spec,
43        duration,
44        iterations,
45        threads,
46        seed,
47        format,
48        rotation,
49    } = &args.command
50    else {
51        return Err(CliError::unexpected_simulation_command());
52    };
53    let (spec, duration, iterations, threads, seed, format) =
54        (*spec, *duration, *iterations, *threads, *seed, *format);
55    let seed_base = seed.unwrap_or(DEFAULT_SEED);
56    let num_threads = threads.unwrap_or_else(num_cpus::get).max(1);
57
58    let handle = super::resolver::create(workspace_root)?;
59    let resolver_source = handle.source.clone();
60    let setup = build_setup(spec, duration, rotation.as_deref(), workspace_root, handle).await?;
61
62    let text_mode = matches!(format, OutputFormat::Text);
63
64    if text_mode {
65        print_configuration(
66            spec,
67            duration,
68            iterations,
69            num_threads,
70            seed,
71            &resolver_source,
72        );
73    }
74
75    let progress = ConsoleProgress::new(iterations, text_mode);
76
77    let start = Instant::now();
78
79    let telemetry_bytes = simulate_intent_parallel(
80        composition.catalog(),
81        &setup.sim_config_toml,
82        ParallelRun {
83            seed_base,
84            total_iterations: iterations,
85            num_threads,
86        },
87        &setup.resolver,
88        &progress,
89    )
90    .await?;
91
92    progress.finish();
93
94    let elapsed = start.elapsed();
95
96    let telemetry = proto::ChunkTelemetry::decode(telemetry_bytes.as_slice())?;
97    let summary =
98        output::SimSummary::build(&telemetry, spec, duration, iterations, elapsed, num_threads);
99
100    match format {
101        OutputFormat::Text => {
102            output::print_text(&summary);
103        }
104        OutputFormat::Json => {
105            output::print_json(&summary);
106        }
107    }
108
109    Ok(())
110}
111
112async fn build_setup(
113    spec: SpecId,
114    duration: f64,
115    rotation_path: Option<&Path>,
116    workspace_root: &Path,
117    handle: ResolverHandle,
118) -> Result<SimSetup, CliError> {
119    let rotation_script =
120        resolve_rotation_script(spec, rotation_path, workspace_root, &handle.resolver).await?;
121
122    let resolver: Box<DynDataResolver<'static>> = DynDataResolver::new_box(
123        OverlayResolver::new(handle.resolver)
124            .with_rotation_script(CLI_ROTATION_ID, &rotation_script),
125    );
126
127    let config = SimConfigIntent::patchwerk(spec, duration, CLI_ROTATION_ID);
128    let sim_config_toml = serialize_sim_config(&config)?;
129
130    Ok(SimSetup {
131        resolver,
132        sim_config_toml,
133    })
134}
135
136async fn resolve_rotation_script(
137    spec: SpecId,
138    rotation_path: Option<&Path>,
139    workspace_root: &Path,
140    resolver: &DynDataResolver<'_>,
141) -> Result<String, CliError> {
142    if let Some(path) = rotation_path {
143        return Ok(file::read_text(path)?);
144    }
145
146    if let Ok(script) = resolve_assisted_rotation_script(spec, resolver).await {
147        return Ok(script);
148    }
149
150    let assisted_id = assisted_rotation_id(spec);
151    let path = workspace_root
152        .join("crates")
153        .join("engine")
154        .join("examples")
155        .join("rotations")
156        .join(format!("{assisted_id}.json"));
157
158    file::read_text(&path).map_err(|source| {
159        CliError::rotation_unavailable(spec.slug().to_string(), path, source.into())
160    })
161}
162
163fn print_configuration(
164    spec: SpecId,
165    duration: f64,
166    iterations: u32,
167    num_threads: usize,
168    seed: Option<u64>,
169    resolver_source: &str,
170) {
171    out::banner(
172        "WoW Lab Engine",
173        crate::composition::EngineBuildMetadata::CURRENT.version(),
174    );
175    out::header("Configuration");
176    out::kv("Resolver", resolver_source);
177    out::kv("Spec", &super::presentation::spec_display_name(spec));
178    out::kv_fmt("Duration", format!("{duration:.0}s"));
179    out::kv_fmt("Iterations", fmt_integer(u64::from(iterations)));
180    out::kv_fmt("Threads", num_threads);
181
182    if let Some(s) = seed {
183        out::kv_fmt("Seed", s);
184    }
185
186    out::blank();
187}