Skip to main content

yog_cli/yres/
mod.rs

1//! `.yres` resource files — a mod's texts, images, sounds, files, colors,
2//! numbers and flags, like a C# project's `Resources/*.resx`. See
3//! Yog-IDLE's `docs/yres.md` for the format.
4//!
5//! `yog build` reads `resources/**/*.yres`: `<Set>.yres` is the neutral
6//! (en_us) file of resource set `<Set>`, `<Set>.<locale>.yres` its culture
7//! variants (texts only). It
8//!
9//! - writes the texts into `assets/<mod id>/lang/<locale>.json` (merged with
10//!   a hand-written one), `{name}`/`{0}` placeholders turned into `%1$s`;
11//! - bundles image/file/sound entries' files under `assets/<mod id>/...`,
12//!   plus a `sounds.json` for the sounds;
13//! - generates `.yog-build/generated/resources/<Set>.g.rs` — a type per
14//!   set with a typed accessor per entry — and `resources.g.rs` including
15//!   them, pulled in as `crate::res` by `yog_api::resources!();`;
16//! - resolves `@<Set>.<name>` references for `.yui` screens and `yog.toml`.
17
18mod 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
36/// The locale of the neutral `<Set>.yres` files.
37pub const NEUTRAL_LOCALE: &str = "en_us";
38/// Where resource files live, under the mod root.
39pub const RESOURCES_DIR: &str = yog_ids::project::RESOURCES;
40/// Where the generated accessors go, under the mod root.
41/// The file `yog_api::resources!()` includes; it includes one
42/// `resources/<Set>.g.rs` per set (shown under its `.yres` in Yog-IDLE).
43pub const GENERATED_FILE: &str = ".yog-build/generated/resources.g.rs";
44/// Where the per-set `<Set>.g.rs` files go.
45pub const GENERATED_DIR: &str = ".yog-build/generated/resources";
46
47/// A generated file: path inside the `.yog` and its bytes.
48pub type Output = (String, Vec<u8>);
49
50pub use yog_formats::yres::{Entry, EntryObject, Kind, Value, parse, write};
51
52/// What `.yui` screens and `yog.toml` can refer to as `@<Set>.<name>`.
53#[derive(Debug, Clone, Default)]
54pub struct Refs {
55    /// `Set.name` → (translation key, neutral text).
56    pub texts: HashMap<String, (String, String)>,
57    /// `Set.name` → image source (`<mod id>:<texture>`).
58    pub images: HashMap<String, String>,
59    /// `Set.name` → color (`0xAARRGGBB`).
60    pub colors: HashMap<String, u32>,
61    /// `Set.name` → an image's picture, as the build writes it.
62    pub pictures: HashMap<String, Picture>,
63}
64
65impl Refs {
66    /// Text `r`: its translation key and neutral words.
67    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    /// Color `r` (`0xAARRGGBB`).
74    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    /// Image `r`'s source.
82    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    /// The translation key text `t` names — `@lang:key`, a thing's name, a
90    /// `.yres` text; none for words, which aren't one.
91    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/// An image's picture: its PNG and the file the build writes it to — which
106/// a texture packed into an atlas takes the place of.
107#[derive(Debug, Clone, Default, PartialEq)]
108pub struct Picture {
109    pub png: Vec<u8>,
110    pub output: String,
111}
112
113/// Everything `yog build` makes of a mod's resources.
114#[derive(Debug, Default)]
115pub struct Compiled {
116    /// Files to add to the `.yog` (lang JSON, images, sounds, sounds.json) —
117    /// they replace a hand-written asset at the same path (merged into them).
118    pub outputs: Vec<Output>,
119    /// `<Set>` → generated Rust source.
120    pub sources: BTreeMap<String, String>,
121    pub refs: Refs,
122    pub warnings: Vec<Diagnostic>,
123    /// Number of `.yres` files read.
124    pub files: usize,
125}
126
127/// Every `resources/**/*.yres` of the mod at `root`, sorted.
128pub fn find(root: &Path) -> Vec<PathBuf> {
129    crate::files::under(&root.join(RESOURCES_DIR), &|p| yog_ids::Format::Res.is(p))
130}
131
132/// What's wrong with `e`'s value for its type, if anything — the build's
133/// rule, which editors show too.
134pub 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
168/// A color entry's value as ARGB, when it is one.
169pub fn color_of(e: &Entry) -> Option<u32> {
170    (e.kind == Kind::Color)
171        .then(|| parse_color(e.text()))
172        .flatten()
173}
174
175/// `#RRGGBB` / `#AARRGGBB`: a `.yres` color is written with its `#`.
176fn parse_color(s: &str) -> Option<u32> {
177    crate::yui::color(s).filter(|_| s.trim().starts_with(crate::yui::HEX_MARK))
178}
179
180/// Resolves a `yog.toml` value: `@<Set>.<name>` of a text → (neutral text,
181/// Some(key)); anything else as is. Errors on an unknown reference.
182pub 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
193/// Resolves a `yog.toml` image value: `@<Set>.<name>` → its image source.
194pub 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
208/// A resources error (`YB0003`), where it is to be said by the caller.
209pub(crate) fn problem(msg: impl std::fmt::Display) -> crate::diagnostics::Diagnostic {
210    crate::diagnostics::Diagnostic::error(yog_ids::build_problem::RESOURCES, msg)
211}
212
213/// A resources warning (`YB0003`).
214pub(crate) fn caveat(msg: impl std::fmt::Display) -> crate::diagnostics::Diagnostic {
215    crate::diagnostics::Diagnostic::warning(yog_ids::build_problem::RESOURCES, msg)
216}