mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
Native mods: a recipe ingredient of a mod's own kind
An ingredient that names a type in a namespace outside minecraft: used to fail the recipe file whole. It now reaches native_ingredient(kind, data, items), a free function in crafting.rs which mixin bundles hook: fill the item list with what the ingredient matches, as a tag is. The list holds item ids only (components are not checked; a match-time hook follows separately). Duplicates are removed keeping first-seen order. The hook is called once per ingredient by each copy that loads the recipes — the crafting tables', the network copy's — so at least twice per load, and the answer must be the same. An unclaimed kind leaves the list empty and the recipe fails, as an unknown kind does; a bare type name is minecraft's own. The catalog target names horizon_client_lib, the package crafting.rs compiles into since #5446's split: horizon-mixins resolves the crate by its package, and refuses horizon_client::crafting::... (proven on a staged build; the same split left menu's and breathing's catalog targets on the old crate name — their bundles would fail the same way, left to their own fix). Part of #5367: predicates over components need a match-time hook, not this load-time list, and follow separately. Co-Authored-By: ZCode AI agent (GLM-5.3)
This commit is contained in:
1 parent
fd88a545d9
commit
df3a7ffd73
4 files changed
+88
No files matched your search
@@ -19,6 +19,30 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A recipe ingredient of a mod's own kind: the entry named a type in a
|
||||
/// namespace outside `minecraft:`, and the loader kept it for this call.
|
||||
/// The kind and the JSON the file wrote are here; fill `items` with the
|
||||
/// item ids the ingredient matches (item ids only: components are not
|
||||
/// checked here; a match-time hook follows separately). Duplicates are
|
||||
/// removed keeping first-seen order. The hook may be called more than
|
||||
/// once per recipe; the answer must be the same. An unclaimed kind
|
||||
/// leaves the list empty and the recipe fails, as an unknown kind does.
|
||||
/// API @API:ingredient@.
|
||||
#[inline(never)]
|
||||
pub fn native_ingredient(
|
||||
kind: &str,
|
||||
data: &serde_json::Map<String, Value>,
|
||||
items: &mut Vec<String>,
|
||||
) {
|
||||
let _ = (kind, data, items);
|
||||
}
|
||||
|
||||
/// Keep first-seen order, drop the rest.
|
||||
fn dedup_keep_order(items: &mut Vec<String>) {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
items.retain(|i| seen.insert(i.clone()));
|
||||
}
|
||||
|
||||
/// A set of accepted items (expanded from an item, list or tag), and the
|
||||
/// items in its `HolderSet` order (`Ingredient.display`'s `TagSlotDisplay`).
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -107,6 +131,14 @@ impl Ingredient {
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
// A mod's own kind: the hook answers with the item list it
|
||||
// matches; a bare type name is minecraft's own, not a mod's.
|
||||
k if k.contains(':') && !k.starts_with("minecraft:") => {
|
||||
let mut list = Vec::new();
|
||||
native_ingredient(k, o, &mut list);
|
||||
dedup_keep_order(&mut list);
|
||||
return (!list.is_empty()).then(|| Self::of(list));
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
(!kept.is_empty()).then(|| Self::of(kept))
|
||||
@@ -1913,6 +1945,23 @@ mod tests {
|
||||
|
||||
/// NeoForge's standard ingredient shapes resolve to their flat item
|
||||
/// lists; a kind the loader doesn't know still fails the file.
|
||||
#[test]
|
||||
fn an_unclaimed_ingredient_kind_fails_and_dedup_keeps_order() {
|
||||
let items = Items::for_tests(HashMap::new());
|
||||
let json = |s: &str| serde_json::from_str::<Value>(s).unwrap();
|
||||
assert!(
|
||||
Ingredient::parse(&json(r#"{"type":"mymod:aspect","aspect":"fire"}"#), &items,)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
Ingredient::parse(&json(r#"{"type":"minecraft:future_shape"}"#), &items,).is_none()
|
||||
);
|
||||
assert!(Ingredient::parse(&json(r#"{"type":"aspect"}"#), &items,).is_none());
|
||||
let mut list = vec!["b".to_string(), "a".to_string(), "b".to_string()];
|
||||
dedup_keep_order(&mut list);
|
||||
assert_eq!(list, vec!["b".to_string(), "a".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neoforge_ingredient_shapes_parse() {
|
||||
let mut tags = HashMap::new();
|
||||
|
||||
@@ -6357,6 +6357,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_ingredient_stays_hookable() {
|
||||
let source = include_str!("../crafting.rs");
|
||||
let at = source.find("pub fn native_ingredient(").unwrap();
|
||||
let start = source[..at].rfind("///").unwrap();
|
||||
let body = &source[start..start + source[start..].find("\n}\n").unwrap()];
|
||||
for line in body.lines() {
|
||||
assert!(
|
||||
!line.contains("cfg"),
|
||||
"a cfg in the ingredient hook: {line}"
|
||||
);
|
||||
for mac in ["!(", "![", "! {", "!{"] {
|
||||
assert!(!line.contains(mac), "a macro: {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loot_table_load_stays_hookable() {
|
||||
let source = include_str!("../../../horizon-world/src/loot.rs").replace("\r\n", "\n");
|
||||
@@ -6459,6 +6476,8 @@ mod tests {
|
||||
let menu_hooks = free_hooks(include_str!("../menu.rs"));
|
||||
// The air supply's (`horizon_client::breathing`).
|
||||
let breath_hooks = free_hooks(include_str!("../breathing.rs"));
|
||||
// The crafting loaders' (`horizon_client_lib::crafting`).
|
||||
let crafting_hooks = free_hooks(include_str!("../crafting.rs"));
|
||||
let declared: BTreeSet<_> = section
|
||||
.lines()
|
||||
.chain(level_section.lines())
|
||||
@@ -6473,6 +6492,7 @@ mod tests {
|
||||
.chain(loot_hooks.iter().copied())
|
||||
.chain(menu_hooks.iter().copied())
|
||||
.chain(breath_hooks.iter().copied())
|
||||
.chain(crafting_hooks.iter().copied())
|
||||
.collect();
|
||||
let mut names = BTreeSet::new();
|
||||
let mut contracts = BTreeSet::new();
|
||||
@@ -6490,6 +6510,10 @@ mod tests {
|
||||
format!("horizon_world::item_defaults::native_{name}")
|
||||
} else if loot_hooks.contains(name) {
|
||||
format!("horizon_world::loot::native_{name}")
|
||||
} else if crafting_hooks.contains(name) {
|
||||
// crafting.rs compiles into horizon-client-lib (#5446's
|
||||
// split): horizon-mixins resolves the crate by its package.
|
||||
format!("horizon_client_lib::crafting::native_{name}")
|
||||
} else if menu_hooks.contains(name) {
|
||||
format!("horizon_client::menu::native_{name}")
|
||||
} else if breath_hooks.contains(name) {
|
||||
|
||||
@@ -436,6 +436,15 @@ position), `block_entity` (the broken block's entity as it was before the break,
|
||||
vanilla's `time_check` and `weather_check`). The game sets it for a block a player or an explosion breaks and for a
|
||||
mob's death loot; on other rolls `place` is `None`.
|
||||
|
||||
### A mod's own ingredient kinds
|
||||
|
||||
A recipe's ingredient that names a type in a namespace outside `minecraft:` reaches
|
||||
`native_ingredient(kind, data, items)` at load time. Fill `items` with the item ids the
|
||||
ingredient matches (item ids only: components are not checked; a match-time hook follows
|
||||
separately). Duplicates are removed keeping first-seen order. The hook may be called more
|
||||
than once per recipe; the answer must be the same. An unclaimed kind leaves the list empty
|
||||
and the recipe fails, as an unknown kind does.
|
||||
|
||||
### Data files
|
||||
|
||||
A bundle's `data/` folder (any files) is staged as `native-data/<mod>/` at the root of the workspace. A helper or
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"hooks": [{"name": "ingredient", "target": "horizon_client_lib::crafting::native_ingredient"}],
|
||||
"rust_summary": "a recipe ingredient of a mod's own kind\n(`native_ingredient`, which the unknown ingredient type in a namespace of\nown reaches with the kind, its JSON and the item list to fill).",
|
||||
"doc_summary": "a recipe ingredient of a mod's own kind. The ingredient entry that names a type in a namespace outside minecraft: reaches native_ingredient(kind, data, items) at load time. Fill items with the item ids the ingredient matches (item ids only: components are not checked; a match-time hook follows separately). Duplicates are removed keeping first-seen order. An unclaimed kind leaves the list empty and the recipe fails. The hook may be called more than once per recipe; the answer must be the same. Bundles that use it need \"hooks\": @API:ingredient@.",
|
||||
"comments": ["crates/horizon-client/src/crafting.rs"]
|
||||
}
|
||||
Reference in new issue
Block a user