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}