veloren_world/site/
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 site 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.
189// TODO: This is a bad idea, site names could conflict
190pub struct SitesGenMeta {
191    seed: u32,
192    sites: DHashMap<String, GenSite>,
193}
194
195fn append_statstr_to_file(file_path: &str, statstr: &str) -> std::io::Result<()> {
196    let mut file = OpenOptions::new()
197        .append(true)
198        .create(true)
199        .open(file_path)?;
200    file.write_all(statstr.as_bytes())?;
201    Ok(())
202}
203
204fn get_bool_env_var(var_name: &str) -> bool {
205    match env::var(var_name).ok().as_deref() {
206        Some("true") => true,
207        Some("false") => false,
208        _ => false,
209    }
210}
211
212fn get_log_opts() -> (bool, Option<String>) {
213    let site_generation_stats_verbose = get_bool_env_var("SITE_GENERATION_STATS_VERBOSE");
214    let site_generation_stats_file_path: Option<String> =
215        env::var("SITE_GENERATION_STATS_LOG").ok();
216    (
217        site_generation_stats_verbose,
218        site_generation_stats_file_path,
219    )
220}
221
222impl SitesGenMeta {
223    pub fn new(seed: u32) -> Self {
224        Self {
225            seed,
226            sites: DHashMap::default(),
227        }
228    }
229
230    pub fn add<'a>(&mut self, site_name: impl Into<Option<&'a str>>, kind: GenStatSiteKind) {
231        let site_name = site_name.into().unwrap_or("");
232        self.sites
233            .entry(site_name.to_owned())
234            .or_insert_with(|| GenSite::new(kind, site_name));
235    }
236
237    pub fn attempt<'a>(&mut self, site_name: impl Into<Option<&'a str>>, kind: GenStatPlotKind) {
238        let site_name = site_name.into().unwrap_or("");
239        if let Some(gensite) = self.sites.get_mut(site_name) {
240            gensite.attempt(kind);
241        } else {
242            error!("Site not found: {}", site_name);
243        }
244    }
245
246    pub fn success<'a>(&mut self, site_name: impl Into<Option<&'a str>>, kind: GenStatPlotKind) {
247        let site_name = site_name.into().unwrap_or("");
248        if let Some(gensite) = self.sites.get_mut(site_name) {
249            gensite.success(kind);
250        } else {
251            error!("Site not found: {}", site_name);
252        }
253    }
254
255    /// Log the site generation statistics.
256    /// Nothing is logged unless the RUST_LOG environment variable is set to
257    /// DEBUG. Two additional environment variables can be set to control
258    /// the output: SITE_GENERATION_STATS_VERBOSE: If set to true, the
259    /// output will include everything shown in the output format below.
260    /// If set to false or not set, only generation errors will be shown.
261    /// SITE_GENERATION_STATS_LOG: If set, the output will be appended to the
262    /// file at the path specified by this variable. The value must be a
263    /// valid absolute or relative path (from the current working directory),
264    /// including the file name. The file will be created if it does not
265    /// exist.
266    pub fn log(&self) {
267        // Get the current tracing log level
268        // This can be set with the RUST_LOG environment variable.
269        let current_log_level = tracing::level_filters::LevelFilter::current();
270        if current_log_level == tracing::Level::DEBUG {
271            let (verbose, log_path) = get_log_opts();
272
273            /*
274               For each world generated, gather this information:
275                   seed
276                   Number of sites generated
277                   Number of each site kind generated
278                   For Each Site
279                       Number of plots generated (success/attempts)
280                       Number of each plot kind generated
281
282               Output format
283                   ------------------ SitesGenMeta seed  12345
284                   Number of sites: 7
285                       Terracotta: 5
286                       Myrmidon: 2
287                       City: 8
288                   Terracotta <Town Name>
289                       Number of plots: 4
290                           InitialPlaza: 1/1
291                           Plaza: 1/3
292                           House: 1/1
293                           ...
294                       GenErrors
295                       GenWarnings
296                   City <Town Name>
297                       Number of plots: 4
298                           InitialPlaza: 1/1
299                           Plaza: 1/3
300                           House: 1/1
301                           ...
302                       GenErrors
303                       GenWarnings
304            */
305            let mut num_sites: u32 = 0;
306            let mut site_counts: DHashMap<GenStatSiteKind, u32> = DHashMap::default();
307            let mut stat_stat_str = String::new();
308            for (_, gensite) in self.sites.iter() {
309                num_sites += 1;
310                *site_counts.entry(*gensite.kind()).or_insert(0) += 1;
311            }
312            stat_stat_str.push_str(&format!(
313                "------------------ SitesGenMeta seed {}\n",
314                self.seed
315            ));
316            if verbose {
317                stat_stat_str.push_str(&format!("Sites: {}\n", num_sites));
318                for (site_kind, count) in site_counts.iter() {
319                    stat_stat_str.push_str(&format!("  {}: {}\n", site_kind, count));
320                }
321            }
322            for (site_name, gensite) in self.sites.iter() {
323                let mut stat_err_str = String::new();
324                let mut stat_warn_str = String::new();
325                let mut num_plots: u32 = 0;
326                let mut plot_counts: DHashMap<GenStatPlotKind, (u32, u32)> = DHashMap::default();
327                for (plotkind, genplot) in gensite.stats.iter() {
328                    num_plots += 1;
329                    plot_counts.entry(*plotkind).or_insert((0, 0)).0 += genplot.successful;
330                    plot_counts.entry(*plotkind).or_insert((0, 0)).1 += genplot.attempts;
331                }
332                match &gensite.kind() {
333                    GenStatSiteKind::Terracotta => {
334                        for (kind, genplot) in gensite.stats.iter() {
335                            match &kind {
336                                GenStatPlotKind::InitialPlaza => {
337                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
338                                },
339                                GenStatPlotKind::Plaza => {
340                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
341                                },
342                                GenStatPlotKind::House => {
343                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
344                                    gensite.success_rate(0.1, kind, genplot, &mut stat_warn_str);
345                                },
346                                GenStatPlotKind::Yard => {
347                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
348                                },
349                                _ => {},
350                            }
351                        }
352                    },
353                    GenStatSiteKind::Myrmidon => {
354                        for (kind, genplot) in gensite.stats.iter() {
355                            match &kind {
356                                GenStatPlotKind::InitialPlaza => {
357                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
358                                },
359                                GenStatPlotKind::Plaza => {
360                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
361                                },
362                                GenStatPlotKind::House => {
363                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
364                                    gensite.success_rate(0.1, kind, genplot, &mut stat_warn_str);
365                                },
366                                _ => {},
367                            }
368                        }
369                    },
370                    GenStatSiteKind::City => {
371                        for (kind, genplot) in gensite.stats.iter() {
372                            match &kind {
373                                GenStatPlotKind::InitialPlaza => {
374                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
375                                },
376                                GenStatPlotKind::Plaza => {
377                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
378                                },
379                                GenStatPlotKind::Workshop => {
380                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
381                                },
382                                GenStatPlotKind::House => {
383                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
384                                    gensite.success_rate(0.2, kind, genplot, &mut stat_warn_str);
385                                },
386                                _ => {},
387                            }
388                        }
389                    },
390                    GenStatSiteKind::CliffTown => {
391                        for (kind, genplot) in gensite.stats.iter() {
392                            match &kind {
393                                GenStatPlotKind::InitialPlaza => {
394                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
395                                },
396                                GenStatPlotKind::Plaza => {
397                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
398                                },
399                                GenStatPlotKind::House => {
400                                    gensite.at_least(5, kind, genplot, &mut stat_err_str);
401                                    gensite.success_rate(0.5, kind, genplot, &mut stat_warn_str);
402                                },
403                                GenStatPlotKind::AirshipDock => {
404                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
405                                    gensite.success_rate(0.1, kind, genplot, &mut stat_warn_str);
406                                },
407                                _ => {},
408                            }
409                        }
410                    },
411                    GenStatSiteKind::SavannahTown => {
412                        for (kind, genplot) in gensite.stats.iter() {
413                            match &kind {
414                                GenStatPlotKind::InitialPlaza => {
415                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
416                                },
417                                GenStatPlotKind::Plaza => {
418                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
419                                },
420                                GenStatPlotKind::Workshop => {
421                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
422                                },
423                                GenStatPlotKind::House => {
424                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
425                                    gensite.success_rate(0.5, kind, genplot, &mut stat_warn_str);
426                                },
427                                GenStatPlotKind::AirshipDock => {
428                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
429                                },
430                                _ => {},
431                            }
432                        }
433                    },
434                    GenStatSiteKind::CoastalTown => {
435                        for (kind, genplot) in gensite.stats.iter() {
436                            match &kind {
437                                GenStatPlotKind::InitialPlaza => {
438                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
439                                },
440                                GenStatPlotKind::Plaza => {
441                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
442                                },
443                                GenStatPlotKind::Workshop => {
444                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
445                                },
446                                GenStatPlotKind::House => {
447                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
448                                    gensite.success_rate(0.5, kind, genplot, &mut stat_warn_str);
449                                },
450                                GenStatPlotKind::AirshipDock => {
451                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
452                                    gensite.at_most(1, kind, genplot, &mut stat_err_str);
453                                },
454                                _ => {},
455                            }
456                        }
457                    },
458                    GenStatSiteKind::DesertCity => {
459                        for (kind, genplot) in gensite.stats.iter() {
460                            match &kind {
461                                GenStatPlotKind::InitialPlaza => {
462                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
463                                },
464                                GenStatPlotKind::Plaza => {
465                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
466                                },
467                                GenStatPlotKind::MultiPlot => {
468                                    gensite.at_least(1, kind, genplot, &mut stat_err_str);
469                                },
470                                GenStatPlotKind::Temple => {
471                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
472                                },
473                                GenStatPlotKind::AirshipDock => {
474                                    gensite.should_not_be_zero(kind, genplot, &mut stat_warn_str);
475                                },
476                                _ => {},
477                            }
478                        }
479                    },
480                }
481                if verbose {
482                    stat_stat_str.push_str(&format!("{} {}\n", gensite.kind(), site_name));
483                    stat_stat_str.push_str(&format!("  Number of plots: {}\n", num_plots));
484                    for (plotkind, count) in plot_counts.iter() {
485                        stat_stat_str
486                            .push_str(&format!("  {}: {}/{}\n", plotkind, count.0, count.1));
487                    }
488                }
489                if !stat_err_str.is_empty() {
490                    stat_stat_str.push_str(&stat_err_str.to_string());
491                }
492                if verbose && !stat_warn_str.is_empty() {
493                    stat_stat_str.push_str(&stat_warn_str.to_string());
494                }
495            }
496            debug!("{}", stat_stat_str);
497            if let Some(log_path) = log_path {
498                if let Err(e) = append_statstr_to_file(&log_path, &stat_stat_str) {
499                    eprintln!("Failed to write to file: {}", e);
500                } else {
501                    println!("Statistics written to {}", log_path);
502                }
503            }
504        }
505    }
506}