Keep both reinforcement and recipe fixture isolation on main refresh

Formatting, all-target Clippy, Python options and Java oracle compilation pass at main 898d3e165. Preserve captured runtime qualifications.

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-05 05:25:55 +01:00
commit 5801a60185
7 files changed
+240 -9

No files matched your search

+60
View File
@@ -1717,6 +1717,14 @@ impl Recipes {
crafting.push((name, recipe));
}
}
// ResourceManagerRegistryLoadTask registers resource-file Identifiers by
// path, then namespace; RecipeMap preserves that first-match order.
// Include .json: the resource file, rather than the bare recipe id, is sorted.
// Other recipe types retain their existing table order.
crafting.sort_by_cached_key(|(name, _)| {
let (namespace, path) = name.split_once(':').unwrap_or(("minecraft", name.as_str()));
(format!("{path}.json"), namespace.to_owned())
});
let facts = items
.items
.iter()
@@ -2312,6 +2320,58 @@ mod tests {
let _ = fs::remove_dir_all(&root);
}
/// ResourceManagerRegistryLoadTask registers by resource-file Identifier,
/// and RecipeMap keeps that order when taking the first matching recipe.
#[test]
fn crafting_selection_follows_resource_file_order() {
let root =
std::env::temp_dir().join(format!("horizon-recipe-order-{}", std::process::id()));
fs::create_dir_all(root.join("data/minecraft/recipe")).unwrap();
let items = Items::for_tests(HashMap::new());
let recipe = |item: &str| {
serde_json::json!({
"type": "minecraft:crafting_shapeless", "ingredients": ["minecraft:flint"],
"result": {"id": item}
})
};
let cases = [
(vec!["minecraft:z_pick", "parity:a_pick"], "parity:a_pick"),
(
vec!["a_priority:z_pick", "z_priority:a_pick"],
"z_priority:a_pick",
),
(
vec!["z_priority:equal", "a_priority:equal"],
"a_priority:equal",
),
// File names include .json: '-' sorts before the parent's '.'.
(
vec!["parity:prefix", "parity:prefix-early"],
"parity:prefix-early",
),
(vec!["z_pick", "parity:a_pick"], "parity:a_pick"),
];
for (ids, expected) in cases {
for reverse in [false, true] {
let mut sources: Vec<_> = ids
.iter()
.map(|id| (id.to_string(), recipe("minecraft:stick")))
.collect();
if reverse {
sources.reverse();
}
let recipes =
Recipes::from_sources(&root, &items, &PackOverlay::NONE, sources).unwrap();
let (id, result) = recipes
.find(&[Some(ItemStack::new("minecraft:flint", 1))], 1)
.unwrap();
assert_eq!(id, expected, "input order: {ids:?}, reversed={reverse}");
assert_eq!(&*result.item, "minecraft:stick");
}
}
let _ = fs::remove_dir_all(&root);
}
/// A recipe whose `neoforge:conditions` hold loads, one whose don't is
/// silently absent, from the root's own files and a pack's alike.
#[test]
@@ -537,6 +537,17 @@ impl Game<'_> {
Ok(())
})?;
}
"crafter_tick" => {
let [x, y, z] =
std::array::from_fn(|i| op["pos"][i].as_i64().unwrap_or(0) as i32);
self.harness_owner(|g| {
let state = g.world.block(x, y, z);
if &*g.res.facts().get(state).name != "minecraft:crafter" {
return Err("command harness: crafter tick without a crafter".into());
}
g.dispense_from(horizon_core::position::BlockPos::new(x, y, z), state)
})?;
}
"break" => {
let pos = std::array::from_fn(|i| op["pos"][i].as_i64().unwrap_or(0) as i32);
self.harness_owner(|g| g.break_block(pos))?;
@@ -92,9 +92,8 @@ impl Game<'_> {
return Ok(());
};
let grid: Vec<Option<ItemStack>> = crafter.items.to_vec();
// RecipeCache.get: the first crafting recipe that matches, assembled.
// RecipeCache.get: query the active world recipe manager, then assemble its first match.
let found = self
.res
.recipes()
.find(&grid, 3)
.map(|(name, result)| (name.to_owned(), result))
@@ -182,7 +181,7 @@ impl Game<'_> {
(0..3).all(|i| (e[i] - c[i]).abs() <= 8.5 + 1.0)
});
if near {
let id = format!("minecraft:{recipe}");
let id = horizon_client::crafting::qualified(recipe);
let me = self.player_entity_for("minecraft:crafter_recipe_crafted");
self.trigger("minecraft:crafter_recipe_crafted", move |c, w| {
let ctx = adv::Entities {
@@ -443,6 +443,14 @@ public final class VanillaCommandExecOracle {
player.gameMode.useItemOn(player, player.level(), player.getMainHandItem(),
net.minecraft.world.InteractionHand.MAIN_HAND, hit);
}
case "crafter_tick" -> {
JsonArray p = op.getAsJsonArray("pos");
BlockPos pos = new BlockPos(p.get(0).getAsInt(), p.get(1).getAsInt(), p.get(2).getAsInt());
var state = player.level().getBlockState(pos);
if (!state.is(net.minecraft.world.level.block.Blocks.CRAFTER))
throw new IllegalStateException("crafter tick without a crafter");
state.tick(player.level(), pos, player.level().getRandom());
}
case "break" -> {
JsonArray pos = op.getAsJsonArray("pos");
player.gameMode.destroyBlock(new BlockPos(pos.get(0).getAsInt(), pos.get(1).getAsInt(), pos.get(2).getAsInt()));
+107 -2
View File
@@ -1794,6 +1794,10 @@ def progression():
def advancement_reward_fixtures(group_names):
if 'crafting recipe priority' in group_names:
return {f'h295:{name}': {'criteria': {'crafted': {
'trigger': 'minecraft:crafter_recipe_crafted', 'conditions': {'recipes': recipe}}}}
for name, recipe in [('custom_crafted', 'parity:aaa_star'), ('wrong_recipe', 'parity:zzz_star')]}
if 'advancement reward functions' not in group_names:
return {}
def advancement(function=None):
@@ -4951,6 +4955,107 @@ def bell_server_glow():
return ops + setup('/scoreboard objectives remove bell_clock')
def crafting_priority_recipes(group_names):
if 'crafting recipe priority' not in group_names:
return {}
def recipe(inputs, item, count=1):
return {'type': 'minecraft:crafting_shapeless', 'ingredients': ['minecraft:' + x for x in inputs],
'result': {'id': 'minecraft:' + item, 'count': count}}
# Write deliberately out of selection order; resource registration decides priority.
return {'parity:zzz_star': recipe(['gunpowder', 'red_dye'], 'emerald'),
'parity:aaa_star': recipe(['gunpowder', 'blue_dye'], 'diamond'),
'a_priority:z_h295_pick': recipe(['gold_nugget', 'paper'], 'diamond'),
'z_priority:a_h295_pick': recipe(['gold_nugget', 'paper'], 'emerald'),
'z_priority:h295_equal': recipe(['coal', 'paper'], 'emerald'),
'a_priority:h295_equal': recipe(['coal', 'paper'], 'gold_nugget', 3),
'parity:h295_resource': recipe(['clay_ball', 'paper'], 'brick'),
'parity:h295_resource-early': recipe(['clay_ball', 'paper'], 'nether_brick'),
'minecraft:oak_planks': recipe(['oak_log'], 'gold_ingot', 2),
'parity:zzzz_h295_unique': recipe(['apple', 'amethyst_shard'], 'quartz', 5)}
def crafting_recipe_priority(world=True):
"""Fresh natural crafters distinguish world selection, resource order and ID replacement."""
ops = setup('/forceload add 0 0 15 15', '/fill 0 199 0 15 199 15 stone',
'/tp @s 8.5 200 8.5 0 0', '/clear @s', '/recipe take @s *',
'/scoreboard objectives add recipe_order dummy',
'/data modify storage h295:identity uuid set from entity @s UUID')
ops += [cmd('/execute if loaded 4 200 4', expect_result=1),
cmd('/execute if entity @s[type=player,nbt={Pos:[8.5d,200d,8.5d],Health:20f}]', expect_result=1)]
if world:
for id in crafting_priority_recipes(['crafting recipe priority']):
ops += [cmd('/recipe give @s ' + id, expect_result=1)]
cases = [('early', ['gunpowder', 'blue_dye'], 'diamond', 1),
('late', ['gunpowder', 'red_dye'], 'firework_star', 1),
('path_before_namespace', ['gold_nugget', 'paper'], 'emerald', 1),
('same_path_namespace', ['coal', 'paper'], 'gold_nugget', 3),
('resource_file_prefix', ['clay_ball', 'paper'], 'nether_brick', 1),
('base_id_replacement', ['oak_log'], 'gold_ingot', 2),
('late_world_only', ['apple', 'amethyst_shard'], 'quartz', 5),
('ordinary', ['birch_log'], 'birch_planks', 4)]
else:
cases = [('base_star', ['gunpowder', 'blue_dye'], 'firework_star', 1),
('base_planks', ['oak_log'], 'oak_planks', 4),
('ordinary', ['birch_log'], 'birch_planks', 4)]
for label, inputs, output, count in cases:
ops += setup('/setblock 4 200 5 air', '/setblock 4 200 4 air', '/setblock 4 200 3 air',
'/setblock 4 200 3 chest[facing=south]', '/setblock 4 200 4 crafter[orientation=north_up]')
for i, item in enumerate(inputs):
ops += setup(f'/item replace block 4 200 4 container.{i} with minecraft:{item} 1')
ops += [cmd('/execute if entity @s[type=player,nbt={Pos:[8.5d,200d,8.5d],Health:20f}]', expect_result=1),
cmd('/execute if block 4 199 4 stone', expect_result=1),
cmd('/execute if block 4 200 4 crafter[orientation=north_up,triggered=false]', expect_result=1),
cmd('/execute if block 4 200 3 chest[facing=south]', expect_result=1),
cmd('/data get block 4 200 4 id'), cmd('/data get block 4 200 4 Items'),
cmd('/data get block 4 200 4 disabled_slots'), cmd('/data get block 4 200 3 Items')]
for i, item in enumerate(inputs):
ops += [cmd(f'/execute if items block 4 200 4 container.{i} minecraft:{item}', expect_result=1)]
ops += setup('/execute store result score start recipe_order run time query gametime')
ops += [cmd('/setblock 4 200 5 redstone_block'),
cmd('/execute if block 4 200 4 crafter[triggered=true]', expect_result=1),
{'op': 'tick', 'n': 10}, cmd('/data get block 4 200 4 Items'),
cmd('/execute unless data block 4 200 4 Items[]', expect_result=1),
cmd('/data get block 4 200 3 Items'),
cmd(f'/execute if items block 4 200 3 container.0 minecraft:{output}', expect_result=count),
cmd(f'/execute if data block 4 200 3 Items[{{Slot:0b,id:"minecraft:{output}",count:{count}}}]', expect_result=1)]
ops += setup('/execute store result score elapsed recipe_order run time query gametime',
'/scoreboard players operation elapsed recipe_order -= start recipe_order')
ops += [cmd('/scoreboard players get elapsed recipe_order', expect_result=10)]
if world:
# Open-air ejection exercises the existing trigger route, independently
# of insertion into a chest. The wrong recipe is a negative control.
for advancement in ['h295:custom_crafted', 'h295:wrong_recipe']:
ops += [cmd('/advancement grant @s only ' + advancement, expect_result=1),
cmd('/advancement revoke @s only ' + advancement, expect_result=1),
cmd('/execute unless entity @s[advancements={' + advancement + '=true}]', expect_result=1)]
ops += setup('/kill @e[type=item]', '/setblock 4 200 5 air', '/setblock 4 200 4 air',
'/setblock 4 200 3 air', '/setblock 4 200 4 crafter[orientation=north_up]',
'/item replace block 4 200 4 container.0 with gunpowder 1',
'/item replace block 4 200 4 container.1 with blue_dye 1')
ops += [cmd('/execute if entity @s[type=player,nbt={Pos:[8.5d,200d,8.5d],Health:20f}]', expect_result=1),
cmd('/execute if block 4 199 4 stone', expect_result=1),
cmd('/execute if block 4 200 3 air', expect_result=1),
cmd('/execute if block 4 200 4 crafter[orientation=north_up,triggered=false]', expect_result=1),
cmd('/execute if items block 4 200 4 container.0 gunpowder', expect_result=1),
cmd('/execute if items block 4 200 4 container.1 blue_dye', expect_result=1)]
ops += setup('/execute store result score start recipe_order run time query gametime')
# Invoke the real block tick in the connected owner's context. Ordinary
# server ticks have no acting player in this harness; recipient dispatch
# is a separate route and is not repaired by recipe-ID normalization.
ops += [cmd('/setblock 4 200 5 redstone_block'),
{'op': 'crafter_tick', 'pos': [4, 200, 4]}, {'op': 'tick', 'n': 10},
cmd('/execute unless data block 4 200 4 Items[]', expect_result=1),
cmd('/execute if entity @e[type=item,nbt={Item:{id:"minecraft:diamond",count:1}}]', expect_result=1),
cmd('/execute if entity @s[advancements={h295:custom_crafted=true}]', expect_result=1),
cmd('/execute unless entity @s[advancements={h295:wrong_recipe=true}]', expect_result=1)]
ops += setup('/execute store result score elapsed recipe_order run time query gametime',
'/scoreboard players operation elapsed recipe_order -= start recipe_order')
ops += [cmd('/scoreboard players get elapsed recipe_order', expect_result=10)]
ops += setup('/execute store success score identity_changed recipe_order run data modify storage h295:identity uuid set from entity @s UUID')
ops += [cmd('/scoreboard players get identity_changed recipe_order', expect_result=0)]
return ops + setup('/setblock 4 200 5 air', '/scoreboard objectives remove recipe_order')
def advancement_reward_functions():
"""Function-only impossible advancements isolate reward dispatch from triggers and XP/loot."""
ops = setup('/forceload add 0 0 15 15', '/setblock 8 229 8 stone',
@@ -5354,8 +5459,8 @@ def groups():
('command reinforcement once', command_zombie_reinforcements(True)),
('advancement world loot rewards', advancement_world_loot_rewards()),
('advancement empty loot reference', advancement_empty_loot_reference()),
('crafting recipe priority', crafting_recipe_priority()),
('crafting base recipe control', crafting_recipe_priority(False)),
('item stack-size override merges', stack_limit_merges(cmd, setup)),
('moving piston command data', moving_piston_data(cmd, setup)),
('mob team command names', mob_team_command_names()),
+19 -4
View File
@@ -39,8 +39,8 @@ except ImportError:
SEED = 1
def install_datapack_fixtures(world, predicates, functions, jar, advancements=None, loot_tables=None):
if not predicates and not functions and not advancements and not loot_tables:
def install_datapack_fixtures(world, predicates, functions, jar, advancements=None, loot_tables=None, recipes=None):
if not predicates and not functions and not advancements and not loot_tables and not recipes:
return
with zipfile.ZipFile(jar) as archive:
version = json.loads(archive.read('version.json'))['pack_version']
@@ -73,6 +73,13 @@ def install_datapack_fixtures(world, predicates, functions, jar, advancements=No
target.write_text(json.dumps(table), encoding='utf-8')
for identifier, recipe in (recipes or {}).items():
namespace, path = identifier.split(':', 1)
target = pack / 'data' / namespace / 'recipe' / (path + '.json')
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json.dumps(recipe), encoding='utf-8')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prepare', action='store_true')
@@ -161,6 +168,11 @@ def main():
groups = [(name, corpus_module.advancement_world_loot_rewards(
named_table=args.advancement_loot_named_diagnostic, include_missing_ack=True)
if name == 'advancement world loot rewards' else ops) for name, ops in groups]
# These overlays replace built-in recipes: run each in its own world, never
# beside the generic corpus's fixed vanilla recipe-count controls.
isolated_recipes = {'crafting recipe priority', 'crafting base recipe control'}
if not args.only:
groups = [(name, ops) for name, ops in groups if name not in isolated_recipes]
if args.loaded_fixture:
groups = [('setup', corpus_module.group_setup()),
('loaded ticket removal', corpus_module.loaded_ticket_removal(args.loaded_block_diagnostic))]
@@ -229,6 +241,8 @@ def main():
'/item fill block 4 200 4 {type:"minecraft:slot_range",slots:"container.99"} with diamond')]))
if any(name in isolated_reinforcements for name, _ in groups) and sum(name != 'setup' for name, _ in groups) != 1:
parser.error('reinforcement fixtures require --only with one exact group')
if any(name in isolated_recipes for name, _ in groups) and sum(name != 'setup' for name, _ in groups) != 1:
parser.error('crafting recipe fixtures require --only with one exact group')
script = []
for _, lines in groups:
script.extend(lines)
@@ -237,6 +251,7 @@ def main():
ensure_ascii=False) + '\n' for op in script), encoding='utf-8')
predicates = corpus_module.predicate_selector_fixtures([name for name, _ in groups])
functions = corpus_module.function_return_fixtures([name for name, _ in groups])
recipes = corpus_module.crafting_priority_recipes([name for name, _ in groups])
advancements = corpus_module.advancement_reward_fixtures([name for name, _ in groups])
loot_advancements, loot_tables = corpus_module.advancement_loot_fixtures([name for name, _ in groups])
advancements.update(loot_advancements)
@@ -249,7 +264,7 @@ def main():
vanilla_dir = build / 'vanilla'
shutil.rmtree(vanilla_dir, ignore_errors=True)
vanilla_dir.mkdir()
install_datapack_fixtures(vanilla_dir / 'universe/harness', predicates, functions, jars['server'], advancements, loot_tables)
install_datapack_fixtures(vanilla_dir / 'universe/harness', predicates, functions, jars['server'], advancements, loot_tables, recipes=recipes)
java_options = []
if args.orb_water_fixture:
(build / 'orb-water-reference.json').unlink(missing_ok=True)
@@ -287,7 +302,7 @@ def main():
rust_dir = build / 'rust'
shutil.rmtree(rust_dir, ignore_errors=True)
rust_dir.mkdir()
install_datapack_fixtures(rust_dir / 'saves/harness', predicates, functions, jars['server'], advancements, loot_tables)
install_datapack_fixtures(rust_dir / 'saves/harness', predicates, functions, jars['server'], advancements, loot_tables, recipes=recipes)
# The oracle's Services resolve no profile (/fetchprofile fails on the next tick): the game gets no services
# either, a discovery address that refuses at once, rather than Mojang's over the network.
env = lab.game_env(HORIZON_PLAYER_NAME="Player", HORIZON_COMMAND_SCRIPT=script_path,
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Natural crafter first-match priority, in separate world-pack and vanilla worlds.
Reuse the actual Game / pinned Java command runner, but keep the overriding
recipe pack out of every other command group's registry and count controls.
"""
import argparse
from pathlib import Path
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=Path, help='use this built Game binary')
parser.add_argument('--prepare', action='store_true')
args = parser.parse_args()
runner = Path(__file__).with_name('test_command_exec_parity.py')
build = runner.resolve().parents[1] / '.local/crafting-priority-parity'
for phase, group in [('world', 'crafting recipe priority'), ('base', 'crafting base recipe control')]:
command = [sys.executable, str(runner), '--only', group, '--work-dir', str(build / phase)]
if args.binary:
command += ['--binary', str(args.binary.resolve())]
if args.prepare:
command.append('--prepare')
result = subprocess.run(command, check=False)
if result.returncode:
return result.returncode
return 0
if __name__ == '__main__':
sys.exit(main())