Skip to main content

yog_cli/yres/
load.rs

1//! Reading a mod's `.yres` files into sets: each one's neutral file and
2//! its culture variants.
3
4use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7use super::names::{name_problem, valid_locale, valid_set_name};
8use super::{Entry, Kind, NEUTRAL_LOCALE, find, parse};
9use crate::shown_path;
10
11/// One `.yres` file.
12#[derive(Debug, Clone)]
13pub struct ResFile {
14    pub path: PathBuf,
15    pub entries: Vec<Entry>,
16}
17
18/// A resource set: its neutral file and culture variants.
19#[derive(Debug, Clone)]
20pub struct Set {
21    pub name: String,
22    pub neutral: ResFile,
23    pub cultures: BTreeMap<String, ResFile>,
24}
25
26/// A `.yres` read: its set, its locale (none for the neutral file), the file.
27struct Read {
28    set: String,
29    locale: Option<String>,
30    file: ResFile,
31}
32
33/// Reads and checks every `.yres` of the mod — errors are `file:line: message`.
34pub(super) fn load(root: &Path, errors: &mut Vec<String>) -> BTreeMap<String, Set> {
35    let mut sets: BTreeMap<String, Set> = BTreeMap::new();
36    let mut cultures: Vec<Read> = Vec::new();
37    for path in find(root) {
38        let Some(read) = read_file(root, path, errors) else {
39            continue;
40        };
41        match read.locale {
42            None => add_neutral(root, &mut sets, read, errors),
43            Some(_) => cultures.push(read),
44        }
45    }
46    for read in cultures {
47        add_culture(root, &mut sets, read, errors);
48    }
49    sets
50}
51
52/// The file at `path`: its name says its set and locale, its entries'
53/// names are valid.
54fn read_file(root: &Path, path: PathBuf, errors: &mut Vec<String>) -> Option<Read> {
55    let shown = shown_path(root, &path);
56    let file_name = path
57        .file_name()
58        .unwrap_or_default()
59        .to_string_lossy()
60        .into_owned();
61    let (set, locale) = yog_formats::yres::split_file_name(&file_name)?;
62    if let Err(e) = check_name(set, locale) {
63        errors.push(format!("{shown}: {e}"));
64        return None;
65    }
66    let entries = match crate::files::read_source(&path)
67        .map_err(|e| e.to_string())
68        .and_then(|t| parse(&t))
69    {
70        Ok(entries) => entries,
71        Err(e) => {
72            errors.push(format!("{shown}:{}", e.trim_start_matches("line ")));
73            return None;
74        }
75    };
76    for e in &entries {
77        if let Some(problem) = name_problem(&e.name) {
78            errors.push(format!("{shown}:{}: {problem}", e.line));
79        }
80    }
81    Some(Read {
82        set: set.to_string(),
83        locale: locale.map(str::to_string),
84        file: ResFile { path, entries },
85    })
86}
87
88/// What's wrong with a file's set name and locale, if anything.
89fn check_name(set: &str, locale: Option<&str>) -> Result<(), String> {
90    if !valid_set_name(set) {
91        return Err(format!(
92            "`{set}` isn't a valid resource set name — use an identifier like Strings or Icons"
93        ));
94    }
95    match locale {
96        Some(NEUTRAL_LOCALE) => Err(format!(
97            "the neutral file {set}.yres is already {NEUTRAL_LOCALE} — put these texts there"
98        )),
99        Some(l) if !valid_locale(l) => Err(format!(
100            "`{l}` isn't a Minecraft locale code (lower case, like ru_ru or pt_br)"
101        )),
102        _ => Ok(()),
103    }
104}
105
106/// A set's neutral file: one a set.
107fn add_neutral(
108    root: &Path,
109    sets: &mut BTreeMap<String, Set>,
110    read: Read,
111    errors: &mut Vec<String>,
112) {
113    if let Some(other) = sets.get(&read.set) {
114        let (shown, other) = (
115            shown_path(root, &read.file.path),
116            shown_path(root, &other.neutral.path),
117        );
118        errors.push(format!(
119            "{shown}: resource set {} is also defined by {other}",
120            read.set
121        ));
122        return;
123    }
124    sets.insert(
125        read.set.clone(),
126        Set {
127            name: read.set,
128            neutral: read.file,
129            cultures: BTreeMap::new(),
130        },
131    );
132}
133
134/// A culture variant: of a set with a neutral file, one a locale, texts only.
135fn add_culture(
136    root: &Path,
137    sets: &mut BTreeMap<String, Set>,
138    read: Read,
139    errors: &mut Vec<String>,
140) {
141    let Read { set, locale, file } = read;
142    let locale = locale.unwrap_or_default();
143    let shown = shown_path(root, &file.path);
144    let Some(target) = sets.get_mut(&set) else {
145        errors.push(format!(
146            "{shown}: no neutral {set}.yres for this culture variant"
147        ));
148        return;
149    };
150    if let Some(other) = target.cultures.get(&locale) {
151        errors.push(format!(
152            "{shown}: {set} {locale} is also defined by {}",
153            shown_path(root, &other.path)
154        ));
155        return;
156    }
157    for e in file
158        .entries
159        .iter()
160        .filter(|e| e.kind != Kind::String || e.key.is_some())
161    {
162        errors.push(format!("{shown}:{}: {:?}: culture variants hold texts only (no type other than string, no `key`)", e.line, e.name));
163    }
164    target.cultures.insert(locale, file);
165}