1use 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
9pub(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 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(©)));
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
70pub(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 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
99pub(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
120pub 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
156struct Merging<'a> {
158 set: OutputSet,
159 root: &'a Path,
160 errors: Vec<String>,
161}
162
163impl Merging<'_> {
164 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 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 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 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
267fn 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
289pub(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
296pub const TAGS_FILE: &str =
300 yog_ids::concatcp!(".yog-build/generated/tags.", yog_ids::Format::Data.ext());
301
302pub type TagLists = BTreeMap<String, BTreeMap<String, Vec<String>>>;
304
305pub 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#[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 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}