1mod accessors;
19mod compile;
20mod load;
21mod names;
22mod outputs;
23mod placeholders;
24mod resource_ids;
25mod script;
26
27pub use compile::compile;
28pub(crate) use names::{accessor_name, default_key, name_problem};
29pub use outputs::write_sources;
30
31use crate::diagnostics::Diagnostic;
32use std::collections::{BTreeMap, HashMap};
33use std::path::{Path, PathBuf};
34
35pub const NEUTRAL_LOCALE: &str = "en_us";
37pub(crate) const RESOURCES_DIR: &str = yog_ids::project::RESOURCES;
39pub const GENERATED_FILE: &str = yog_ids::concatcp!(
43 yog_ids::project::GENERATED,
44 yog_ids::sep::PATH,
45 "resources.g.rs"
46);
47pub const GENERATED_SCRIPT: &str = yog_ids::concatcp!(
49 yog_ids::project::GENERATED,
50 yog_ids::sep::PATH,
51 "res.g.",
52 yog_ids::Format::Script.ext()
53);
54pub const GENERATED_DIR: &str =
56 yog_ids::concatcp!(yog_ids::project::GENERATED, yog_ids::sep::PATH, "resources");
57
58pub(crate) type Output = (String, Vec<u8>);
60
61pub use yog_formats::yres::{Entry, Kind, Value, parse, write};
62
63#[derive(Debug, Clone, Default)]
65pub struct Refs {
66 pub(crate) texts: HashMap<String, (String, String)>,
68 pub(crate) images: HashMap<String, String>,
70 pub(crate) colors: HashMap<String, u32>,
72 pub(crate) pictures: HashMap<String, Picture>,
74 pub(crate) sounds: HashMap<String, String>,
76}
77
78impl Refs {
79 pub(crate) fn text(&self, r: &yog_data::Res) -> Result<&(String, String), String> {
81 self.texts
82 .get(&r.name())
83 .ok_or_else(|| format!("`{r}`: no text `{}` in resources/*.yres", r.name()))
84 }
85
86 pub(crate) fn sound(&self, r: &yog_data::Res) -> Result<&str, String> {
88 self.sounds
89 .get(&r.name())
90 .map(String::as_str)
91 .ok_or_else(|| format!("`{r}`: no sound `{}` in resources/*.yres", r.name()))
92 }
93
94 pub fn color(&self, r: &yog_data::Res) -> Result<u32, String> {
96 self.colors
97 .get(&r.name())
98 .copied()
99 .ok_or_else(|| format!("`{r}`: no color `{}` in resources/*.yres", r.name()))
100 }
101
102 pub(crate) fn key_of(&self, t: &yog_data::Text) -> Result<Option<String>, String> {
105 use yog_data::Text;
106 Ok(match t {
107 Text::Literal(_) => None,
108 Text::Lang(k) => Some(k.clone()),
109 Text::NameOf(r) => Some(yog_ids::lang_key(
110 &r.kind,
111 yog_formats::book::refs::bare(&r.id),
112 )),
113 Text::Res(r) => Some(self.text(r)?.0.clone()),
114 })
115 }
116}
117
118#[derive(Debug, Clone, Default, PartialEq)]
121pub(crate) struct Picture {
122 pub(crate) png: Vec<u8>,
123 pub(crate) output: String,
124}
125
126#[derive(Debug, Default)]
128pub struct Compiled {
129 pub outputs: Vec<Output>,
133 pub sources: BTreeMap<String, String>,
135 pub scripts: BTreeMap<String, (String, Vec<String>)>,
138 pub refs: Refs,
140 pub warnings: Vec<Diagnostic>,
142 pub files: usize,
144}
145
146pub(crate) fn find(root: &Path) -> Vec<PathBuf> {
148 yog_analysis::files::under(&root.join(RESOURCES_DIR), &|p| yog_ids::Format::Res.is(p))
149}
150
151pub(crate) fn value_problem(e: &Entry) -> Option<String> {
154 match (e.kind, &e.value) {
155 (k, Value::Bytes(_))
156 if k.holds_bytes()
157 && !yog_ids::valid_path(&e.name.replace('.', yog_ids::sep::PATH)) =>
158 {
159 Some(format!(
160 "{:?}: Minecraft resource paths are lower case — only a-z, 0-9, `_`, `-`, `.`",
161 e.name
162 ))
163 }
164 (k, Value::Bytes(_)) if k.holds_bytes() => None,
165 (k, _) if k.holds_bytes() => Some(format!(
166 "{:?}: a {} entry is bytes (`@bytes:…`)",
167 e.name,
168 k.name()
169 )),
170 (Kind::Color, _) => parse_color(e.text()).is_none().then(|| {
171 format!(
172 "{:?}: `{}` isn't a color — #RRGGBB or #AARRGGBB",
173 e.name,
174 e.text()
175 )
176 }),
177 (Kind::Number, Value::Num(_))
178 | (Kind::Bool, Value::Bool(_))
179 | (Kind::String, Value::Str(_)) => None,
180 (Kind::Number, _) => Some(format!("{:?}: a number", e.name)),
181 (Kind::Bool, _) => Some(format!("{:?}: true or false", e.name)),
182 (Kind::String, _) => Some(format!("{:?}: a text", e.name)),
183 (Kind::Image | Kind::File | Kind::Sound | Kind::Video, _) => None,
184 }
185}
186
187pub(crate) fn color_of(e: &Entry) -> Option<u32> {
189 (e.kind == Kind::Color)
190 .then(|| parse_color(e.text()))
191 .flatten()
192}
193
194fn parse_color(s: &str) -> Option<u32> {
196 crate::ui::yui::color(s).filter(|_| s.trim().starts_with(crate::ui::yui::HEX_MARK))
197}
198
199pub fn resolve_text(refs: &Refs, value: &str) -> Result<(String, Option<String>), String> {
202 if let Some(rest) = value.strip_prefix(yog_ids::sep::TYPED)
203 && rest.starts_with(yog_ids::sep::TYPED)
204 {
205 return Ok((rest.to_string(), None));
206 }
207 match value.strip_prefix(yog_ids::reference::RES) {
208 Some(rest) => match refs.texts.get(rest) {
209 Some((key, text)) => Ok((text.clone(), Some(key.clone()))),
210 None => Err(format!("`{value}`: no text `{rest}` in resources/*.yres")),
211 },
212 None => Ok((value.to_string(), None)),
213 }
214}
215
216pub fn resolve_image(refs: &Refs, value: &str) -> Result<String, String> {
218 match value.strip_prefix(yog_ids::reference::RES) {
219 Some(rest) => refs
220 .images
221 .get(rest)
222 .cloned()
223 .ok_or_else(|| format!("`{value}`: no image `{rest}` in resources/*.yres")),
224 None => Ok(value.to_string()),
225 }
226}
227
228#[cfg(test)]
229mod tests;
230
231pub(crate) fn problem(msg: impl std::fmt::Display) -> crate::diagnostics::Diagnostic {
233 crate::diagnostics::Diagnostic::error(
234 yog_data::Atom::new(yog_ids::build_problem::RESOURCES),
235 msg,
236 )
237}
238
239pub(crate) fn caveat(msg: impl std::fmt::Display) -> crate::diagnostics::Diagnostic {
241 crate::diagnostics::Diagnostic::warning(
242 yog_data::Atom::new(yog_ids::build_problem::RESOURCES),
243 msg,
244 )
245}