pub const REGISTRATIONS_FILE: &str = "registrations.txt";
pub fn registration_changes(
registered: &str,
new: &str,
) -> (Vec<String>, Vec<String>, Vec<String>) {
fn read(text: &str) -> std::collections::BTreeMap<String, String> {
text.lines()
.filter_map(|l| {
let (key, hash) = l.trim().rsplit_once(' ')?;
Some((key.to_string(), hash.to_string()))
})
.collect()
}
let (old, new) = (read(registered), read(new));
let added = new
.keys()
.filter(|k| !old.contains_key(*k))
.cloned()
.collect();
let removed = old
.keys()
.filter(|k| !new.contains_key(*k))
.cloned()
.collect();
let changed = new
.iter()
.filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h))
.map(|(k, _)| k.clone())
.collect();
(added, removed, changed)
}
mod crafting;
mod held;
mod looks;
mod world;
pub use crafting::loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
pub use crafting::{loot, recipe_kinds};
pub use held::effects::{
AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef,
PotionEffect,
};
pub use held::item_looks::{ItemLooks, ItemTint, LookModels};
pub use held::tool_levels::{LevelTag, ToolLadder, ToolLevel};
pub use held::{effects, item_looks, tool_levels};
pub use looks::atlases::Atlas;
pub use looks::particles::{Blend, Frames, Lifetime, Particle, ParticleDef, ParticleRender};
pub use looks::videos::{Clock, Plan, VideoDef};
pub use looks::{atlases, particles, videos};
pub use crafting::recipes::{COOK_EXPERIENCE, Cooking, Recipe, RecipeKind, StartupGrant};
pub use held::equipment::{
ArmorDefense, ArmorMaterial, ArmorPiece, ArmorSlot, DIGGERS, GameTool, REFERENCE_KIND,
REFERENCE_MATERIAL, TOOL_KINDS, ToolItem, ToolMaterial,
};
pub use held::items::{FoodDef, Hangs, ItemDef};
pub use world::blocks::{BlockDef, BlockProperty, Facing, Placement};
pub use world::entities::{EntityDef, EntityKind};
pub use world::sounds::SoundGroup;
pub use world::worldgen::BiomeFeature;
#[cfg(test)]
mod registration_tests {
#[test]
fn a_hot_reload_sees_what_registration_changed() {
let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
let (added, removed, changed) = super::registration_changes(old, new);
assert_eq!(added, ["item mymod:d"]);
assert_eq!(removed, ["item mymod:c"]);
assert_eq!(changed, ["block mymod:b"]);
assert_eq!(
super::registration_changes(old, old),
(vec![], vec![], vec![])
);
}
}
#[cfg(test)]
mod material_tests {
use super::*;
#[test]
fn the_games_materials_are_built_in() {
assert_eq!(ToolMaterial::built_ins().len(), 6);
let iron = ToolMaterial::built_in("iron").unwrap();
assert_eq!(
(iron.durability, iron.level_index, iron.incorrect.as_str()),
(250, 2, "minecraft:incorrect_for_iron_tool")
);
assert_eq!(
ArmorMaterial::built_in("minecraft:netherite")
.unwrap()
.knockback_resistance,
0.1
);
assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
assert_eq!(
(mythril.id.as_str(), mythril.durability),
("mymod:mythril", 1561)
);
}
}
#[cfg(test)]
mod sound_tests {
use super::*;
#[test]
fn the_games_sound_groups_are_built_in() {
assert!(SoundGroup::built_ins().len() > 100);
let stone = SoundGroup::built_in("stone").unwrap();
assert_eq!(
(stone.break_sound.as_str(), stone.volume),
("minecraft:block.stone.break", 1.0)
);
assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
assert!(SoundGroup::built_in("mymod:crystal").is_none());
}
#[test]
fn a_mods_group_crosses_as_yog_data() {
use yog_data::Schema;
let g = SoundGroup::new("mymod:crystal").pitch(1.2);
assert_eq!(g.fall, "mymod:crystal.fall");
let back = yog_data::from_value::<SoundGroup>(&g.write())
.unwrap()
.value;
assert_eq!(back, g);
assert_eq!(
g.write().get("break").and_then(|v| v.as_str()),
Some("mymod:crystal.break")
);
}
}
#[cfg(test)]
impl Recipe {
fn to_json(&self) -> String {
yog_data::json::to_json(&self.to_data()).to_string()
}
}
#[cfg(test)]
mod recipe_tests {
use super::*;
#[test]
fn writes_the_yog_format() {
let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
let shaped = Recipe::shaped("m:a", "m:b", 2)
.row("D ")
.key('D', "#c:dusts")
.key_any('E', ["m:x", "m:y"])
.group("g");
assert_eq!(
js(&shaped.to_json()),
js(
r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##
)
);
assert_eq!(
shaped.to_data().as_map().unwrap().keys().next(),
Some("type"),
"the type first"
);
let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
assert_eq!(
js(&blast.to_json()),
js(
r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##
)
);
let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
assert_eq!(js(&smith.to_json())["template"], "m:t");
}
}
#[cfg(test)]
mod json_tests {
use super::*;
#[test]
fn json_survives_any_string() {
let odd = "m:a\"b\\c\nd\te\u{1}";
let parse = |j: String| {
serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"))
};
for r in [
Recipe::shaped(odd, odd, 1)
.row("A")
.key('A', odd)
.group(odd)
.category(odd),
Recipe::shapeless(odd, odd, 3)
.ingredient(odd)
.ingredient_any([odd, "m:x"]),
Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
Recipe::stonecutting(odd, odd, odd, 2),
Recipe::smithing(odd, odd, odd, odd, odd),
] {
let v = parse(r.to_json());
assert_eq!(v["result"], odd, "the string comes back as it was");
}
}
#[test]
fn recipes_and_grants_name_their_parts() {
let r = Recipe::cooking(
"mymod:a",
Cooking::Campfire,
"minecraft:beef",
"minecraft:cooked_beef",
1,
);
let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
assert_eq!(v["type"], "campfire_cooking");
assert_eq!(v["cook_time"], 600, "the method's default time");
assert_eq!(r.ns_name(), ("mymod", "a"));
assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
}
}
#[cfg(test)]
mod built_in_tests {
use super::*;
#[test]
fn every_built_in_table_reads() {
let reads = |name: &str, ok: bool| assert!(ok, "{name} won't read");
reads(
"tool materials",
yog_data::from_text::<Vec<ToolMaterial>>(include_str!(
"../assets/tool_materials.ydata"
))
.is_ok(),
);
reads(
"armor materials",
yog_data::from_text::<Vec<ArmorMaterial>>(include_str!(
"../assets/armor_materials.ydata"
))
.is_ok(),
);
reads(
"sound groups",
yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
.is_ok(),
);
reads(
"item looks",
yog_data::from_text::<item_looks::ItemLooks>(include_str!(
"../assets/item_looks.ydata"
))
.is_ok(),
);
reads(
"tool levels",
yog_data::from_text::<tool_levels::ToolLadder>(include_str!(
"../assets/tool_levels.ydata"
))
.is_ok(),
);
reads(
"atlases",
yog_data::from_text::<Vec<atlases::Atlas>>(include_str!("../assets/atlases.ydata"))
.is_ok(),
);
reads(
"recipe kinds",
yog_data::from_text::<Vec<recipe_kinds::RecipeKindDef>>(include_str!(
"../data/recipe_kinds.ydata"
))
.is_ok(),
);
}
}