veloren_world/site2/
genstat.rs

1//! Data structures and functions for tracking site generation statistics.
2
3use crate::util::DHashMap;
4use std::{env, fmt, fs::OpenOptions, io::Write};
5use tracing::{debug, error};
6
7/// Plot kinds for site generation statistics.
8/// These are similar but discrete from the PlotKind enum in the site2 plot
9/// module. For tracking site generation, similar plot kinds are grouped by
10/// these enum variants. For example, the House variant includes all kinds of
11/// houses (e.g. House, CoastalHouse, DesertCityHouse).
12#[derive(Eq, Hash, PartialEq, Copy, Clone)]
13pub enum GenStatPlotKind {
14    InitialPlaza,
15    Plaza,
16    Workshop,
17    House,
18    GuardTower,
19    Castle,
20    AirshipDock,
21    Tavern,
22    Yard,
23    MultiPlot,
24    Temple,
25}
26
27impl fmt::Display for GenStatPlotKind {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        let s = match self {
30            GenStatPlotKind::InitialPlaza => "InitialPlaza",
31            GenStatPlotKind::Plaza => "Plaza",
32            GenStatPlotKind::Workshop => "Workshop",
33            GenStatPlotKind::House => "House",
34            GenStatPlotKind::GuardTower => "GuardTower",
35            GenStatPlotKind::Castle => "Castle",
36            GenStatPlotKind::AirshipDock => "AirshipDock",
37            GenStatPlotKind::Tavern => "Tavern",
38            GenStatPlotKind::Yard => "Yard",
39            GenStatPlotKind::MultiPlot => "MultiPlot",
40            GenStatPlotKind::Temple => "Temple",
41        };
42        write!(f, "{}", s)
43    }
44}
45
46/// Site kinds for site generation statistics.
47/// Only the sites that are tracked for generation statistics are included here,
48/// which includes all sites that use the find_roadside_aabr function.
49#[derive(Eq, Hash, PartialEq, Copy, Clone, Default)]
50pub enum GenStatSiteKind {
51    Terracotta,
52    Myrmidon,
53    #[default]
54    City,
55    CliffTown,
56    SavannahTown,
57    CoastalTown,
58    DesertCity,
59}
60
61impl fmt::Display for GenStatSiteKind {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        let s = match self {
64            GenStatSiteKind::Terracotta => "Terracotta",
65            GenStatSiteKind::Myrmidon => "Myrmidon",
66            GenStatSiteKind::City => "City",
67            GenStatSiteKind::CliffTown => "CliffTown",
68            GenStatSiteKind::SavannahTown => "SavannahTown",
69            GenStatSiteKind::CoastalTown => "CoastalTown",
70            GenStatSiteKind::DesertCity => "DesertCity",
71        };
72        write!(f, "{}", s)
73    }
74}
75
76/// Plot generation statistics.
77/// The attempts field increments each time a plot is attempted to be generated.
78/// An attempt is counted only once even if find_roadside_aabr is called
79/// multiple times.
80pub struct GenPlot {
81    attempts: u32,
82    successful: u32,
83}
84impl Default for GenPlot {
85    fn default() -> Self { Self::new() }
86}
87
88impl GenPlot {
89    pub fn new() -> Self {
90        Self {
91            attempts: 0,
92            successful: 0,
93        }
94    }
95
96    pub fn attempt(&mut self) { self.attempts += 1; }
97
98    pub fn success(&mut self) { self.successful += 1; }
99}
100
101/// Site generation statistics.
102pub struct GenSite {
103    kind: GenStatSiteKind,
104    name: String,
105    stats: DHashMap<GenStatPlotKind, GenPlot>,
106}
107
108impl GenSite {
109    pub fn new(kind: GenStatSiteKind, name: &str) -> Self {
110        Self {
111            kind,
112            name: name.to_owned(),
113            stats: DHashMap::default(),
114        }
115    }
116
117    pub fn kind(&self) -> &GenStatSiteKind { &self.kind }
118
119    pub fn attempt(&mut self, kind: GenStatPlotKind) {
120        self.stats.entry(kind).or_default().attempt();
121    }
122
123    pub fn success(&mut self, kind: GenStatPlotKind) {
124        self.stats.entry(kind).or_default().success();
125    }
126
127    fn at_least(
128        &self,
129        count: u32,
130        plotkind: &GenStatPlotKind,
131        genplot: &GenPlot,
132        statstr: &mut String,
133    ) {
134        if genplot.successful < count {
135            statstr.push_str(&format!(
136                "  {} {} {}: {}/{} GenError: expected at least {}\n",
137                self.kind, self.name, plotkind, genplot.successful, genplot.attempts, count
138            ));
139        }
140    }
141
142    fn at_most(
143        &self,
144        count: u32,
145        plotkind: &GenStatPlotKind,
146        genplot: &GenPlot,
147        statstr: &mut String,
148    ) {
149        if genplot.successful > count {
150            statstr.push_str(&format!(
151                "  {} {} {}: {}/{} GenError: expected at most {}\n",
152                self.kind, self.name, plotkind, genplot.successful, genplot.attempts, count
153            ));
154        }
155    }
156
157    fn should_not_be_zero(
158        &self,
159        plotkind: &GenStatPlotKind,
160        genplot: &GenPlot,
161        statstr: &mut String,
162    ) {
163        if genplot.successful == 0 {
164            statstr.push_str(&format!(
165                "  {} {} {}: {}/{} GenWarn: should not be zero\n",
166                self.kind, self.name, plotkind, genplot.successful, genplot.attempts
167            ));
168        }
169    }
170
171    fn success_rate(
172        &self,
173        rate: f32,
174        plotkind: &GenStatPlotKind,
175        genplot: &GenPlot,
176        statstr: &mut String,
177    ) {
178        if (genplot.successful as f32 / genplot.attempts as f32) < rate {
179            statstr.push_str(&format!(
180                "  {} {} {}: GenWarn: success rate less than {} ({}/{})\n",
181                self.kind, self.name, plotkind, rate, genplot.successful, genplot.attempts
182            ));
183        }
184    }
185}
186
187/// World site generation statistics.
188/// The map is keyed by site name.
189pub struct SitesGenMeta {
190    seed: u32,
191    sites: DHashMap<String, GenSite>,
192}
193
194fn append_statstr_to_file(file_path: &str, statstr: &str) -> std::io::Result<()> {
195    let mut file = OpenOptions::new()
196        .append(true)
197        .create(true)
198        .open(file_path)?;
199    file.write_all(statstr.as_bytes())?;
200    Ok(())
201}
202
203fn get_bool_env_var(var_name: &str) -> bool {
204    match env::var(var_name).ok().as_deref() {
205        Some("true") => true,
206        Some("false") => false,
207        _ => false,
208    }
209}
210
211fn get_log_opts() -> (bool, Option<String>) {
212    let site_generation_stats_verbose = get_bool_env_var("SITE_GENERATION_STATS_VERBOSE");
213    let site_generation_stats_file_path: Option<String> =
214        env::var("SITE_GENERATION_STATS_LOG").ok();
215    (
216        site_generation_stats_verbose,
217        site_generation_stats_file_path,
218    )
219}
220
221impl SitesGenMeta {
222    pub fn new(seed: u32) -> Self {
223        Self {
224            seed,
225            sites: DHashMap::default(),
226        }
227    }
228
229    pub fn add(&mut self, site_name: &str, kind: GenStatSiteKind) {
230        self.sites
231            .entry(site_name.to_owned())
232            .or_insert_with(|| GenSite::new(kind, site_name));
233    }
234
235    pub fn attempt(&mut self, site_name: &String, kind: GenStatPlotKind) {
236        if let Some(gensite) = self.sites.get_mut(site_name) {
237            gensite.attempt(kind);
238        } else {
239            error!("Site not found: {}", site_name);
240        }
241    }
242
243    pub fn success(&mut self, site_name: &String, kind: GenStatPlotKind) {
244        if let Some(gensite) = self.sites.get_mut(site_name) {
245            gensite.success(kind);
246        } else {
247            error!("Site not found: {}", site_name);
248        }
249    }
250
251    /// Log the site generation statistics.
252    /// Nothing is logged unless the RUST_LOG environment variable is set to
253    /// DEBUG. Two additional environment variables can be set to control
254    /// the output: SITE_GENERATION_STATS_VERBOSE: If set to true, the
255    /// output will include everything shown in the output format below.
256    /// If set to false or not set, only generation errors will be shown.
257    /// SITE_GENERATION_STATS_LOG: If set, the output will be appended to the
258    /// file at the path specified by this variable. The value must be a
259    /// valid absolute or relative path (from the current working directory),
260    /// including the file name. The file will be created if it does not
261    /// exist.
262    pub fn log(&self) {
263        // Get the current tracing log level
264        // This can be set with the RUST_LOG environment variable.
265        let current_log_level = tracing::level_filters::LevelFilter::current();
266        if current_log_level == tracing::Level::DEBUG {
267            let (verbose, log_path) = get_log_opts();
268
269            /*
270               For each world generated, gather this information:
271                   seed
272                   Number of sites generated
273                   Number of each site kind generated
274                   For Each Site
275                       Number of plots generated (success/attempts)
276                       Number of each plot kind generated
277
278               Output format
279                   ------------------ SitesGenMeta seed  12345
280                   Number of sites: 7
281                       Terracotta: 5
282                       Myrmidon: 2
283                       City: 8
284                   Terracotta <Town Name>
285                       Number of plots: 4
286                           InitialPlaza: 1/1
287                           Plaza: 1/3
288                           House: 1/1
289                           ...
290                       GenErrors
291                       GenWarnings
292                   City <Town Name>
293                       Number of plots: 4
294                           InitialPlaza: 1/1
295                           Plaza: 1/3
296                           House: 1/1
297                           ...
298                       GenErrors
299                       GenWarnings
300            */
301            let mut num_sites: u32 = 0;
302            let mut site_counts: DHashMap<GenStatSiteKind, u32> = DHashMap::default();
303            let mut stat_stat_str = String::new();
304            for (_, gensite) in self.sites.iter() {
305                num_sites += 1;
306                *site_counts.entry(*gensite.kind()).or_insert(0) += 1;
307            }
308            stat_stat_str.push_str(&format!(
309                "------------------ SitesGenMeta seed {}\n",
310                self.seed
311            ));
312            if verbose {
313                stat_stat_str.push_str(&format!("Sites: {}\n", num_sites));
314                for (site_kind, count) in site_counts.iter() {
315                    stat_stat_str.push_str(&format!("  {}: {}\n", site_kind, count));
316                }
317            }
318            for (site_name, gensite) in self.sites.iter() {
319                let mut stat_err_str = String::new();
320                let mut stat_warn_str = String::new();
321                let mut num_plots: u32 = 0;
322                let mut plot_counts: DHashMap<GenStatPlotKind, (u32, u32)> = DHashMap::default();
323                for (plotkind, genplot) in gensite.stats.iter() {
324                    num_plots += 1;
325                    plot_counts.entry(*plotkind).or_insert((0, 0)).0 += genplot.successful;
326                    plot_counts.entry(*plotkind).or_insert((0, 0)).1 += genplot.attempts;
327                }
328                match &gensite.kind() {
329                    GenStatSiteKind::Terracotta => {
330                        for (kind, genplot) in gensite.stats.iter() {
331                            match &kind {
332                                GenStatPlotKind::InitialPlaza => {
333                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
334                                },
335                                GenStatPlotKind::Plaza => {
336                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
337                                },
338                                GenStatPlotKind::House => {
339                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
340                                    gensite.success_rate(0.1, kind, genplot, &mut stat_warn_str);
341                                },
342                                GenStatPlotKind::Yard => {
343                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
344                                },
345                                _ => {},
346                            }
347                        }
348                    },
349                    GenStatSiteKind::Myrmidon => {
350                        for (kind, genplot) in gensite.stats.iter() {
351                            match &kind {
352                                GenStatPlotKind::InitialPlaza => {
353                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
354                                },
355                                GenStatPlotKind::Plaza => {
356                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
357                                },
358                                GenStatPlotKind::House => {
359                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
360                                    gensite.success_rate(0.1, kind, genplot, &mut stat_warn_str);
361                                },
362                                _ => {},
363                            }
364                        }
365                    },
366                    GenStatSiteKind::City => {
367                        for (kind, genplot) in gensite.stats.iter() {
368                            match &kind {
369                                GenStatPlotKind::InitialPlaza => {
370                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
371                                },
372                                GenStatPlotKind::Plaza => {
373                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
374                                },
375                                GenStatPlotKind::Workshop => {
376                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
377                                },
378                                GenStatPlotKind::House => {
379                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
380                                    gensite.success_rate(0.2, kind, genplot, &mut stat_warn_str);
381                                },
382                                _ => {},
383                            }
384                        }
385                    },
386                    GenStatSiteKind::CliffTown => {
387                        for (kind, genplot) in gensite.stats.iter() {
388                            match &kind {
389                                GenStatPlotKind::InitialPlaza => {
390                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
391                                },
392                                GenStatPlotKind::Plaza => {
393                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
394                                },
395                                GenStatPlotKind::House => {
396                                    gensite.at_least(5, kind, genplot, &mut stat_err_str);
397                                    gensite.success_rate(0.5, kind, genplot, &mut stat_warn_str);
398                                },
399                                GenStatPlotKind::AirshipDock => {
400                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
401                                    gensite.success_rate(0.1, kind, genplot, &mut stat_warn_str);
402                                },
403                                _ => {},
404                            }
405                        }
406                    },
407                    GenStatSiteKind::SavannahTown => {
408                        for (kind, genplot) in gensite.stats.iter() {
409                            match &kind {
410                                GenStatPlotKind::InitialPlaza => {
411                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
412                                },
413                                GenStatPlotKind::Plaza => {
414                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
415                                },
416                                GenStatPlotKind::Workshop => {
417                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
418                                },
419                                GenStatPlotKind::House => {
420                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
421                                    gensite.success_rate(0.5, kind, genplot, &mut stat_warn_str);
422                                },
423                                GenStatPlotKind::AirshipDock => {
424                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
425                                },
426                                _ => {},
427                            }
428                        }
429                    },
430                    GenStatSiteKind::CoastalTown => {
431                        for (kind, genplot) in gensite.stats.iter() {
432                            match &kind {
433                                GenStatPlotKind::InitialPlaza => {
434                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
435                                },
436                                GenStatPlotKind::Plaza => {
437                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
438                                },
439                                GenStatPlotKind::Workshop => {
440                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
441                                },
442                                GenStatPlotKind::House => {
443                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
444                                    gensite.success_rate(0.5, kind, genplot, &mut stat_warn_str);
445                                },
446                                GenStatPlotKind::AirshipDock => {
447                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
448                                    gensite.at_most(1, kind, genplot, &mut stat_err_str);
449                                },
450                                _ => {},
451                            }
452                        }
453                    },
454                    GenStatSiteKind::DesertCity => {
455                        for (kind, genplot) in gensite.stats.iter() {
456                            match &kind {
457                                GenStatPlotKind::InitialPlaza => {
458                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
459                                },
460                                GenStatPlotKind::Plaza => {
461                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
462                                },
463                                GenStatPlotKind::MultiPlot => {
464                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
465                                },
466                                GenStatPlotKind::Temple => {
467                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
468                                },
469                                GenStatPlotKind::AirshipDock => {
470                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
471                                },
472                                _ => {},
473                            }
474                        }
475                    },
476                }
477                if verbose {
478                    stat_stat_str.push_str(&format!("{} {}\n", gensite.kind(), site_name));
479                    stat_stat_str.push_str(&format!("  Number of plots: {}\n", num_plots));
480                    for (plotkind, count) in plot_counts.iter() {
481                        stat_stat_str
482                            .push_str(&format!("  {}: {}/{}\n", plotkind, count.0, count.1));
483                    }
484                }
485                if !stat_err_str.is_empty() {
486                    stat_stat_str.push_str(&stat_err_str.to_string());
487                }
488                if verbose && !stat_warn_str.is_empty() {
489                    stat_stat_str.push_str(&stat_warn_str.to_string());
490                }
491            }
492            debug!("{}", stat_stat_str);
493            if let Some(log_path) = log_path {
494                if let Err(e) = append_statstr_to_file(&log_path, &stat_stat_str) {
495                    eprintln!("Failed to write to file: {}", e);
496                } else {
497                    println!("Statistics written to {}", log_path);
498                }
499            }
500        }
501    }
502}