mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 02:29:50 -10:00
fix(mobs): let farmers tend registered mod crops
Recognize farmland and crop templates through their retained class chains and plant loaded tagged block items in inventory order. Add vanilla-oracle classification and real content-root regressions. Co-Authored-By: GPT-6.1 Sol <[email protected]>
This commit is contained in:
1 parent
f8bfa06b90
commit
873ccc1ecf
7 files changed
+460
-46
No files matched your search
@@ -345,25 +345,28 @@ pub fn work_sound(profession: u8) -> Option<&'static str> {
|
||||
}
|
||||
/// A `CropBlock` at `p`.
|
||||
pub fn crop(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
matches!(
|
||||
w.block_name(p[0], p[1], p[2]),
|
||||
"minecraft:wheat"
|
||||
| "minecraft:carrots"
|
||||
| "minecraft:potatoes"
|
||||
| "minecraft:beetroots"
|
||||
| "minecraft:torchflower_crop"
|
||||
)
|
||||
w.facts
|
||||
.get(w.world.block(p[0], p[1], p[2]))
|
||||
.has_class("CropBlock")
|
||||
}
|
||||
/// `CropBlock.isMaxAge` for the block at `p`.
|
||||
pub fn ripe_crop(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
let max = match w.block_name(p[0], p[1], p[2]) {
|
||||
"minecraft:wheat" | "minecraft:carrots" | "minecraft:potatoes" => "7",
|
||||
"minecraft:beetroots" => "3",
|
||||
// TorchflowerCropBlock.getMaxAge is 2, past its age property (0-1):
|
||||
// the crop is never at max age (age 2 is the torchflower itself).
|
||||
_ => return false,
|
||||
let facts = w.facts.get(w.world.block(p[0], p[1], p[2]));
|
||||
if !facts.has_class("CropBlock") {
|
||||
return false;
|
||||
}
|
||||
// The class chain survives content registration, including the two
|
||||
// getMaxAge overrides. PitcherCropBlock is not a CropBlock.
|
||||
let max = if facts.has_class("BeetrootBlock") {
|
||||
3
|
||||
} else if facts.has_class("TorchflowerCropBlock") {
|
||||
2
|
||||
} else {
|
||||
7
|
||||
};
|
||||
w.prop(p[0], p[1], p[2], "age") == Some(max)
|
||||
w.prop(p[0], p[1], p[2], "age")
|
||||
.and_then(|age| age.parse::<i32>().ok())
|
||||
.is_some_and(|age| age >= max)
|
||||
}
|
||||
|
||||
/// The gift loot table of a villager (`GiveGiftToHero.getLootTableToThrow`).
|
||||
|
||||
@@ -775,7 +775,7 @@ fn sense_babies(m: &mut Mob, w: &MobWorld) {
|
||||
}
|
||||
mem(m).visible_villager_babies.set_list(babies);
|
||||
}
|
||||
/// `SecondaryPoiSensor`: a farmer's farmland within four blocks.
|
||||
/// `SecondaryPoiSensor.doTick`: a farmer's farmland within four blocks.
|
||||
fn sense_secondary(m: &mut Mob, w: &MobWorld) {
|
||||
let mut sites = Vec::new();
|
||||
if super::villager::is_farmer(m) {
|
||||
@@ -784,7 +784,7 @@ fn sense_secondary(m: &mut Mob, w: &MobWorld) {
|
||||
for y in -2..=2 {
|
||||
for z in -4..=4 {
|
||||
let p = [c[0] + x, c[1] + y, c[2] + z];
|
||||
if w.block_name(p[0], p[1], p[2]) == "minecraft:farmland" {
|
||||
if farmland(w, p) {
|
||||
sites.push(p);
|
||||
}
|
||||
}
|
||||
@@ -3494,6 +3494,10 @@ fn restock_if_due(m: &mut Mob, w: &MobWorld) {
|
||||
t.restock(w.game_time);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[path = "villager_farming_tests.rs"]
|
||||
mod farming_tests;
|
||||
|
||||
/// `HarvestFarmland.validFarmlandAroundVillager`: ripe crops and bare
|
||||
/// farmland around the villager.
|
||||
fn farmland_around(m: &Mob, w: &MobWorld) -> Vec<[i32; 3]> {
|
||||
@@ -3514,10 +3518,16 @@ fn farmland_around(m: &Mob, w: &MobWorld) -> Vec<[i32; 3]> {
|
||||
}
|
||||
valid
|
||||
}
|
||||
/// Farmland subclasses include mod templates.
|
||||
fn farmland(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
w.facts
|
||||
.get(w.world.block(p[0], p[1], p[2]))
|
||||
.has_class("FarmlandBlock")
|
||||
}
|
||||
/// `HarvestFarmland.validPos`: a ripe CropBlock or air above farmland.
|
||||
fn farmland_valid(w: &MobWorld, p: [i32; 3]) -> bool {
|
||||
super::villager::ripe_crop(w, p)
|
||||
|| w.facts.is_air(w.world.block(p[0], p[1], p[2]))
|
||||
&& w.block_name(p[0], p[1] - 1, p[2]) == "minecraft:farmland"
|
||||
|| w.facts.is_air(w.world.block(p[0], p[1], p[2])) && farmland(w, [p[0], p[1] - 1, p[2]])
|
||||
}
|
||||
/// `HarvestFarmland.getValidFarmland`: one picked by the level random.
|
||||
fn pick_farmland(valid: &[[i32; 3]], cx: &mut Bx) -> Option<[i32; 3]> {
|
||||
@@ -3527,17 +3537,13 @@ fn pick_farmland(valid: &[[i32; 3]], cx: &mut Bx) -> Option<[i32; 3]> {
|
||||
let i = cx.lr.next_int_bound(valid.len() as i32).unwrap_or(0) as usize;
|
||||
Some(valid[i])
|
||||
}
|
||||
/// The seeds a villager plants (`VILLAGER_PLANTABLE_SEEDS`) and their crops.
|
||||
fn seed_crop(item: &str) -> Option<&'static str> {
|
||||
Some(match item {
|
||||
"minecraft:wheat_seeds" => "minecraft:wheat",
|
||||
"minecraft:potato" => "minecraft:potatoes",
|
||||
"minecraft:carrot" => "minecraft:carrots",
|
||||
"minecraft:beetroot_seeds" => "minecraft:beetroots",
|
||||
"minecraft:torchflower_seeds" => "minecraft:torchflower_crop",
|
||||
"minecraft:pitcher_pod" => "minecraft:pitcher_crop",
|
||||
_ => return None,
|
||||
})
|
||||
/// A tagged BlockItem plants its block's default state.
|
||||
fn seed_crop(w: &MobWorld, item: &str) -> Option<horizon_world::registry::BlockStateId> {
|
||||
let items = w.items?;
|
||||
if !items.item_in_tag("minecraft:villager_plantable_seeds", item) {
|
||||
return None;
|
||||
}
|
||||
items.get(item)?.block
|
||||
}
|
||||
/// `HarvestFarmland.tick` at the farmland: harvest a ripe crop, sow bare
|
||||
/// farmland, and move on from a growing crop.
|
||||
@@ -3559,23 +3565,22 @@ fn harvest_tick(
|
||||
if ripe {
|
||||
cx.events.push(MobEvent::BreakBlock { pos: p, drop: true });
|
||||
}
|
||||
if air && w.block_name(p[0], p[1] - 1, p[2]) == "minecraft:farmland" {
|
||||
if air && farmland(w, [p[0], p[1] - 1, p[2]]) {
|
||||
let e = super::villager::ext_mut(m);
|
||||
if let Some(slot) = e
|
||||
if let Some((slot, crop)) = e
|
||||
.inventory
|
||||
.iter()
|
||||
.position(|s| s.count > 0 && seed_crop(&s.item).is_some())
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.count > 0)
|
||||
.find_map(|(i, s)| seed_crop(w, &s.item).map(|crop| (i, crop)))
|
||||
{
|
||||
let crop = seed_crop(&e.inventory[slot].item).unwrap_or("minecraft:wheat");
|
||||
e.inventory[slot].count -= 1;
|
||||
if e.inventory[slot].count == 0 {
|
||||
e.inventory.remove(slot);
|
||||
}
|
||||
cx.events.push(MobEvent::SetBlockName {
|
||||
cx.events.push(MobEvent::SetBlock {
|
||||
pos: p,
|
||||
name: crop,
|
||||
effect: false,
|
||||
flags: horizon_world::level::flags::UPDATE_ALL,
|
||||
state: crop,
|
||||
});
|
||||
cx.events.push(MobEvent::Sound(
|
||||
"item.crop.plant",
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
use super::*;
|
||||
use crate::{items::ItemStack, mod_content::ContentMod};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
fn resources() -> &'static std::path::Path {
|
||||
static ROOT: OnceLock<PathBuf> = OnceLock::new();
|
||||
ROOT.get_or_init(|| {
|
||||
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client/26.3");
|
||||
let scratch = std::env::temp_dir().join(format!("fresh-farming-{}", std::process::id()));
|
||||
let dir = scratch.join("mod");
|
||||
let mut files = vec![(
|
||||
"horizon-mod.toml".to_owned(),
|
||||
"id = \"fieldtest\"\nname = \"Field test\"\nversion = \"1.0\"\napi = \"0.2\"\nside = \"both\"\ncapabilities = [\"registries\"]\n".to_owned(),
|
||||
)];
|
||||
for (id, template) in [
|
||||
("soil", "farmland"),
|
||||
("grain", "wheat"),
|
||||
("roots", "beetroots"),
|
||||
("flower", "torchflower_crop"),
|
||||
("pod", "pitcher_crop"),
|
||||
("fake_crop", "stone"),
|
||||
] {
|
||||
files.push((
|
||||
format!("data/fieldtest/horizon/block/{id}.json"),
|
||||
format!(r#"{{"template":"minecraft:{template}"}}"#),
|
||||
));
|
||||
}
|
||||
files.push((
|
||||
"data/minecraft/tags/item/villager_plantable_seeds.json".into(),
|
||||
r#"{"replace":true,"values":["fieldtest:grain","fieldtest:roots","minecraft:stick"]}"#.into(),
|
||||
));
|
||||
for (name, text) in files {
|
||||
let path = dir.join(name);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, text).unwrap();
|
||||
}
|
||||
let package = horizon_mods::Package::read(&dir).unwrap();
|
||||
let content = horizon_mods::Content::read(&dir, &package.manifest).unwrap();
|
||||
let out = scratch.join("resources");
|
||||
crate::mod_content::build(&base, &[ContentMod { id: "fieldtest".into(), content }], &out)
|
||||
.unwrap();
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
fn farmer(types: &super::super::MobTypes) -> Mob {
|
||||
let mut m = Mob::new(
|
||||
1,
|
||||
Arc::new(types.types["minecraft:villager"].clone()),
|
||||
Vec3::new(0.5, 1.0, 0.5),
|
||||
0.0,
|
||||
);
|
||||
super::super::villager::set_profession(&mut m, 5);
|
||||
m
|
||||
}
|
||||
|
||||
fn harvest() -> B {
|
||||
B::HarvestFarmland {
|
||||
t: timed(60, 60),
|
||||
next_ok: 0,
|
||||
target: None,
|
||||
valid: Vec::new(),
|
||||
worked: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn put(
|
||||
world: &mut crate::world::World,
|
||||
x: i32,
|
||||
y: i32,
|
||||
state: horizon_world::registry::BlockStateId,
|
||||
) {
|
||||
if !world.chunks.contains_key(&(x >> 4, 0)) {
|
||||
world
|
||||
.chunks
|
||||
.insert((x >> 4, 0), Arc::new(crate::chunk::Chunk::empty(x >> 4, 0)));
|
||||
}
|
||||
world.set(x, y, 0, state).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "full content root: tools/test_villager_farming_parity.py runs it"]
|
||||
fn mod_farmland_is_a_secondary_site_and_farming_target() {
|
||||
assert!(super::super::world::with_resource_test_world(
|
||||
resources(),
|
||||
&[],
|
||||
true,
|
||||
|world, reg| {
|
||||
put(world, 0, 0, reg.default_state("fieldtest:soil").unwrap());
|
||||
},
|
||||
|w, types| {
|
||||
let mut m = farmer(types);
|
||||
sense_secondary(&mut m, w);
|
||||
assert_eq!(
|
||||
mem(&mut m).secondary_job_site.value(),
|
||||
Some(vec![[0, 0, 0]])
|
||||
);
|
||||
let mut lr = LegacyRandom::new(11);
|
||||
let mut events = Vec::new();
|
||||
let mut cx = Bx {
|
||||
w,
|
||||
lr: &mut lr,
|
||||
events: &mut events,
|
||||
now: 1000,
|
||||
refresh: false,
|
||||
};
|
||||
assert!(harvest().try_start(&mut m, &mut cx));
|
||||
let mut denied = w.clone();
|
||||
denied.mob_griefing = false;
|
||||
mem(&mut m).look_target.clear();
|
||||
mem(&mut m).walk_target.clear();
|
||||
cx.w = &denied;
|
||||
assert!(!harvest().try_start(&mut m, &mut cx));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "full content root: tools/test_villager_farming_parity.py runs it"]
|
||||
fn mod_crops_are_harvested_only_at_their_vanilla_templates_max_age() {
|
||||
assert!(super::super::world::with_resource_test_world(
|
||||
resources(),
|
||||
&[],
|
||||
true,
|
||||
|world, reg| {
|
||||
for (x, block, age) in [
|
||||
(0, "grain", "6"),
|
||||
(1, "grain", "7"),
|
||||
(2, "roots", "3"),
|
||||
(3, "flower", "1"),
|
||||
(4, "pod", "4"),
|
||||
] {
|
||||
let mut state = reg
|
||||
.state(reg.default_state(&format!("fieldtest:{block}")).unwrap())
|
||||
.unwrap()
|
||||
.clone();
|
||||
state.properties.insert("age".into(), age.into());
|
||||
put(world, x, 1, reg.resolve(&state).unwrap());
|
||||
}
|
||||
put(
|
||||
world,
|
||||
5,
|
||||
1,
|
||||
reg.default_state("fieldtest:fake_crop").unwrap(),
|
||||
);
|
||||
},
|
||||
|w, types| {
|
||||
let mut m = farmer(types);
|
||||
let mut lr = LegacyRandom::new(12);
|
||||
let mut events = Vec::new();
|
||||
for (x, ripe) in [
|
||||
(0, false),
|
||||
(1, true),
|
||||
(2, true),
|
||||
(3, false),
|
||||
(4, false),
|
||||
(5, false),
|
||||
] {
|
||||
events.clear();
|
||||
harvest_tick(
|
||||
&mut m,
|
||||
&mut Bx {
|
||||
w,
|
||||
lr: &mut lr,
|
||||
events: &mut events,
|
||||
now: 1000,
|
||||
refresh: false,
|
||||
},
|
||||
&mut 0,
|
||||
&mut Some([x, 1, 0]),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
assert_eq!(events.iter().any(|e| matches!(e, MobEvent::BreakBlock { pos, drop: true } if *pos == [x, 1, 0])), ripe, "x={x}");
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "full content root: tools/test_villager_farming_parity.py runs it"]
|
||||
fn farming_uses_the_first_tagged_block_item_and_consumes_one_seed() {
|
||||
assert!(super::super::world::with_resource_test_world(
|
||||
resources(),
|
||||
&[],
|
||||
true,
|
||||
|world, reg| {
|
||||
for x in 0..=1 {
|
||||
put(world, x, 0, reg.default_state("fieldtest:soil").unwrap());
|
||||
}
|
||||
},
|
||||
|w, types| {
|
||||
let mut m = farmer(types);
|
||||
let inventory = &mut super::super::villager::ext_mut(&mut m).inventory;
|
||||
*inventory = vec![
|
||||
ItemStack::new("minecraft:wheat_seeds", 2),
|
||||
ItemStack::new("minecraft:stick", 2),
|
||||
ItemStack::new("fieldtest:flower", 2),
|
||||
ItemStack::new("fieldtest:roots", 1),
|
||||
ItemStack::new("fieldtest:grain", 2),
|
||||
];
|
||||
let mut lr = LegacyRandom::new(13);
|
||||
let mut events = Vec::new();
|
||||
harvest_tick(
|
||||
&mut m,
|
||||
&mut Bx {
|
||||
w,
|
||||
lr: &mut lr,
|
||||
events: &mut events,
|
||||
now: 1000,
|
||||
refresh: false,
|
||||
},
|
||||
&mut 0,
|
||||
&mut Some([0, 1, 0]),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
let planted = events.iter().find_map(|e| match e {
|
||||
MobEvent::SetBlock { state, .. } => {
|
||||
Some(w.registry.state(*state).unwrap().name.as_str())
|
||||
}
|
||||
MobEvent::SetBlockName { name, .. } => Some(*name),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(planted, Some("fieldtest:roots"));
|
||||
let inventory = &super::super::villager::ext(&m).unwrap().inventory;
|
||||
assert_eq!(
|
||||
inventory
|
||||
.iter()
|
||||
.map(|s| (&*s.item, s.count))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("minecraft:wheat_seeds", 2),
|
||||
("minecraft:stick", 2),
|
||||
("fieldtest:flower", 2),
|
||||
("fieldtest:grain", 2)
|
||||
]
|
||||
);
|
||||
events.clear();
|
||||
harvest_tick(
|
||||
&mut m,
|
||||
&mut Bx {
|
||||
w,
|
||||
lr: &mut lr,
|
||||
events: &mut events,
|
||||
now: 1001,
|
||||
refresh: false,
|
||||
},
|
||||
&mut 0,
|
||||
&mut Some([1, 1, 0]),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
let expected = w.registry.default_state("fieldtest:grain").unwrap();
|
||||
assert!(events.iter().any(
|
||||
|e| matches!(e, MobEvent::SetBlock { pos: [1, 1, 0], state } if *state == expected)
|
||||
));
|
||||
assert_eq!(
|
||||
super::super::villager::ext(&m)
|
||||
.unwrap()
|
||||
.inventory
|
||||
.last()
|
||||
.unwrap()
|
||||
.count,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
events
|
||||
.iter()
|
||||
.filter(|e| matches!(e, MobEvent::Sound("item.crop.plant", _, 1.0, 1.0)))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "tools/test_villager_farming_parity.py supplies the vanilla oracle"]
|
||||
fn vanilla_farming_classification_matches_the_oracle() {
|
||||
let path = std::env::var_os("HORIZON_FARMING_ORACLE").expect("vanilla oracle output");
|
||||
let rows: Vec<(u32, Vec<bool>)> = std::fs::read_to_string(path)
|
||||
.unwrap()
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let mut fields = line.split_whitespace();
|
||||
(
|
||||
fields.next().unwrap().parse().unwrap(),
|
||||
fields.map(|f| f.parse().unwrap()).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert!(!rows.is_empty());
|
||||
assert!(super::super::world::with_built_test_world(
|
||||
&[],
|
||||
|world, _| {
|
||||
for (x, (state, _)) in rows.iter().enumerate() {
|
||||
put(
|
||||
world,
|
||||
x as i32,
|
||||
0,
|
||||
horizon_world::registry::BlockStateId(*state),
|
||||
);
|
||||
}
|
||||
},
|
||||
|w, _| {
|
||||
for (x, (state, expected)) in rows.iter().enumerate() {
|
||||
let p = [x as i32, 0, 0];
|
||||
assert_eq!(
|
||||
vec![
|
||||
super::super::villager::crop(w, p),
|
||||
super::super::villager::ripe_crop(w, p),
|
||||
farmland_valid(w, [p[0], 1, 0])
|
||||
],
|
||||
*expected,
|
||||
"state={state}"
|
||||
);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -417,24 +417,35 @@ pub(crate) fn with_built_test_world_rules(
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
with_resource_test_world(&root, others, mob_griefing, build, f)
|
||||
}
|
||||
/// The same mob world loaded from a content mod's resource root.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_resource_test_world(
|
||||
root: &std::path::Path,
|
||||
others: &[Seen],
|
||||
mob_griefing: bool,
|
||||
build: impl FnOnce(&mut crate::world::World, &horizon_world::registry::BlockRegistry),
|
||||
f: impl FnOnce(&MobWorld<'_>, &super::MobTypes),
|
||||
) -> bool {
|
||||
if !root.join("entity-types.json").exists() || !root.join("movement-properties.nbt").exists() {
|
||||
return false;
|
||||
}
|
||||
let assets = crate::assets::Assets::load(&root).unwrap();
|
||||
let assets = crate::assets::Assets::load(root).unwrap();
|
||||
let registry = assets.registry;
|
||||
let facts = BlockFacts::load(&root, registry).unwrap();
|
||||
let motion = crate::motion::MotionResources::load(&root, &assets).unwrap();
|
||||
let data = super::MobData::load(&root, &facts);
|
||||
let types = super::MobTypes::load(&root).unwrap();
|
||||
let biomes = crate::biome::Biomes::load(&root).unwrap();
|
||||
let facts = BlockFacts::load(root, registry).unwrap();
|
||||
let motion = crate::motion::MotionResources::load(root, &assets).unwrap();
|
||||
let data = super::MobData::load(root, &facts);
|
||||
let types = super::MobTypes::load(root).unwrap();
|
||||
let biomes = crate::biome::Biomes::load(root).unwrap();
|
||||
let items = crate::items::Items::load(
|
||||
&root,
|
||||
root,
|
||||
registry,
|
||||
&facts,
|
||||
&horizon_world::loot::PackOverlay::NONE,
|
||||
)
|
||||
.unwrap();
|
||||
let collision = crate::collisions::load(&root, registry).unwrap();
|
||||
let collision = crate::collisions::load(root, registry).unwrap();
|
||||
let air = registry.default_state("minecraft:air").unwrap();
|
||||
let mut world = crate::world::World::new(air, 0);
|
||||
build(&mut world, registry);
|
||||
|
||||
@@ -11,6 +11,16 @@ switch. The first install shows a trust notice, and every build waits behind a r
|
||||
after a review (its later releases are checked by machine); and they play in a game of their own, built for the
|
||||
instance they are installed in.
|
||||
|
||||
## Villager farming
|
||||
|
||||
Farmers recognize mod blocks made from farmland and `CropBlock` templates, including declarations from
|
||||
`native_blocks`. A crop block's generated item is its seed: add that item to
|
||||
`minecraft:villager_plantable_seeds` to let farmers plant its default block state from their inventory.
|
||||
Untagged items and tagged items without a block are skipped. Crop maturity follows the template's class:
|
||||
beetroots mature at age 3, ordinary crops at 7; torchflower crops never reach their maximum age 2 while
|
||||
still crops. Pitcher crops are not `CropBlock`s and are not harvested by this behavior. Farming still
|
||||
requires a farmer at work and `mob_griefing` enabled.
|
||||
|
||||
## Pinned to a Nightly
|
||||
|
||||
A native mod builds on one exact Horizon commit: `[compatibility] horizon_revision` in its `horizon-mixin.toml`,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.CropBlock;
|
||||
import net.minecraft.world.level.block.FarmlandBlock;
|
||||
|
||||
/** HarvestFarmland's class and maturity checks, including non-CropBlock age properties. */
|
||||
public final class VanillaVillagerFarmingOracle {
|
||||
public static void main(String[] args) throws Exception {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
try (var out = Files.newBufferedWriter(Path.of(args[0]))) {
|
||||
for (String name : new String[] {"wheat", "carrots", "potatoes", "beetroots",
|
||||
"torchflower_crop", "pitcher_crop", "cocoa", "farmland", "stone"}) {
|
||||
var block = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + name));
|
||||
for (var state : block.getStateDefinition().getPossibleStates()) {
|
||||
boolean crop = block instanceof CropBlock;
|
||||
boolean ripe = block instanceof CropBlock c && c.isMaxAge(state);
|
||||
out.write(Block.getId(state) + " " + crop + " " + ripe + " " + (block instanceof FarmlandBlock));
|
||||
out.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare villager farming classification with the actual vanilla 26.3 block objects.
|
||||
|
||||
Also runs the content-mod farming regressions. Outputs are private dev data, not game resources.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from . import lab, oracle_harness, test_rust_parity as base, test_model_baking_parity as baking
|
||||
except ImportError:
|
||||
import lab, oracle_harness, test_rust_parity as base, test_model_baking_parity as baking
|
||||
|
||||
|
||||
def main():
|
||||
build = Path(os.environ.get("HORIZON_FARMING_PARITY_DIR", lab.LOCAL / "villager-farming-parity"))
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
lock, jars, _ = base.verify_artifacts()
|
||||
classes, dependencies, _ = oracle_harness.compile_oracle(
|
||||
build, "VanillaVillagerFarmingOracle", jars, baking.client_libraries(), lock["minecraft"]["java_major"])
|
||||
expected = build / "vanilla.txt"
|
||||
oracle_harness.java_run(build, classes, dependencies, "VanillaVillagerFarmingOracle", [expected], "vanilla")
|
||||
env = dict(lab.game_env(), HORIZON_FARMING_ORACLE=str(expected))
|
||||
oracle_harness.run_logged([
|
||||
"cargo", "test", "--locked", "-p", "horizon-client-lib", "--lib",
|
||||
"mobs::villager_brain::farming_tests", "--", "--include-ignored", "--nocapture",
|
||||
], build, "rust", env=env)
|
||||
print(f"{len(expected.read_text().splitlines())} vanilla block states match; content-mod farming regressions pass")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user