1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use yog_ids::Format;
12use yog_sync::{Mutex, OnceLock};
13
14use yog_data::Value;
15
16use crate::design::assets::{Resources, Vanilla};
17use crate::design::project::{self, mod_root};
18
19const FILE_KINDS: [(&str, &[Format]); 4] = [
22 (yog_ids::kind::ITEM, &[Format::Item, Format::Block]),
23 (yog_ids::kind::BLOCK, &[Format::Block]),
24 (yog_ids::kind::ENTITY, &[Format::Entity]),
25 (yog_ids::kind::RECIPE, &[Format::Recipe]),
26];
27
28const BODY_FORMATS: [Format; 2] = [Format::Model, Format::Anim];
31
32const RESOURCE_KINDS: [(&str, Format); 2] = [
35 (yog_ids::kind::TEXTURE, Format::Texture),
36 (yog_ids::kind::MATERIAL, Format::Material),
37];
38
39enum GameIds {
44 Lang {
46 kinds: &'static [&'static str],
47 dotted: bool,
48 },
49 LangPrefix(&'static str),
51 Data,
52 Keys(&'static str),
53}
54
55const SOUNDS_ASSET: &str = yog_ids::res::SOUNDS_JSON;
57const ATTRIBUTE_LANG_PREFIX: &str = "attribute.name.";
59const POTION_LANG_PREFIX: &str = "potion.effect.";
61
62const GAME_IDS: [(&str, GameIds); 12] = [
63 (
64 yog_ids::kind::ITEM,
65 GameIds::Lang {
66 kinds: &[yog_ids::kind::ITEM, yog_ids::kind::BLOCK],
67 dotted: false,
68 },
69 ),
70 (
71 yog_ids::kind::BLOCK,
72 GameIds::Lang {
73 kinds: &[yog_ids::kind::BLOCK],
74 dotted: false,
75 },
76 ),
77 (
78 yog_ids::kind::ENTITY,
79 GameIds::Lang {
80 kinds: &[yog_ids::kind::ENTITY],
81 dotted: false,
82 },
83 ),
84 (
85 yog_ids::kind::EFFECT,
86 GameIds::Lang {
87 kinds: &[yog_ids::kind::EFFECT],
88 dotted: false,
89 },
90 ),
91 (
92 yog_ids::kind::ATTRIBUTE,
93 GameIds::LangPrefix(ATTRIBUTE_LANG_PREFIX),
94 ),
95 (
96 yog_ids::kind::BIOME,
97 GameIds::Lang {
98 kinds: &[yog_ids::kind::BIOME],
99 dotted: false,
100 },
101 ),
102 (
103 ENCHANTMENT,
104 GameIds::Lang {
105 kinds: &[ENCHANTMENT],
106 dotted: false,
107 },
108 ),
109 (yog_ids::kind::LOOT_TABLE, GameIds::Data),
110 (yog_ids::kind::ADVANCEMENT, GameIds::Data),
111 (yog_ids::kind::DAMAGE_TYPE, GameIds::Data),
112 (yog_ids::kind::RECIPE, GameIds::Data),
113 (SOUND, GameIds::Keys(SOUNDS_ASSET)),
114];
115
116const ENCHANTMENT: &str = "enchantment";
118const SOUND: &str = "sound";
119
120const GAME_TAG_KINDS: [&str; 6] = [
122 yog_ids::kind::ITEM,
123 yog_ids::kind::BLOCK,
124 yog_ids::kind::ENTITY,
125 yog_ids::kind::FLUID,
126 yog_ids::kind::BIOME,
127 yog_ids::kind::DAMAGE_TYPE,
128];
129
130#[derive(Debug, Clone, PartialEq, yog_data::Schema)]
132pub struct OwnRef {
133 pub id: String,
134 pub file: String,
135}
136
137#[derive(Debug, Default, yog_data::Schema)]
139pub struct RefChoices {
140 pub own: Vec<OwnRef>,
141 pub external: Vec<String>,
143 pub game: Vec<String>,
145}
146
147#[derive(Debug, yog_data::Schema)]
149pub struct OwnDef {
150 pub id: String,
151 pub file: String,
152 pub value: Value,
153}
154
155fn files(root: &Path, format: Format) -> Vec<PathBuf> {
156 crate::files::of(&root.join(yog_ids::project::CONTENT), format)
157}
158
159fn read(file: &Path) -> Option<Value> {
161 yog_data::text::parse_value(&crate::files::read_source(file).ok()?)
162 .ok()
163 .filter(|v| matches!(v, Value::Map(_)))
164}
165
166pub fn refs(file: &Path, kind: &str) -> RefChoices {
170 let mut out = RefChoices::default();
171 if let Some(root) = mod_root(file) {
172 out.own = own(&root, kind);
173 out.external = external(&root, kind);
174 }
175 if body_format(kind).is_none() {
176 out.game = game(mod_root(file).as_deref(), kind);
177 }
178 out
179}
180
181fn body_format(kind: &str) -> Option<Format> {
183 Format::of(kind).filter(|f| BODY_FORMATS.contains(f))
184}
185
186fn own(root: &Path, kind: &str) -> Vec<OwnRef> {
187 let ns = crate::design::project::mod_id(root).unwrap_or_else(|| yog_ids::ns::MINECRAFT.into());
188 let mut out = Vec::new();
189 let tag_of = format!("{}{}", yog_ids::typed::TAG, yog_ids::sep::NAMESPACE);
190 if let Some(of) = kind.strip_prefix(tag_of.as_str()) {
191 for d in defs(root, yog_ids::kind::TAG) {
192 if d.value
193 .get(yog_ids::content_field::OF)
194 .and_then(Value::as_str)
195 == Some(of)
196 {
197 out.push(OwnRef {
198 id: d.id,
199 file: d.file,
200 });
201 }
202 }
203 return out;
204 }
205 let by_path = body_format(kind)
207 .map(|f| (yog_ids::project::CONTENT, f))
208 .or_else(|| {
209 RESOURCE_KINDS
210 .iter()
211 .find(|(k, _)| *k == kind)
212 .map(|(_, f)| (yog_ids::project::RESOURCES, *f))
213 });
214 if let Some((dir, format)) = by_path {
215 let dir = root.join(dir);
216 for f in crate::files::of(&dir, format) {
217 let rel = f.strip_prefix(&dir).unwrap_or(&f).with_extension("");
218 out.push(OwnRef {
219 id: yog_ids::join(
220 &ns,
221 &rel.to_string_lossy().replace('\\', yog_ids::sep::PATH),
222 ),
223 file: f.to_string_lossy().into_owned(),
224 });
225 }
226 return out;
227 }
228 if let Some((_, exts)) = FILE_KINDS.iter().find(|(k, _)| *k == kind) {
229 for format in *exts {
230 for f in files(root, *format) {
231 let given = read(&f).and_then(|d| {
232 d.get(yog_ids::content_field::ID)
233 .and_then(Value::as_str)
234 .map(str::to_string)
235 });
236 let id = given.unwrap_or_else(|| {
237 f.file_stem()
238 .unwrap_or_default()
239 .to_string_lossy()
240 .into_owned()
241 });
242 out.push(OwnRef {
243 id: yog_ids::qualify_in(&ns, &id),
244 file: f.to_string_lossy().into_owned(),
245 });
246 }
247 }
248 } else {
249 out = defs(root, kind)
250 .into_iter()
251 .map(|d| OwnRef {
252 id: d.id,
253 file: d.file,
254 })
255 .collect();
256 }
257 out.sort_by(|a, b| a.id.cmp(&b.id));
258 out.dedup_by(|a, b| a.id == b.id);
259 out
260}
261
262fn defs(root: &Path, kind: &str) -> Vec<OwnDef> {
264 let ns = crate::design::project::mod_id(root).unwrap_or_else(|| yog_ids::ns::MINECRAFT.into());
265 let mut out = Vec::new();
266 for f in files(root, Format::Def) {
267 let Some(doc) = read(&f) else { continue };
268 if doc
269 .get(yog_ids::content_field::KIND)
270 .and_then(Value::as_str)
271 != Some(kind)
272 {
273 continue;
274 }
275 let Some(id) = doc.get(yog_ids::content_field::ID).and_then(Value::as_str) else {
276 continue;
277 };
278 out.push(OwnDef {
279 id: yog_ids::qualify_in(&ns, id),
280 file: f.to_string_lossy().into_owned(),
281 value: doc,
282 });
283 }
284 out
285}
286
287pub fn own_defs(file: &Path, kind: &str) -> Vec<OwnDef> {
289 mod_root(file).map(|r| defs(&r, kind)).unwrap_or_default()
290}
291
292fn external(root: &Path, kind: &str) -> Vec<String> {
293 let Some(kind) = crate::content::refs::RefKind::from_word(kind) else {
294 return Vec::new();
295 };
296 let Ok(manifest) = crate::manifest::YogToml::read(&project::path(root)) else {
297 return Vec::new();
298 };
299 let mut out = Vec::new();
300 for external in &manifest.externals {
301 for (_, ids) in external.ids.iter().filter(|(k, _)| *k == kind) {
302 out.extend(
303 ids.iter()
304 .map(|id| yog_ids::qualify_in(&external.namespace, id)),
305 );
306 }
307 }
308 out
309}
310
311fn game(root: Option<&Path>, kind: &str) -> Vec<String> {
313 let tag_of = format!("{}{}", yog_ids::typed::TAG, yog_ids::sep::NAMESPACE);
314 let tags = kind
315 .strip_prefix(tag_of.as_str())
316 .filter(|of| GAME_TAG_KINDS.contains(of))
317 .map(yog_ids::registry_dirs);
318 let how = GAME_IDS.iter().find(|(k, _)| *k == kind).map(|(_, h)| h);
319 if tags.is_none() && how.is_none() {
320 return Vec::new();
321 }
322 let Some(vanilla) = Resources::for_mod(root).vanilla else {
323 return Vec::new();
324 };
325 type Cache = Mutex<HashMap<(PathBuf, String), Vec<String>>>;
326 static CACHE: OnceLock<Cache> = OnceLock::new();
327 let key = (vanilla.jar.clone(), kind.to_string());
328 if let Some(hit) = CACHE
329 .get_or_init(Default::default)
330 .lock()
331 .get(&key)
332 .cloned()
333 {
334 return hit;
335 }
336 let mut out: Vec<String> = match (tags, how) {
337 (Some(dirs), _) => data_ids(
338 &vanilla,
339 dirs.iter()
340 .map(|d| format!("{}/{d}", yog_ids::pack::TAGS))
341 .collect(),
342 ),
343 (None, None) => Vec::new(),
344 (None, Some(GameIds::Lang { kinds, dotted })) => {
345 let prefixes = kinds
346 .iter()
347 .map(|k| yog_ids::lang_prefix(k, yog_ids::ns::MINECRAFT))
348 .collect();
349 lang_ids(&vanilla, kind, prefixes, *dotted)
350 }
351 (None, Some(GameIds::LangPrefix(prefix))) => {
352 lang_ids(&vanilla, kind, vec![prefix.to_string()], true)
353 }
354 (None, Some(GameIds::Data)) => data_ids(&vanilla, yog_ids::registry_dirs(kind)),
355 (None, Some(GameIds::Keys(asset))) => {
356 asset_keys(&vanilla, &format!("{}/{asset}", yog_ids::ns::MINECRAFT))
357 .keys()
358 .map(|k| game_id(k))
359 .collect()
360 }
361 };
362 out.sort();
363 out.dedup();
364 CACHE
365 .get_or_init(Default::default)
366 .lock()
367 .insert(key, out.clone());
368 out
369}
370
371fn game_id(path: &str) -> String {
373 yog_ids::join(yog_ids::ns::MINECRAFT, path)
374}
375
376fn asset_keys(vanilla: &Vanilla, asset: &str) -> serde_json::Map<String, serde_json::Value> {
378 vanilla
379 .read_all(asset)
380 .into_iter()
381 .find_map(|b| serde_json::from_slice::<serde_json::Map<String, serde_json::Value>>(&b).ok())
382 .unwrap_or_default()
383}
384
385fn data_ids(vanilla: &Vanilla, dirs: Vec<String>) -> Vec<String> {
387 dirs.iter()
388 .flat_map(|d| {
389 vanilla.names_under(&format!(
390 "{}/{}/{d}/",
391 yog_ids::pack::DATA,
392 yog_ids::ns::MINECRAFT
393 ))
394 })
395 .filter_map(|n| n.strip_suffix(yog_ids::res::JSON).map(game_id))
396 .collect()
397}
398
399fn lang_ids(vanilla: &Vanilla, kind: &str, prefixes: Vec<String>, dotted: bool) -> Vec<String> {
402 let lang = format!(
403 "{}/{}",
404 yog_ids::ns::MINECRAFT,
405 yog_ids::lang_file(yog_ids::game::LOCALE)
406 );
407 asset_keys(vanilla, &lang)
408 .keys()
409 .filter_map(|k| prefixes.iter().find_map(|p| k.strip_prefix(p.as_str())))
410 .filter(|rest| rest.starts_with(POTION_LANG_PREFIX) || dotted || !rest.contains('.'))
411 .map(|rest| {
412 match rest
413 .strip_prefix(POTION_LANG_PREFIX)
414 .filter(|_| kind == yog_ids::kind::ITEM)
415 {
416 Some(name) => format!("{}{name}", yog_ids::yog::POTION_ALIAS),
418 None => game_id(rest),
419 }
420 })
421 .collect()
422}
423
424pub fn block_kinds() -> Value {
428 use yog_data::Schema;
429 Value::List(
430 yog_formats::content::BlockKind::built_ins()
431 .iter()
432 .map(Schema::write)
433 .collect(),
434 )
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 fn write(root: &Path, rel: &str, text: &str) {
442 let p = root.join(rel);
443 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
444 std::fs::write(p, text).unwrap();
445 }
446
447 #[test]
448 fn a_mods_own_content_external_ids_and_defs() {
449 let dir = std::env::temp_dir().join(format!("yog-idle-refs-{}", std::process::id()));
450 let _ = std::fs::remove_dir_all(&dir);
451 write(
452 &dir,
453 "yog.toml",
454 "[mod]\nid = \"mymod\"\n\n[external.create]\nentities = [\"seat\"]\n",
455 );
456 write(&dir, "content/items/wand.yitem", "{ name: \"Wand\" }");
457 write(
458 &dir,
459 "content/items/potion.yitem",
460 "{ item: { potion: { effects: [] } } }",
461 );
462 write(
463 &dir,
464 "content/blocks/lamp.yblock",
465 "// a lamp\n{ id: \"glow_lamp\" }",
466 );
467 write(
468 &dir,
469 "content/mobs/wisp.yentity",
470 "{ kind: \"mob\", width: 0.5, height: 0.5 }",
471 );
472 write(
473 &dir,
474 "content/body/wisp.ymodel",
475 "{ texture: \"\", bones: [] }",
476 );
477 write(
478 &dir,
479 "content/fx/glow.ydef",
480 "{ kind: \"effect\", id: \"glow\", category: \"beneficial\", color: 255 }",
481 );
482 write(
483 &dir,
484 "content/fx/dim.ydef",
485 "{ kind: \"attribute\", id: \"dim\", default: 1, min: 0, max: 2 }",
486 );
487 write(
488 &dir,
489 "content/tags/plants.ydef",
490 "{ kind: \"tag\", of: \"block\", id: \"plants\", values: [] }",
491 );
492 let probe = dir.join("content/items/wand.yitem");
493 let ids = |kind: &str| {
494 refs(&probe, kind)
495 .own
496 .into_iter()
497 .map(|r| r.id)
498 .collect::<Vec<_>>()
499 };
500 assert_eq!(
501 ids("item"),
502 ["mymod:glow_lamp", "mymod:potion", "mymod:wand"],
503 "items include potion-only .yitems and a block's item form"
504 );
505 assert_eq!(ids("block"), ["mymod:glow_lamp"]);
506 assert_eq!(ids("entity"), ["mymod:wisp"]);
507 assert_eq!(ids("ymodel"), ["mymod:body/wisp"]);
508 assert_eq!(ids("effect"), ["mymod:glow"]);
509 assert_eq!(refs(&probe, "entity").external, ["create:seat"]);
510 assert_eq!(ids("tag:block"), ["mymod:plants"]);
511 assert!(ids("tag:item").is_empty());
512 let effects = own_defs(&probe, "effect");
513 assert_eq!(effects.len(), 1);
514 assert_eq!(effects[0].value.get("color"), Some(&Value::Int(255)));
515 std::fs::remove_dir_all(&dir).unwrap();
516 }
517
518 #[test]
519 fn the_games_block_kinds_are_listed() {
520 let kinds = block_kinds();
521 let names: Vec<&str> = kinds
522 .as_list()
523 .unwrap()
524 .iter()
525 .filter_map(|k| k.get("name").and_then(Value::as_str))
526 .collect();
527 assert!(
528 names.contains(&"stairs") && names.contains(&"pressure_plate"),
529 "{names:?}"
530 );
531 }
532}