mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Lithostitched's limited and guaranteed elements and its processor-list modifiers
The game reads lithostitched:limited and lithostitched:guaranteed pool elements as written (spec-lithostitched §3): a limited element places its delegate at most `limit` times per start, a guaranteed one is tried first at each jigsaw of its pool until `count` are placed, and neither goes below a parent shallower than `min_depth` (IQ-10). Layouts stay vanilla's (L5); with no such element vanilla's draws are unchanged. Saved pieces of these types read as their delegate. The carry keeps them (delegating still places its delegate), writes add_processor_list_processors as a processor-list addition (§2.2, 1.8.1's `processor_lists` key) and both modifier kinds with their priority; the game merges additions by `horizon:priority`, then mod and file order. Harnesses: the layout harness checks the probe pack's limits on Horizon's own probe villages, and the blocks harness gets a lithostitched-probe profile; the probe pack is built into a jar from the oracle's profile. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
eb6c81945a
commit
cc2a2a8741
8 files changed
+461
-113
No files matched your search
@@ -729,24 +729,29 @@ fn worldgen_entry(file: &str) -> Option<(&'static str, String)> {
|
||||
Some((bundle, format!("{ns}:{}", path.strip_suffix(".json")?)))
|
||||
}
|
||||
|
||||
/// The key of a mod's structure set or template pool file that adds its
|
||||
/// entries to other sets or pools (vanilla's, another mod's) instead of
|
||||
/// being one of its own. Vanilla data never has it.
|
||||
/// The key of a mod's structure set, template pool or processor list file
|
||||
/// that adds its entries to other sets, pools or lists (vanilla's, another
|
||||
/// mod's) instead of being one of its own. Vanilla data never has it.
|
||||
const ADD_TO: &str = "horizon:add_to";
|
||||
/// An addition's place among the others (lower first; 1000 by default), as
|
||||
/// Lithostitched's modifiers' `priority`.
|
||||
const PRIORITY: &str = "horizon:priority";
|
||||
/// The bundles a mod may add to, and the list each entry goes into.
|
||||
const ADDABLE: [(&str, &str); 2] = [
|
||||
const ADDABLE: [(&str, &str); 3] = [
|
||||
("worldgen/structure_sets.json", "structures"),
|
||||
("worldgen/template_pools.json", "elements"),
|
||||
("worldgen/processor_lists.json", "processors"),
|
||||
];
|
||||
|
||||
/// A mod file's additions (`horizon:add_to`): its entries appended to the
|
||||
/// list of each set or pool it names, as lithostitched's
|
||||
/// `add_structure_set_entries` and `add_template_pool_elements` do for
|
||||
/// NeoForge and Fabric packs.
|
||||
/// list of each set, pool or processor list it names, as Lithostitched's
|
||||
/// `add_structure_set_entries`, `add_template_pool_elements` and
|
||||
/// `add_processor_list_processors` do for NeoForge and Fabric packs.
|
||||
struct Addition {
|
||||
targets: Vec<String>,
|
||||
list: &'static str,
|
||||
entries: Vec<J>,
|
||||
priority: i64,
|
||||
}
|
||||
impl Addition {
|
||||
/// What a mod file of `bundle` adds, if it is an addition file.
|
||||
@@ -776,10 +781,16 @@ impl Addition {
|
||||
let Some(J::Arr(entries)) = doc.get(list) else {
|
||||
return Err(format!("{ADD_TO} without a {list} list").into());
|
||||
};
|
||||
let priority = match doc.get(PRIORITY) {
|
||||
None => 1000,
|
||||
Some(J::Num(n)) => n.as_i64().ok_or(format!("{PRIORITY}: not an integer"))?,
|
||||
Some(_) => return Err(format!("{PRIORITY}: not an integer").into()),
|
||||
};
|
||||
Ok(Some(Self {
|
||||
targets,
|
||||
list,
|
||||
entries: entries.clone(),
|
||||
priority,
|
||||
}))
|
||||
}
|
||||
/// Appends the entries to each target in the bundle `doc` (id → the
|
||||
@@ -1215,7 +1226,11 @@ pub fn build(base: &Path, mods: &[ContentMod], out: &Path) -> Result<Vec<String>
|
||||
overlay.write(BIOME_TAGS, &serde_json::to_vec(&doc)?)?;
|
||||
}
|
||||
// Structures and the pools, sets, processors and features they use, then
|
||||
// what mods add to sets and pools, in mod and file order.
|
||||
// what mods add to sets, pools and lists, by priority, then in mod and
|
||||
// file order.
|
||||
for list in additions.values_mut() {
|
||||
list.sort_by_key(|a| a.priority);
|
||||
}
|
||||
for (bundle, entries) in &worldgen {
|
||||
if !base.join(bundle).exists() {
|
||||
continue;
|
||||
|
||||
@@ -605,7 +605,8 @@ fn content_root_carries_a_structure_mod() {
|
||||
"data/huts/tags/worldgen/biome/has_hut.json",
|
||||
r##"{"values": ["#minecraft:is_overworld", "#c:is_plains", "othermod:glade", "minecraft:plains"]}"##,
|
||||
),
|
||||
// Additions to vanilla's set and pool; a target nobody defines is skipped.
|
||||
// Additions to vanilla's set, pool and processor list; a target nobody
|
||||
// defines is skipped. A lower priority goes first.
|
||||
(
|
||||
"data/huts/worldgen/structure_set/added/villages.json",
|
||||
r#"{"horizon:add_to": "minecraft:villages",
|
||||
@@ -617,6 +618,16 @@ fn content_root_carries_a_structure_mod() {
|
||||
"elements": [{"weight": 2, "element": {"element_type": "minecraft:single_pool_element",
|
||||
"location": "huts:hut", "projection": "rigid", "processors": "minecraft:empty"}}]}"#,
|
||||
),
|
||||
(
|
||||
"data/huts/worldgen/template_pool/added/more_houses.json",
|
||||
r#"{"horizon:add_to": "minecraft:village/plains/houses", "horizon:priority": 10,
|
||||
"elements": [{"weight": 1, "element": {"element_type": "minecraft:empty_pool_element"}}]}"#,
|
||||
),
|
||||
(
|
||||
"data/huts/worldgen/processor_list/added/mossy.json",
|
||||
r#"{"horizon:add_to": "minecraft:mossify_10_percent",
|
||||
"processors": [{"processor_type": "minecraft:nop"}]}"#,
|
||||
),
|
||||
];
|
||||
for (name, text) in files {
|
||||
let path = dir.join(name);
|
||||
@@ -652,14 +663,23 @@ fn content_root_carries_a_structure_mod() {
|
||||
"structure_sets.json",
|
||||
"minecraft:villages",
|
||||
"structures",
|
||||
serde_json::json!({"structure": "huts:hut", "weight": 3}),
|
||||
vec![serde_json::json!({"structure": "huts:hut", "weight": 3})],
|
||||
),
|
||||
(
|
||||
"template_pools.json",
|
||||
"minecraft:village/plains/houses",
|
||||
"elements",
|
||||
serde_json::json!({"weight": 2, "element": {"element_type": "minecraft:single_pool_element",
|
||||
vec![
|
||||
serde_json::json!({"weight": 1, "element": {"element_type": "minecraft:empty_pool_element"}}),
|
||||
serde_json::json!({"weight": 2, "element": {"element_type": "minecraft:single_pool_element",
|
||||
"location": "huts:hut", "projection": "rigid", "processors": "minecraft:empty"}}),
|
||||
],
|
||||
),
|
||||
(
|
||||
"processor_lists.json",
|
||||
"minecraft:mossify_10_percent",
|
||||
"processors",
|
||||
vec![serde_json::json!({"processor_type": "minecraft:nop"})],
|
||||
),
|
||||
] {
|
||||
let (ours, vanilla) = (bundle(name, &root), bundle(name, &base));
|
||||
@@ -668,7 +688,7 @@ fn content_root_carries_a_structure_mod() {
|
||||
v[list].as_array().unwrap().clone()
|
||||
};
|
||||
let mut expected = entries(&vanilla[id]);
|
||||
expected.push(added);
|
||||
expected.extend(added);
|
||||
assert_eq!(entries(&ours[id]), expected, "{id}");
|
||||
assert!(!ours.keys().any(|k| k.starts_with("huts:added/")), "{name}");
|
||||
assert!(!ours.contains_key("othermod:houses"));
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
//! the processors nested in them carried in turn. CTOV's
|
||||
//! `ctov:modular_compat` (a reference that runs only when its mod is loaded)
|
||||
//! becomes a `reference` when the mod is ported along, else nothing.
|
||||
//! - `add_template_pool_elements` modifiers become `horizon:add_to` pool files
|
||||
//! (`docs/mods.md`), when their load conditions pass. Lithostitched's own
|
||||
//! pool elements (`limited`, `guaranteed`, `delegating`) place their
|
||||
//! `delegate`; what they add to it is counted as lost.
|
||||
//! - `add_template_pool_elements` and `add_processor_list_processors`
|
||||
//! modifiers become `horizon:add_to` pool and processor list files
|
||||
//! (`docs/mods.md`), with their `priority`, when their load conditions pass.
|
||||
//! - Its `limited` and `guaranteed` pool elements are the game's own
|
||||
//! (`horizon_world`'s `structure::jigsaw`); `delegating` places its
|
||||
//! `delegate`, and what it adds is counted as lost.
|
||||
use crate::{processors::ProcessorData, worldgen::Upgrade};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
@@ -88,17 +90,21 @@ pub(crate) fn upgrade_pool_element(
|
||||
let Some(kind) = element["element_type"].as_str() else {
|
||||
return false;
|
||||
};
|
||||
match kind {
|
||||
"lithostitched:limited" | "lithostitched:guaranteed" | "lithostitched:delegating" => {
|
||||
count(lost, kind);
|
||||
let delegate = element["delegate"].take();
|
||||
*element = delegate;
|
||||
// A delegate wrapping another (only `delegating` allows it).
|
||||
upgrade_pool_element(element, lost);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
let unwrap = match kind {
|
||||
"lithostitched:guaranteed" => false,
|
||||
// A `limit` is what it adds (spec-lithostitched §3).
|
||||
"lithostitched:limited" => element["limit"].as_i64().is_none(),
|
||||
"lithostitched:delegating" => true,
|
||||
_ => return false,
|
||||
};
|
||||
if unwrap {
|
||||
count(lost, kind);
|
||||
let delegate = element["delegate"].take();
|
||||
*element = delegate;
|
||||
// A delegate wrapping another (only `delegating` allows it).
|
||||
upgrade_pool_element(element, lost);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The ids a modifier targets: an id or a list of ids (`None` for tags and
|
||||
@@ -140,8 +146,9 @@ fn conditions_pass(v: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
}
|
||||
|
||||
/// A worldgen modifier as a file the game merges: `("template_pool", file)`
|
||||
/// for `add_template_pool_elements`; the others are counted in
|
||||
/// `report.modifiers_left`.
|
||||
/// for `add_template_pool_elements`, `("processor_list", file)` for
|
||||
/// `add_processor_list_processors` (spec-lithostitched §2.1, §2.2); the
|
||||
/// others are counted in `report.modifiers_left`.
|
||||
pub(crate) fn modifier(
|
||||
v: &Value,
|
||||
loaded: &dyn Fn(&str) -> bool,
|
||||
@@ -152,22 +159,31 @@ pub(crate) fn modifier(
|
||||
count(&mut report.modifiers_left, "conditions");
|
||||
return None;
|
||||
}
|
||||
if kind != "lithostitched:add_template_pool_elements" {
|
||||
count(&mut report.modifiers_left, kind);
|
||||
return None;
|
||||
}
|
||||
let (Some(targets), Some(elements)) = (ids(&v["template_pools"]), v["elements"].as_array())
|
||||
else {
|
||||
// 1.8.1 reads `processor_lists`, not the wiki's `processor_list`.
|
||||
let (to, targets, list) = match kind {
|
||||
"lithostitched:add_template_pool_elements" => {
|
||||
("template_pool", "template_pools", "elements")
|
||||
}
|
||||
"lithostitched:add_processor_list_processors" => {
|
||||
("processor_list", "processor_lists", "processors")
|
||||
}
|
||||
_ => {
|
||||
count(&mut report.modifiers_left, kind);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let (Some(targets), Some(entries)) = (ids(&v[targets]), v[list].as_array()) else {
|
||||
count(&mut report.modifiers_left, "target");
|
||||
return None;
|
||||
};
|
||||
for target in &targets {
|
||||
*report.added_to.entry(target.clone()).or_default() += elements.len();
|
||||
*report.added_to.entry(target.clone()).or_default() += entries.len();
|
||||
}
|
||||
Some((
|
||||
"template_pool",
|
||||
json!({"horizon:add_to": targets, "elements": elements}),
|
||||
))
|
||||
let mut file = json!({"horizon:add_to": targets, list: entries});
|
||||
if let Some(priority) = v["priority"].as_i64() {
|
||||
file["horizon:priority"] = priority.into();
|
||||
}
|
||||
Some((to, file))
|
||||
}
|
||||
|
||||
/// Whether the game reads a Lithostitched processor as it is: one of its
|
||||
@@ -322,16 +338,28 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_elements_place_their_delegate() {
|
||||
fn limited_and_guaranteed_elements_stay_theirs() {
|
||||
let mut lost = BTreeMap::new();
|
||||
let delegate = json!({"element_type": "minecraft:single_pool_element", "location": "a:b",
|
||||
"projection": "rigid", "processors": "minecraft:empty"});
|
||||
let mut e =
|
||||
json!({"element_type": "lithostitched:limited", "limit": 1, "delegate": delegate});
|
||||
assert!(upgrade_pool_element(&mut e, &mut lost));
|
||||
assert_eq!(e, delegate);
|
||||
assert_eq!(lost["lithostitched:limited"], 1);
|
||||
assert!(!upgrade_pool_element(&mut e, &mut lost));
|
||||
for kept in [
|
||||
json!({"element_type": "lithostitched:limited", "limit": 1, "delegate": delegate}),
|
||||
json!({"element_type": "lithostitched:guaranteed", "count": 1, "min_depth": 3,
|
||||
"delegate": delegate}),
|
||||
] {
|
||||
let mut e = kept.clone();
|
||||
assert!(upgrade_pool_element(&mut e, &mut lost));
|
||||
assert_eq!(e, kept);
|
||||
}
|
||||
assert!(lost.is_empty());
|
||||
// Without a limit, and `delegating`: the delegate, the rest counted.
|
||||
for kind in ["lithostitched:limited", "lithostitched:delegating"] {
|
||||
let mut e = json!({"element_type": kind, "delegate": delegate});
|
||||
assert!(upgrade_pool_element(&mut e, &mut lost));
|
||||
assert_eq!(e, delegate);
|
||||
assert_eq!(lost[kind], 1);
|
||||
}
|
||||
assert!(!upgrade_pool_element(&mut delegate.clone(), &mut lost));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -357,12 +385,27 @@ mod tests {
|
||||
assert!(modifier(&add("waystones", json!("#a:pools")), &loaded, &mut report).is_none());
|
||||
let aliases = json!({"type": "lithostitched:set_pool_aliases"});
|
||||
assert!(modifier(&aliases, &loaded, &mut report).is_none());
|
||||
// Processors appended to lists, with the modifier's priority; 1.8.1
|
||||
// reads `processor_lists` (spec-lithostitched §2.2).
|
||||
let nop = json!([{"processor_type": "minecraft:nop"}]);
|
||||
let processors = json!({"type": "lithostitched:add_processor_list_processors",
|
||||
"priority": 10, "processor_lists": ["minecraft:mossify_10_percent"], "processors": nop});
|
||||
let (kind, file) = modifier(&processors, &loaded, &mut report).unwrap();
|
||||
assert_eq!(kind, "processor_list");
|
||||
assert_eq!(
|
||||
file,
|
||||
json!({"horizon:add_to": ["minecraft:mossify_10_percent"], "processors": nop,
|
||||
"horizon:priority": 10})
|
||||
);
|
||||
let wiki = json!({"type": "lithostitched:add_processor_list_processors",
|
||||
"processor_list": "minecraft:mossify_10_percent", "processors": nop});
|
||||
assert!(modifier(&wiki, &loaded, &mut report).is_none());
|
||||
assert_eq!(
|
||||
report.modifiers_left,
|
||||
BTreeMap::from([
|
||||
("conditions".into(), 1),
|
||||
("lithostitched:set_pool_aliases".into(), 1),
|
||||
("target".into(), 1),
|
||||
("target".into(), 2),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,6 +119,11 @@ impl PoolElement {
|
||||
};
|
||||
Ok(match kind {
|
||||
"minecraft:empty_pool_element" => Self::Empty,
|
||||
// Lithostitched's elements place what their `delegate` places;
|
||||
// their limits are the pool entry's (`Pool::limits`).
|
||||
"lithostitched:limited" | "lithostitched:guaranteed" | "lithostitched:delegating" => {
|
||||
Self::decode(&value["delegate"], resources, bindings, inherited)?
|
||||
}
|
||||
// YUNG's API's element places what a single element places; its
|
||||
// `name` and `max_count` are the pool entry's (`Pool::limits`).
|
||||
"minecraft:single_pool_element"
|
||||
@@ -321,11 +326,14 @@ pub(super) fn can_attach(source: &Jigsaw, target: &Jigsaw) -> bool {
|
||||
pub struct Pool {
|
||||
/// Each element repeated by its weight.
|
||||
pub(super) templates: Vec<Arc<PoolElement>>,
|
||||
/// The entry of `raw` each of `templates` repeats.
|
||||
entries: Vec<usize>,
|
||||
/// The elements with their weights, in file order (`rawTemplates`).
|
||||
pub(super) raw: Vec<(Arc<PoolElement>, i32)>,
|
||||
/// Per entry of `raw`: a YUNG element's `name` and `max_count`, the most
|
||||
/// pieces of that name in one start.
|
||||
limits: Vec<Option<(Arc<str>, i32)>>,
|
||||
/// Per entry of `raw`: how many pieces of it one start may hold.
|
||||
limits: Vec<Option<Limit>>,
|
||||
/// The entries of `raw` that are Lithostitched's `guaranteed` elements.
|
||||
guaranteed: Vec<usize>,
|
||||
pub(super) fallback: String,
|
||||
max_size: OnceLock<i32>,
|
||||
}
|
||||
@@ -339,10 +347,11 @@ impl Pool {
|
||||
.unwrap_or(0) as usize]
|
||||
.clone()
|
||||
}
|
||||
fn shuffled(&self, random: &mut impl RandomSource) -> Vec<Arc<PoolElement>> {
|
||||
let mut copy = self.templates.clone();
|
||||
shuffle(&mut copy, random);
|
||||
copy
|
||||
/// The entries of `templates`, shuffled.
|
||||
fn shuffled(&self, random: &mut impl RandomSource) -> Vec<usize> {
|
||||
let mut order: Vec<usize> = (0..self.templates.len()).collect();
|
||||
shuffle(&mut order, random);
|
||||
order.into_iter().map(|i| self.entries[i]).collect()
|
||||
}
|
||||
/// YUNG's API's pick: `nextInt` of the entries' total weight, walked in
|
||||
/// `order` (indices into `raw`); the entry that takes it below zero.
|
||||
@@ -365,6 +374,52 @@ impl Pool {
|
||||
})
|
||||
}
|
||||
}
|
||||
/// How many pieces of a pool entry one start may hold, and where.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Limit {
|
||||
/// The pieces counted together: a YUNG element's `name`; a Lithostitched
|
||||
/// element itself (its JSON), wherever it is listed.
|
||||
key: Arc<str>,
|
||||
max: i32,
|
||||
/// The parent piece's least depth (the start is at 0).
|
||||
min_depth: i32,
|
||||
/// `lithostitched:guaranteed`: tried before the pool's other entries.
|
||||
guaranteed: bool,
|
||||
}
|
||||
impl Limit {
|
||||
/// A pool entry's element's limit: YUNG's `max_count` (spec-yungsapi §3),
|
||||
/// Lithostitched's `limited` and `guaranteed` (spec-lithostitched §3).
|
||||
fn of(element: &Value) -> Option<Self> {
|
||||
let int = |key: &str| {
|
||||
element[key]
|
||||
.as_i64()
|
||||
.map(|v| v.clamp(0, i32::MAX.into()) as i32)
|
||||
};
|
||||
let litho = |max: Option<i32>, guaranteed| {
|
||||
Some(Self {
|
||||
key: element.to_string().into(),
|
||||
max: max?,
|
||||
min_depth: int("min_depth").unwrap_or(0),
|
||||
guaranteed,
|
||||
})
|
||||
};
|
||||
match element["element_type"].as_str()? {
|
||||
"yungsapi:yung_single_element" => Some(Self {
|
||||
key: element["name"].as_str()?.into(),
|
||||
max: int("max_count")?,
|
||||
min_depth: 0,
|
||||
guaranteed: false,
|
||||
}),
|
||||
"lithostitched:limited" => litho(int("limit"), false),
|
||||
"lithostitched:guaranteed" => litho(Some(int("count").unwrap_or(1)), true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Whether one more piece may go below a parent at `depth`.
|
||||
fn allows(&self, counts: &HashMap<Arc<str>, i32>, depth: i32) -> bool {
|
||||
depth >= self.min_depth && counts.get(&self.key).copied().unwrap_or(0) < self.max
|
||||
}
|
||||
}
|
||||
/// Lazily decoded template pools.
|
||||
pub struct Pools {
|
||||
raw: BTreeMap<String, String>,
|
||||
@@ -393,7 +448,8 @@ impl Pools {
|
||||
}
|
||||
let pool = self.raw.get(name).and_then(|json| {
|
||||
let value: Value = serde_json::from_str(json).ok()?;
|
||||
let (mut templates, mut raw, mut limits) = (Vec::new(), Vec::new(), Vec::new());
|
||||
let (mut templates, mut entries, mut raw, mut limits) =
|
||||
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
|
||||
for entry in value["elements"].as_array()? {
|
||||
let element =
|
||||
match PoolElement::decode(&entry["element"], resources, bindings, None) {
|
||||
@@ -408,24 +464,21 @@ impl Pools {
|
||||
let weight = entry["weight"].as_i64().unwrap_or(1);
|
||||
for _ in 0..weight {
|
||||
templates.push(element.clone());
|
||||
entries.push(raw.len());
|
||||
}
|
||||
raw.push((element, weight as i32));
|
||||
let fields = &entry["element"];
|
||||
limits.push(
|
||||
match (fields["name"].as_str(), fields["max_count"].as_i64()) {
|
||||
(Some(name), Some(max))
|
||||
if fields["element_type"] == "yungsapi:yung_single_element" =>
|
||||
{
|
||||
Some((name.into(), max as i32))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
);
|
||||
limits.push(Limit::of(&entry["element"]));
|
||||
}
|
||||
let guaranteed = (limits.iter().enumerate())
|
||||
.filter(|(_, l)| l.as_ref().is_some_and(|l| l.guaranteed))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
Some(Arc::new(Pool {
|
||||
templates,
|
||||
entries,
|
||||
raw,
|
||||
limits,
|
||||
guaranteed,
|
||||
fallback: value["fallback"]
|
||||
.as_str()
|
||||
.unwrap_or("minecraft:empty")
|
||||
@@ -669,9 +722,11 @@ pub enum Draws {
|
||||
|
||||
/// The elements a jigsaw tries, in order.
|
||||
enum Candidates {
|
||||
/// Vanilla: the target pool's weight-expanded elements, then the
|
||||
/// fallback's, each shuffled up front.
|
||||
Listed(std::vec::IntoIter<Arc<PoolElement>>),
|
||||
/// Vanilla: the target pool's weight-expanded entries, then the
|
||||
/// fallback's, each shuffled up front. Lithostitched's `guaranteed`
|
||||
/// entries of the target pool come first, in file order, and are not
|
||||
/// drawn again (spec-lithostitched §3, OPEN-L4).
|
||||
Listed(std::vec::IntoIter<(Arc<Pool>, usize)>),
|
||||
/// YUNG's: weighted picks from each pool in turn (the fallback's only
|
||||
/// once the target's are spent), each pool's entries shuffled when it
|
||||
/// is reached.
|
||||
@@ -694,27 +749,36 @@ impl Candidates {
|
||||
vec![fallback.clone()]
|
||||
};
|
||||
match draws {
|
||||
Draws::Vanilla => Self::Listed(
|
||||
pools
|
||||
.iter()
|
||||
.flat_map(|pool| pool.shuffled(random))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter(),
|
||||
),
|
||||
Draws::Vanilla => {
|
||||
let first = (target.guaranteed.iter()).filter(|_| below_max_depth);
|
||||
let mut listed: Vec<_> = first.map(|&e| (target.clone(), e)).collect();
|
||||
for pool in &pools {
|
||||
let drawn = pool.shuffled(random).into_iter();
|
||||
let drawn = drawn.filter(|&e| !pool.guaranteed.contains(&e));
|
||||
listed.extend(drawn.map(|e| (pool.clone(), e)));
|
||||
}
|
||||
Self::Listed(listed.into_iter())
|
||||
}
|
||||
Draws::Yung => Self::Weighted {
|
||||
pools: pools.into_iter().rev().collect(),
|
||||
current: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
/// The next element to try, with its YUNG name and limit.
|
||||
/// The next element to try below a parent at `depth`, with the key
|
||||
/// its pieces are counted under.
|
||||
fn next(
|
||||
&mut self,
|
||||
counts: &HashMap<Arc<str>, i32>,
|
||||
depth: i32,
|
||||
random: &mut impl RandomSource,
|
||||
) -> Option<(Arc<PoolElement>, Option<Arc<str>>)> {
|
||||
match self {
|
||||
Self::Listed(elements) => elements.next().map(|e| (e, None)),
|
||||
Self::Listed(entries) => entries.find_map(|(pool, entry)| {
|
||||
let limit = pool.limits[entry].as_ref();
|
||||
(limit.is_none_or(|l| l.allows(counts, depth)))
|
||||
.then(|| (pool.raw[entry].0.clone(), limit.map(|l| l.key.clone())))
|
||||
}),
|
||||
Self::Weighted { pools, current } => loop {
|
||||
let (pool, order) = match current {
|
||||
Some((pool, order)) if !order.is_empty() => (pool, order),
|
||||
@@ -731,13 +795,11 @@ impl Candidates {
|
||||
continue;
|
||||
};
|
||||
let entry = order.remove(at);
|
||||
let limit = pool.limits[entry].clone();
|
||||
if let Some((name, max)) = &limit
|
||||
&& counts.get(name).copied().unwrap_or(0) >= *max
|
||||
{
|
||||
let limit = pool.limits[entry].as_ref();
|
||||
if limit.is_some_and(|l| !l.allows(counts, depth)) {
|
||||
continue;
|
||||
}
|
||||
return Some((pool.raw[entry].0.clone(), limit.map(|(name, _)| name)));
|
||||
return Some((pool.raw[entry].0.clone(), limit.map(|l| l.key.clone())));
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -830,7 +892,7 @@ pub fn assemble_drawing(
|
||||
let rotation = Rotation::random(random);
|
||||
let entry = order[center_pool.weighted(&order, random)?];
|
||||
let (element, _) = ¢er_pool.raw[entry];
|
||||
let limit = center_pool.limits[entry].clone().map(|(name, _)| name);
|
||||
let limit = center_pool.limits[entry].as_ref().map(|l| l.key.clone());
|
||||
(rotation, element.clone(), limit)
|
||||
}
|
||||
};
|
||||
@@ -972,7 +1034,7 @@ struct Placer<'a, 'p> {
|
||||
/// SequencedPriorityIterator: highest priority first, FIFO within one.
|
||||
queue: BTreeMap<i32, VecDeque<PieceState>>,
|
||||
draws: Draws,
|
||||
/// Pieces placed per YUNG element name.
|
||||
/// Pieces placed per limited element (`Limit::key`).
|
||||
counts: HashMap<Arc<str>, i32>,
|
||||
}
|
||||
impl Placer<'_, '_> {
|
||||
@@ -1052,7 +1114,7 @@ impl Placer<'_, '_> {
|
||||
depth != self.config.max_depth,
|
||||
random,
|
||||
);
|
||||
while let Some((target, name)) = candidates.next(&self.counts, random) {
|
||||
while let Some((target, name)) = candidates.next(&self.counts, depth, random) {
|
||||
if matches!(target.as_ref(), PoolElement::Empty) {
|
||||
break;
|
||||
}
|
||||
@@ -1175,3 +1237,117 @@ impl Placer<'_, '_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
/// A pool of empty elements, told apart by identity, with these limits.
|
||||
fn pool(entries: &[(i32, Value)]) -> Arc<Pool> {
|
||||
let (mut templates, mut indices, mut raw, mut limits) =
|
||||
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
|
||||
for (i, (weight, element)) in entries.iter().enumerate() {
|
||||
let e = Arc::new(PoolElement::Empty);
|
||||
for _ in 0..*weight {
|
||||
templates.push(e.clone());
|
||||
indices.push(i);
|
||||
}
|
||||
raw.push((e, *weight));
|
||||
limits.push(Limit::of(element));
|
||||
}
|
||||
let guaranteed = (0..entries.len())
|
||||
.filter(|&i| limits[i].as_ref().is_some_and(|l: &Limit| l.guaranteed))
|
||||
.collect();
|
||||
Arc::new(Pool {
|
||||
templates,
|
||||
entries: indices,
|
||||
raw,
|
||||
limits,
|
||||
guaranteed,
|
||||
fallback: "minecraft:empty".into(),
|
||||
max_size: OnceLock::new(),
|
||||
})
|
||||
}
|
||||
fn delegate() -> Value {
|
||||
json!({"element_type": "minecraft:single_pool_element", "location": "a:b"})
|
||||
}
|
||||
/// The entries a vanilla jigsaw tries below a parent at `depth`, by index.
|
||||
fn tried(
|
||||
pool: &Arc<Pool>,
|
||||
counts: &HashMap<Arc<str>, i32>,
|
||||
depth: i32,
|
||||
below_max: bool,
|
||||
) -> Vec<usize> {
|
||||
let empty = self::pool(&[]);
|
||||
let mut random = LegacyRandom::new(7);
|
||||
let mut candidates = Candidates::new(Draws::Vanilla, pool, &empty, below_max, &mut random);
|
||||
let mut out = Vec::new();
|
||||
while let Some((element, _)) = candidates.next(counts, depth, &mut random) {
|
||||
out.push(
|
||||
pool.raw
|
||||
.iter()
|
||||
.position(|(e, _)| Arc::ptr_eq(e, &element))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_read_yungs_and_lithostitcheds_fields() {
|
||||
let yung =
|
||||
json!({"element_type": "yungsapi:yung_single_element", "name": "pile", "max_count": 2});
|
||||
let yung = Limit::of(&yung).unwrap();
|
||||
assert_eq!(
|
||||
(&*yung.key, yung.max, yung.min_depth, yung.guaranteed),
|
||||
("pile", 2, 0, false)
|
||||
);
|
||||
let limited =
|
||||
json!({"element_type": "lithostitched:limited", "limit": 1, "delegate": delegate()});
|
||||
let l = Limit::of(&limited).unwrap();
|
||||
assert_eq!((l.max, l.min_depth, l.guaranteed), (1, 0, false));
|
||||
assert_eq!(*l.key, *limited.to_string());
|
||||
let guaranteed = json!({"element_type": "lithostitched:guaranteed", "count": 2, "min_depth": 3,
|
||||
"delegate": delegate()});
|
||||
let g = Limit::of(&guaranteed).unwrap();
|
||||
assert_eq!((g.max, g.min_depth, g.guaranteed), (2, 3, true));
|
||||
// No limit: none to keep.
|
||||
for none in [
|
||||
json!({"element_type": "lithostitched:limited", "delegate": delegate()}),
|
||||
json!({"element_type": "yungsapi:yung_single_element", "name": "pile"}),
|
||||
delegate(),
|
||||
] {
|
||||
assert!(Limit::of(&none).is_none(), "{none}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vanilla_jigsaws_keep_lithostitcheds_limits() {
|
||||
let limited =
|
||||
json!({"element_type": "lithostitched:limited", "limit": 1, "delegate": delegate()});
|
||||
let guaranteed = json!({"element_type": "lithostitched:guaranteed", "count": 1, "min_depth": 3,
|
||||
"delegate": delegate()});
|
||||
let p = pool(&[
|
||||
(3, delegate()),
|
||||
(2, limited.clone()),
|
||||
(1, guaranteed.clone()),
|
||||
]);
|
||||
let mut counts = HashMap::new();
|
||||
// Too shallow for the guaranteed entry: the weight-expanded list without it.
|
||||
let mut shallow = tried(&p, &counts, 2, true);
|
||||
shallow.sort();
|
||||
assert_eq!(shallow, [0, 0, 0, 1, 1]);
|
||||
// Deep enough: it comes first, and is not drawn again.
|
||||
let deep = tried(&p, &counts, 3, true);
|
||||
assert_eq!(deep[0], 2);
|
||||
assert_eq!(deep[1..].iter().filter(|&&e| e == 2).count(), 0);
|
||||
// At the most depth only the fallback is tried.
|
||||
assert!(tried(&p, &counts, 3, false).is_empty());
|
||||
// Entries at their limit are skipped.
|
||||
counts.insert(Arc::from(limited.to_string()), 1);
|
||||
counts.insert(Arc::from(guaranteed.to_string()), 1);
|
||||
let full = tried(&p, &counts, 3, true);
|
||||
assert_eq!(full, [0, 0, 0]);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -449,7 +449,9 @@ Two cases the oracle fills in:
|
||||
- `lithostitched:jigsaw`, MoogsStructureLib's and Repurposed Structures' generic jigsaws
|
||||
become `minecraft:jigsaw`;
|
||||
- their `advanced_random_spread` becomes `minecraft:random_spread`;
|
||||
- their versioned, legacy and empty pool elements become vanilla's.
|
||||
- their versioned, legacy and empty pool elements become vanilla's; Lithostitched's
|
||||
`limited` and `guaranteed` elements are the game's own (its `delegating` places its
|
||||
`delegate`).
|
||||
|
||||
Three more rewrites make the mapped structures decode and start where their mod would:
|
||||
- **Block states.** 1.21.1 writes a block or fluid state as `{"Name", "Properties"}`, 26.3
|
||||
@@ -516,6 +518,11 @@ Two cases the oracle fills in:
|
||||
CTOV's compat hooks for other mods (its `modular_compat`, 30 of them in 17 lists) become a
|
||||
`reference` when the mod they name is ported along, and are taken out (a no-op, as in the
|
||||
mod) when it is not. In ATM10 they all name Nature's Spirit, which the pack does not have.
|
||||
Lithostitched's `add_template_pool_elements` and `add_processor_list_processors` modifiers
|
||||
(CTOV's houses for 11 of ATM10's mods) become the game's additions, when the mods their
|
||||
load conditions name are ported along. The houses are `limited` (at most once in a
|
||||
village) or `guaranteed` (tried first, from the depth they name, until placed): the game
|
||||
keeps both.
|
||||
In Java, Lithostitched also lays out every jigsaw structure its own way (villages,
|
||||
outposts, trial chambers; CTOV's villages are `lithostitched:jigsaw`, carried as vanilla's
|
||||
jigsaw). Horizon keeps vanilla's layouts, so with Lithostitched in a pack, structure layouts
|
||||
|
||||
+19
-9
@@ -333,21 +333,31 @@ vanilla's entries and those of adds are left out, ...". Tags are the only
|
||||
`minecraft` files a mod may hold: its other content stays in its own
|
||||
namespaces.
|
||||
|
||||
A mod adds to structure sets and template pools that are not its own
|
||||
(vanilla's villages, another mod's houses) with a set or pool file in its own
|
||||
namespace holding `horizon:add_to`, a key vanilla data never has. Its entries
|
||||
are appended to each set or pool named, after every mod's own files, in mod
|
||||
and file order; the file is no set or pool itself. A name nobody defines is
|
||||
skipped.
|
||||
A mod adds to structure sets, template pools and processor lists that are not
|
||||
its own (vanilla's villages, another mod's houses) with a set, pool or
|
||||
processor list file in its own namespace holding `horizon:add_to`, a key
|
||||
vanilla data never has. Its entries are appended to each set, pool or list
|
||||
named, after every mod's own files, by `horizon:priority` (lower first, 1000
|
||||
when it has none), then in mod and file order; the file is no set, pool or
|
||||
list itself. A name nobody defines is skipped.
|
||||
|
||||
```json
|
||||
{ "horizon:add_to": "minecraft:villages",
|
||||
"structures": [ { "structure": "mymod:village_beach", "weight": 10 } ] }
|
||||
```
|
||||
|
||||
A pool addition holds `"elements"` as a pool does. This is what lithostitched's
|
||||
`add_structure_set_entries` and `add_template_pool_elements` modifiers do on
|
||||
NeoForge and Fabric, and `horizon-port-pack carry` writes them from those.
|
||||
A pool addition holds `"elements"` as a pool does, a processor list addition
|
||||
`"processors"`. This is what Lithostitched's `add_structure_set_entries`,
|
||||
`add_template_pool_elements` and `add_processor_list_processors` modifiers do
|
||||
on NeoForge and Fabric, and `horizon-port-pack carry` writes the last two from
|
||||
those.
|
||||
|
||||
A pool element may be one of Lithostitched's: `lithostitched:limited`
|
||||
(`delegate`, `limit`, `min_depth`) places its `delegate` at most `limit` times
|
||||
in one structure start, and `lithostitched:guaranteed` (`delegate`, `count`,
|
||||
`min_depth`) is tried before the pool's other elements at each jigsaw of that
|
||||
pool until `count` are placed. Neither goes below a piece shallower than
|
||||
`min_depth` (the start is at depth 0).
|
||||
|
||||
A structure (and any of its pool elements, in place of the structure's) may
|
||||
hold `horizon:enhanced_terrain_adaptation`: MoogsStructureLib's terrain
|
||||
|
||||
@@ -21,7 +21,8 @@ A mismatch is counted by the oracle's no-mod block, the expected block and Horiz
|
||||
missing rule reads as one line. KNOWN names the mismatch classes the implementation does not cover yet, with
|
||||
the reason; they are reported, not failed.
|
||||
|
||||
usage: test_structure_blocks_parity.py [--profile betterdungeons|ctov] [--filter REGEX] [--limit N]
|
||||
usage: test_structure_blocks_parity.py [--profile betterdungeons|ctov|lithostitched-probe] [--filter REGEX]
|
||||
[--limit N]
|
||||
"""
|
||||
import argparse
|
||||
import gzip
|
||||
@@ -61,6 +62,13 @@ KNOWN = {
|
||||
'changed the terrain',
|
||||
r'^minecraft:village_plains$': 'decoration (grass, trees) differs where the village changed the terrain',
|
||||
},
|
||||
# The probe pack's swaps and references match block for block; its random choices do not (OPEN-L6).
|
||||
'lithostitched-probe': {
|
||||
r'^minecraft:village_plains$': "Lithostitched's random is not known (spec-lithostitched OPEN-L6): the "
|
||||
'probe\'s blackstone/deepslate and per-piece palettes match by share, not '
|
||||
'by block; and decoration (grass, trees) differs where the village changed '
|
||||
'the terrain',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -81,11 +89,9 @@ def chunk_rect(box):
|
||||
|
||||
|
||||
def saved_element(element):
|
||||
"""A saved pool element as the game reads it: Lithostitched's wrappers place their `delegate`
|
||||
(spec-lithostitched §3), and 1.21.1's `{"Name", "Properties"}` states are 26.3's `{"id", "properties"}`."""
|
||||
"""A saved pool element as the game reads it: 1.21.1's `{"Name", "Properties"}` states are 26.3's
|
||||
`{"id", "properties"}` (Lithostitched's wrapped elements are read as saved, spec-lithostitched §7.3)."""
|
||||
if isinstance(element, dict):
|
||||
if 'delegate' in element and str(element.get('element_type', '')).startswith('lithostitched:'):
|
||||
return saved_element(element['delegate'])
|
||||
if 'Name' in element and set(element) <= {'Name', 'Properties'}:
|
||||
return {'id': element['Name'], **({'properties': element['Properties']} if 'Properties' in element else {})}
|
||||
return {k: saved_element(v) for k, v in element.items()}
|
||||
@@ -193,10 +199,12 @@ def main():
|
||||
profile = PROFILES[args.profile]
|
||||
results = sorted((layout.ORACLE / args.profile).glob('seed-*.json.gz'))
|
||||
missing = [jar for jar in profile['mods'].values() if not layout.jar_path(jar).exists()]
|
||||
missing += [n for n in profile.get('packs', {}).values() if not (layout.ORACLE / 'profiles' / f'{n}.json').exists()]
|
||||
if not results or missing:
|
||||
print(f'SKIP: oracle results or mod jars missing under {layout.ORACLE} (see test_structure_layout_parity.py)')
|
||||
return 0
|
||||
resources = layout.overlay(args.profile, layout.carry(args.profile, profile['mods'], profile['carry']))
|
||||
data = layout.carry(args.profile, profile['mods'], profile['carry'], profile.get('packs'))
|
||||
resources = layout.overlay(args.profile, data)
|
||||
binary = layout.probe_binary()
|
||||
names = state_names(lab.RESOURCES)
|
||||
# structure -> [starts, compared by region, mismatches by region]
|
||||
|
||||
@@ -21,9 +21,17 @@ Mod profiles compare the mod world's `pieces` instead, through the path players'
|
||||
(`.local/structure-oracle/jars/`, or HORIZON_STRUCTURE_JARS) are carried by `horizon-port-pack carry` into
|
||||
a fresh work folder, and an overlay of the 26.3 resources adds the carried structures, sets, pools,
|
||||
processor lists, biome tags and templates. The jars are the player's; nothing of them enters the
|
||||
repository.
|
||||
repository. A profile whose oracle run added a data pack of the spec team's own (`lithostitched-probe`)
|
||||
carries that pack as one more mod, built at run time from the oracle's profile file
|
||||
(`.local/structure-oracle/profiles/<profile>.json`).
|
||||
|
||||
usage: test_structure_layout_parity.py [--profile lithostitched|betterdungeons] [--filter REGEX] [--verbose]
|
||||
Lithostitched lays jigsaws out its own way (spec-lithostitched OPEN-L5) and Horizon keeps vanilla's
|
||||
layouts, so the `lithostitched-probe` profile checks its pool elements' limits instead of the layout: in
|
||||
every start Horizon builds, no `limited` or `guaranteed` element's template appears more often than its
|
||||
limit, and how many starts hold one is printed beside the oracle's count.
|
||||
|
||||
usage: test_structure_layout_parity.py [--profile lithostitched|betterdungeons|lithostitched-probe]
|
||||
[--filter REGEX] [--verbose]
|
||||
"""
|
||||
import argparse
|
||||
import gzip
|
||||
@@ -33,6 +41,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
@@ -55,6 +64,12 @@ PROFILES = {
|
||||
'betterdungeons': {'side': 'pieces', 'mods': {'yungsapi': 'YungsApi-1.21.1-NeoForge-5.1.9.jar',
|
||||
'betterdungeons': 'YungsBetterDungeons-1.21.1-NeoForge-5.1.4.jar'},
|
||||
'carry': ['betterdungeons'], 'structures': r'^betterdungeons:(small|skeleton|zombie)_dungeon$'},
|
||||
# The probe pack's limited (limit 1) and guaranteed (count 1) elements on vanilla's plains houses.
|
||||
'lithostitched-probe': {'side': 'pieces', 'mods': {'lithostitched': 'lithostitched-1.8.1-neoforge-21.1.jar'},
|
||||
'packs': {'probe': 'lithostitched-probe'}, 'carry': ['lithostitched', 'probe'],
|
||||
'structures': r'^minecraft:village_plains$',
|
||||
'limits': {'minecraft:village/taiga/houses/taiga_small_house_1': 1,
|
||||
'minecraft:village/snowy/houses/snowy_library_1': 1}},
|
||||
}
|
||||
BUNDLES = {'structure': 'structures', 'structure_set': 'structure_sets', 'template_pool': 'template_pools',
|
||||
'processor_list': 'processor_lists'}
|
||||
@@ -101,14 +116,28 @@ CENSUS = ['blockstates', 'item_models', 'recipes', 'loot_tables', 'tags', 'world
|
||||
'textures', 'sounds', 'classes', 'mixin_configs']
|
||||
|
||||
|
||||
def carry(profile, mods, carried):
|
||||
def pack_jar(work, mod, profile):
|
||||
"""The data pack an oracle profile added to its world, as a jar of mod `mod`."""
|
||||
files = json.loads((ORACLE / 'profiles' / f'{profile}.json').read_text())['config_files']
|
||||
jar = work / f'{mod}.jar'
|
||||
with zipfile.ZipFile(jar, 'w') as z:
|
||||
for path, text in files.items():
|
||||
parts = Path(path).parts
|
||||
if parts[:2] == ('world', 'datapacks') and len(parts) > 3:
|
||||
z.writestr(str(Path(*parts[3:])), text)
|
||||
return jar
|
||||
|
||||
|
||||
def carry(profile, mods, carried, packs=None):
|
||||
"""`horizon-port-pack carry` of the profile's mods; the carried mods' data folders."""
|
||||
work = lab.LOCAL / 'structure-layout' / profile / 'work'
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
work.mkdir(parents=True)
|
||||
mods = {key: str(jar_path(jar)) for key, jar in mods.items()}
|
||||
mods.update({key: str(pack_jar(work, key, name)) for key, name in (packs or {}).items()})
|
||||
records = [{'key': key, 'name': key, 'version': '', 'path': jar, 'tier': 'data', 'confidence': 'known',
|
||||
'class': 'content' if key in carried else 'library', 'source': 'file', 'license': '',
|
||||
'client': '', 'server': '', 'depends': [], 'layer': 0, 'jar': str(jar_path(jar)),
|
||||
'client': '', 'server': '', 'depends': [], 'layer': 0, 'jar': jar,
|
||||
'info': {'mod_id': key, 'name': key, 'version': '', 'license': '', 'depends': [],
|
||||
'census': dict.fromkeys(CENSUS, 0)}}
|
||||
for key, jar in mods.items()]
|
||||
@@ -149,7 +178,8 @@ def overlay(profile, data_dirs):
|
||||
elif path.suffix == '.json' and kind == 'worldgen' and parts[2] in BUNDLES:
|
||||
text = path.read_text()
|
||||
if 'horizon:add_to' in text:
|
||||
additions.append((BUNDLES[parts[2]], json.loads(text)))
|
||||
value = json.loads(text)
|
||||
additions.append((value.get('horizon:priority', 1000), BUNDLES[parts[2]], value))
|
||||
else:
|
||||
merged[BUNDLES[parts[2]]][f'{namespace}:' + '/'.join(parts[3:])[:-5]] = text
|
||||
elif path.suffix == '.json' and parts[1:4] == ('tags', 'worldgen', 'biome'):
|
||||
@@ -158,11 +188,12 @@ def overlay(profile, data_dirs):
|
||||
if tag in merged['biome_tags'] and not value.get('replace'):
|
||||
value['values'] = json.loads(merged['biome_tags'][tag])['values'] + value['values']
|
||||
merged['biome_tags'][tag] = json.dumps(value)
|
||||
# A carried file with `horizon:add_to` adds its entries to other sets or pools, as the game's
|
||||
# mod_content.rs merges them.
|
||||
for bundle, addition in additions:
|
||||
# A carried file with `horizon:add_to` adds its entries to other sets, pools or processor lists, by
|
||||
# priority, as the game's mod_content.rs merges them.
|
||||
lists = {'structure_sets': 'structures', 'template_pools': 'elements', 'processor_lists': 'processors'}
|
||||
for _, bundle, addition in sorted(additions, key=lambda a: a[0]):
|
||||
targets = addition['horizon:add_to']
|
||||
key = 'structures' if bundle == 'structure_sets' else 'elements'
|
||||
key = lists[bundle]
|
||||
for target in [targets] if isinstance(targets, str) else targets:
|
||||
if target in merged[bundle]:
|
||||
value = json.loads(merged[bundle][target])
|
||||
@@ -209,6 +240,40 @@ def compare(expected, actual):
|
||||
return None
|
||||
|
||||
|
||||
def check_limits(binary, resources, results, profile, pattern):
|
||||
"""Each limited template at most its limit in every start Horizon builds; starts holding it, Horizon
|
||||
beside the oracle."""
|
||||
limits, failed = profile['limits'], []
|
||||
holding = {template: [0, 0] for template in limits}
|
||||
starts = 0
|
||||
for path in results:
|
||||
oracle = json.load(gzip.open(path))
|
||||
seed = oracle['seed']
|
||||
wanted = [s for s in oracle['structures'] if s.get('pieces') and re.search(pattern, s['id'])
|
||||
and re.search(profile['structures'], s['id'])]
|
||||
chunks = sorted({tuple(s['start_chunk']) for s in wanted})
|
||||
actual = horizon_starts(binary, resources, seed, 'overworld', chunks)
|
||||
for start in wanted:
|
||||
got = actual.get((start['id'], *start['start_chunk']))
|
||||
if got is None:
|
||||
print(f'skip seed {seed} {start["id"]} chunk {start["start_chunk"]}: no start in Horizon')
|
||||
continue
|
||||
starts += 1
|
||||
for template, limit in limits.items():
|
||||
have = sum(p.get('element') == template for p in got)
|
||||
want = sum(element(p['pool_element'].get('delegate', p['pool_element'])) == template
|
||||
for p in start['pieces'] if p['id'] == 'minecraft:jigsaw')
|
||||
holding[template][0] += have > 0
|
||||
holding[template][1] += want > 0
|
||||
if have > limit:
|
||||
failed.append(f'seed {seed} {start["id"]} chunk {start["start_chunk"]}: {have} {template}')
|
||||
for line in failed:
|
||||
print(f'FAIL {line} (over its limit)')
|
||||
for template, (ours, theirs) in holding.items():
|
||||
print(f'{template} (limit {limits[template]}): in {ours} of {starts} Horizon starts; the oracle: {theirs}')
|
||||
return 1 if failed or not starts else 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('--profile', default='lithostitched', choices=sorted(PROFILES))
|
||||
@@ -221,13 +286,17 @@ def main():
|
||||
return 0
|
||||
profile = PROFILES[args.profile]
|
||||
missing = [jar for jar in profile['mods'].values() if not jar_path(jar).exists()]
|
||||
missing += [f'profiles/{n}.json' for n in profile.get('packs', {}).values() if not (ORACLE / 'profiles' / f'{n}.json').exists()]
|
||||
if missing:
|
||||
print(f'SKIP: mod jars not in {JARS}: {", ".join(missing)}')
|
||||
print(f'SKIP: not in {ORACLE}: {", ".join(missing)}')
|
||||
return 0
|
||||
resources = lab.RESOURCES
|
||||
if profile['carry']:
|
||||
resources = overlay(args.profile, carry(args.profile, profile['mods'], profile['carry']))
|
||||
data = carry(args.profile, profile['mods'], profile['carry'], profile.get('packs'))
|
||||
resources = overlay(args.profile, data)
|
||||
binary = probe_binary()
|
||||
if 'limits' in profile:
|
||||
return check_limits(binary, resources, results, profile, args.filter)
|
||||
matched, known, failed = 0, 0, []
|
||||
for path in results:
|
||||
oracle = json.load(gzip.open(path))
|
||||
|
||||
Reference in new issue
Block a user