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