Skip to main content

yog_cli/content/
outputs.rs

1//! Merging what every file produced: tags, lang files, per-version overlays.
2
3use crate::content::{Compiled, Output, datapack, pretty, refs};
4use serde_json::{Map, Value};
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6use std::path::Path;
7use yog_ids::tag_file as tf;
8
9/// The mod's data names vanilla content as its datapack version does; for
10/// each other version that calls some of it otherwise, an overlay of the
11/// file with those names (`when/any/<version>/…`). What a version hasn't
12/// got under any name is said in a warning.
13pub(super) fn version_overlays(out: &mut Compiled, datapack: &str) {
14    fn map(v: &mut Value, version: &str, missing: &mut BTreeSet<String>) -> bool {
15        match v {
16            Value::String(s) if yog_ids::game_path(s).is_some() => {
17                let id = s.clone();
18                let eq = refs::equivalent(refs::RefKind::Item, &id, version)
19                    .or_else(|| refs::equivalent(refs::RefKind::Block, &id, version));
20                match eq {
21                    Some(e) if e != id => {
22                        *s = e;
23                        true
24                    }
25                    Some(_) => false,
26                    // Not an id Yog knows (a loot function, a tag), or one this version hasn't got.
27                    None => {
28                        if refs::is_vanilla(&id) {
29                            missing.insert(id);
30                        }
31                        false
32                    }
33                }
34            }
35            Value::Array(a) => a
36                .iter_mut()
37                .fold(false, |c, x| map(x, version, missing) | c),
38            Value::Object(o) => o
39                .values_mut()
40                .fold(false, |c, x| map(x, version, missing) | c),
41            _ => false,
42        }
43    }
44    let mut overlays = Vec::new();
45    for (path, bytes) in &out.outputs {
46        if !path.starts_with(&format!("{}{}", yog_ids::pack::DATA, yog_ids::sep::PATH))
47            || !path.ends_with(yog_ids::res::JSON)
48        {
49            continue;
50        }
51        let Ok(json) = serde_json::from_slice::<Value>(bytes) else {
52            continue;
53        };
54        for version in refs::versions().iter().filter(|v| *v != datapack) {
55            let mut copy = json.clone();
56            let mut missing = BTreeSet::new();
57            if map(&mut copy, version, &mut missing) {
58                overlays.push((datapack::for_version(version, path), pretty(&copy)));
59            }
60            for id in missing {
61                out.warnings.push(format!(
62                    "{path} names {id}, which Minecraft {version} hasn't got"
63                ));
64            }
65        }
66    }
67    out.outputs.extend(overlays);
68}
69
70/// Writes [`TAGS_FILE`]: `{ item: { "ns:path": ["ns:id", "#ns:tag", …] }, block: … }`.
71pub(super) fn write_tags(root: &Path, compiled: &Compiled) -> Result<(), String> {
72    use yog_ids::pack::{DATA, TAGS};
73    use yog_ids::sep::PATH;
74    let data_dir = format!("{DATA}{PATH}");
75    let tags_dir = format!("{PATH}{TAGS}{PATH}");
76    let mut by_kind = TagLists::new();
77    for (path, entries) in &compiled.tags {
78        // `[when/<loader>/<version>/]data/<ns>/tags/<kind>/<path>.json`
79        let data = path
80            .find(&data_dir)
81            .map_or(path.as_str(), |i| &path[i + data_dir.len()..]);
82        let Some((ns, rest)) = data.split_once(&tags_dir) else {
83            continue;
84        };
85        let Some((kind, tag)) = rest.split_once(PATH) else {
86            continue;
87        };
88        let tag = tag.trim_end_matches(yog_ids::res::JSON);
89        by_kind
90            .entry(kind.to_string())
91            .or_default()
92            .entry(yog_ids::join(ns, tag))
93            .or_default()
94            .extend(entries.keys().cloned());
95    }
96    crate::write_if_changed(&root.join(TAGS_FILE), yog_data::to_bytes(&by_kind))
97}
98
99/// A JSON object output at `path`: the one already in `outputs`, else the
100/// mod's own file there, else empty.
101pub(super) fn json_output(
102    outputs: &OutputSet,
103    root: &Path,
104    path: &str,
105) -> Result<Map<String, Value>, String> {
106    let read;
107    let bytes = match outputs.get(path) {
108        Some(b) => b,
109        None => match std::fs::read(root.join(path)) {
110            Ok(b) => {
111                read = b;
112                &read
113            }
114            Err(_) => return Ok(Map::new()),
115        },
116    };
117    serde_json::from_slice::<Map<String, Value>>(bytes).map_err(|e| format!("{path}: {e}"))
118}
119
120/// Adds the content's outputs to `outputs` (the `.yres` ones, say): lang
121/// entries and tags merged into what's there (or into the mod's own files),
122/// the rest appended.
123pub fn merge_outputs(
124    root: &Path,
125    outputs: &mut Vec<Output>,
126    compiled: Compiled,
127) -> Result<(), String> {
128    let mut m = Merging {
129        set: OutputSet::from(std::mem::take(outputs)),
130        root,
131        errors: Vec::new(),
132    };
133    if !compiled.lang_aliases.is_empty() {
134        m.lang_aliases(&compiled.lang_aliases);
135    }
136    for ((ns, locale), entries) in &compiled.lang {
137        m.lang(ns, locale, entries);
138    }
139    for (path, ids) in &compiled.tags {
140        m.tag(path, ids);
141    }
142    for (path, bytes) in compiled.outputs {
143        m.set.put(path, bytes);
144    }
145    *outputs = m.set.into_vec();
146    if m.errors.is_empty() {
147        Ok(())
148    } else {
149        Err(format!(
150            "merging blocks/items into the mod's files:\n  {}",
151            m.errors.join("\n  ")
152        ))
153    }
154}
155
156/// The outputs being merged into, the mod's root (its own files), the problems.
157struct Merging<'a> {
158    set: OutputSet,
159    root: &'a Path,
160    errors: Vec<String>,
161}
162
163impl Merging<'_> {
164    /// The JSON object at `path` (an output, the mod's own file, or empty); a
165    /// problem noted when it won't read.
166    fn object(&mut self, path: &str) -> Option<Map<String, Value>> {
167        json_output(&self.set, self.root, path)
168            .map_err(|e| self.errors.push(e))
169            .ok()
170    }
171
172    /// `.yres` texts under the keys the game looks up: in every lang file
173    /// (the outputs' and the mod's own) that has the text.
174    fn lang_aliases(&mut self, aliases: &[(String, String)]) {
175        let mut files: BTreeSet<String> = self
176            .set
177            .paths()
178            .filter(|p| is_lang_path(p))
179            .cloned()
180            .collect();
181        files.extend(own_lang_files(self.root));
182        for path in files {
183            let Some(mut map) = self.object(&path) else {
184                continue;
185            };
186            let mut changed = false;
187            for (from, to) in aliases {
188                if let Some(text) = map.get(from).cloned()
189                    && !map.contains_key(to)
190                {
191                    map.insert(to.clone(), text);
192                    changed = true;
193                }
194            }
195            if changed {
196                self.set.put(path, pretty(&Value::Object(map)));
197            }
198        }
199    }
200
201    /// A namespace's entries in a locale's lang file, under what's there.
202    fn lang(
203        &mut self,
204        ns: &str,
205        locale: &str,
206        entries: &std::collections::BTreeMap<String, String>,
207    ) {
208        let path = yog_ids::asset_file(ns, &yog_ids::lang_file(locale));
209        let Some(mut map) = self.object(&path) else {
210            return;
211        };
212        for (k, v) in entries {
213            map.entry(k.clone())
214                .or_insert_with(|| Value::String(v.clone()));
215        }
216        self.set.put(path, pretty(&Value::Object(map)));
217    }
218
219    /// A tag file's entries (`required` false: kept only if present) added
220    /// to what's there. The mod's own file may use the pre-1.21 spelling; the
221    /// loader maps both to one file, so it's merged into whichever it has.
222    fn tag(&mut self, path: &str, ids: &std::collections::BTreeMap<String, bool>) {
223        let block_tags = |dir: &str| {
224            format!(
225                "{}{}{dir}{}",
226                yog_ids::sep::PATH,
227                yog_ids::pack::TAGS,
228                yog_ids::sep::PATH
229            )
230        };
231        let plural = path.replacen(
232            &block_tags(yog_ids::kind::BLOCK),
233            &block_tags(yog_ids::word::BLOCKS),
234            1,
235        );
236        let path = if !self.set.contains(path) && self.root.join(&plural).is_file() {
237            plural
238        } else {
239            path.to_string()
240        };
241        let Some(mut map) = self.object(&path) else {
242            return;
243        };
244        if let Value::Array(list) = map
245            .entry(tf::VALUES)
246            .or_insert_with(|| Value::Array(Vec::new()))
247        {
248            for (id, &required) in ids {
249                let listed = |v: &Value| {
250                    v.as_str().or_else(|| v.get(tf::ID).and_then(Value::as_str))
251                        == Some(id.as_str())
252                };
253                if !list.iter().any(listed) {
254                    list.push(if required {
255                        Value::String(id.clone())
256                    } else {
257                        serde_json::json!({ tf::ID: id, tf::REQUIRED: false })
258                    });
259                }
260            }
261        }
262        map.entry(tf::REPLACE).or_insert(Value::Bool(false));
263        self.set.put(path, pretty(&Value::Object(map)));
264    }
265}
266
267/// The mod's own lang files (`assets/<ns>/lang/<locale>.json`), as paths from `root`.
268fn own_lang_files(root: &Path) -> Vec<String> {
269    let mut out = Vec::new();
270    let Ok(dirs) = std::fs::read_dir(root.join(yog_ids::pack::ASSETS)) else {
271        return out;
272    };
273    for ns in dirs.flatten() {
274        let Ok(files) = std::fs::read_dir(ns.path().join(yog_ids::res::LANG)) else {
275            continue;
276        };
277        for f in files.flatten() {
278            if let Ok(rel) = f.path().strip_prefix(root) {
279                let rel = rel.to_string_lossy().replace('\\', yog_ids::sep::PATH);
280                if is_lang_path(&rel) {
281                    out.push(rel);
282                }
283            }
284        }
285    }
286    out
287}
288
289/// Whether `p` is a lang file: `assets/<ns>/lang/<locale>.json`.
290pub(super) fn is_lang_path(p: &str) -> bool {
291    let lang = yog_ids::res::LANG.trim_end_matches(yog_ids::sep::PATH);
292    matches!(p.split(yog_ids::sep::PATH).collect::<Vec<_>>().as_slice(),
293        [assets, _, dir, file] if *assets == yog_ids::pack::ASSETS && *dir == lang && file.ends_with(yog_ids::res::JSON))
294}
295
296/// Where the mod's tags are listed for tools (`ns:path` → its entries, by
297/// kind), as binary Yog Data — the IDE shows a tag's items from here, as the
298/// build makes them.
299pub const TAGS_FILE: &str =
300    yog_ids::concatcp!(".yog-build/generated/tags.", yog_ids::Format::Data.ext());
301
302/// The mod's tags as [`TAGS_FILE`] lists them: kind → tag → its entries.
303pub type TagLists = BTreeMap<String, BTreeMap<String, Vec<String>>>;
304
305/// [`TAGS_FILE`] of the mod at `root`, as the build last wrote it; empty without one.
306pub fn read_tags(root: &Path) -> TagLists {
307    std::fs::read(root.join(TAGS_FILE))
308        .ok()
309        .and_then(|b| yog_data::read_bytes::<TagLists>(&b).ok())
310        .unwrap_or_default()
311}
312
313/// Outputs in order, keyed by path: [`OutputSet::put`] replaces a path's
314/// output (moving it to the end) without rescanning the list.
315#[derive(Default)]
316pub struct OutputSet {
317    items: Vec<Option<Output>>,
318    at: HashMap<String, usize>,
319}
320
321impl OutputSet {
322    pub fn get(&self, path: &str) -> Option<&[u8]> {
323        let i = *self.at.get(path)?;
324        self.items[i].as_ref().map(|(_, b)| b.as_slice())
325    }
326
327    pub fn contains(&self, path: &str) -> bool {
328        self.at.contains_key(path)
329    }
330
331    pub fn paths(&self) -> impl Iterator<Item = &String> {
332        self.at.keys()
333    }
334
335    /// Sets `path`'s output, dropping the one it had.
336    pub fn put(&mut self, path: String, bytes: Vec<u8>) {
337        if let Some(i) = self.at.insert(path.clone(), self.items.len()) {
338            self.items[i] = None;
339        }
340        self.items.push(Some((path, bytes)));
341    }
342
343    pub fn into_vec(self) -> Vec<Output> {
344        self.items.into_iter().flatten().collect()
345    }
346}
347
348impl From<Vec<Output>> for OutputSet {
349    fn from(outputs: Vec<Output>) -> Self {
350        let mut set = OutputSet::default();
351        for (path, bytes) in outputs {
352            set.put(path, bytes);
353        }
354        set
355    }
356}