use std::collections::HashMap;
use std::path::{Path, PathBuf};
use yog_ids::Format;
use yog_sync::{Mutex, OnceLock};
use yog_data::Value;
use crate::design::assets::{Resources, Vanilla};
use crate::design::project::{self, mod_root};
const FILE_KINDS: [(&str, &[Format]); 4] = [
(yog_ids::kind::ITEM, &[Format::Item, Format::Block]),
(yog_ids::kind::BLOCK, &[Format::Block]),
(yog_ids::kind::ENTITY, &[Format::Entity]),
(yog_ids::kind::RECIPE, &[Format::Recipe]),
];
const BODY_FORMATS: [Format; 2] = [Format::Model, Format::Anim];
const RESOURCE_KINDS: [(&str, Format); 2] = [
(yog_ids::kind::TEXTURE, Format::Texture),
(yog_ids::kind::MATERIAL, Format::Material),
];
enum GameIds {
Lang {
kinds: &'static [&'static str],
dotted: bool,
},
LangPrefix(&'static str),
Data,
Keys(&'static str),
}
const SOUNDS_ASSET: &str = yog_ids::res::SOUNDS_JSON;
const ATTRIBUTE_LANG_PREFIX: &str = "attribute.name.";
const POTION_LANG_PREFIX: &str = "potion.effect.";
const GAME_IDS: [(&str, GameIds); 12] = [
(
yog_ids::kind::ITEM,
GameIds::Lang {
kinds: &[yog_ids::kind::ITEM, yog_ids::kind::BLOCK],
dotted: false,
},
),
(
yog_ids::kind::BLOCK,
GameIds::Lang {
kinds: &[yog_ids::kind::BLOCK],
dotted: false,
},
),
(
yog_ids::kind::ENTITY,
GameIds::Lang {
kinds: &[yog_ids::kind::ENTITY],
dotted: false,
},
),
(
yog_ids::kind::EFFECT,
GameIds::Lang {
kinds: &[yog_ids::kind::EFFECT],
dotted: false,
},
),
(
yog_ids::kind::ATTRIBUTE,
GameIds::LangPrefix(ATTRIBUTE_LANG_PREFIX),
),
(
yog_ids::kind::BIOME,
GameIds::Lang {
kinds: &[yog_ids::kind::BIOME],
dotted: false,
},
),
(
ENCHANTMENT,
GameIds::Lang {
kinds: &[ENCHANTMENT],
dotted: false,
},
),
(yog_ids::kind::LOOT_TABLE, GameIds::Data),
(yog_ids::kind::ADVANCEMENT, GameIds::Data),
(yog_ids::kind::DAMAGE_TYPE, GameIds::Data),
(yog_ids::kind::RECIPE, GameIds::Data),
(SOUND, GameIds::Keys(SOUNDS_ASSET)),
];
const ENCHANTMENT: &str = "enchantment";
const SOUND: &str = "sound";
const GAME_TAG_KINDS: [&str; 6] = [
yog_ids::kind::ITEM,
yog_ids::kind::BLOCK,
yog_ids::kind::ENTITY,
yog_ids::kind::FLUID,
yog_ids::kind::BIOME,
yog_ids::kind::DAMAGE_TYPE,
];
#[derive(Debug, Clone, PartialEq, yog_data::Schema)]
pub struct OwnRef {
pub id: String,
pub file: String,
}
#[derive(Debug, Default, yog_data::Schema)]
pub struct RefChoices {
pub own: Vec<OwnRef>,
pub external: Vec<String>,
pub game: Vec<String>,
}
#[derive(Debug, yog_data::Schema)]
pub struct OwnDef {
pub id: String,
pub file: String,
pub value: Value,
}
fn files(root: &Path, format: Format) -> Vec<PathBuf> {
crate::files::of(&root.join(yog_ids::project::CONTENT), format)
}
fn read(file: &Path) -> Option<Value> {
yog_data::text::parse_value(&crate::files::read_source(file).ok()?)
.ok()
.filter(|v| matches!(v, Value::Map(_)))
}
pub fn refs(file: &Path, kind: &str) -> RefChoices {
let mut out = RefChoices::default();
if let Some(root) = mod_root(file) {
out.own = own(&root, kind);
out.external = external(&root, kind);
}
if body_format(kind).is_none() {
out.game = game(mod_root(file).as_deref(), kind);
}
out
}
fn body_format(kind: &str) -> Option<Format> {
Format::of(kind).filter(|f| BODY_FORMATS.contains(f))
}
fn own(root: &Path, kind: &str) -> Vec<OwnRef> {
let ns = crate::design::project::mod_id(root).unwrap_or_else(|| yog_ids::ns::MINECRAFT.into());
let mut out = Vec::new();
let tag_of = format!("{}{}", yog_ids::typed::TAG, yog_ids::sep::NAMESPACE);
if let Some(of) = kind.strip_prefix(tag_of.as_str()) {
for d in defs(root, yog_ids::kind::TAG) {
if d.value
.get(yog_ids::content_field::OF)
.and_then(Value::as_str)
== Some(of)
{
out.push(OwnRef {
id: d.id,
file: d.file,
});
}
}
return out;
}
let by_path = body_format(kind)
.map(|f| (yog_ids::project::CONTENT, f))
.or_else(|| {
RESOURCE_KINDS
.iter()
.find(|(k, _)| *k == kind)
.map(|(_, f)| (yog_ids::project::RESOURCES, *f))
});
if let Some((dir, format)) = by_path {
let dir = root.join(dir);
for f in crate::files::of(&dir, format) {
let rel = f.strip_prefix(&dir).unwrap_or(&f).with_extension("");
out.push(OwnRef {
id: yog_ids::join(
&ns,
&rel.to_string_lossy().replace('\\', yog_ids::sep::PATH),
),
file: f.to_string_lossy().into_owned(),
});
}
return out;
}
if let Some((_, exts)) = FILE_KINDS.iter().find(|(k, _)| *k == kind) {
for format in *exts {
for f in files(root, *format) {
let given = read(&f).and_then(|d| {
d.get(yog_ids::content_field::ID)
.and_then(Value::as_str)
.map(str::to_string)
});
let id = given.unwrap_or_else(|| {
f.file_stem()
.unwrap_or_default()
.to_string_lossy()
.into_owned()
});
out.push(OwnRef {
id: yog_ids::qualify_in(&ns, &id),
file: f.to_string_lossy().into_owned(),
});
}
}
} else {
out = defs(root, kind)
.into_iter()
.map(|d| OwnRef {
id: d.id,
file: d.file,
})
.collect();
}
out.sort_by(|a, b| a.id.cmp(&b.id));
out.dedup_by(|a, b| a.id == b.id);
out
}
fn defs(root: &Path, kind: &str) -> Vec<OwnDef> {
let ns = crate::design::project::mod_id(root).unwrap_or_else(|| yog_ids::ns::MINECRAFT.into());
let mut out = Vec::new();
for f in files(root, Format::Def) {
let Some(doc) = read(&f) else { continue };
if doc
.get(yog_ids::content_field::KIND)
.and_then(Value::as_str)
!= Some(kind)
{
continue;
}
let Some(id) = doc.get(yog_ids::content_field::ID).and_then(Value::as_str) else {
continue;
};
out.push(OwnDef {
id: yog_ids::qualify_in(&ns, id),
file: f.to_string_lossy().into_owned(),
value: doc,
});
}
out
}
pub fn own_defs(file: &Path, kind: &str) -> Vec<OwnDef> {
mod_root(file).map(|r| defs(&r, kind)).unwrap_or_default()
}
fn external(root: &Path, kind: &str) -> Vec<String> {
let Some(kind) = crate::content::refs::RefKind::from_word(kind) else {
return Vec::new();
};
let Ok(manifest) = crate::manifest::YogToml::read(&project::path(root)) else {
return Vec::new();
};
let mut out = Vec::new();
for external in &manifest.externals {
for (_, ids) in external.ids.iter().filter(|(k, _)| *k == kind) {
out.extend(
ids.iter()
.map(|id| yog_ids::qualify_in(&external.namespace, id)),
);
}
}
out
}
fn game(root: Option<&Path>, kind: &str) -> Vec<String> {
let tag_of = format!("{}{}", yog_ids::typed::TAG, yog_ids::sep::NAMESPACE);
let tags = kind
.strip_prefix(tag_of.as_str())
.filter(|of| GAME_TAG_KINDS.contains(of))
.map(yog_ids::registry_dirs);
let how = GAME_IDS.iter().find(|(k, _)| *k == kind).map(|(_, h)| h);
if tags.is_none() && how.is_none() {
return Vec::new();
}
let Some(vanilla) = Resources::for_mod(root).vanilla else {
return Vec::new();
};
type Cache = Mutex<HashMap<(PathBuf, String), Vec<String>>>;
static CACHE: OnceLock<Cache> = OnceLock::new();
let key = (vanilla.jar.clone(), kind.to_string());
if let Some(hit) = CACHE
.get_or_init(Default::default)
.lock()
.get(&key)
.cloned()
{
return hit;
}
let mut out: Vec<String> = match (tags, how) {
(Some(dirs), _) => data_ids(
&vanilla,
dirs.iter()
.map(|d| format!("{}/{d}", yog_ids::pack::TAGS))
.collect(),
),
(None, None) => Vec::new(),
(None, Some(GameIds::Lang { kinds, dotted })) => {
let prefixes = kinds
.iter()
.map(|k| yog_ids::lang_prefix(k, yog_ids::ns::MINECRAFT))
.collect();
lang_ids(&vanilla, kind, prefixes, *dotted)
}
(None, Some(GameIds::LangPrefix(prefix))) => {
lang_ids(&vanilla, kind, vec![prefix.to_string()], true)
}
(None, Some(GameIds::Data)) => data_ids(&vanilla, yog_ids::registry_dirs(kind)),
(None, Some(GameIds::Keys(asset))) => {
asset_keys(&vanilla, &format!("{}/{asset}", yog_ids::ns::MINECRAFT))
.keys()
.map(|k| game_id(k))
.collect()
}
};
out.sort();
out.dedup();
CACHE
.get_or_init(Default::default)
.lock()
.insert(key, out.clone());
out
}
fn game_id(path: &str) -> String {
yog_ids::join(yog_ids::ns::MINECRAFT, path)
}
fn asset_keys(vanilla: &Vanilla, asset: &str) -> serde_json::Map<String, serde_json::Value> {
vanilla
.read_all(asset)
.into_iter()
.find_map(|b| serde_json::from_slice::<serde_json::Map<String, serde_json::Value>>(&b).ok())
.unwrap_or_default()
}
fn data_ids(vanilla: &Vanilla, dirs: Vec<String>) -> Vec<String> {
dirs.iter()
.flat_map(|d| {
vanilla.names_under(&format!(
"{}/{}/{d}/",
yog_ids::pack::DATA,
yog_ids::ns::MINECRAFT
))
})
.filter_map(|n| n.strip_suffix(yog_ids::res::JSON).map(game_id))
.collect()
}
fn lang_ids(vanilla: &Vanilla, kind: &str, prefixes: Vec<String>, dotted: bool) -> Vec<String> {
let lang = format!(
"{}/{}",
yog_ids::ns::MINECRAFT,
yog_ids::lang_file(yog_ids::game::LOCALE)
);
asset_keys(vanilla, &lang)
.keys()
.filter_map(|k| prefixes.iter().find_map(|p| k.strip_prefix(p.as_str())))
.filter(|rest| rest.starts_with(POTION_LANG_PREFIX) || dotted || !rest.contains('.'))
.map(|rest| {
match rest
.strip_prefix(POTION_LANG_PREFIX)
.filter(|_| kind == yog_ids::kind::ITEM)
{
Some(name) => format!("{}{name}", yog_ids::yog::POTION_ALIAS),
None => game_id(rest),
}
})
.collect()
}
pub fn block_kinds() -> Value {
use yog_data::Schema;
Value::List(
yog_formats::content::BlockKind::built_ins()
.iter()
.map(Schema::write)
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn write(root: &Path, rel: &str, text: &str) {
let p = root.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
}
#[test]
fn a_mods_own_content_external_ids_and_defs() {
let dir = std::env::temp_dir().join(format!("yog-idle-refs-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
write(
&dir,
"yog.toml",
"[mod]\nid = \"mymod\"\n\n[external.create]\nentities = [\"seat\"]\n",
);
write(&dir, "content/items/wand.yitem", "{ name: \"Wand\" }");
write(
&dir,
"content/items/potion.yitem",
"{ item: { potion: { effects: [] } } }",
);
write(
&dir,
"content/blocks/lamp.yblock",
"// a lamp\n{ id: \"glow_lamp\" }",
);
write(
&dir,
"content/mobs/wisp.yentity",
"{ kind: \"mob\", width: 0.5, height: 0.5 }",
);
write(
&dir,
"content/body/wisp.ymodel",
"{ texture: \"\", bones: [] }",
);
write(
&dir,
"content/fx/glow.ydef",
"{ kind: \"effect\", id: \"glow\", category: \"beneficial\", color: 255 }",
);
write(
&dir,
"content/fx/dim.ydef",
"{ kind: \"attribute\", id: \"dim\", default: 1, min: 0, max: 2 }",
);
write(
&dir,
"content/tags/plants.ydef",
"{ kind: \"tag\", of: \"block\", id: \"plants\", values: [] }",
);
let probe = dir.join("content/items/wand.yitem");
let ids = |kind: &str| {
refs(&probe, kind)
.own
.into_iter()
.map(|r| r.id)
.collect::<Vec<_>>()
};
assert_eq!(
ids("item"),
["mymod:glow_lamp", "mymod:potion", "mymod:wand"],
"items include potion-only .yitems and a block's item form"
);
assert_eq!(ids("block"), ["mymod:glow_lamp"]);
assert_eq!(ids("entity"), ["mymod:wisp"]);
assert_eq!(ids("ymodel"), ["mymod:body/wisp"]);
assert_eq!(ids("effect"), ["mymod:glow"]);
assert_eq!(refs(&probe, "entity").external, ["create:seat"]);
assert_eq!(ids("tag:block"), ["mymod:plants"]);
assert!(ids("tag:item").is_empty());
let effects = own_defs(&probe, "effect");
assert_eq!(effects.len(), 1);
assert_eq!(effects[0].value.get("color"), Some(&Value::Int(255)));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_games_block_kinds_are_listed() {
let kinds = block_kinds();
let names: Vec<&str> = kinds
.as_list()
.unwrap()
.iter()
.filter_map(|k| k.get("name").and_then(Value::as_str))
.collect();
assert!(
names.contains(&"stairs") && names.contains(&"pressure_plate"),
"{names:?}"
);
}
}