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