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//!   (sounds as Ogg Vorbis: `.ysound` events name them);
13//! - generates `obj/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;
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
35/// The locale of the neutral `<Set>.yres` files.
36pub const NEUTRAL_LOCALE: &str = "en_us";
37/// Where resource files live, under the mod root.
38pub(crate) const RESOURCES_DIR: &str = yog_ids::project::RESOURCES;
39/// Where the generated accessors go, under the mod root.
40/// The file `yog_api::resources!()` includes; it includes one
41/// `resources/<Set>.g.rs` per set (shown under its `.yres` in Yog-IDLE).
42pub const GENERATED_FILE: &str = yog_ids::concatcp!(
43    yog_ids::project::GENERATED,
44    yog_ids::sep::PATH,
45    "resources.g.rs"
46);
47/// The generated Yog Script of every set (`Res.<Set>.<Entry>`).
48pub 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);
54/// Where the per-set `<Set>.g.rs` files go.
55pub const GENERATED_DIR: &str =
56    yog_ids::concatcp!(yog_ids::project::GENERATED, yog_ids::sep::PATH, "resources");
57
58/// A generated file: path inside the `.yog` and its bytes.
59pub(crate) type Output = (String, Vec<u8>);
60
61pub use yog_formats::yres::{Entry, Kind, Value, parse, write};
62
63/// What `.yui` screens and `yog.toml` can refer to as `@<Set>.<name>`.
64#[derive(Debug, Clone, Default)]
65pub struct Refs {
66    /// `Set.name` → (translation key, neutral text).
67    pub(crate) texts: HashMap<String, (String, String)>,
68    /// `Set.name` → image source (`<mod id>:<texture>`).
69    pub(crate) images: HashMap<String, String>,
70    /// `Set.name` → color (`0xAARRGGBB`).
71    pub(crate) colors: HashMap<String, u32>,
72    /// `Set.name` → an image's picture, as the build writes it.
73    pub(crate) pictures: HashMap<String, Picture>,
74    /// `Set.name` → a sound's file as `sounds.json` names it (`<mod id>:<path>`).
75    pub(crate) sounds: HashMap<String, String>,
76}
77
78impl Refs {
79    /// Text `r`: its translation key and neutral words.
80    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    /// Sound `r`: its file as `sounds.json` names it.
87    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    /// Color `r` (`0xAARRGGBB`).
95    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    /// The translation key text `t` names — `@lang:key`, a thing's name, a
103    /// `.yres` text; none for words, which aren't one.
104    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/// An image's picture: its PNG and the file the build writes it to — which
119/// a texture packed into an atlas takes the place of.
120#[derive(Debug, Clone, Default, PartialEq)]
121pub(crate) struct Picture {
122    pub(crate) png: Vec<u8>,
123    pub(crate) output: String,
124}
125
126/// Everything `yog build` makes of a mod's resources.
127#[derive(Debug, Default)]
128pub struct Compiled {
129    /// Files to add to the `.yog` (lang JSON, images, sounds, sounds.json) —
130    /// they replace a hand-written asset at the same path (merged into them).
131    /// The files it made: `(path in the .yog, bytes)`.
132    pub outputs: Vec<Output>,
133    /// `<Set>` → generated Rust source.
134    pub sources: BTreeMap<String, String>,
135    /// `<Set>` → where it's read from and its Yog Script members
136    /// ([`GENERATED_SCRIPT`]).
137    pub scripts: BTreeMap<String, (String, Vec<String>)>,
138    /// What screens and `yog.toml` can refer to (`@<Set>.<name>`).
139    pub refs: Refs,
140    /// What to warn of.
141    pub warnings: Vec<Diagnostic>,
142    /// Number of `.yres` files read.
143    pub files: usize,
144}
145
146/// Every `resources/**/*.yres` of the mod at `root`, sorted.
147pub(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
151/// What's wrong with `e`'s value for its type, if anything — the build's
152/// rule, which editors show too.
153pub(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
187/// A color entry's value as ARGB, when it is one.
188pub(crate) fn color_of(e: &Entry) -> Option<u32> {
189    (e.kind == Kind::Color)
190        .then(|| parse_color(e.text()))
191        .flatten()
192}
193
194/// `#RRGGBB` / `#AARRGGBB`: a `.yres` color is written with its `#`.
195fn parse_color(s: &str) -> Option<u32> {
196    crate::ui::yui::color(s).filter(|_| s.trim().starts_with(crate::ui::yui::HEX_MARK))
197}
198
199/// Resolves a `yog.toml` value: `@<Set>.<name>` of a text → (neutral text,
200/// Some(key)); anything else as is. Errors on an unknown reference.
201pub 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
216/// Resolves a `yog.toml` image value: `@<Set>.<name>` → its image source.
217pub 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
231/// A resources error (`YB0003`), where it is to be said by the caller.
232pub(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
239/// A resources warning (`YB0003`).
240pub(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}