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