1mod accessors;
19mod compile;
20mod load;
21mod names;
22mod outputs;
23mod placeholders;
24
25pub use compile::compile;
26pub use load::{ResFile, Set};
27pub use names::{
28 accessor_name, default_key, name_problem, valid_locale, valid_name, valid_set_name,
29};
30pub use outputs::write_sources;
31
32use crate::diagnostics::Diagnostic;
33use std::collections::{BTreeMap, HashMap};
34use std::path::{Path, PathBuf};
35
36pub const NEUTRAL_LOCALE: &str = "en_us";
38pub const RESOURCES_DIR: &str = yog_ids::project::RESOURCES;
40pub const GENERATED_FILE: &str = ".yog-build/generated/resources.g.rs";
44pub const GENERATED_DIR: &str = ".yog-build/generated/resources";
46
47pub type Output = (String, Vec<u8>);
49
50pub use yog_formats::yres::{Entry, EntryObject, Kind, Value, parse, write};
51
52#[derive(Debug, Clone, Default)]
54pub struct Refs {
55 pub texts: HashMap<String, (String, String)>,
57 pub images: HashMap<String, String>,
59 pub colors: HashMap<String, u32>,
61 pub pictures: HashMap<String, Picture>,
63}
64
65impl Refs {
66 pub fn text(&self, r: &yog_data::Res) -> Result<&(String, String), String> {
68 self.texts
69 .get(&r.name())
70 .ok_or_else(|| format!("`{r}`: no text `{}` in resources/*.yres", r.name()))
71 }
72
73 pub fn color(&self, r: &yog_data::Res) -> Result<u32, String> {
75 self.colors
76 .get(&r.name())
77 .copied()
78 .ok_or_else(|| format!("`{r}`: no color `{}` in resources/*.yres", r.name()))
79 }
80
81 pub fn image(&self, r: &yog_data::Res) -> Result<&str, String> {
83 self.images
84 .get(&r.name())
85 .map(String::as_str)
86 .ok_or_else(|| format!("`{r}`: no image `{}` in resources/*.yres", r.name()))
87 }
88
89 pub fn key_of(&self, t: &yog_data::Text) -> Result<Option<String>, String> {
92 use yog_data::Text;
93 Ok(match t {
94 Text::Literal(_) => None,
95 Text::Lang(k) => Some(k.clone()),
96 Text::NameOf(r) => Some(yog_ids::lang_key(
97 &r.kind,
98 yog_formats::book::refs::bare(&r.id),
99 )),
100 Text::Res(r) => Some(self.text(r)?.0.clone()),
101 })
102 }
103}
104
105#[derive(Debug, Clone, Default, PartialEq)]
108pub struct Picture {
109 pub png: Vec<u8>,
110 pub output: String,
111}
112
113#[derive(Debug, Default)]
115pub struct Compiled {
116 pub outputs: Vec<Output>,
119 pub sources: BTreeMap<String, String>,
121 pub refs: Refs,
122 pub warnings: Vec<Diagnostic>,
123 pub files: usize,
125}
126
127pub fn find(root: &Path) -> Vec<PathBuf> {
129 crate::files::under(&root.join(RESOURCES_DIR), &|p| yog_ids::Format::Res.is(p))
130}
131
132pub fn value_problem(e: &Entry) -> Option<String> {
135 let media = |what: &str| format!("{:?}: {what} are bytes (`@bytes:…`)", e.name);
136 match (e.kind, &e.value) {
137 (Kind::Image | Kind::File | Kind::Sound, Value::Bytes(_))
138 if !yog_ids::valid_path(&e.name.replace('.', yog_ids::sep::PATH)) =>
139 {
140 Some(format!(
141 "{:?}: Minecraft resource paths are lower case — only a-z, 0-9, `_`, `-`, `.`",
142 e.name
143 ))
144 }
145 (Kind::Sound, Value::Bytes(b)) if !b.starts_with(yog_formats::yres::OGG_MAGIC) => {
146 Some(format!("{:?}: sounds are Ogg Vorbis", e.name))
147 }
148 (Kind::Image | Kind::File | Kind::Sound, Value::Bytes(_)) => None,
149 (Kind::Image, _) => Some(media("images")),
150 (Kind::File, _) => Some(media("files")),
151 (Kind::Sound, _) => Some(media("sounds")),
152 (Kind::Color, _) => parse_color(e.text()).is_none().then(|| {
153 format!(
154 "{:?}: `{}` isn't a color — #RRGGBB or #AARRGGBB",
155 e.name,
156 e.text()
157 )
158 }),
159 (Kind::Number, Value::Num(_))
160 | (Kind::Bool, Value::Bool(_))
161 | (Kind::String, Value::Str(_)) => None,
162 (Kind::Number, _) => Some(format!("{:?}: a number", e.name)),
163 (Kind::Bool, _) => Some(format!("{:?}: true or false", e.name)),
164 (Kind::String, _) => Some(format!("{:?}: a text", e.name)),
165 }
166}
167
168pub fn color_of(e: &Entry) -> Option<u32> {
170 (e.kind == Kind::Color)
171 .then(|| parse_color(e.text()))
172 .flatten()
173}
174
175fn parse_color(s: &str) -> Option<u32> {
177 crate::yui::color(s).filter(|_| s.trim().starts_with(crate::yui::HEX_MARK))
178}
179
180pub fn resolve_text(refs: &Refs, value: &str) -> Result<(String, Option<String>), String> {
183 match value.strip_prefix('@') {
184 Some(rest) if rest.starts_with('@') => Ok((rest.to_string(), None)),
185 Some(rest) => match refs.texts.get(rest) {
186 Some((key, text)) => Ok((text.clone(), Some(key.clone()))),
187 None => Err(format!("`{value}`: no text `{rest}` in resources/*.yres")),
188 },
189 None => Ok((value.to_string(), None)),
190 }
191}
192
193pub fn resolve_image(refs: &Refs, value: &str) -> Result<String, String> {
195 match value.strip_prefix('@') {
196 Some(rest) => refs
197 .images
198 .get(rest)
199 .cloned()
200 .ok_or_else(|| format!("`{value}`: no image `{rest}` in resources/*.yres")),
201 None => Ok(value.to_string()),
202 }
203}
204
205#[cfg(test)]
206mod tests;
207
208pub(crate) fn problem(msg: impl std::fmt::Display) -> crate::diagnostics::Diagnostic {
210 crate::diagnostics::Diagnostic::error(yog_ids::build_problem::RESOURCES, msg)
211}
212
213pub(crate) fn caveat(msg: impl std::fmt::Display) -> crate::diagnostics::Diagnostic {
215 crate::diagnostics::Diagnostic::warning(yog_ids::build_problem::RESOURCES, msg)
216}