use std::collections::{HashMap, HashSet};
pub use yog_formats::tags::{describe_hosts, hosts, tag_hosts, tags_known, yog_tag_for};
use yog_sync::LazyLock;
use yog_formats::book::refs::bare;
use crate::manifest::External;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RefKind {
Item,
Block,
Entity,
Recipe,
LootTable,
}
impl RefKind {
pub const ALL: [RefKind; 5] = [
RefKind::Item,
RefKind::Block,
RefKind::Entity,
RefKind::Recipe,
RefKind::LootTable,
];
pub fn word(self) -> &'static str {
match self {
RefKind::Item => yog_ids::kind::ITEM,
RefKind::Block => yog_ids::kind::BLOCK,
RefKind::Entity => yog_ids::kind::ENTITY,
RefKind::Recipe => yog_ids::kind::RECIPE,
RefKind::LootTable => yog_ids::kind::LOOT_TABLE,
}
}
pub fn from_word(word: &str) -> Option<RefKind> {
RefKind::ALL.into_iter().find(|k| k.word() == word)
}
}
struct Vanilla {
versions: Vec<String>,
ids: HashMap<(RefKind, String), Option<Vec<String>>>,
renamed: HashMap<(RefKind, String), String>,
potion_aliases: HashMap<String, Option<Vec<String>>>,
}
static VANILLA: LazyLock<Vanilla> = LazyLock::new(|| {
let text = include_str!("../vanilla/ids.txt");
let mut versions = Vec::new();
let mut ids = HashMap::new();
let mut potion_aliases = HashMap::new();
for line in text.lines() {
if let Some(v) = line.strip_prefix("# versions:") {
versions = v.split_whitespace().map(str::to_string).collect();
continue;
}
let mut p = line.split_whitespace();
let (Some(kind), Some(path), Some(vs)) = (p.next(), p.next(), p.next()) else {
continue;
};
let vs = (vs != "*").then(|| vs.split(',').map(str::to_string).collect());
if kind == yog_ids::kind::POTION {
potion_aliases.insert(path.to_string(), vs);
continue;
}
let Some(kind) = RefKind::ALL.into_iter().find(|k| k.word() == kind) else {
continue;
};
ids.insert((kind, path.to_string()), vs);
}
let mut renamed = HashMap::new();
for line in include_str!("../vanilla/renames.txt")
.lines()
.filter(|l| !l.starts_with('#'))
{
let mut p = line.split_whitespace();
let (Some(kind), Some(old), Some(new)) = (p.next(), p.next(), p.next()) else {
continue;
};
let Some(kind) = RefKind::from_word(kind) else {
continue;
};
renamed.insert((kind, old.to_string()), new.to_string());
renamed.insert((kind, new.to_string()), old.to_string());
}
Vanilla {
versions,
ids,
renamed,
potion_aliases,
}
});
pub fn versions() -> &'static [String] {
&VANILLA.versions
}
fn in_version(kind: RefKind, path: &str, version: &str) -> bool {
VANILLA
.ids
.get(&(kind, path.to_string()))
.is_some_and(|vs| vs.as_ref().is_none_or(|vs| vs.iter().any(|v| v == version)))
}
pub fn is_vanilla(id: &str) -> bool {
let Some(path) = yog_ids::game_path(id) else {
return false;
};
[RefKind::Item, RefKind::Block]
.iter()
.any(|k| VANILLA.ids.contains_key(&(*k, path.to_string())))
}
fn potion_of(id: &str) -> Option<&str> {
bare(id).strip_prefix(yog_ids::yog::POTION_ALIAS)
}
pub fn is_vanilla_potion(id: &str) -> bool {
potion_of(id).is_some_and(|p| VANILLA.potion_aliases.contains_key(p))
}
pub fn is_vanilla_potion_in(id: &str, version: &str) -> bool {
potion_of(id)
.and_then(|p| VANILLA.potion_aliases.get(p))
.is_some_and(|versions| {
versions
.as_ref()
.is_none_or(|versions| versions.iter().any(|v| v == version))
})
}
pub fn equivalent(kind: RefKind, id: &str, version: &str) -> Option<String> {
let path = yog_ids::game_path(id)?;
let kinds: &[RefKind] = if kind == RefKind::Item {
&[RefKind::Item, RefKind::Block]
} else {
std::slice::from_ref(&kind)
};
if kinds.iter().any(|k| in_version(*k, path, version)) {
return Some(id.to_string());
}
kinds.iter().find_map(|k| {
let other = VANILLA.renamed.get(&(*k, path.to_string()))?;
in_version(*k, other, version).then(|| format!("minecraft:{other}"))
})
}
#[derive(Default)]
pub struct Known {
pub ns: String,
pub datapack: String,
pub own: HashMap<RefKind, HashSet<String>>,
pub external: HashMap<RefKind, HashSet<String>>,
pub external_ns: HashSet<String>,
}
impl Known {
pub fn new(ns: &str, externals: &[External], datapack: Option<&str>) -> Known {
let datapack = datapack
.map(str::to_string)
.unwrap_or_else(|| VANILLA.versions.last().cloned().unwrap_or_default());
let mut k = Known {
ns: ns.to_string(),
datapack,
..Default::default()
};
for e in externals {
k.external_ns.insert(e.namespace.clone());
for (kind, paths) in &e.ids {
let set = k.external.entry(*kind).or_default();
for p in paths {
set.insert(if p.contains(':') {
p.clone()
} else {
format!("{}:{p}", e.namespace)
});
}
}
}
k
}
pub fn add(&mut self, kind: RefKind, id: impl Into<String>) {
self.own.entry(kind).or_default().insert(id.into());
}
pub fn check_tag(&self, kind: &str, id: &str) -> Result<(), String> {
let Some((ns, _)) = id.split_once(':') else {
return Err(format!(
"`{id}`: write the tag's namespace (`yog:{id}`, `{}:{id}`)",
self.ns
));
};
if !tags_known(ns) || tag_hosts(yog_ids::registry_of(kind), id).is_some() {
return Ok(());
}
Err(if ns == yog_ids::ns::YOG {
format!("no {kind} tag {id} among Yog's common tags")
} else {
format!("no {kind} tag {id} in Minecraft or its loaders")
})
}
pub fn check(&self, kind: RefKind, id: &str) -> Result<(), String> {
let id = bare(id);
let Some((ns, path)) = id.split_once(':') else {
return Err(format!(
"`{id}`: write its namespace (`minecraft:{id}`, `{}:{id}`)",
self.ns
));
};
if kind == RefKind::Item && potion_of(id).is_some() {
return self.check_potion(id);
}
let kinds: &[RefKind] = if kind == RefKind::Item {
&[RefKind::Item, RefKind::Block]
} else {
std::slice::from_ref(&kind)
};
if ns == self.ns {
if kind == RefKind::LootTable
|| kinds
.iter()
.any(|k| self.own.get(k).is_some_and(|s| s.contains(id)))
{
return Ok(());
}
return Err(format!("no {} {id} in this mod", kind.word()));
}
if ns == yog_ids::ns::MINECRAFT {
return self.check_vanilla(kind, kinds, id, path);
}
if kinds
.iter()
.any(|k| self.external.get(k).is_some_and(|s| s.contains(id)))
{
return Ok(());
}
Err(self.undeclared(kind, id, ns, path))
}
fn check_potion(&self, id: &str) -> Result<(), String> {
if is_vanilla_potion_in(id, &self.datapack) {
Ok(())
} else if is_vanilla_potion(id) {
Err(format!(
"potion alias {id} isn't registered in Minecraft {}",
self.datapack
))
} else {
Err(format!(
"no item {id} among Yog's versioned default potion aliases"
))
}
}
fn check_vanilla(
&self,
kind: RefKind,
kinds: &[RefKind],
id: &str,
path: &str,
) -> Result<(), String> {
let v = &*VANILLA;
if !v.versions.contains(&self.datapack) {
return Err(format!(
"the mod's datapack is Minecraft {}, which Yog doesn't know ({})",
self.datapack,
v.versions.join(", ")
));
}
match kinds
.iter()
.find_map(|k| v.ids.get(&(*k, path.to_string())))
{
None => Err(format!("no {} {id} in Minecraft", kind.word())),
Some(_) if kinds.iter().any(|k| in_version(*k, path, &self.datapack)) => Ok(()),
Some(Some(only)) => {
let hint = equivalent(kind, id, &self.datapack)
.map(|e| format!(" — there it's {e}"))
.unwrap_or_default();
Err(format!(
"{id} isn't in Minecraft {} (the mod's datapack; it's in {}){hint}",
self.datapack,
only.join(", ")
))
}
Some(None) => Ok(()),
}
}
fn undeclared(&self, kind: RefKind, id: &str, ns: &str, path: &str) -> String {
let section = format!("[external.{ns}]");
if self.external_ns.contains(ns) {
return format!("{id} isn't declared under {section} in yog.toml");
}
match external_list(kind) {
Some(list) => format!(
"{id} is another mod's: declare it in yog.toml — {section} {list} = [\"{path}\"]"
),
None => format!(
"{id} is another mod's, and yog.toml can't declare its {}s",
kind.word()
),
}
}
}
fn external_list(kind: RefKind) -> Option<&'static str> {
match kind {
RefKind::Item => Some(yog_ids::word::ITEMS),
RefKind::Block => Some(yog_ids::word::BLOCKS),
RefKind::Entity => Some(yog_ids::word::ENTITIES),
RefKind::Recipe => Some(yog_ids::word::RECIPES),
RefKind::LootTable => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_hosts_know_the_tags_the_build_does() {
assert_eq!(
yog_formats::tags::TABLE,
include_str!("../../../../../java-common/src/main/resources/yog/tags.txt"),
"java-common's yog/tags.txt is a copy of yog-formats/data/tags.txt"
);
}
#[test]
fn the_hosts_rename_as_the_build_does() {
assert_eq!(
include_str!("../vanilla/renames.txt"),
include_str!("../../../../../java-common/src/main/resources/yog/renames.txt"),
"java-common's yog/renames.txt is a copy of vanilla/renames.txt"
);
}
#[test]
fn references_are_checked_against_every_source() {
let ext = External {
namespace: "farmersdelight".into(),
ids: vec![(RefKind::Item, vec!["cutting_board".into()])],
};
let mut k = Known::new("mymod", std::slice::from_ref(&ext), None);
k.add(RefKind::Item, "mymod:ruby");
assert!(k.check(RefKind::Item, "mymod:ruby").is_ok());
assert!(
k.check(RefKind::Item, "mymod:nope")
.unwrap_err()
.contains("no item mymod:nope")
);
assert!(k.check(RefKind::Item, "minecraft:stick").is_ok());
assert!(
k.check(RefKind::Item, "yog:potion/awkward").is_ok(),
"Yog's stable vanilla potion item alias"
);
assert!(k.check(RefKind::Item, "yog:potion/long_swiftness").is_ok());
assert!(k.check(RefKind::Item, "yog:potion/wind_charged").is_ok());
assert!(
k.check(RefKind::Item, "yog:potion/levitation").is_err(),
"an effect name isn't a potion registry id"
);
assert!(
k.check(RefKind::Item, "minecraft:stone").is_ok(),
"a block's item"
);
assert!(
k.check(RefKind::Block, "minecraft:furnace[facing=north]")
.is_ok()
);
assert!(k.check(RefKind::Entity, "minecraft:allay").is_ok());
assert!(
k.check(RefKind::Item, "minecraft:short_grass").is_ok(),
"the datapack is the newest by default"
);
let newest = versions().last().expect("supported versions");
let wanted = format!(
"isn't in Minecraft {newest} (the mod's datapack; it's in 1.20.1) — there it's minecraft:short_grass"
);
assert!(
k.check(RefKind::Item, "minecraft:grass")
.unwrap_err()
.contains(&wanted)
);
assert_eq!(
equivalent(RefKind::Item, "minecraft:chain", newest).as_deref(),
Some("minecraft:iron_chain"),
"renamed in 1.21.9"
);
let old = Known::new("mymod", &[ext], Some("1.20.1"));
assert!(old.check(RefKind::Item, "yog:potion/awkward").is_ok());
assert!(old.check(RefKind::Item, "yog:potion/wind_charged").is_err());
assert!(old.check(RefKind::Item, "minecraft:grass").is_ok());
assert!(
old.check(RefKind::Block, "minecraft:crafter")
.unwrap_err()
.contains("isn't in Minecraft 1.20.1")
);
assert_eq!(
equivalent(RefKind::Item, "minecraft:short_grass", "1.20.1").as_deref(),
Some("minecraft:grass")
);
assert_eq!(
equivalent(RefKind::Item, "minecraft:turtle_scute", "1.20.1").as_deref(),
Some("minecraft:scute")
);
assert_eq!(
equivalent(RefKind::Item, "minecraft:stick", "1.20.1").as_deref(),
Some("minecraft:stick")
);
assert_eq!(
equivalent(RefKind::Block, "minecraft:crafter", "1.20.1"),
None,
"1.20.1 has no crafter"
);
assert!(
k.check(RefKind::Item, "minecraft:nope")
.unwrap_err()
.contains("in Minecraft")
);
assert!(
k.check(RefKind::Item, "farmersdelight:cutting_board")
.is_ok()
);
assert!(
k.check(RefKind::Item, "farmersdelight:knife")
.unwrap_err()
.contains("isn't declared")
);
assert!(
k.check(RefKind::Item, "create:wrench")
.unwrap_err()
.contains("[external.create]")
);
assert!(
k.check(RefKind::Item, "stick")
.unwrap_err()
.contains("namespace")
);
}
}