mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #1765 from ludovic111/feat/lithostitched-processors
Lithostitched's processors as the game's own types; replace the old Lithostitched conversion
This commit is contained in:
13 files changed
+1383
-1020
No files matched your search
@@ -828,9 +828,9 @@ pub fn carry(
|
||||
// Additions to other sets and pools, as files the game merges.
|
||||
let modifier = serde_json::from_slice::<Value>(&bytes).ok();
|
||||
let types = &mut report.worldgen_types;
|
||||
if let Some((kind, mut added)) = modifier.and_then(|v| {
|
||||
crate::legacy_structure_mods::lithostitched::modifier(&v, loaded, types)
|
||||
}) {
|
||||
if let Some((kind, mut added)) =
|
||||
modifier.and_then(|v| crate::lithostitched::modifier(&v, loaded, types))
|
||||
{
|
||||
crate::processors::carry(
|
||||
kind,
|
||||
&mut added,
|
||||
|
||||
@@ -157,6 +157,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "modular compat is crate::lithostitched's now (its tests)"]
|
||||
fn modular_compat_runs_its_list_only_with_its_mod() {
|
||||
let mut data = ProcessorData::default();
|
||||
let swap = json!({"processor_type": "minecraft:rule", "rules": []});
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
//! Lithostitched: its jigsaw structure, its worldgen modifiers and its
|
||||
//! processors.
|
||||
use super::ctov;
|
||||
use crate::processors::{ProcessorData, carry_all, processors, referenced};
|
||||
use crate::worldgen::{ENHANCED_TERRAIN, Upgrade, start_checks};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A `structure` file of Lithostitched's jigsaw type as vanilla's, with the
|
||||
/// start rules it shares with the other libraries' jigsaws (`worldgen`'s
|
||||
/// `start_checks`). `None` for any other file.
|
||||
pub(crate) fn upgrade(
|
||||
kind: &str,
|
||||
v: &mut Value,
|
||||
lost: &mut BTreeMap<String, usize>,
|
||||
) -> Option<bool> {
|
||||
if kind != "structure" || v["type"] != "lithostitched:jigsaw" {
|
||||
return None;
|
||||
}
|
||||
start_checks(v, "lowest", lost, false);
|
||||
if v.as_object_mut()
|
||||
.and_then(|o| o.remove(ENHANCED_TERRAIN))
|
||||
.is_some()
|
||||
{
|
||||
*lost.entry(ENHANCED_TERRAIN.into()).or_default() += 1;
|
||||
}
|
||||
v["type"] = "minecraft:jigsaw".into();
|
||||
Some(true)
|
||||
}
|
||||
|
||||
/// A lithostitched worldgen modifier
|
||||
/// (`data/<ns>/lithostitched/worldgen_modifier/`) that adds to structure
|
||||
/// sets or template pools, as a file the game merges into them (the key
|
||||
/// `horizon:add_to`, `horizon_client::mod_content`): (kind, file). The
|
||||
/// modifier's conditions are checked against `loaded`, the mods ported
|
||||
/// along: a compat addition for a mod that is not would place that mod's
|
||||
/// blocks, which the game does not have. Other modifier types are left out.
|
||||
/// Both are counted in `report`.
|
||||
pub(crate) fn modifier(
|
||||
v: &Value,
|
||||
loaded: &dyn Fn(&str) -> bool,
|
||||
report: &mut Upgrade,
|
||||
) -> Option<(&'static str, Value)> {
|
||||
let t = v["type"].as_str().unwrap_or_default();
|
||||
let mut left = |why: &str| *report.modifiers_left.entry(why.to_owned()).or_default() += 1;
|
||||
let (kind, targets, from, to) = match t {
|
||||
"lithostitched:add_structure_set_entries" => {
|
||||
("structure_set", "structure_sets", "entries", "structures")
|
||||
}
|
||||
"lithostitched:add_template_pool_elements" => {
|
||||
("template_pool", "template_pools", "elements", "elements")
|
||||
}
|
||||
_ => {
|
||||
left(t);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !conditions_hold(v, loaded) {
|
||||
left("conditions");
|
||||
return None;
|
||||
}
|
||||
let targets: Option<Vec<&str>> = match &v[targets] {
|
||||
Value::String(s) => Some(vec![s.as_str()]),
|
||||
Value::Array(a) => a.iter().map(Value::as_str).collect(),
|
||||
_ => None,
|
||||
};
|
||||
let Some(targets) = targets.filter(|t| !t.is_empty() && t.iter().all(|t| !t.starts_with('#')))
|
||||
else {
|
||||
left("target");
|
||||
return None;
|
||||
};
|
||||
let entries = v[from].as_array().cloned().unwrap_or_default();
|
||||
for target in &targets {
|
||||
*report.added_to.entry((*target).to_owned()).or_default() += entries.len();
|
||||
}
|
||||
Some((
|
||||
kind,
|
||||
serde_json::json!({"horizon:add_to": targets, to: entries}),
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether a modifier's load conditions hold: lithostitched's `predicate`,
|
||||
/// and the loader's conditions (NeoForge's, else Fabric's). A condition
|
||||
/// type not known here does not hold.
|
||||
fn conditions_hold(v: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
let all = |list: &Value, test: &dyn Fn(&Value) -> bool| {
|
||||
list.as_array().is_some_and(|a| a.iter().all(test))
|
||||
};
|
||||
fn predicate(p: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
let test = |q: &Value| predicate(q, loaded);
|
||||
let list = &p["predicates"];
|
||||
match p["type"]
|
||||
.as_str()
|
||||
.map(|t| t.trim_start_matches("lithostitched:"))
|
||||
{
|
||||
Some("true") => true,
|
||||
Some("mod_loaded") => p["mod_id"].as_str().is_some_and(loaded),
|
||||
Some("not") => !predicate(&p["predicate"], loaded),
|
||||
Some("all_of") => list.as_array().is_some_and(|a| a.iter().all(test)),
|
||||
Some("any_of") => list.as_array().is_some_and(|a| a.iter().any(test)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn neoforge(c: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
let test = |q: &Value| neoforge(q, loaded);
|
||||
match c["type"].as_str() {
|
||||
Some("neoforge:true") => true,
|
||||
Some("neoforge:mod_loaded") => c["modid"].as_str().is_some_and(loaded),
|
||||
Some("neoforge:not") => !neoforge(&c["value"], loaded),
|
||||
Some("neoforge:and") => c["values"].as_array().is_some_and(|a| a.iter().all(test)),
|
||||
Some("neoforge:or") => c["values"].as_array().is_some_and(|a| a.iter().any(test)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn fabric(c: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
let test = |q: &Value| fabric(q, loaded);
|
||||
let mods = || {
|
||||
c["values"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Value::as_str)
|
||||
};
|
||||
match c["condition"].as_str() {
|
||||
Some("fabric:true") => true,
|
||||
Some("fabric:all_mods_loaded") => mods().all(|m| m.is_some_and(loaded)),
|
||||
Some("fabric:any_mods_loaded") => mods().any(|m| m.is_some_and(loaded)),
|
||||
Some("fabric:not") => !fabric(&c["value"], loaded),
|
||||
Some("fabric:and") => c["values"].as_array().is_some_and(|a| a.iter().all(test)),
|
||||
Some("fabric:or") => c["values"].as_array().is_some_and(|a| a.iter().any(test)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
let litho = v.get("predicate").is_none_or(|p| predicate(p, loaded));
|
||||
let loader = match (
|
||||
v.get("neoforge:conditions"),
|
||||
v.get("fabric:load_conditions"),
|
||||
) {
|
||||
(Some(c), _) => all(c, &|c| neoforge(c, loaded)),
|
||||
// One condition or a list of them.
|
||||
(None, Some(c)) if c.is_array() => all(c, &|c| fabric(c, loaded)),
|
||||
(None, Some(c)) => fabric(c, loaded),
|
||||
(None, None) => true,
|
||||
};
|
||||
litho && loader
|
||||
}
|
||||
|
||||
/// The data form of one of Lithostitched's processors: the processors to put
|
||||
/// in its place, or `None` when it has none (or is not Lithostitched's).
|
||||
pub(crate) fn processor(
|
||||
kind: &str,
|
||||
p: &Value,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
) -> Option<Vec<Value>> {
|
||||
match kind {
|
||||
"lithostitched:apply_random" => apply_random(p, data, carried).map(|p| vec![p]),
|
||||
"lithostitched:condition" => condition(p, data, carried).map(|p| vec![p]),
|
||||
"lithostitched:block_swap" => {
|
||||
// The same map; the game's swap keeps the properties both have.
|
||||
p["blocks"].as_object()?;
|
||||
Some(vec![
|
||||
json!({"processor_type": "horizon:block_swap", "blocks": p["blocks"]}),
|
||||
])
|
||||
}
|
||||
"lithostitched:set_block" => {
|
||||
// A simple or weighted state provider, the block entity left as
|
||||
// it is.
|
||||
let provider = p["state_provider"]["type"].as_str()?;
|
||||
let modifier = p["block_entity_modifier"]["type"].as_str();
|
||||
let plain =
|
||||
modifier.is_none_or(|m| m.trim_start_matches("minecraft:") == "passthrough");
|
||||
if !plain
|
||||
|| !matches!(
|
||||
provider,
|
||||
"minecraft:simple_state_provider" | "minecraft:weighted_state_provider"
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut out = p.clone();
|
||||
out["processor_type"] = "horizon:set_block".into();
|
||||
if let Some(o) = out.as_object_mut() {
|
||||
o.remove("block_entity_modifier");
|
||||
}
|
||||
Some(vec![out])
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The references (Lithostitched's, and CTOV's compat processors) in a
|
||||
/// processor list written in; whether any was.
|
||||
pub(crate) fn expand_references(
|
||||
list: Vec<Value>,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
) -> (Vec<Value>, bool) {
|
||||
references(list, data, carried, 0)
|
||||
}
|
||||
|
||||
/// A processor list inside a carried processor: an id stays (the game reads
|
||||
/// that file, carried on its own); an inline one is carried here.
|
||||
fn nested(list: &Value, data: &ProcessorData, carried: &mut BTreeMap<String, usize>) -> Value {
|
||||
let mut list = list.clone();
|
||||
if let Some(ps) = processors(&mut list) {
|
||||
*ps = carry_all(std::mem::take(ps), data, carried).0;
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
/// Lithostitched's `apply_random` as `horizon:apply_random`: its weighted
|
||||
/// lists, or its set of lists at weight 1. A tag is not carried.
|
||||
fn apply_random(
|
||||
p: &Value,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
) -> Option<Value> {
|
||||
let lists = &p["processor_lists"];
|
||||
let weighted = lists.as_array().is_some_and(|a| {
|
||||
a.iter()
|
||||
.all(|e| e.get("weight").is_some() && e.get("data").is_some())
|
||||
});
|
||||
let entries: Vec<(Value, Value)> = if weighted {
|
||||
lists
|
||||
.as_array()?
|
||||
.iter()
|
||||
.map(|e| (e["weight"].clone(), e["data"].clone()))
|
||||
.collect()
|
||||
} else {
|
||||
match lists {
|
||||
Value::String(id) if !id.starts_with('#') => vec![(1.into(), lists.clone())],
|
||||
Value::Array(a) if a.iter().any(|x| x.get("processor_type").is_some()) => {
|
||||
vec![(1.into(), lists.clone())]
|
||||
}
|
||||
Value::Array(a)
|
||||
if a.iter()
|
||||
.all(|x| x.as_str().is_some_and(|s| !s.starts_with('#')) || x.is_object()) =>
|
||||
{
|
||||
a.iter().map(|x| (1.into(), x.clone())).collect()
|
||||
}
|
||||
Value::Object(_) => vec![(1.into(), lists.clone())],
|
||||
_ => return None,
|
||||
}
|
||||
};
|
||||
let lists: Vec<Value> = entries
|
||||
.into_iter()
|
||||
.map(|(weight, list)| json!({"weight": weight, "data": nested(&list, data, carried)}))
|
||||
.collect();
|
||||
let mut out = json!({"processor_type": "horizon:apply_random", "processor_lists": lists});
|
||||
if let Some(mode) = p.get("mode") {
|
||||
out["mode"] = mode.clone();
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Lithostitched's `condition` as `horizon:condition`, when the game runs
|
||||
/// every condition in it (true, all_of, any_of, not, random_chance,
|
||||
/// matching_blocks with plain property values); else it stays Java.
|
||||
fn condition(
|
||||
p: &Value,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
) -> Option<Value> {
|
||||
fn runs(c: &Value) -> bool {
|
||||
if let Some(all) = c.as_array() {
|
||||
return all.iter().all(runs);
|
||||
}
|
||||
let kind = c["type"].as_str().unwrap_or_default();
|
||||
match kind.strip_prefix("lithostitched:").unwrap_or(kind) {
|
||||
"true" | "random_chance" => true,
|
||||
"all_of" | "any_of" => c["conditions"]
|
||||
.as_array()
|
||||
.is_some_and(|a| a.iter().all(runs)),
|
||||
"not" => runs(&c["condition"]),
|
||||
"matching_blocks" => c.get("properties").is_none_or(|p| {
|
||||
p.as_object()
|
||||
.is_some_and(|o| o.values().all(Value::is_string))
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
if !runs(&p["if_true"]) {
|
||||
return None;
|
||||
}
|
||||
// One processor or a list of them.
|
||||
let mut branch = |key: &str| -> Value {
|
||||
let list = match p.get(key) {
|
||||
None => Vec::new(),
|
||||
Some(Value::Array(a)) => a.clone(),
|
||||
Some(one) => vec![one.clone()],
|
||||
};
|
||||
carry_all(list, data, carried).0.into()
|
||||
};
|
||||
let (then, otherwise) = (branch("then"), branch("else"));
|
||||
let mut out = json!({"processor_type": "horizon:condition", "if_true": p["if_true"],
|
||||
"then": then, "else": otherwise});
|
||||
if let Some(mode) = p.get("random_mode") {
|
||||
out["random_mode"] = mode.clone();
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Lithostitched's references in a list, each replaced by the processors of
|
||||
/// the lists it names, in order (references in them too, to a depth of 8).
|
||||
/// One naming a tag or a list the mod does not have stays, to be dropped.
|
||||
/// Whether any was replaced.
|
||||
fn references(
|
||||
list: Vec<Value>,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
depth: usize,
|
||||
) -> (Vec<Value>, bool) {
|
||||
let mut out = Vec::with_capacity(list.len());
|
||||
let mut changed = false;
|
||||
for p in list {
|
||||
if let Some(inner) = ctov::modular_compat(&p, data, carried, depth) {
|
||||
changed = true;
|
||||
out.extend(references(inner, data, carried, depth + 1).0);
|
||||
continue;
|
||||
}
|
||||
let named = (p["processor_type"] == "lithostitched:reference" && depth < 8)
|
||||
.then(|| referenced(&p["processor_lists"], data))
|
||||
.flatten();
|
||||
match named {
|
||||
Some(inner) => {
|
||||
*carried.entry("lithostitched:reference".into()).or_default() += 1;
|
||||
out.extend(references(inner, data, carried, depth + 1).0);
|
||||
changed = true;
|
||||
}
|
||||
None => out.push(p),
|
||||
}
|
||||
}
|
||||
(out, changed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::legacy_structure_mods::data;
|
||||
use crate::processors::carry;
|
||||
|
||||
#[test]
|
||||
fn modifiers_add_to_sets_and_pools() {
|
||||
let mut report = Upgrade::default();
|
||||
let only_fd = |m: &str| m == "farmersdelight";
|
||||
let set = json!({"type": "lithostitched:add_structure_set_entries",
|
||||
"structure_sets": "minecraft:villages",
|
||||
"entries": [{"structure": "x:village", "weight": 4}]});
|
||||
let (kind, added) = modifier(&set, &only_fd, &mut report).unwrap();
|
||||
assert_eq!(kind, "structure_set");
|
||||
assert_eq!(
|
||||
added,
|
||||
json!({"horizon:add_to": ["minecraft:villages"],
|
||||
"structures": [{"structure": "x:village", "weight": 4}]})
|
||||
);
|
||||
// A compat addition holds only when its mod is ported along.
|
||||
let pool = |modid: &str| {
|
||||
json!({"type": "lithostitched:add_template_pool_elements",
|
||||
"neoforge:conditions": [{"type": "neoforge:mod_loaded", "modid": modid}],
|
||||
"fabric:load_conditions": {"condition": "fabric:all_mods_loaded", "values": ["other"]},
|
||||
"template_pools": ["x:houses", "y:houses"],
|
||||
"elements": [{"weight": 5, "element": {"element_type": "minecraft:single_pool_element",
|
||||
"location": "x:fd_farm", "projection": "rigid", "processors": "minecraft:empty"}}]})
|
||||
};
|
||||
let (kind, added) = modifier(&pool("farmersdelight"), &only_fd, &mut report).unwrap();
|
||||
assert_eq!(kind, "template_pool");
|
||||
assert_eq!(added["horizon:add_to"], json!(["x:houses", "y:houses"]));
|
||||
assert_eq!(added["elements"][0]["weight"], 5);
|
||||
assert!(modifier(&pool("create"), &only_fd, &mut report).is_none());
|
||||
// Fabric's conditions when there are no NeoForge ones; lithostitched's own.
|
||||
let fabric = json!({"type": "lithostitched:add_structure_set_entries",
|
||||
"fabric:load_conditions": [{"condition": "fabric:not",
|
||||
"value": {"condition": "fabric:any_mods_loaded", "values": ["create"]}}],
|
||||
"predicate": {"type": "lithostitched:all_of", "predicates": [
|
||||
{"type": "lithostitched:mod_loaded", "mod_id": "farmersdelight"},
|
||||
{"type": "lithostitched:true"}]},
|
||||
"structure_sets": "minecraft:villages", "entries": []});
|
||||
assert!(modifier(&fabric, &only_fd, &mut report).is_some());
|
||||
let tagged = json!({"type": "lithostitched:add_structure_set_entries",
|
||||
"structure_sets": "#x:sets", "entries": []});
|
||||
assert!(modifier(&tagged, &only_fd, &mut report).is_none());
|
||||
let features = json!({"type": "lithostitched:add_features"});
|
||||
assert!(modifier(&features, &only_fd, &mut report).is_none());
|
||||
assert_eq!(
|
||||
report.added_to,
|
||||
BTreeMap::from([
|
||||
("minecraft:villages".into(), 1),
|
||||
("x:houses".into(), 1),
|
||||
("y:houses".into(), 1),
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
report.modifiers_left,
|
||||
BTreeMap::from([
|
||||
("conditions".into(), 1),
|
||||
("lithostitched:add_features".into(), 1),
|
||||
("target".into(), 1),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithostitched_block_swaps_become_the_games() {
|
||||
let mut list = json!({"processors": [
|
||||
{"processor_type": "lithostitched:block_swap",
|
||||
"blocks": {"minecraft:cobblestone": "minecraft:mossy_cobblestone"}},
|
||||
{"processor_type": "lithostitched:block_swap", "blocks": []}]});
|
||||
let mut carried = BTreeMap::new();
|
||||
assert!(carry("processor_list", &mut list, &data(), &mut carried));
|
||||
let p = list["processors"].as_array().unwrap();
|
||||
assert_eq!(
|
||||
p[0],
|
||||
json!({"processor_type": "horizon:block_swap",
|
||||
"blocks": {"minecraft:cobblestone": "minecraft:mossy_cobblestone"}})
|
||||
);
|
||||
// A map that is not one stays Java (dropped later).
|
||||
assert_eq!(p[1]["processor_type"], "lithostitched:block_swap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithostitched_references_are_written_in() {
|
||||
let mut data = data();
|
||||
let gravity = json!({"processor_type": "minecraft:gravity", "heightmap": "WORLD_SURFACE_WG", "offset": 0});
|
||||
data.lists
|
||||
.insert("v:road".into(), json!({"processors": [gravity.clone()]}));
|
||||
data.lists.insert(
|
||||
"v:house".into(),
|
||||
json!({"processors": [
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": "v:road"},
|
||||
{"processor_type": "lithostitched:block_swap", "blocks": {"minecraft:dirt": "minecraft:grass_block"}}]}),
|
||||
);
|
||||
let mut list = json!({"processors": [
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": ["v:house", "v:road"]},
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": "#v:roads"},
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": ["v:nowhere"]}]});
|
||||
let mut carried = BTreeMap::new();
|
||||
assert!(carry("processor_list", &mut list, &data, &mut carried));
|
||||
let p = list["processors"].as_array().unwrap();
|
||||
// House (its own reference to road first, then its swap), then road;
|
||||
// the swap inside is carried too.
|
||||
assert_eq!(p[0], gravity);
|
||||
assert_eq!(p[1]["processor_type"], "horizon:block_swap");
|
||||
assert_eq!(p[2], gravity);
|
||||
// A tag or a list the mod lacks stays, to be dropped.
|
||||
assert_eq!(p[3]["processor_lists"], "#v:roads");
|
||||
assert_eq!(p[4]["processor_lists"], json!(["v:nowhere"]));
|
||||
assert_eq!(carried["lithostitched:reference"], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lithostitched_random_and_condition_become_the_games() {
|
||||
let mut data = data();
|
||||
let swap = json!({"processor_type": "lithostitched:block_swap",
|
||||
"blocks": {"minecraft:dirt": "minecraft:grass_block"}});
|
||||
data.lists
|
||||
.insert("v:green".into(), json!({"processors": [swap.clone()]}));
|
||||
let mut list = json!({"processors": [
|
||||
{"processor_type": "lithostitched:apply_random", "mode": "per_piece", "processor_lists": [
|
||||
{"weight": 3, "data": "minecraft:empty"},
|
||||
{"weight": 1, "data": {"processors": [
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": "v:green"},
|
||||
{"processor_type": "v:java"}]}}]},
|
||||
{"processor_type": "lithostitched:apply_random", "mode": "per_block",
|
||||
"processor_lists": ["v:green", "v:red"]},
|
||||
{"processor_type": "lithostitched:apply_random", "mode": "per_block",
|
||||
"processor_lists": "#v:colours"},
|
||||
{"processor_type": "lithostitched:condition",
|
||||
"if_true": [{"type": "lithostitched:matching_blocks", "blocks": "minecraft:cobblestone"}],
|
||||
"then": swap, "else": {"processor_type": "lithostitched:set_block", "state_provider":
|
||||
{"type": "minecraft:simple_state_provider", "state": {"Name": "minecraft:stone"}}}},
|
||||
{"processor_type": "lithostitched:condition",
|
||||
"if_true": {"type": "lithostitched:matching_biomes", "biomes": "#c:is_cold"},
|
||||
"then": []}]});
|
||||
let mut carried = BTreeMap::new();
|
||||
assert!(carry("processor_list", &mut list, &data, &mut carried));
|
||||
let p = list["processors"].as_array().unwrap();
|
||||
let green = json!({"processor_type": "horizon:block_swap",
|
||||
"blocks": {"minecraft:dirt": "minecraft:grass_block"}});
|
||||
// Inline lists are carried; Java left in them is dropped later.
|
||||
assert_eq!(
|
||||
p[0],
|
||||
json!({"processor_type": "horizon:apply_random", "mode": "per_piece", "processor_lists": [
|
||||
{"weight": 3, "data": "minecraft:empty"},
|
||||
{"weight": 1, "data": {"processors": [green.clone(), {"processor_type": "v:java"}]}}]})
|
||||
);
|
||||
assert_eq!(
|
||||
p[1]["processor_lists"],
|
||||
json!([{"weight": 1, "data": "v:green"}, {"weight": 1, "data": "v:red"}])
|
||||
);
|
||||
assert_eq!(p[2]["processor_type"], "lithostitched:apply_random");
|
||||
assert_eq!(
|
||||
p[3],
|
||||
json!({"processor_type": "horizon:condition",
|
||||
"if_true": [{"type": "lithostitched:matching_blocks", "blocks": "minecraft:cobblestone"}],
|
||||
"then": [green], "else": [{"processor_type": "horizon:set_block", "state_provider":
|
||||
{"type": "minecraft:simple_state_provider", "state": {"Name": "minecraft:stone"}}}]})
|
||||
);
|
||||
assert_eq!(p[4]["processor_type"], "lithostitched:condition");
|
||||
assert_eq!(carried["lithostitched:apply_random"], 2);
|
||||
assert_eq!(carried["lithostitched:condition"], 1);
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,11 @@
|
||||
//! The general code reaches them only through the `pub(crate)` entry points
|
||||
//! of these three modules. Nothing in them is used by another mod's
|
||||
//! conversion.
|
||||
#[allow(dead_code)] // its modular compat is `crate::lithostitched`'s now; the rest goes with C1
|
||||
pub(crate) mod ctov;
|
||||
pub(crate) mod lithostitched;
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)] // the old Lithostitched tests used it
|
||||
fn data() -> crate::processors::ProcessorData {
|
||||
let mut d = crate::processors::ProcessorData::default();
|
||||
d.spawners.insert(
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod import;
|
||||
pub mod inventory;
|
||||
pub mod jar;
|
||||
pub(crate) mod legacy_structure_mods;
|
||||
mod lithostitched;
|
||||
pub mod modrinth;
|
||||
pub mod oracle;
|
||||
pub mod pipeline;
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
//! Lithostitched's worldgen, carried as the game reads it (clean room:
|
||||
//! `cleanroom/structures/spec-lithostitched.md`; CTOV's processor:
|
||||
//! `spec-ctov.md` §4).
|
||||
//!
|
||||
//! - `lithostitched:jigsaw` structures become vanilla's jigsaw. Lithostitched
|
||||
//! lays jigsaws out its own way (OPEN-L5): Horizon keeps vanilla's layouts.
|
||||
//! - Its processors (`block_swap`, `reference`, `apply_random`, `condition`,
|
||||
//! `set_block`, `discard_input`) are the game's own types
|
||||
//! (`horizon_world`'s `structure::processor`): they are carried as written,
|
||||
//! 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.
|
||||
use crate::{processors::ProcessorData, worldgen::Upgrade};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const JIGSAW: &str = "lithostitched:jigsaw";
|
||||
/// Vanilla's JigsawStructure.MAX_DEPTH.
|
||||
const MAX_SIZE: i64 = 20;
|
||||
const MODULAR_COMPAT: &str = "ctov:modular_compat";
|
||||
/// The processor types the game reads.
|
||||
const PROCESSORS: [&str; 6] = [
|
||||
"lithostitched:block_swap",
|
||||
"lithostitched:reference",
|
||||
"lithostitched:apply_random",
|
||||
"lithostitched:condition",
|
||||
"lithostitched:set_block",
|
||||
"lithostitched:discard_input",
|
||||
];
|
||||
|
||||
fn count(map: &mut BTreeMap<String, usize>, name: &str) {
|
||||
*map.entry(name.to_owned()).or_default() += 1;
|
||||
}
|
||||
|
||||
/// A `lithostitched:jigsaw` structure as vanilla's jigsaw: `Some(true)`, else
|
||||
/// `None`.
|
||||
pub(crate) fn upgrade(
|
||||
kind: &str,
|
||||
v: &mut Value,
|
||||
lost: &mut BTreeMap<String, usize>,
|
||||
) -> Option<bool> {
|
||||
if kind != "structure" || v["type"] != JIGSAW {
|
||||
return None;
|
||||
}
|
||||
v["type"] = "minecraft:jigsaw".into();
|
||||
// `size` may be an int provider (sampled per start): its largest value.
|
||||
let size = match &v["size"] {
|
||||
Value::Null => None,
|
||||
Value::Number(n) => n.as_i64(),
|
||||
provider => {
|
||||
count(lost, "lithostitched:jigsaw size provider");
|
||||
["max_inclusive", "value"]
|
||||
.iter()
|
||||
.find_map(|k| provider[k].as_i64())
|
||||
}
|
||||
};
|
||||
if let Some(size) = size {
|
||||
if size > MAX_SIZE {
|
||||
count(lost, "lithostitched:jigsaw size over 20");
|
||||
}
|
||||
v["size"] = size.clamp(0, MAX_SIZE).into();
|
||||
}
|
||||
let o = v.as_object_mut()?;
|
||||
match o.remove("start_projection") {
|
||||
Some(Value::String(heightmap)) if heightmap != "floor" && heightmap != "ceiling" => {
|
||||
o.entry("project_start_to_heightmap")
|
||||
.or_insert(heightmap.into());
|
||||
}
|
||||
Some(_) => count(lost, "lithostitched:jigsaw start_projection"),
|
||||
None => {}
|
||||
}
|
||||
if o.remove("fixed_rotation").is_some_and(|f| f == true) {
|
||||
count(lost, "lithostitched:jigsaw fixed_rotation");
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
/// Lithostitched's pool elements as the element they wrap; whether `element`
|
||||
/// was one.
|
||||
pub(crate) fn upgrade_pool_element(
|
||||
element: &mut Value,
|
||||
lost: &mut BTreeMap<String, usize>,
|
||||
) -> bool {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids a modifier targets: an id or a list of ids (`None` for tags and
|
||||
/// Lithostitched's object forms).
|
||||
fn ids(v: &Value) -> Option<Vec<String>> {
|
||||
let id = |v: &Value| {
|
||||
v.as_str()
|
||||
.filter(|s| !s.starts_with('#'))
|
||||
.map(str::to_owned)
|
||||
};
|
||||
match v {
|
||||
Value::Array(a) => a.iter().map(id).collect(),
|
||||
other => Some(vec![id(other)?]),
|
||||
}
|
||||
}
|
||||
|
||||
/// NeoForge's load conditions (`neoforge:conditions`): all must pass.
|
||||
fn conditions_pass(v: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
fn pass(c: &Value, loaded: &dyn Fn(&str) -> bool) -> bool {
|
||||
let all = |key: &str| {
|
||||
c[key]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.all(|c| pass(c, loaded))
|
||||
};
|
||||
match c["type"].as_str() {
|
||||
Some("neoforge:mod_loaded") => c["modid"].as_str().is_some_and(loaded),
|
||||
Some("neoforge:true") => true,
|
||||
Some("neoforge:not") => !pass(&c["value"], loaded),
|
||||
Some("neoforge:and") => all("values"),
|
||||
Some("neoforge:or") => {
|
||||
(c["values"].as_array().into_iter().flatten()).any(|c| pass(c, loaded))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
(v["neoforge:conditions"].as_array().into_iter().flatten()).all(|c| pass(c, loaded))
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
pub(crate) fn modifier(
|
||||
v: &Value,
|
||||
loaded: &dyn Fn(&str) -> bool,
|
||||
report: &mut Upgrade,
|
||||
) -> Option<(&'static str, Value)> {
|
||||
let kind = v["type"].as_str().unwrap_or("no type");
|
||||
if !conditions_pass(v, loaded) {
|
||||
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 {
|
||||
count(&mut report.modifiers_left, "target");
|
||||
return None;
|
||||
};
|
||||
for target in &targets {
|
||||
*report.added_to.entry(target.clone()).or_default() += elements.len();
|
||||
}
|
||||
Some((
|
||||
"template_pool",
|
||||
json!({"horizon:add_to": targets, "elements": elements}),
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether the game reads a Lithostitched processor as it is: one of its
|
||||
/// types, with no processor list named by tag (the game has no processor
|
||||
/// list tags) and a state provider it runs.
|
||||
pub(crate) fn game_reads(p: &Value) -> bool {
|
||||
let Some(kind) = p["processor_type"]
|
||||
.as_str()
|
||||
.filter(|t| PROCESSORS.contains(t))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let tag = |v: &Value| v.as_str().is_some_and(|s| s.starts_with('#'));
|
||||
match kind {
|
||||
"lithostitched:reference" => match &p["processor_lists"] {
|
||||
Value::Array(a) => !a.iter().any(tag),
|
||||
other => other.is_string() && !tag(other),
|
||||
},
|
||||
"lithostitched:apply_random" => p["processor_lists"]
|
||||
.as_array()
|
||||
.is_some_and(|a| a.iter().all(|e| !tag(e) && !tag(&e["data"]))),
|
||||
"lithostitched:set_block" => matches!(
|
||||
p["state_provider"]["type"].as_str(),
|
||||
Some("minecraft:simple_state_provider" | "minecraft:weighted_state_provider")
|
||||
),
|
||||
"lithostitched:block_swap" => p["blocks"].is_object(),
|
||||
"lithostitched:condition" => !p["if_true"].is_null(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The processor lists inside a Lithostitched processor, for the general
|
||||
/// code that walks lists (`then`, `else`, `apply_random`'s inline data).
|
||||
pub(crate) fn inner_lists(p: &mut Value) -> Vec<&mut Value> {
|
||||
match p["processor_type"].as_str() {
|
||||
Some("lithostitched:apply_random") => (p["processor_lists"].as_array_mut().into_iter())
|
||||
.flatten()
|
||||
.filter_map(|e| e.get_mut("data").filter(|d| !d.is_string()))
|
||||
.collect(),
|
||||
Some("lithostitched:condition") => (p.as_object_mut().into_iter())
|
||||
.flat_map(|o| o.iter_mut())
|
||||
.filter(|(k, _)| *k == "then" || *k == "else")
|
||||
.map(|(_, v)| v)
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A Lithostitched processor with the processors nested in it carried
|
||||
/// (`Some`, when one of them changed); `None` for any other processor.
|
||||
pub(crate) fn processor(
|
||||
kind: &str,
|
||||
p: &Value,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
) -> Option<Vec<Value>> {
|
||||
if !PROCESSORS.contains(&kind) {
|
||||
return None;
|
||||
}
|
||||
let mut out = p.clone();
|
||||
let mut changed = false;
|
||||
for list in inner_lists(&mut out) {
|
||||
// One processor alone is a list of one.
|
||||
if list.get("processor_type").is_some() {
|
||||
*list = Value::Array(vec![list.take()]);
|
||||
changed = true;
|
||||
}
|
||||
if let Some(processors) = crate::processors::processors(list) {
|
||||
let (inner, inner_changed) =
|
||||
crate::processors::carry_all(std::mem::take(processors), data, carried);
|
||||
*processors = inner;
|
||||
changed |= inner_changed;
|
||||
}
|
||||
}
|
||||
changed.then(|| vec![out])
|
||||
}
|
||||
|
||||
/// The list with CTOV's modular compat processors written as the game's
|
||||
/// `reference` (its mod is ported along) or taken out; whether any was.
|
||||
pub(crate) fn expand_references(
|
||||
list: Vec<Value>,
|
||||
data: &ProcessorData,
|
||||
carried: &mut BTreeMap<String, usize>,
|
||||
) -> (Vec<Value>, bool) {
|
||||
let mut changed = false;
|
||||
let mut out = Vec::with_capacity(list.len());
|
||||
for p in list {
|
||||
if p["processor_type"] != MODULAR_COMPAT {
|
||||
out.push(p);
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
count(carried, MODULAR_COMPAT);
|
||||
let loaded = p["modid"]
|
||||
.as_str()
|
||||
.is_some_and(|m| data.compat_loaded.contains(m));
|
||||
if loaded {
|
||||
out.push(json!({"processor_type": "lithostitched:reference",
|
||||
"processor_lists": p["reference"].clone()}));
|
||||
}
|
||||
}
|
||||
(out, changed)
|
||||
}
|
||||
|
||||
/// Notes which mods CTOV's modular compat processors name are `loaded`.
|
||||
pub(crate) fn mark_loaded(data: &mut ProcessorData, loaded: &dyn Fn(&str) -> bool) {
|
||||
fn visit(v: &Value, out: &mut Vec<String>) {
|
||||
match v {
|
||||
Value::Object(o) => {
|
||||
if o.get("processor_type").and_then(Value::as_str) == Some(MODULAR_COMPAT)
|
||||
&& let Some(m) = o.get("modid").and_then(Value::as_str)
|
||||
{
|
||||
out.push(m.to_owned());
|
||||
}
|
||||
o.values().for_each(|v| visit(v, out));
|
||||
}
|
||||
Value::Array(a) => a.iter().for_each(|v| visit(v, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut named = Vec::new();
|
||||
data.lists.values().for_each(|l| visit(l, &mut named));
|
||||
data.compat_loaded
|
||||
.extend(named.into_iter().filter(|m| loaded(m)));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::worldgen::upgrade as upgrade_file;
|
||||
|
||||
#[test]
|
||||
fn jigsaws_become_vanillas() {
|
||||
let mut lost = BTreeMap::new();
|
||||
let mut s = json!({"type": JIGSAW, "size": {"type": "uniform", "min_inclusive": 3,
|
||||
"max_inclusive": 40}, "start_projection": "WORLD_SURFACE_WG", "fixed_rotation": true});
|
||||
assert_eq!(upgrade("structure", &mut s, &mut lost), Some(true));
|
||||
assert_eq!(
|
||||
s,
|
||||
json!({"type": "minecraft:jigsaw", "size": 20,
|
||||
"project_start_to_heightmap": "WORLD_SURFACE_WG"})
|
||||
);
|
||||
assert_eq!(lost.len(), 3);
|
||||
assert_eq!(
|
||||
upgrade(
|
||||
"structure",
|
||||
&mut json!({"type": "minecraft:jigsaw"}),
|
||||
&mut lost
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_elements_place_their_delegate() {
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifiers_add_to_pools_when_their_mods_are_loaded() {
|
||||
let mut report = Upgrade::default();
|
||||
let loaded = |m: &str| m == "waystones";
|
||||
let add = |modid: &str, pools: Value| {
|
||||
json!({"type": "lithostitched:add_template_pool_elements", "template_pools": pools,
|
||||
"elements": [{"weight": 2, "element": {"element_type": "minecraft:empty_pool_element"}}],
|
||||
"neoforge:conditions": [{"type": "neoforge:mod_loaded", "modid": modid}]})
|
||||
};
|
||||
let (kind, file) = modifier(
|
||||
&add("waystones", json!("ctov:village/plains/house")),
|
||||
&loaded,
|
||||
&mut report,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(kind, "template_pool");
|
||||
assert_eq!(file["horizon:add_to"], json!(["ctov:village/plains/house"]));
|
||||
assert_eq!(file["elements"][0]["weight"], 2);
|
||||
assert_eq!(report.added_to["ctov:village/plains/house"], 1);
|
||||
assert!(modifier(&add("sawmill", json!("a:b")), &loaded, &mut report).is_none());
|
||||
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());
|
||||
assert_eq!(
|
||||
report.modifiers_left,
|
||||
BTreeMap::from([
|
||||
("conditions".into(), 1),
|
||||
("lithostitched:set_pool_aliases".into(), 1),
|
||||
("target".into(), 1),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/// A CTOV list (spec-ctov §4): references, a condition with a single
|
||||
/// processor and a nested condition, a random list holding the compat
|
||||
/// hook, and processors the game cannot read.
|
||||
#[test]
|
||||
fn processors_stay_lithostitcheds_with_their_insides_carried() {
|
||||
let hook = json!({"processor_type": MODULAR_COMPAT, "modid": "natures_spirit",
|
||||
"reference": "ctov:village/mesa/house_wisteria"});
|
||||
let list = json!({"processors": [
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": ["ctov:village/mesa/house"]},
|
||||
{"processor_type": "lithostitched:condition", "random_mode": "per_block",
|
||||
"if_true": {"type": "lithostitched:matching_blocks", "blocks": "minecraft:water",
|
||||
"match_type": "location"},
|
||||
"then": {"processor_type": "lithostitched:set_block", "state_provider": {
|
||||
"type": "minecraft:simple_state_provider", "state": {"Name": "minecraft:spruce_planks"}}},
|
||||
"else": [{"processor_type": "somemod:java_processor"}]},
|
||||
{"processor_type": "lithostitched:apply_random", "mode": "per_piece", "processor_lists": [
|
||||
{"weight": 1, "data": "minecraft:empty"},
|
||||
{"weight": 1, "data": {"processors": [hook]}}]},
|
||||
{"processor_type": "lithostitched:reference", "processor_lists": "#ctov:palettes"},
|
||||
{"processor_type": "lithostitched:set_block", "state_provider": {
|
||||
"type": "minecraft:noise_provider"}}]});
|
||||
for (loaded, wisteria) in [
|
||||
(false, json!([])),
|
||||
(
|
||||
true,
|
||||
json!([{"processor_type":
|
||||
"lithostitched:reference", "processor_lists": "ctov:village/mesa/house_wisteria"}]),
|
||||
),
|
||||
] {
|
||||
let mut data = ProcessorData::default();
|
||||
data.lists
|
||||
.insert("ctov:village/mesa/house".into(), list.clone());
|
||||
mark_loaded(&mut data, &|m| loaded && m == "natures_spirit");
|
||||
let mut v = list.clone();
|
||||
let mut report = Upgrade::default();
|
||||
crate::processors::carry(
|
||||
"processor_list",
|
||||
&mut v,
|
||||
&data,
|
||||
&mut report.processors_carried,
|
||||
);
|
||||
upgrade_file("processor_list", &mut v, &mut report, &BTreeMap::new());
|
||||
let p = &v["processors"];
|
||||
assert_eq!(p.as_array().unwrap().len(), 3, "{p}");
|
||||
assert_eq!(p[0], list["processors"][0]);
|
||||
// One processor is a list of one; the Java one is taken out.
|
||||
assert_eq!(
|
||||
p[1]["then"][0]["state_provider"]["state"]["id"],
|
||||
"minecraft:spruce_planks"
|
||||
);
|
||||
assert_eq!(p[1]["else"], json!([]));
|
||||
assert_eq!(p[2]["processor_lists"][0]["data"], "minecraft:empty");
|
||||
assert_eq!(p[2]["processor_lists"][1]["data"]["processors"], wisteria);
|
||||
assert_eq!(report.processors_carried[MODULAR_COMPAT], 1);
|
||||
assert_eq!(
|
||||
report.processors_dropped,
|
||||
BTreeMap::from([
|
||||
("lithostitched:reference".into(), 1),
|
||||
("lithostitched:set_block".into(), 1),
|
||||
("somemod:java_processor".into(), 1),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@
|
||||
//! - Repurposed Structures' mineshaft sky view becomes the game's
|
||||
//! `horizon:mineshaft_sky_view`.
|
||||
//!
|
||||
//! The processors of YUNG's mods, Lithostitched and Choice Theorem's Overhauled
|
||||
//! Village are carried by `legacy_structure_mods`, called from [`carry`].
|
||||
//! Lithostitched's processors and Choice Theorem's Overhauled Village's
|
||||
//! compat hook are carried by `crate::lithostitched`, called from [`carry`].
|
||||
//!
|
||||
//! What is not here is taken out of its list later and counted
|
||||
//! (`worldgen::upgrade`). The entity NBT is written in 26.3's shape (an
|
||||
//! `equipment` map, not 1.21.1's `HandItems` and `ArmorItems`).
|
||||
use crate::legacy_structure_mods::{ctov, lithostitched};
|
||||
use crate::lithostitched;
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -42,7 +42,7 @@ pub struct ProcessorData {
|
||||
impl ProcessorData {
|
||||
/// Notes which mods its compat processors name are `loaded`.
|
||||
pub fn mark_loaded(&mut self, loaded: &dyn Fn(&str) -> bool) {
|
||||
ctov::mark_loaded(self, loaded);
|
||||
lithostitched::mark_loaded(self, loaded);
|
||||
}
|
||||
|
||||
pub fn read<R: Read + Seek>(zip: &mut zip::ZipArchive<R>) -> Self {
|
||||
@@ -177,8 +177,7 @@ fn carry_at(
|
||||
let mut out = Vec::with_capacity(expanded.len());
|
||||
for p in expanded {
|
||||
let kind = p["processor_type"].as_str().unwrap_or_default().to_owned();
|
||||
// Lithostitched's processors are carried apart from this code
|
||||
// (`legacy_structure_mods`).
|
||||
// Lithostitched's processors stay theirs, with what they hold carried.
|
||||
let replacement = lithostitched::processor(&kind, &p, data, carried).or_else(|| {
|
||||
match pillar_type(&kind) {
|
||||
Some(to) => Some(vec![pillar(&p, to, data, carried, depth)]),
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
//! closely.
|
||||
//!
|
||||
//! YUNG's API's files are carried as written ([`crate::yung`]).
|
||||
//! Lithostitched's and CTOV's worldgen files are converted by
|
||||
//! `legacy_structure_mods`, called from [`upgrade`], [`crate::carry`] and the
|
||||
//! processors.
|
||||
use crate::{legacy_structure_mods::lithostitched, yung};
|
||||
//! Lithostitched's jigsaws, elements, modifiers and processors are carried by
|
||||
//! [`crate::lithostitched`]; the entries CTOV adds to vanilla's structure sets
|
||||
//! by `legacy_structure_mods`, called from [`crate::carry`].
|
||||
use crate::{lithostitched, yung};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -191,28 +191,19 @@ fn drop_java_processors(list: &mut Value, dropped: &mut BTreeMap<String, usize>)
|
||||
let before = processors.len();
|
||||
processors.retain(|p| {
|
||||
let t = p["processor_type"].as_str().unwrap_or_default();
|
||||
let java = !t.starts_with("minecraft:") && !t.starts_with("horizon:");
|
||||
let java = !t.starts_with("minecraft:")
|
||||
&& !t.starts_with("horizon:")
|
||||
&& !lithostitched::game_reads(p);
|
||||
if java {
|
||||
*dropped.entry(t.to_owned()).or_default() += 1;
|
||||
}
|
||||
!java
|
||||
});
|
||||
let mut changed = processors.len() != before;
|
||||
// The inline lists inside the game's apply_random and condition.
|
||||
// The inline lists inside Lithostitched's apply_random and condition.
|
||||
for p in processors.iter_mut() {
|
||||
let inner: Vec<&mut Value> = match p["processor_type"].as_str() {
|
||||
Some("horizon:apply_random") => p["processor_lists"]
|
||||
.as_array_mut()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|e| &mut e["data"])
|
||||
.collect(),
|
||||
Some("horizon:condition") => {
|
||||
let o = p.as_object_mut().into_iter().flat_map(|o| o.iter_mut());
|
||||
o.filter(|(k, _)| *k == "then" || *k == "else")
|
||||
.map(|(_, v)| v)
|
||||
.collect()
|
||||
}
|
||||
Some(t) if t.starts_with("lithostitched:") => lithostitched::inner_lists(p),
|
||||
Some("horizon:pillar" | "horizon:bottom_pillar") => {
|
||||
p.get_mut("pillar_processor_list").into_iter().collect()
|
||||
}
|
||||
@@ -398,7 +389,9 @@ fn upgrade_types(
|
||||
}
|
||||
}
|
||||
for element in elements {
|
||||
if yung::upgrade_pool_element(element, lost) {
|
||||
if yung::upgrade_pool_element(element, lost)
|
||||
|| lithostitched::upgrade_pool_element(element, lost)
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use horizon_core::{
|
||||
use serde_json::Value;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
mod lithostitched;
|
||||
mod litho;
|
||||
mod pillars;
|
||||
mod sea_level;
|
||||
|
||||
@@ -238,6 +238,23 @@ impl Modifier {
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Modifier {
|
||||
/// RuleBlockEntityModifier.apply: the block entity the output gets.
|
||||
fn apply(&self, nbt: Option<&Compound>, random: &mut impl ProcessorRandom) -> Option<Compound> {
|
||||
match self {
|
||||
Self::Passthrough => nbt.cloned(),
|
||||
Self::Clear => Some(Compound::new()),
|
||||
Self::AppendLoot(table) => {
|
||||
let mut nbt = nbt.cloned().unwrap_or_default();
|
||||
nbt.insert("LootTable".into(), Tag::String(table.as_str().into()));
|
||||
nbt.insert("LootTableSeed".into(), Tag::Long(random.long()));
|
||||
Some(nbt)
|
||||
}
|
||||
Self::AppendStatic(data) => Some(edited(nbt, &[], data)),
|
||||
Self::Edit { remove, data } => Some(edited(nbt, remove, data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A block entity with `remove`'s keys taken out and `data` merged in
|
||||
/// (append_static is the one with nothing to remove).
|
||||
fn edited(nbt: Option<&Compound>, remove: &[String], data: &Compound) -> Compound {
|
||||
@@ -292,17 +309,21 @@ pub enum Processor {
|
||||
SpawnerMobs(Box<SpawnerMobs>),
|
||||
Pick(Box<Pick>),
|
||||
CountCap(Box<CountCap>),
|
||||
/// `horizon:block_swap` (Lithostitched's `block_swap`): blocks by name
|
||||
/// become others, keeping the properties both have.
|
||||
/// `lithostitched:block_swap`: blocks by name become others, keeping
|
||||
/// the properties both have (a block the game lacks is left alone).
|
||||
BlockSwap(std::collections::BTreeMap<String, String>),
|
||||
/// `horizon:apply_random` (Lithostitched's `apply_random`): a processor
|
||||
/// list drawn by weight runs on the block.
|
||||
ApplyRandom(Box<lithostitched::ApplyRandom>),
|
||||
/// `horizon:condition` (Lithostitched's `condition`): `then` or `else`
|
||||
/// `lithostitched:apply_random`: a processor list drawn by weight runs
|
||||
/// on the block.
|
||||
ApplyRandom(Box<litho::ApplyRandom>),
|
||||
/// `lithostitched:reference`: the named lists run on the block in turn.
|
||||
Reference(Vec<ProcessorList>),
|
||||
/// `lithostitched:discard_input`: the world's block stays.
|
||||
DiscardInput,
|
||||
/// `lithostitched:condition`: `then` or `else`
|
||||
/// runs on the block, by a test of it.
|
||||
Condition(Box<lithostitched::Conditional>),
|
||||
/// `horizon:set_block` (Lithostitched's `set_block`).
|
||||
SetBlock(Box<lithostitched::SetBlock>),
|
||||
Condition(Box<litho::Conditional>),
|
||||
/// `lithostitched:set_block`: the block from a state provider.
|
||||
SetBlock(Box<litho::SetBlock>),
|
||||
/// `horizon:pillar` (Repurposed Structures' and Moog's pillars): a
|
||||
/// trigger block starts a column down to the ground.
|
||||
Pillar(Box<pillars::Pillar>),
|
||||
@@ -786,7 +807,7 @@ impl Processor {
|
||||
})?))
|
||||
}
|
||||
"horizon:pick" => Self::Pick(Box::new(Pick::decode(value, state)?)),
|
||||
"horizon:block_swap" => Self::BlockSwap(
|
||||
"lithostitched:block_swap" => Self::BlockSwap(
|
||||
value["blocks"]
|
||||
.as_object()
|
||||
.ok_or(DecodeError::Field("blocks"))?
|
||||
@@ -801,12 +822,16 @@ impl Processor {
|
||||
.collect::<Result<_, DecodeError>>()?,
|
||||
),
|
||||
"horizon:count_cap" => Self::CountCap(Box::new(CountCap::decode(value, state)?)),
|
||||
"horizon:apply_random" => Self::ApplyRandom(Box::new(
|
||||
lithostitched::ApplyRandom::decode(value, resources, bindings)?,
|
||||
"lithostitched:apply_random" => Self::ApplyRandom(Box::new(
|
||||
litho::ApplyRandom::decode(value, resources, bindings)?,
|
||||
)),
|
||||
"horizon:set_block" => {
|
||||
Self::SetBlock(Box::new(lithostitched::SetBlock::decode(value, state)?))
|
||||
"lithostitched:set_block" => {
|
||||
Self::SetBlock(Box::new(litho::SetBlock::decode(value, state)?))
|
||||
}
|
||||
"lithostitched:reference" => {
|
||||
Self::Reference(litho::reference(value, resources, bindings)?)
|
||||
}
|
||||
"lithostitched:discard_input" => Self::DiscardInput,
|
||||
"horizon:pillar" => Self::Pillar(Box::new(pillars::Pillar::decode(
|
||||
value, resources, bindings, state,
|
||||
)?)),
|
||||
@@ -834,7 +859,7 @@ impl Processor {
|
||||
"horizon:keep_world_block" => {
|
||||
Self::KeepWorldBlock(Box::new(sea_level::KeepWorldBlock::decode(value)?))
|
||||
}
|
||||
"horizon:condition" => Self::Condition(Box::new(lithostitched::Conditional::decode(
|
||||
"lithostitched:condition" => Self::Condition(Box::new(litho::Conditional::decode(
|
||||
value, resources, bindings,
|
||||
)?)),
|
||||
_ => return Err(DecodeError::Field("structure processor type")),
|
||||
@@ -1115,24 +1140,10 @@ fn process_block<W: PlacementContext>(
|
||||
if !position_ok {
|
||||
continue;
|
||||
}
|
||||
let nbt = match &rule.modifier {
|
||||
Modifier::Passthrough => info.nbt.clone(),
|
||||
Modifier::Clear => Some(Compound::new()),
|
||||
Modifier::AppendLoot(table) => {
|
||||
let mut nbt = info.nbt.clone().unwrap_or_default();
|
||||
nbt.insert("LootTable".into(), Tag::String(table.as_str().into()));
|
||||
nbt.insert("LootTableSeed".into(), Tag::Long(random.next_long()));
|
||||
Some(nbt)
|
||||
}
|
||||
Modifier::AppendStatic(data) => Some(edited(info.nbt.as_ref(), &[], data)),
|
||||
Modifier::Edit { remove, data } => {
|
||||
Some(edited(info.nbt.as_ref(), remove, data))
|
||||
}
|
||||
};
|
||||
return Ok(Some(BlockInfo {
|
||||
pos: info.pos,
|
||||
state: rule.output,
|
||||
nbt,
|
||||
nbt: rule.modifier.apply(info.nbt.as_ref(), &mut random),
|
||||
}));
|
||||
}
|
||||
Some(info)
|
||||
@@ -1165,8 +1176,7 @@ fn process_block<W: PlacementContext>(
|
||||
false => Some(info),
|
||||
},
|
||||
Processor::ApplyRandom(apply) => {
|
||||
let seed = world.world_seed()?;
|
||||
let mut random = apply.random.create(seed, position, reference, info.pos);
|
||||
let mut random = apply.draws.random(world.world_seed()?, position, info.pos);
|
||||
match apply.pick(&mut random) {
|
||||
Some(list) => {
|
||||
return process_list(
|
||||
@@ -1176,21 +1186,34 @@ fn process_block<W: PlacementContext>(
|
||||
None => Some(info),
|
||||
}
|
||||
}
|
||||
Processor::Reference(lists) => {
|
||||
let mut current = info;
|
||||
for list in lists {
|
||||
match process_list(
|
||||
list, world, resources, position, reference, relative, current, settings,
|
||||
)? {
|
||||
Some(next) => current = next,
|
||||
None => return Ok(None),
|
||||
}
|
||||
}
|
||||
Some(current)
|
||||
}
|
||||
Processor::DiscardInput => None,
|
||||
Processor::SetBlock(set) => {
|
||||
let seed = world.world_seed()?;
|
||||
let state = set.state(seed, position, reference, info.pos);
|
||||
let state = match set.preserves() {
|
||||
let mut random = set.draws.random(world.world_seed()?, position, info.pos);
|
||||
let state = set.state(&mut random);
|
||||
let state = match set.preserve {
|
||||
true => registry
|
||||
.state(state)
|
||||
.and_then(|s| with_properties_of(registry, &s.name, info.state))
|
||||
.unwrap_or(state),
|
||||
false => state,
|
||||
};
|
||||
Some(BlockInfo { state, ..info })
|
||||
let nbt = set.modifier.apply(info.nbt.as_ref(), &mut random);
|
||||
Some(BlockInfo { state, nbt, ..info })
|
||||
}
|
||||
Processor::Condition(c) => {
|
||||
let seed = world.world_seed()?;
|
||||
let mut random = c.random.create(seed, position, reference, info.pos);
|
||||
let mut random = c.draws.random(world.world_seed()?, position, info.pos);
|
||||
let location = world.block_state(info.pos)?;
|
||||
let passed = c
|
||||
.condition
|
||||
|
||||
@@ -0,0 +1,841 @@
|
||||
//! Lithostitched's processors (spec-lithostitched §5), read as its data
|
||||
//! writes them: `lithostitched:block_swap` (in `processor.rs`), `reference`,
|
||||
//! `apply_random`, `condition`, `set_block` and `discard_input`.
|
||||
//!
|
||||
//! The draws of `random_chance`, `apply_random` and `set_block` follow
|
||||
//! Lithostitched's `random_mode` (per block or per piece), but its seeding is
|
||||
//! not known (OPEN-L6): a positional random of the world seed, the block's or
|
||||
//! the piece's position and the processor itself, so the shares match while
|
||||
//! the blocks chosen do not.
|
||||
use super::{BlockStateId, DecodeError, Modifier, ProcessorList, ProcessorRandom};
|
||||
use crate::worldgen::{block_feature::FeatureBlockBindings, structure::StructureResources};
|
||||
use horizon_core::{
|
||||
position::BlockPos,
|
||||
random::{LegacyRandom, position_seed},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// `random_mode` (`condition`, `set_block`) or `mode` (`apply_random`).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct Draws {
|
||||
per_piece: bool,
|
||||
/// The processor's own salt, so two processors on a block draw apart.
|
||||
salt: i64,
|
||||
}
|
||||
impl Draws {
|
||||
fn decode(mode: Option<&Value>, processor: &Value) -> Result<Self, DecodeError> {
|
||||
let per_piece = match mode.and_then(Value::as_str) {
|
||||
None | Some("per_block") => false,
|
||||
Some("per_piece") => true,
|
||||
_ => return Err(DecodeError::Field("random_mode")),
|
||||
};
|
||||
// FNV-1a over the processor's JSON: the same in every run.
|
||||
let salt = (processor.to_string().bytes()).fold(0xcbf2_9ce4_8422_2325_u64, |h, b| {
|
||||
(h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)
|
||||
}) as i64;
|
||||
Ok(Self { per_piece, salt })
|
||||
}
|
||||
/// The random for the block at `pos` of the piece at `piece`.
|
||||
pub(super) fn random(self, world_seed: i64, piece: [i32; 3], pos: [i32; 3]) -> LegacyRandom {
|
||||
let [x, y, z] = if self.per_piece { piece } else { pos };
|
||||
LegacyRandom::new(world_seed ^ self.salt ^ position_seed(BlockPos::new(x, y, z)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A processor or a list of them (`then`, `else`, `apply_random`'s data):
|
||||
/// a list holder (`ProcessorList::decode`), or one processor object.
|
||||
fn processors(
|
||||
value: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<ProcessorList, DecodeError> {
|
||||
let one = value.get("processor_type").is_some();
|
||||
let holder = if one {
|
||||
Value::Array(vec![value.clone()])
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
ProcessorList::decode(&holder.to_string(), resources, bindings)
|
||||
}
|
||||
|
||||
/// `reference`: the named lists, run in turn on the block.
|
||||
pub(super) fn reference(
|
||||
value: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<Vec<ProcessorList>, DecodeError> {
|
||||
let names = match &value["processor_lists"] {
|
||||
Value::Array(a) => a.clone(),
|
||||
other => vec![other.clone()],
|
||||
};
|
||||
(names.iter())
|
||||
.map(|name| match name.as_str() {
|
||||
Some(n) if n.starts_with('#') => Err(DecodeError::Field("processor list tag")),
|
||||
Some(_) => ProcessorList::decode(&name.to_string(), resources, bindings),
|
||||
None => Err(DecodeError::Field("processor_lists")),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `apply_random`: one list drawn by weight runs on the block.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApplyRandom {
|
||||
pub(super) draws: Draws,
|
||||
lists: Vec<(i32, ProcessorList)>,
|
||||
total: i32,
|
||||
}
|
||||
impl ApplyRandom {
|
||||
pub(super) fn decode(
|
||||
value: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let entries = value["processor_lists"]
|
||||
.as_array()
|
||||
.ok_or(DecodeError::Field("apply_random processor_lists"))?;
|
||||
let lists = (entries.iter())
|
||||
.map(|e| match e.get("data") {
|
||||
Some(data) => {
|
||||
let weight = e["weight"]
|
||||
.as_i64()
|
||||
.unwrap_or(1)
|
||||
.clamp(0, i64::from(i32::MAX));
|
||||
Ok((weight as i32, processors(data, resources, bindings)?))
|
||||
}
|
||||
None => Ok((1, processors(e, resources, bindings)?)),
|
||||
})
|
||||
.collect::<Result<Vec<_>, DecodeError>>()?;
|
||||
let total = lists
|
||||
.iter()
|
||||
.map(|(w, _)| *w)
|
||||
.fold(0i32, i32::saturating_add);
|
||||
Ok(Self {
|
||||
draws: Draws::decode(value.get("mode"), value)?,
|
||||
lists,
|
||||
total,
|
||||
})
|
||||
}
|
||||
/// The list drawn: one bounded draw over the total weight.
|
||||
pub(super) fn pick(&self, random: &mut impl ProcessorRandom) -> Option<&ProcessorList> {
|
||||
if self.total <= 0 {
|
||||
return None;
|
||||
}
|
||||
let mut at = random.int(self.total);
|
||||
for (weight, list) in &self.lists {
|
||||
at -= weight;
|
||||
if at < 0 {
|
||||
return Some(list);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Lithostitched's processor conditions.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum Condition {
|
||||
True,
|
||||
AllOf(Vec<Condition>),
|
||||
AnyOf(Vec<Condition>),
|
||||
Not(Box<Condition>),
|
||||
Chance(f32),
|
||||
/// `matching_blocks`: the template's block (`input`) or the world's
|
||||
/// (`location`) is one of `blocks`, with `properties`.
|
||||
Matching {
|
||||
blocks: BTreeSet<String>,
|
||||
properties: Vec<(String, String)>,
|
||||
location: bool,
|
||||
},
|
||||
}
|
||||
impl Condition {
|
||||
/// One condition, or a list of them that must all pass (`if_true`).
|
||||
pub(super) fn decode(
|
||||
value: &Value,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let list = |v: &Value| -> Result<Vec<Self>, DecodeError> {
|
||||
(v.as_array().ok_or(DecodeError::Field("conditions"))?.iter())
|
||||
.map(|c| Self::decode(c, bindings))
|
||||
.collect()
|
||||
};
|
||||
if value.is_array() {
|
||||
return Ok(Self::AllOf(list(value)?));
|
||||
}
|
||||
let kind = value["type"].as_str().unwrap_or_default();
|
||||
Ok(match crate::worldgen::tags::identifier(kind)?.as_str() {
|
||||
"lithostitched:true" => Self::True,
|
||||
"lithostitched:all_of" => Self::AllOf(list(&value["conditions"])?),
|
||||
"lithostitched:any_of" => Self::AnyOf(list(&value["conditions"])?),
|
||||
"lithostitched:not" => {
|
||||
Self::Not(Box::new(Self::decode(&value["condition"], bindings)?))
|
||||
}
|
||||
"lithostitched:random_chance" => Self::Chance(
|
||||
value["chance"]
|
||||
.as_f64()
|
||||
.ok_or(DecodeError::Field("random_chance chance"))? as f32,
|
||||
),
|
||||
"lithostitched:matching_blocks" => Self::Matching {
|
||||
blocks: blocks(&value["blocks"], bindings)?,
|
||||
properties: (value
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.into_iter())
|
||||
.flatten()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.clone(),
|
||||
v.as_str().map_or_else(|| v.to_string(), str::to_owned),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
location: match value.get("match_type").and_then(Value::as_str) {
|
||||
None | Some("input") => false,
|
||||
Some("location") => true,
|
||||
_ => return Err(DecodeError::Field("matching_blocks match_type")),
|
||||
},
|
||||
},
|
||||
_ => return Err(DecodeError::Field("lithostitched condition type")),
|
||||
})
|
||||
}
|
||||
pub(super) fn test(
|
||||
&self,
|
||||
input: BlockStateId,
|
||||
location: BlockStateId,
|
||||
registry: &crate::registry::BlockRegistry,
|
||||
random: &mut impl ProcessorRandom,
|
||||
) -> bool {
|
||||
match self {
|
||||
Self::True => true,
|
||||
Self::AllOf(all) => all
|
||||
.iter()
|
||||
.all(|c| c.test(input, location, registry, random)),
|
||||
Self::AnyOf(any) => any
|
||||
.iter()
|
||||
.any(|c| c.test(input, location, registry, random)),
|
||||
Self::Not(c) => !c.test(input, location, registry, random),
|
||||
Self::Chance(chance) => random.float() < *chance,
|
||||
Self::Matching {
|
||||
blocks,
|
||||
properties,
|
||||
location: at_location,
|
||||
} => {
|
||||
let Some(state) = registry.state(if *at_location { location } else { input })
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
blocks.contains(&state.name)
|
||||
&& (properties.iter()).all(|(k, v)| state.properties.get(k) == Some(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A block set (an id, a list, `#tag`) by block name. Blocks the game does
|
||||
/// not have are left out: no template block can be one.
|
||||
fn blocks(
|
||||
value: &Value,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<BTreeSet<String>, DecodeError> {
|
||||
let names: Vec<&str> = match value {
|
||||
Value::String(s) => vec![s.as_str()],
|
||||
Value::Array(a) => a.iter().filter_map(Value::as_str).collect(),
|
||||
_ => return Err(DecodeError::Field("matching_blocks blocks")),
|
||||
};
|
||||
let registry = bindings.registry();
|
||||
let mut out = BTreeSet::new();
|
||||
for name in names {
|
||||
match name.strip_prefix('#') {
|
||||
Some(tag) => {
|
||||
let tag = crate::worldgen::tags::identifier(tag)?;
|
||||
let states = bindings.tags.get(&tag).into_iter().flat_map(|s| s.iter());
|
||||
out.extend(states.filter_map(|s| Some(registry.state(*s)?.name.clone())));
|
||||
}
|
||||
None => {
|
||||
out.insert(crate::worldgen::tags::identifier(name)?.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `condition`: `then` when the conditions pass, else `else` (none: the
|
||||
/// block as it is).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Conditional {
|
||||
pub(super) draws: Draws,
|
||||
pub(super) condition: Condition,
|
||||
pub(super) then: ProcessorList,
|
||||
pub(super) otherwise: ProcessorList,
|
||||
}
|
||||
impl Conditional {
|
||||
pub(super) fn decode(
|
||||
value: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let branch = |key| match value.get(key) {
|
||||
Some(v) => processors(v, resources, bindings),
|
||||
None => Ok(ProcessorList(Vec::new())),
|
||||
};
|
||||
Ok(Self {
|
||||
draws: Draws::decode(value.get("random_mode"), value)?,
|
||||
condition: Condition::decode(&value["if_true"], bindings)?,
|
||||
then: branch("then")?,
|
||||
otherwise: branch("else")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `set_block`: the block from a state provider (simple or weighted).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SetBlock {
|
||||
pub(super) draws: Draws,
|
||||
states: Vec<(i32, BlockStateId)>,
|
||||
total: i32,
|
||||
/// `preserve_state` (true by default): the template block's properties
|
||||
/// the new block has too are kept.
|
||||
pub(super) preserve: bool,
|
||||
pub(super) modifier: Modifier,
|
||||
}
|
||||
impl SetBlock {
|
||||
pub(super) fn decode(
|
||||
value: &Value,
|
||||
state: impl Fn(&Value) -> Result<BlockStateId, DecodeError>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let provider = &value["state_provider"];
|
||||
let kind = provider["type"].as_str().unwrap_or_default();
|
||||
let states = match crate::worldgen::tags::identifier(kind)?.as_str() {
|
||||
"minecraft:simple_state_provider" => vec![(1, state(&provider["state"])?)],
|
||||
"minecraft:weighted_state_provider" => (provider["entries"].as_array().into_iter())
|
||||
.flatten()
|
||||
.map(|e| {
|
||||
Ok((
|
||||
e["weight"].as_i64().unwrap_or(1).clamp(0, 1 << 30) as i32,
|
||||
state(&e["data"])?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<_, DecodeError>>()?,
|
||||
_ => return Err(DecodeError::Field("set_block state_provider")),
|
||||
};
|
||||
let total = states.iter().map(|(w, _)| *w).sum();
|
||||
if total <= 0 {
|
||||
return Err(DecodeError::Field("set_block state_provider"));
|
||||
}
|
||||
Ok(Self {
|
||||
draws: Draws::decode(value.get("random_mode"), value)?,
|
||||
states,
|
||||
total,
|
||||
preserve: value
|
||||
.get("preserve_state")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true),
|
||||
modifier: Modifier::decode(value.get("block_entity_modifier"))?,
|
||||
})
|
||||
}
|
||||
/// The provider's state: WeightedList.getRandom, one bounded draw.
|
||||
pub(super) fn state(&self, random: &mut impl ProcessorRandom) -> BlockStateId {
|
||||
let mut at = random.int(self.total);
|
||||
for (weight, state) in &self.states {
|
||||
at -= weight;
|
||||
if at < 0 {
|
||||
return *state;
|
||||
}
|
||||
}
|
||||
self.states[0].1
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::{BlockInfo, PlaceSettings, ProcessorList, process_list};
|
||||
use crate::{
|
||||
heightmap::HeightmapType,
|
||||
nbt::{JavaString, Tag},
|
||||
registry::{BlockRegistry, BlockStateId},
|
||||
worldgen::{
|
||||
anchor::WorldGenerationContext,
|
||||
block_feature::FeatureBlockBindings,
|
||||
placement::{PlacementContext, PlacementQueryError},
|
||||
structure::{
|
||||
Rotation, StateTransforms, StructureResources, template::parse_state_string,
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Every combination of `properties`' values, the first one the default.
|
||||
fn states(name: &str, properties: &[(&str, &[&str])]) -> Vec<Value> {
|
||||
let mut out = vec![serde_json::Map::new()];
|
||||
for (key, values) in properties {
|
||||
out = (out.iter())
|
||||
.flat_map(|o| {
|
||||
values.iter().map(move |v| {
|
||||
let mut o = o.clone();
|
||||
o.insert((*key).into(), (*v).into());
|
||||
o
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
(out.into_iter())
|
||||
.map(|p| json!([name, name.ends_with("air"), false, "", {"id": name, "properties": p}]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A registry of the blocks CTOV's lists and the probe pack use.
|
||||
fn registry() -> &'static BlockRegistry {
|
||||
let ages: Vec<String> = (0..8).map(|a| a.to_string()).collect();
|
||||
let ages: Vec<&str> = ages.iter().map(String::as_str).collect();
|
||||
let levels: Vec<String> = (0..16).map(|a| a.to_string()).collect();
|
||||
let levels: Vec<&str> = levels.iter().map(String::as_str).collect();
|
||||
let water = [("level", &levels[..])];
|
||||
let stairs: &[(&str, &[&str])] =
|
||||
&[("facing", &["north", "east"]), ("half", &["bottom", "top"])];
|
||||
let blocks: Vec<(&str, &[(&str, &[&str])])> = vec![
|
||||
("minecraft:air", &[]),
|
||||
("minecraft:cave_air", &[]),
|
||||
("minecraft:void_air", &[]),
|
||||
("minecraft:lava", &water),
|
||||
("minecraft:cobblestone", &[]),
|
||||
("minecraft:blackstone", &[]),
|
||||
("minecraft:deepslate", &[]),
|
||||
("minecraft:water", &water),
|
||||
("minecraft:spruce_planks", &[]),
|
||||
("minecraft:chest", &[]),
|
||||
("minecraft:oak_log", &[("axis", &["y", "x"])]),
|
||||
("minecraft:stripped_cherry_log", &[("axis", &["y", "x"])]),
|
||||
("minecraft:oak_stairs", stairs),
|
||||
("minecraft:warped_stairs", stairs),
|
||||
];
|
||||
let wheat = [("age", &ages[..])];
|
||||
let beetroots = [("age", &ages[..4])];
|
||||
let mut rows = Vec::new();
|
||||
let mut defaults = BTreeMap::new();
|
||||
let crops: [(&str, &[(&str, &[&str])]); 3] = [
|
||||
("minecraft:wheat", &wheat),
|
||||
("minecraft:carrots", &wheat),
|
||||
("minecraft:beetroots", &beetroots),
|
||||
];
|
||||
for (name, properties) in blocks.into_iter().chain(crops) {
|
||||
defaults.insert(name, rows.len());
|
||||
rows.extend(states(name, properties));
|
||||
}
|
||||
let facts = json!({"states": rows, "defaults": defaults});
|
||||
let registry = BlockRegistry::from_facts_json(facts.to_string().as_bytes()).unwrap();
|
||||
Box::leak(Box::new(registry))
|
||||
}
|
||||
|
||||
/// The world a piece is placed in: one block everywhere.
|
||||
struct World {
|
||||
seed: i64,
|
||||
block: BlockStateId,
|
||||
}
|
||||
impl PlacementContext for World {
|
||||
fn world_seed(&mut self) -> Result<i64, PlacementQueryError> {
|
||||
Ok(self.seed)
|
||||
}
|
||||
fn block_state(&mut self, _: [i32; 3]) -> Result<BlockStateId, PlacementQueryError> {
|
||||
Ok(self.block)
|
||||
}
|
||||
fn generation(&self) -> WorldGenerationContext {
|
||||
WorldGenerationContext::new(-64, 384, 63)
|
||||
}
|
||||
fn min_y(&mut self) -> Result<i32, PlacementQueryError> {
|
||||
Ok(-64)
|
||||
}
|
||||
fn height(&mut self, _: HeightmapType, _: i32, _: i32) -> Result<i32, PlacementQueryError> {
|
||||
Err(PlacementQueryError::Unavailable)
|
||||
}
|
||||
}
|
||||
|
||||
struct Lists {
|
||||
registry: &'static BlockRegistry,
|
||||
resources: StructureResources,
|
||||
}
|
||||
impl Lists {
|
||||
fn new(lists: &[(&str, Value)]) -> Self {
|
||||
let registry = registry();
|
||||
let lists = (lists.iter())
|
||||
.map(|(id, list)| ((*id).to_owned(), list.to_string()))
|
||||
.collect();
|
||||
let resources = StructureResources::new(
|
||||
registry,
|
||||
StateTransforms::default(),
|
||||
std::path::Path::new("/nonexistent"),
|
||||
lists,
|
||||
&BTreeMap::new(),
|
||||
);
|
||||
Self {
|
||||
registry,
|
||||
resources,
|
||||
}
|
||||
}
|
||||
fn id(&self, state: &str) -> BlockStateId {
|
||||
parse_state_string(self.registry, state).unwrap()
|
||||
}
|
||||
fn decode(&self, processors: Value) -> Result<ProcessorList, crate::worldgen::DecodeError> {
|
||||
let bindings = FeatureBlockBindings::new(self.registry, BTreeSet::new()).unwrap();
|
||||
ProcessorList::decode(&processors.to_string(), &self.resources, &bindings)
|
||||
}
|
||||
/// The block the list makes of template block `state` at `pos` of the
|
||||
/// piece at `piece`, in a world of `world`: None when it is not placed.
|
||||
fn run(
|
||||
&self,
|
||||
list: &ProcessorList,
|
||||
world: (i64, &str),
|
||||
piece: [i32; 3],
|
||||
pos: [i32; 3],
|
||||
state: &str,
|
||||
) -> Option<BlockInfo> {
|
||||
let mut world = World {
|
||||
seed: world.0,
|
||||
block: self.id(world.1),
|
||||
};
|
||||
let info = BlockInfo {
|
||||
pos,
|
||||
state: self.id(state),
|
||||
nbt: None,
|
||||
};
|
||||
let mut settings = PlaceSettings::new(Rotation::None);
|
||||
process_list(
|
||||
list,
|
||||
&mut world,
|
||||
&self.resources,
|
||||
piece,
|
||||
piece,
|
||||
[0; 3],
|
||||
info,
|
||||
&mut settings,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
fn name(&self, info: Option<BlockInfo>) -> String {
|
||||
match info {
|
||||
None => "none".into(),
|
||||
Some(info) => {
|
||||
let s = self.registry.state(info.state).unwrap();
|
||||
let props: Vec<String> = s
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}={v}"))
|
||||
.collect();
|
||||
match props.is_empty() {
|
||||
true => s.name.clone(),
|
||||
false => format!("{}[{}]", s.name, props.join(",")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `name(run(...))` for one template block in an air world of seed 0.
|
||||
fn out(&self, list: &ProcessorList, state: &str) -> String {
|
||||
self.name(self.run(list, (0, "minecraft:air"), [0; 3], [0; 3], state))
|
||||
}
|
||||
}
|
||||
|
||||
fn swap(blocks: Value) -> Value {
|
||||
json!({"processor_type": "lithostitched:block_swap", "blocks": blocks})
|
||||
}
|
||||
fn set_block(provider: Value) -> Value {
|
||||
json!({"processor_type": "lithostitched:set_block", "state_provider": provider})
|
||||
}
|
||||
fn simple(name: &str) -> Value {
|
||||
json!({"type": "minecraft:simple_state_provider", "state": {"id": name}})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_swap_keeps_the_properties_the_new_block_can_have() {
|
||||
let t = Lists::new(&[]);
|
||||
let list = t
|
||||
.decode(json!([swap(json!({
|
||||
"minecraft:wheat": "minecraft:carrots",
|
||||
"oak_stairs": "minecraft:warped_stairs",
|
||||
"minecraft:cobblestone": "natures_spirit:missing"}))]))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t.out(&list, "minecraft:wheat[age=7]"),
|
||||
"minecraft:carrots[age=7]"
|
||||
);
|
||||
assert_eq!(
|
||||
t.out(&list, "minecraft:oak_stairs[facing=east,half=top]"),
|
||||
"minecraft:warped_stairs[facing=east,half=top]"
|
||||
);
|
||||
// A block the game lacks leaves the template's; others pass.
|
||||
assert_eq!(
|
||||
t.out(&list, "minecraft:cobblestone"),
|
||||
"minecraft:cobblestone"
|
||||
);
|
||||
assert_eq!(
|
||||
t.out(&list, "minecraft:oak_log[axis=x]"),
|
||||
"minecraft:oak_log[axis=x]"
|
||||
);
|
||||
// Beetroots' age stops at 3: ripe wheat gives the default (spec-ctov §4).
|
||||
let beet = t
|
||||
.decode(json!([swap(
|
||||
json!({"minecraft:wheat": "minecraft:beetroots"})
|
||||
)]))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t.out(&beet, "minecraft:wheat[age=7]"),
|
||||
"minecraft:beetroots[age=0]"
|
||||
);
|
||||
assert_eq!(
|
||||
t.out(&beet, "minecraft:wheat[age=2]"),
|
||||
"minecraft:beetroots[age=2]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_run_the_named_lists_in_turn() {
|
||||
let t = Lists::new(&[
|
||||
(
|
||||
"t:logs",
|
||||
json!({"processors": [swap(json!({"minecraft:oak_log": "minecraft:stripped_cherry_log"}))]}),
|
||||
),
|
||||
(
|
||||
"t:stone",
|
||||
json!([swap(
|
||||
json!({"minecraft:stripped_cherry_log": "minecraft:cobblestone"})
|
||||
)]),
|
||||
),
|
||||
(
|
||||
"t:discard",
|
||||
json!([{"processor_type": "lithostitched:discard_input"}]),
|
||||
),
|
||||
]);
|
||||
let reference = |lists: Value| json!([{"processor_type": "lithostitched:reference", "processor_lists": lists}]);
|
||||
let one = t.decode(reference(json!("t:logs"))).unwrap();
|
||||
assert_eq!(
|
||||
t.out(&one, "minecraft:oak_log[axis=x]"),
|
||||
"minecraft:stripped_cherry_log[axis=x]"
|
||||
);
|
||||
let both = t.decode(reference(json!(["t:logs", "t:stone"]))).unwrap();
|
||||
assert_eq!(t.out(&both, "minecraft:oak_log"), "minecraft:cobblestone");
|
||||
let reversed = t.decode(reference(json!(["t:stone", "t:logs"]))).unwrap();
|
||||
assert_eq!(
|
||||
t.out(&reversed, "minecraft:oak_log"),
|
||||
"minecraft:stripped_cherry_log[axis=y]"
|
||||
);
|
||||
// discard_input: the template block is not placed, and the lists after it do not run.
|
||||
let discard = t.decode(reference(json!(["t:discard", "t:logs"]))).unwrap();
|
||||
assert_eq!(t.out(&discard, "minecraft:oak_log"), "none");
|
||||
// The game has no processor list tags; unknown lists are errors.
|
||||
assert!(t.decode(reference(json!(["#t:palettes"]))).is_err());
|
||||
assert!(t.decode(reference(json!("t:missing"))).is_err());
|
||||
}
|
||||
|
||||
/// The probe pack's nested condition (spec-lithostitched §7.3): template
|
||||
/// cobblestone becomes blackstone or deepslate, half each.
|
||||
#[test]
|
||||
fn conditions_run_then_or_else() {
|
||||
let t = Lists::new(&[]);
|
||||
let inner = json!({"processor_type": "lithostitched:condition", "random_mode": "per_block",
|
||||
"if_true": {"type": "lithostitched:random_chance", "chance": 0.5},
|
||||
"then": set_block(simple("minecraft:blackstone")),
|
||||
"else": [set_block(simple("minecraft:deepslate"))]});
|
||||
let list = t
|
||||
.decode(json!([{"processor_type": "lithostitched:condition",
|
||||
"if_true": [{"type": "lithostitched:matching_blocks", "blocks": "minecraft:cobblestone"}],
|
||||
"then": [inner]}]))
|
||||
.unwrap();
|
||||
let mut counts = BTreeMap::<String, u32>::new();
|
||||
for i in 0..4000 {
|
||||
let out = t.run(
|
||||
&list,
|
||||
(1234, "minecraft:air"),
|
||||
[0; 3],
|
||||
[i % 64, i / 64, 7],
|
||||
"minecraft:cobblestone",
|
||||
);
|
||||
*counts.entry(t.name(out)).or_default() += 1;
|
||||
}
|
||||
assert_eq!(counts.len(), 2, "{counts:?}");
|
||||
assert!(
|
||||
(1850..2150).contains(&counts["minecraft:blackstone"]),
|
||||
"{counts:?}"
|
||||
);
|
||||
// Not cobblestone, and no `else`: the block as it is.
|
||||
assert_eq!(
|
||||
t.out(&list, "minecraft:oak_log"),
|
||||
"minecraft:oak_log[axis=y]"
|
||||
);
|
||||
|
||||
// `location` tests the world's block, with properties on the input.
|
||||
let water = t
|
||||
.decode(
|
||||
json!([{"processor_type": "lithostitched:condition", "random_mode": "per_piece",
|
||||
"if_true": {"type": "lithostitched:matching_blocks", "blocks": ["minecraft:water"],
|
||||
"match_type": "location"},
|
||||
"then": set_block(simple("minecraft:spruce_planks"))}]),
|
||||
)
|
||||
.unwrap();
|
||||
let at = |world| {
|
||||
t.name(t.run(
|
||||
&water,
|
||||
(0, world),
|
||||
[0; 3],
|
||||
[1, 2, 3],
|
||||
"minecraft:cobblestone",
|
||||
))
|
||||
};
|
||||
assert_eq!(at("minecraft:water"), "minecraft:spruce_planks");
|
||||
assert_eq!(at("minecraft:air"), "minecraft:cobblestone");
|
||||
let ripe = t
|
||||
.decode(json!([{"processor_type": "lithostitched:condition",
|
||||
"if_true": {"type": "lithostitched:all_of", "conditions": [
|
||||
{"type": "lithostitched:true"},
|
||||
{"type": "lithostitched:matching_blocks", "blocks": "minecraft:wheat", "properties": {"age": "7"}},
|
||||
{"type": "lithostitched:not", "condition": {"type": "lithostitched:any_of", "conditions": [
|
||||
{"type": "lithostitched:random_chance", "chance": 0.0},
|
||||
{"type": "lithostitched:matching_blocks", "blocks": "minecraft:water", "match_type": "location"}]}}]},
|
||||
"then": swap(json!({"minecraft:wheat": "minecraft:carrots"}))}]))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
t.out(&ripe, "minecraft:wheat[age=7]"),
|
||||
"minecraft:carrots[age=7]"
|
||||
);
|
||||
assert_eq!(
|
||||
t.out(&ripe, "minecraft:wheat[age=6]"),
|
||||
"minecraft:wheat[age=6]"
|
||||
);
|
||||
let bad = json!([{"processor_type": "lithostitched:condition", "random_mode": "per_chunk",
|
||||
"if_true": {"type": "lithostitched:true"}}]);
|
||||
assert!(t.decode(bad).is_err());
|
||||
}
|
||||
|
||||
/// `apply_random` (spec-lithostitched §7.3: crimson 1 : birch 3, one
|
||||
/// choice per piece): here blackstone 1 : deepslate 3.
|
||||
#[test]
|
||||
fn apply_random_draws_per_piece_or_per_block() {
|
||||
let t = Lists::new(&[(
|
||||
"t:deepslate",
|
||||
json!([swap(
|
||||
json!({"minecraft:cobblestone": "minecraft:deepslate"})
|
||||
)]),
|
||||
)]);
|
||||
let random = |mode: &str| {
|
||||
t.decode(json!([{"processor_type": "lithostitched:apply_random", "mode": mode,
|
||||
"processor_lists": [
|
||||
{"weight": 1, "data": [swap(json!({"minecraft:cobblestone": "minecraft:blackstone"}))]},
|
||||
{"weight": 3, "data": "t:deepslate"}]}]))
|
||||
.unwrap()
|
||||
};
|
||||
let (per_piece, per_block) = (random("per_piece"), random("per_block"));
|
||||
let outcomes = |list: &ProcessorList, seed: i64, piece: [i32; 3]| -> Vec<String> {
|
||||
(0..20)
|
||||
.map(|i| {
|
||||
let pos = [piece[0] + i, piece[1], piece[2] + 3 * i];
|
||||
t.name(t.run(
|
||||
list,
|
||||
(seed, "minecraft:air"),
|
||||
piece,
|
||||
pos,
|
||||
"minecraft:cobblestone",
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let mut blackstone_pieces = 0;
|
||||
for p in 0..400 {
|
||||
let piece = [16 * p, 70, -40 * p];
|
||||
let blocks = outcomes(&per_piece, 1234, piece);
|
||||
assert!(blocks.iter().all(|b| *b == blocks[0]), "{blocks:?}");
|
||||
blackstone_pieces += u32::from(blocks[0] == "minecraft:blackstone");
|
||||
// The same piece and seed give the same blocks.
|
||||
assert_eq!(outcomes(&per_piece, 1234, piece), blocks);
|
||||
}
|
||||
assert!(
|
||||
(70..130).contains(&blackstone_pieces),
|
||||
"{blackstone_pieces}"
|
||||
);
|
||||
// Per block, one piece mixes both.
|
||||
let mixed: BTreeSet<String> = (0..10)
|
||||
.flat_map(|p| outcomes(&per_block, 1234, [p, 0, 0]))
|
||||
.collect();
|
||||
assert_eq!(mixed.len(), 2, "{mixed:?}");
|
||||
// A plain list: every entry weight 1. An entry list holding nothing is a no-op.
|
||||
let plain = t
|
||||
.decode(
|
||||
json!([{"processor_type": "lithostitched:apply_random", "mode": "per_block",
|
||||
"processor_lists": ["t:deepslate", []]}]),
|
||||
)
|
||||
.unwrap();
|
||||
let seen: BTreeSet<String> = (0..10)
|
||||
.flat_map(|p| outcomes(&plain, 9, [p, 0, 0]))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
seen,
|
||||
BTreeSet::from(["minecraft:cobblestone".into(), "minecraft:deepslate".into()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_block_runs_its_provider() {
|
||||
let t = Lists::new(&[]);
|
||||
let stairs = "minecraft:oak_stairs[facing=east,half=top]";
|
||||
let warped = |preserve: Value| {
|
||||
let mut p = set_block(simple("minecraft:warped_stairs"));
|
||||
if !preserve.is_null() {
|
||||
p["preserve_state"] = preserve;
|
||||
}
|
||||
t.decode(json!([p])).unwrap()
|
||||
};
|
||||
// preserve_state (true by default) keeps the properties both blocks have.
|
||||
assert_eq!(
|
||||
t.out(&warped(Value::Null), stairs),
|
||||
"minecraft:warped_stairs[facing=east,half=top]"
|
||||
);
|
||||
assert_eq!(
|
||||
t.out(&warped(json!(false)), stairs),
|
||||
"minecraft:warped_stairs[facing=north,half=bottom]"
|
||||
);
|
||||
|
||||
let weighted = t
|
||||
.decode(json!([set_block(
|
||||
json!({"type": "minecraft:weighted_state_provider", "entries": [
|
||||
{"weight": 1, "data": {"id": "minecraft:blackstone"}},
|
||||
{"weight": 3, "data": {"id": "minecraft:deepslate"}}]})
|
||||
)]))
|
||||
.unwrap();
|
||||
let blackstone = (0..2000)
|
||||
.filter(|i| {
|
||||
let out = t.run(
|
||||
&weighted,
|
||||
(42, "minecraft:air"),
|
||||
[0; 3],
|
||||
[*i, 0, -*i],
|
||||
"minecraft:cobblestone",
|
||||
);
|
||||
t.name(out) == "minecraft:blackstone"
|
||||
})
|
||||
.count();
|
||||
assert!((420..580).contains(&blackstone), "{blackstone}");
|
||||
|
||||
// block_entity_modifier gives the output its block entity.
|
||||
let mut loot = set_block(simple("minecraft:chest"));
|
||||
loot["block_entity_modifier"] =
|
||||
json!({"type": "minecraft:append_loot", "loot_table": "t:chests/house"});
|
||||
let out = t
|
||||
.run(
|
||||
&t.decode(json!([loot])).unwrap(),
|
||||
(0, "minecraft:air"),
|
||||
[0; 3],
|
||||
[0; 3],
|
||||
"minecraft:air",
|
||||
)
|
||||
.unwrap();
|
||||
let table = out
|
||||
.nbt
|
||||
.as_ref()
|
||||
.and_then(|n| n.get(&JavaString::from("LootTable")));
|
||||
assert_eq!(table, Some(&Tag::String("t:chests/house".into())));
|
||||
|
||||
// Providers the game does not run are refused.
|
||||
assert!(
|
||||
t.decode(json!([set_block(
|
||||
json!({"type": "minecraft:noise_provider"})
|
||||
)]))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
//! Lithostitched's processors that pick processors, carried as data
|
||||
//! (`horizon:apply_random` and `horizon:condition`): one runs a processor
|
||||
//! list drawn by weight, the other one list or another by a test of the
|
||||
//! block. Both draw from Lithostitched's RandomSettings: a random seeded from
|
||||
//! the world seed and a name, at the block, the piece or the structure.
|
||||
//! `horizon:set_block` is its `set_block`, which the conditions mostly run.
|
||||
use super::{Processor, ProcessorList};
|
||||
use crate::{
|
||||
registry::{BlockRegistry, BlockStateId},
|
||||
worldgen::{DecodeError, block_feature::FeatureBlockBindings, structure::StructureResources},
|
||||
};
|
||||
use horizon_core::{
|
||||
position::BlockPos,
|
||||
random::{LegacyRandom, PositionalRandom, RandomSource},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
/// RandomSettings: where the random is taken, and the name salting it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RandomSettings {
|
||||
mode: Mode,
|
||||
/// ResourceLocation.hashCode of the name (`lithostitched:default`).
|
||||
salt: i32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Mode {
|
||||
Block,
|
||||
Piece,
|
||||
Structure,
|
||||
}
|
||||
impl RandomSettings {
|
||||
/// A mode alone, or `{"mode": ..., "name": ...}`; per block when unset.
|
||||
fn decode(v: Option<&Value>) -> Result<Self, DecodeError> {
|
||||
let mode = Mode::decode;
|
||||
let (mode, name) = match v {
|
||||
None => (Mode::Block, "lithostitched:default"),
|
||||
Some(Value::String(m)) => (mode(Some(m))?, "lithostitched:default"),
|
||||
Some(o) => (
|
||||
mode(o["mode"].as_str())?,
|
||||
o["name"]
|
||||
.as_str()
|
||||
.ok_or(DecodeError::Field("random name"))?,
|
||||
),
|
||||
};
|
||||
let (namespace, path) = name.split_once(':').unwrap_or(("minecraft", name));
|
||||
Ok(Self {
|
||||
mode,
|
||||
salt: java_hash(namespace)
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(java_hash(path)),
|
||||
})
|
||||
}
|
||||
/// RandomSource.create(seed + hash).forkPositional().at(the chosen
|
||||
/// position): the piece's position, the structure's pivot or the block.
|
||||
pub(super) fn create(
|
||||
&self,
|
||||
seed: i64,
|
||||
piece: [i32; 3],
|
||||
pivot: [i32; 3],
|
||||
block: [i32; 3],
|
||||
) -> LegacyRandom {
|
||||
let [x, y, z] = self.mode.select(piece, pivot, block);
|
||||
LegacyRandom::new(seed.wrapping_add(i64::from(self.salt)))
|
||||
.fork_positional()
|
||||
.at(BlockPos::new(x, y, z))
|
||||
}
|
||||
}
|
||||
impl Mode {
|
||||
fn decode(m: Option<&str>) -> Result<Self, DecodeError> {
|
||||
match m {
|
||||
None | Some("per_block") => Ok(Self::Block),
|
||||
Some("per_piece") => Ok(Self::Piece),
|
||||
Some("per_structure") => Ok(Self::Structure),
|
||||
_ => Err(DecodeError::Field("random mode")),
|
||||
}
|
||||
}
|
||||
fn select(self, piece: [i32; 3], pivot: [i32; 3], block: [i32; 3]) -> [i32; 3] {
|
||||
match self {
|
||||
Self::Block => block,
|
||||
Self::Piece => piece,
|
||||
Self::Structure => pivot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SetBlockProcessor with a simple or weighted state provider and no block
|
||||
/// entity change: the block becomes the state, keeping the properties it
|
||||
/// shares with the old one unless `preserve_state` is off.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SetBlock {
|
||||
/// The states and their weights; one state alone is a simple provider,
|
||||
/// which draws nothing.
|
||||
states: Vec<(BlockStateId, i32)>,
|
||||
weighted: bool,
|
||||
preserve: bool,
|
||||
mode: Mode,
|
||||
}
|
||||
impl SetBlock {
|
||||
pub(super) fn decode(
|
||||
v: &Value,
|
||||
state: impl Fn(&Value) -> Result<BlockStateId, DecodeError>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let provider = &v["state_provider"];
|
||||
let (states, weighted) = match provider["type"].as_str() {
|
||||
Some("minecraft:simple_state_provider") => {
|
||||
(vec![(state(&provider["state"])?, 1)], false)
|
||||
}
|
||||
Some("minecraft:weighted_state_provider") => (
|
||||
provider["entries"]
|
||||
.as_array()
|
||||
.ok_or(DecodeError::Field("set_block entries"))?
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let weight = e["weight"]
|
||||
.as_i64()
|
||||
.ok_or(DecodeError::Field("set_block weight"))?;
|
||||
Ok((state(&e["data"])?, weight as i32))
|
||||
})
|
||||
.collect::<Result<_, DecodeError>>()?,
|
||||
true,
|
||||
),
|
||||
_ => return Err(DecodeError::Field("set_block state_provider")),
|
||||
};
|
||||
if states.is_empty() {
|
||||
return Err(DecodeError::Field("set_block entries"));
|
||||
}
|
||||
Ok(Self {
|
||||
states,
|
||||
weighted,
|
||||
preserve: v["preserve_state"].as_bool().unwrap_or(true),
|
||||
mode: Mode::decode(v["random_mode"].as_str())?,
|
||||
})
|
||||
}
|
||||
/// The state to set, before `preserve`: drawn with
|
||||
/// RandomSource.create(seed).forkPositional().at(the chosen position).
|
||||
pub(super) fn state(
|
||||
&self,
|
||||
seed: i64,
|
||||
piece: [i32; 3],
|
||||
pivot: [i32; 3],
|
||||
block: [i32; 3],
|
||||
) -> BlockStateId {
|
||||
if !self.weighted {
|
||||
return self.states[0].0;
|
||||
}
|
||||
let [x, y, z] = self.mode.select(piece, pivot, block);
|
||||
let mut random = LegacyRandom::new(seed)
|
||||
.fork_positional()
|
||||
.at(BlockPos::new(x, y, z));
|
||||
let total: i32 = self.states.iter().map(|s| s.1).sum();
|
||||
let mut value = random.next_int_bound(total.max(1)).unwrap_or(0);
|
||||
for (state, weight) in &self.states {
|
||||
if value < *weight {
|
||||
return *state;
|
||||
}
|
||||
value -= weight;
|
||||
}
|
||||
self.states[0].0
|
||||
}
|
||||
pub(super) fn preserves(&self) -> bool {
|
||||
self.preserve
|
||||
}
|
||||
}
|
||||
|
||||
/// String.hashCode over UTF-16 units.
|
||||
fn java_hash(s: &str) -> i32 {
|
||||
s.encode_utf16()
|
||||
.fold(0i32, |h, c| h.wrapping_mul(31).wrapping_add(i32::from(c)))
|
||||
}
|
||||
|
||||
fn list(
|
||||
v: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<ProcessorList, DecodeError> {
|
||||
ProcessorList::decode(&v.to_string(), resources, bindings)
|
||||
}
|
||||
|
||||
/// ApplyRandomStructureProcessor: a weighted list of processor lists, one
|
||||
/// drawn per block with `nextInt(total)`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApplyRandom {
|
||||
pub(super) random: RandomSettings,
|
||||
lists: Vec<(ProcessorList, i32)>,
|
||||
total: i32,
|
||||
}
|
||||
impl ApplyRandom {
|
||||
pub(super) fn decode(
|
||||
v: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let lists = v["processor_lists"]
|
||||
.as_array()
|
||||
.ok_or(DecodeError::Field("apply_random processor_lists"))?
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let weight = e["weight"]
|
||||
.as_i64()
|
||||
.ok_or(DecodeError::Field("apply_random weight"))?;
|
||||
Ok((list(&e["data"], resources, bindings)?, weight as i32))
|
||||
})
|
||||
.collect::<Result<Vec<_>, DecodeError>>()?;
|
||||
Ok(Self {
|
||||
random: RandomSettings::decode(v.get("mode"))?,
|
||||
total: lists.iter().map(|l| l.1).sum(),
|
||||
lists,
|
||||
})
|
||||
}
|
||||
/// WeightedList.getRandom: none when the weights add up to 0.
|
||||
pub(super) fn pick(&self, random: &mut impl RandomSource) -> Option<&ProcessorList> {
|
||||
if self.total <= 0 {
|
||||
return None;
|
||||
}
|
||||
let mut value = random.next_int_bound(self.total).unwrap_or(0);
|
||||
for (list, weight) in &self.lists {
|
||||
if value < *weight {
|
||||
return Some(list);
|
||||
}
|
||||
value -= weight;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// ConditionProcessor: `then` when the condition holds for the block, else
|
||||
/// `else`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Conditional {
|
||||
pub(super) random: RandomSettings,
|
||||
pub(super) condition: Condition,
|
||||
pub(super) then: ProcessorList,
|
||||
pub(super) otherwise: ProcessorList,
|
||||
}
|
||||
impl Conditional {
|
||||
pub(super) fn decode(
|
||||
v: &Value,
|
||||
resources: &StructureResources,
|
||||
bindings: &FeatureBlockBindings<'_>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
// A processor alone or a list of them.
|
||||
let processors = |key: &str| -> Result<ProcessorList, DecodeError> {
|
||||
match v.get(key) {
|
||||
None => Ok(ProcessorList::default()),
|
||||
Some(Value::Object(p)) if p.contains_key("processor_type") => Ok(ProcessorList(
|
||||
vec![Processor::decode(&v[key], resources, bindings)?],
|
||||
)),
|
||||
Some(other) => list(other, resources, bindings),
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
random: RandomSettings::decode(v.get("random_mode"))?,
|
||||
condition: Condition::decode(&v["if_true"], bindings)?,
|
||||
then: processors("then")?,
|
||||
otherwise: processors("else")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Lithostitched's processor conditions the game runs.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum Condition {
|
||||
True,
|
||||
AllOf(Vec<Condition>),
|
||||
AnyOf(Vec<Condition>),
|
||||
Not(Box<Condition>),
|
||||
RandomChance(f32),
|
||||
/// The block placed (`input`) or the one in the world (`location`) is
|
||||
/// one of these, with these property values.
|
||||
MatchingBlocks {
|
||||
blocks: Arc<BTreeSet<BlockStateId>>,
|
||||
properties: Vec<(String, String)>,
|
||||
location: bool,
|
||||
},
|
||||
}
|
||||
impl Condition {
|
||||
fn decode(v: &Value, bindings: &FeatureBlockBindings<'_>) -> Result<Self, DecodeError> {
|
||||
let all = |a: &Value| -> Result<Vec<Condition>, DecodeError> {
|
||||
a.as_array()
|
||||
.ok_or(DecodeError::Field("processor conditions"))?
|
||||
.iter()
|
||||
.map(|c| Self::decode(c, bindings))
|
||||
.collect()
|
||||
};
|
||||
if v.is_array() {
|
||||
return Ok(Self::AllOf(all(v)?));
|
||||
}
|
||||
let kind = v["type"].as_str().unwrap_or_default();
|
||||
Ok(match kind.strip_prefix("lithostitched:").unwrap_or(kind) {
|
||||
"true" => Self::True,
|
||||
"all_of" => Self::AllOf(all(&v["conditions"])?),
|
||||
"any_of" => Self::AnyOf(all(&v["conditions"])?),
|
||||
"not" => Self::Not(Box::new(Self::decode(&v["condition"], bindings)?)),
|
||||
"random_chance" => Self::RandomChance(super::float(v, "chance")?),
|
||||
"matching_blocks" => Self::MatchingBlocks {
|
||||
blocks: super::holder_set(&v["blocks"], bindings)?,
|
||||
properties: match v.get("properties") {
|
||||
None => Vec::new(),
|
||||
Some(p) => p
|
||||
.as_object()
|
||||
.ok_or(DecodeError::Field("matching_blocks properties"))?
|
||||
.iter()
|
||||
.map(|(k, x)| {
|
||||
let x = x
|
||||
.as_str()
|
||||
.ok_or(DecodeError::Field("matching_blocks property"))?;
|
||||
Ok((k.clone(), x.to_owned()))
|
||||
})
|
||||
.collect::<Result<_, DecodeError>>()?,
|
||||
},
|
||||
location: match v["match_type"].as_str() {
|
||||
None | Some("input") => false,
|
||||
Some("location") => true,
|
||||
_ => return Err(DecodeError::Field("matching_blocks match_type")),
|
||||
},
|
||||
},
|
||||
_ => return Err(DecodeError::Field("processor condition type")),
|
||||
})
|
||||
}
|
||||
/// ProcessorCondition.test with the block placed and the one there.
|
||||
pub(super) fn test(
|
||||
&self,
|
||||
input: BlockStateId,
|
||||
location: BlockStateId,
|
||||
registry: &BlockRegistry,
|
||||
random: &mut impl RandomSource,
|
||||
) -> bool {
|
||||
match self {
|
||||
Self::True => true,
|
||||
Self::AllOf(all) => all
|
||||
.iter()
|
||||
.all(|c| c.test(input, location, registry, random)),
|
||||
Self::AnyOf(any) => any
|
||||
.iter()
|
||||
.any(|c| c.test(input, location, registry, random)),
|
||||
Self::Not(c) => !c.test(input, location, registry, random),
|
||||
Self::RandomChance(chance) => random.next_float() < *chance,
|
||||
Self::MatchingBlocks {
|
||||
blocks,
|
||||
properties,
|
||||
location: at_location,
|
||||
} => {
|
||||
let state = if *at_location { location } else { input };
|
||||
blocks.contains(&state)
|
||||
&& properties.iter().all(|(k, x)| {
|
||||
registry
|
||||
.state(state)
|
||||
.and_then(|s| s.properties.get(k))
|
||||
.is_some_and(|v| v == x)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn random_settings_salt_and_position() {
|
||||
// ResourceLocation.hashCode: 31 * namespace + path.
|
||||
let default = RandomSettings::decode(Some(&"per_piece".into())).unwrap();
|
||||
assert_eq!(
|
||||
default.salt,
|
||||
java_hash("lithostitched")
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(java_hash("default"))
|
||||
);
|
||||
assert_eq!(java_hash("default"), 1544803905);
|
||||
let (seed, piece, pivot, block) = (12345, [16, 64, -32], [8, 70, 8], [17, 65, -30]);
|
||||
let mut ours = default.create(seed, piece, pivot, block);
|
||||
let mut theirs = LegacyRandom::new(seed + i64::from(default.salt))
|
||||
.fork_positional()
|
||||
.at(BlockPos::new(16, 64, -32));
|
||||
assert_eq!(ours.next_long(), theirs.next_long());
|
||||
let named: Value = serde_json::json!({"mode": "per_structure", "name": "v:roof"});
|
||||
let named = RandomSettings::decode(Some(&named)).unwrap();
|
||||
assert_eq!(named.mode, Mode::Structure);
|
||||
assert_eq!(
|
||||
named.salt,
|
||||
java_hash("v")
|
||||
.wrapping_mul(31)
|
||||
.wrapping_add(java_hash("roof"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_random_draws_by_weight() {
|
||||
let apply = ApplyRandom {
|
||||
random: RandomSettings {
|
||||
mode: Mode::Block,
|
||||
salt: 0,
|
||||
},
|
||||
lists: vec![
|
||||
(ProcessorList(vec![Processor::Nop]), 3),
|
||||
(ProcessorList::default(), 1),
|
||||
],
|
||||
total: 4,
|
||||
};
|
||||
let mut ours = LegacyRandom::new(9);
|
||||
let mut theirs = LegacyRandom::new(9);
|
||||
for _ in 0..32 {
|
||||
let draw = theirs.next_int_bound(4).unwrap();
|
||||
let picked = apply.pick(&mut ours).unwrap();
|
||||
assert_eq!(picked.0.len(), usize::from(draw < 3));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_block_draws_from_the_unsalted_seed() {
|
||||
let (a, b) = (BlockStateId(1), BlockStateId(2));
|
||||
let set = SetBlock::decode(
|
||||
&serde_json::json!({"state_provider": {"type": "minecraft:weighted_state_provider",
|
||||
"entries": [{"weight": 1, "data": 1}, {"weight": 2, "data": 2}]},
|
||||
"random_mode": "per_piece"}),
|
||||
|v| Ok(BlockStateId(v.as_u64().unwrap() as u32)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(set.preserves());
|
||||
for x in 0..16 {
|
||||
let mut java = LegacyRandom::new(77)
|
||||
.fork_positional()
|
||||
.at(BlockPos::new(x, 5, 3));
|
||||
let want = if java.next_int_bound(3).unwrap() < 1 {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
};
|
||||
assert_eq!(set.state(77, [x, 5, 3], [0; 3], [9, 9, 9]), want);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-15
@@ -504,21 +504,22 @@ Two cases the oracle fills in:
|
||||
mod. Better Dungeons' small dungeons keep one chest and each other one with a fifth's
|
||||
chance, two at most (the game's `horizon:count_cap`, counted per placement as the mod's
|
||||
DungeonContext is): 12 chests in five small dungeons at seed 1234 become 6.
|
||||
Lithostitched's `block_swap` (137 lists, all in ChoiceTheorem's Overhauled Village) is the
|
||||
game's `horizon:block_swap`: the same map, with Block.withPropertiesOf's properties kept;
|
||||
a target block the game does not have (most of them are Nature's Spirit's and Croptopia's)
|
||||
leaves the block as it is, as the mod does. Lithostitched's `reference` (CTOV builds most
|
||||
of its lists from others) has the referenced lists' processors written in its place; its
|
||||
`apply_random`, `condition` and `set_block` are the game's `horizon:apply_random`,
|
||||
`horizon:condition` and `horizon:set_block`, with Lithostitched's random (the world seed
|
||||
plus the name's hash, at the block, piece or structure). In a CTOV plains village at seed
|
||||
1234 the farms now grow carrots, potatoes and beetroot besides wheat, and 1,655
|
||||
cobblestone blocks become paths, grass, mossy cobblestone and planks. No Lithostitched processor is
|
||||
dropped now (130 before). CTOV's compat hooks for other mods inside them (its
|
||||
`modular_compat`, 142 copies) run a list of their own only when the mod they name is
|
||||
loaded: carry writes that list in when the mod is ported along and takes the hook out (a
|
||||
no-op, as in the mod) when it is not. In ATM10 all 142 name Nature's Spirit, which the
|
||||
pack does not have.
|
||||
Lithostitched's processors (all in ChoiceTheorem's Overhauled Village in ATM10) are the
|
||||
game's own types, read as the mod writes them. `block_swap` keeps the properties the new
|
||||
block can have: a farm's ripe wheat becomes ripe carrots, but beetroot, whose age stops at
|
||||
3, keeps its default; a target block the game does not have (most are Nature's Spirit's and
|
||||
Croptopia's) leaves the block as it is. `reference` runs the lists it names, `condition`
|
||||
tests the template's block or the world's, `apply_random` and `set_block` draw per block or
|
||||
per piece as the data says. Lithostitched's own random is not known yet, so the shares of a
|
||||
random choice are the Java pack's but the blocks chosen differ. Lists named by tag and state
|
||||
providers other than simple and weighted ones are dropped and counted (ATM10 has none).
|
||||
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.
|
||||
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
|
||||
differ from the Java pack's for the same seed (their positions do not).
|
||||
The processors that build outside their template are carried too: Repurposed's and
|
||||
Moog's pillar processors become the game's `horizon:pillar` (a column from a placeholder
|
||||
block down to the ground, with the processor list they run on each of its blocks written
|
||||
|
||||
@@ -55,6 +55,12 @@ KNOWN = {
|
||||
"Better Dungeons' own structure types (spec-betterdungeons §5, §6; OPEN-B14, B15) are not "
|
||||
'implemented yet',
|
||||
},
|
||||
'ctov': {
|
||||
r'^ctov:': "Lithostitched's random is not known (spec-lithostitched OPEN-L6): per-block road and palette "
|
||||
'choices match by share, not by block; and decoration (grass, trees) differs where the village '
|
||||
'changed the terrain',
|
||||
r'^minecraft:village_plains$': 'decoration (grass, trees) differs where the village changed the terrain',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user