1use 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#[derive(Debug, Clone)]
13pub struct ResFile {
14 pub path: PathBuf,
15 pub entries: Vec<Entry>,
16}
17
18#[derive(Debug, Clone)]
20pub struct Set {
21 pub name: String,
22 pub neutral: ResFile,
23 pub cultures: BTreeMap<String, ResFile>,
24}
25
26struct Read {
28 set: String,
29 locale: Option<String>,
30 file: ResFile,
31}
32
33pub(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
52fn 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
88fn 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
106fn 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
134fn 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}