1use 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
11pub(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 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(©)));
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
72pub(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 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
101pub(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
122pub 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
160struct Merging<'a> {
162 set: OutputSet,
163 root: &'a Path,
164 errors: Vec<Diagnostic>,
165}
166
167impl Merging<'_> {
168 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 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 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 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
271fn 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
293pub(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
300pub const TAGS_FILE: &str =
304 yog_ids::concatcp!(".yog-build/generated/tags.", yog_ids::Format::Data.ext());
305
306pub type TagLists = BTreeMap<String, BTreeMap<String, Vec<String>>>;
308
309pub 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#[derive(Default)]
320pub struct OutputSet {
321 items: Vec<Option<Output>>,
322 at: HashMap<String, usize>,
323}
324
325impl OutputSet {
326 pub(crate) 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(crate) fn contains(&self, path: &str) -> bool {
332 self.at.contains_key(path)
333 }
334
335 pub(crate) fn paths(&self) -> impl Iterator<Item = &String> {
336 self.at.keys()
337 }
338
339 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> {
349 self.items.into_iter().flatten().collect()
350 }
351}
352
353impl From<Vec<Output>> for OutputSet {
354 fn from(outputs: Vec<Output>) -> Self {
355 let mut set = OutputSet::default();
356 for (path, bytes) in outputs {
357 set.put(path, bytes);
358 }
359 set
360 }
361}