mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Decode vanilla blockstate variants and multipart conditions
This commit is contained in:
1 parent
b3a508dba8
commit
54a2b552ee
10 files changed
+469
-1
No files matched your search
Generated
+1
@@ -578,6 +578,7 @@ checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||
name = "horizon-assets"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"minecraft": "26.3",
|
||||
"manifest_sha1": "4fe1aa1ef8da1cb95c5bad1fb98890ca56dd8ca3",
|
||||
"platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43",
|
||||
"java": "openjdk 25.0.4.1 2026-08-18",
|
||||
"rustc": "rustc 1.99.0 (b940084d7 2026-09-28)",
|
||||
"resource_blockstates": 1288,
|
||||
"synthetic_definitions": 1351,
|
||||
"rejected": 468,
|
||||
"sources_sha256": {
|
||||
"mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockStateModelOracle.java": "f6e3bacca9dcd04f4dde093c588f4cda435bb0237f57d27521af2dfe409358d2",
|
||||
"mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCuboidModelOracle.java": "7526f20513c549483122ab0d5ad24b9e87f38dc2f6aae55addd701a91ac85e43",
|
||||
"tools/test_blockstate_model_parity.py": "adf720719432c677f37bd10ed6e02089195c1fc4dcf227bb01187c3fd5a1747a",
|
||||
"tools/test_cuboid_model_parity.py": "4a8b1cdcac335751c23413341ae939eea1c49c38d33d70d0708b8904ab8d0461",
|
||||
"tools/lab.py": "d00bf107fa01b6c579734a082c3ca4ade3018f0d6851d46844acf17574f66e64",
|
||||
"tools/test_rust_parity.py": "29d3db08005873528894b45ca10dcd6278753650473b3eb574e53e06ede40ec6",
|
||||
"Cargo.toml": "3f93319c1eb70dbd7ba25cd74ef343fbf35c52c7c4752f955605c58699cbff9d",
|
||||
"Cargo.lock": "3b8e27b28da541475b05111f24f2ab96d2e5db88e62d34ab848ac057c09d33e7",
|
||||
"crates/horizon-assets/Cargo.toml": "d09f70e42198d36c97e6f0be9d3c251f649690824f78ac241158e01171d1f81c",
|
||||
"crates/horizon-assets/examples/blockstate_model_fixture.rs": "8f063322e57abdf9361a5a3a3badce2a7e727ee6d5e3947071cb448dfb38e8fd",
|
||||
"crates/horizon-assets/examples/cuboid_model_fixture.rs": "97609d90e3f6ce340f3590296db2dc778113bb2b33c7d1b54080a1d6f3023f40",
|
||||
"crates/horizon-assets/examples/model_baking_fixture.rs": "59caca09dd5afd0b70a2e8fdb8e949deb9322f00adc617918b33d1e7e5b0b522",
|
||||
"crates/horizon-assets/examples/model_resolution_fixture.rs": "c071067140299e49345a6ebd94e15b3b6d853cb9ae11d8de46e3cbdfb8b01f0e",
|
||||
"crates/horizon-assets/examples/sprite_animation_fixture.rs": "254314d52deda129ab461221d3d8b95323a60730d374e566c1cd599fba85f072",
|
||||
"crates/horizon-assets/examples/support/mod.rs": "bcf5a6d8a54845982191e037911c24139237771d6afa4b01b1e83c011489c3f5",
|
||||
"crates/horizon-assets/src/animation.rs": "ebbf7952146a0e66b41396174492f5702627170b03a57e6cd5a45c97a08c975e",
|
||||
"crates/horizon-assets/src/bake.rs": "0bc519e93ad8200d89543ceb5c811dd82bbeb0e9003b143385c8a03e758dfe65",
|
||||
"crates/horizon-assets/src/blockstate.rs": "ec5fef8eb7cf4c5e367c76d394554e97c598371e4d4dc6029b15c6483a75616c",
|
||||
"crates/horizon-assets/src/json.rs": "99fe8af4fbf5d4eed408a92e829ac1a39597a0d92471a6bff73771fad9c830a8",
|
||||
"crates/horizon-assets/src/lib.rs": "e39f21c59a2c3e1c53d977b29bd5e0638e5fc41d86ff0fcc1882a6d90fd961e4",
|
||||
"crates/horizon-assets/src/model.rs": "57ddb3d65e88e7a8aa22c62bc298a2dae7f4e994326a873a31f06c0f36ed051b",
|
||||
"crates/horizon-assets/src/resolve.rs": "3927ac926cdcbd7d334901cc32d0ecc1e498b1bec98f76a46bca6d4964c13541"
|
||||
},
|
||||
"fixtures_sha256": {
|
||||
".local/rust-blockstate-model-parity/inputs.jsonl": "9ac61d1c85d00d1c4ca1246a832359459a055077d04401599534885cd6f341b9"
|
||||
},
|
||||
"rust_output_sha256": "53300626594228aeb6fb75923b49430a26a230f729aa0ead7f05080d039c34b0",
|
||||
"references": [
|
||||
{
|
||||
"side": "client",
|
||||
"jar_sha256": "4508d006323f24fa02876310c192d739af56516eb259000ac50f0909a68c9a2d",
|
||||
"output_sha256": "53300626594228aeb6fb75923b49430a26a230f729aa0ead7f05080d039c34b0",
|
||||
"matched_output_lines": 2639
|
||||
}
|
||||
],
|
||||
"scope": "Actual StrictJsonParser and BlockStateModelDispatcher codec used by the pinned resource loader. Every bundled blockstate JSON plus variants, XYZ quarter turns, UV-lock flags, weighted entries, source order, multipart conditions and malformed synthetic cases. No registry binding, property selection, random draw selection, transformed baking, rendered pixels or gameplay parity claim."
|
||||
}
|
||||
@@ -7,4 +7,5 @@ rust-version.workspace = true
|
||||
description = "Horizon version-specific model and resource decoding"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.229"
|
||||
serde_json = { version = "1.0.151", features = ["raw_value"] }
|
||||
@@ -0,0 +1,44 @@
|
||||
use horizon_assets::blockstate::{BlockStateDefinition, Condition, Variant, VariantSet};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
};
|
||||
fn variant(v: &Variant) -> Value {
|
||||
json!({"model":v.model,"rotations":v.rotations,"uvlock":v.uv_lock})
|
||||
}
|
||||
fn model(v: &VariantSet) -> Value {
|
||||
match v {
|
||||
VariantSet::Single(v) => json!({"single":variant(v)}),
|
||||
VariantSet::Weighted(entries) => {
|
||||
json!({"weighted":entries.iter().map(|(v,w)|json!([variant(v),w])).collect::<Vec<_>>()})
|
||||
}
|
||||
}
|
||||
}
|
||||
fn condition(v: &Condition) -> Value {
|
||||
match v {
|
||||
Condition::Or(v) => json!({"or":v.iter().map(condition).collect::<Vec<_>>()}),
|
||||
Condition::And(v) => json!({"and":v.iter().map(condition).collect::<Vec<_>>()}),
|
||||
Condition::Properties(v) => {
|
||||
json!({"properties":v.iter().map(|(key,terms)| (key, terms.iter().map(|t|json!({"value":t.value,"negated":t.negated})).collect::<Vec<_>>())).collect::<BTreeMap<_,_>>()})
|
||||
}
|
||||
}
|
||||
}
|
||||
fn definition(v: &BlockStateDefinition) -> Value {
|
||||
json!({"variants":v.variants.as_ref().map(|v|v.iter().map(|(k,v)|json!([k,model(v)])).collect::<Vec<_>>()),"multipart":v.multipart.as_ref().map(|v|v.iter().map(|s|json!({"when":s.condition.as_ref().map(condition),"apply":model(&s.variant)})).collect::<Vec<_>>())})
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut output = BufWriter::new(File::create(&args[2])?);
|
||||
for line in input.lines() {
|
||||
let row: Vec<String> = serde_json::from_str(&line?)?;
|
||||
let result = match BlockStateDefinition::from_json(&row[1]) {
|
||||
Ok(v) => json!({"name":row[0],"definition":definition(&v)}),
|
||||
Err(_) => json!({"name":row[0],"error":true}),
|
||||
};
|
||||
writeln!(output, "{result}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! Pinned blockstate resource definitions, before registry binding and baking.
|
||||
use crate::json::*;
|
||||
use serde::{
|
||||
Deserializer,
|
||||
de::{MapAccess, Visitor},
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Variant {
|
||||
pub model: String,
|
||||
/// X, Y, Z rotations in quarter turns.
|
||||
pub rotations: [u8; 3],
|
||||
pub uv_lock: bool,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VariantSet {
|
||||
Single(Variant),
|
||||
Weighted(Vec<(Variant, i32)>),
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Term {
|
||||
pub value: String,
|
||||
pub negated: bool,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Condition {
|
||||
Or(Vec<Condition>),
|
||||
And(Vec<Condition>),
|
||||
Properties(BTreeMap<String, Vec<Term>>),
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Selector {
|
||||
pub condition: Option<Condition>,
|
||||
pub variant: VariantSet,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BlockStateDefinition {
|
||||
/// Preserve resource order: overlapping variant selectors have order-sensitive
|
||||
/// native recovery behavior when bound to a registry.
|
||||
pub variants: Option<Vec<(String, VariantSet)>>,
|
||||
pub multipart: Option<Vec<Selector>>,
|
||||
}
|
||||
fn optional<'a>(fields: &Object<'a>, name: &str) -> Option<&'a RawValue> {
|
||||
fields.get(name).copied().filter(|v| v.get() != "null")
|
||||
}
|
||||
fn ordered_object(raw: &RawValue) -> Result<Vec<(String, &RawValue)>> {
|
||||
struct Ordered;
|
||||
impl<'de> Visitor<'de> for Ordered {
|
||||
type Value = Vec<(String, &'de RawValue)>;
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an object")
|
||||
}
|
||||
fn visit_map<M: MapAccess<'de>>(
|
||||
self,
|
||||
mut map: M,
|
||||
) -> std::result::Result<Self::Value, M::Error> {
|
||||
let mut values: Self::Value = Vec::new();
|
||||
let mut indices = BTreeMap::new();
|
||||
while let Some((name, value)) = map.next_entry::<String, &'de RawValue>()? {
|
||||
if let Some(&index) = indices.get(&name) {
|
||||
values[index] = (name, value);
|
||||
} else {
|
||||
indices.insert(name.clone(), values.len());
|
||||
values.push((name, value));
|
||||
}
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
}
|
||||
Ok(serde_json::Deserializer::from_str(raw.get()).deserialize_map(Ordered)?)
|
||||
}
|
||||
impl BlockStateDefinition {
|
||||
pub fn from_json(text: &str) -> Result<Self> {
|
||||
let raw: &RawValue = serde_json::from_str(text.strip_prefix('\u{feff}').unwrap_or(text))?;
|
||||
validate_number_tokens(raw)?;
|
||||
let fields = object(raw)?;
|
||||
let variants = optional(&fields, "variants")
|
||||
.map(|v| {
|
||||
let values = ordered_object(v)?;
|
||||
if values.is_empty() {
|
||||
return Err(error("empty variant map"));
|
||||
}
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(key, value)| Ok((key, VariantSet::decode(value)?)))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.transpose()?;
|
||||
let multipart = optional(&fields, "multipart")
|
||||
.map(|v| {
|
||||
let values = array(v)?;
|
||||
if values.is_empty() {
|
||||
return Err(error("empty multipart list"));
|
||||
}
|
||||
values
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
let fields = object(v)?;
|
||||
Ok(Selector {
|
||||
condition: optional(&fields, "when")
|
||||
.map(Condition::decode)
|
||||
.transpose()?,
|
||||
variant: VariantSet::decode(required(&fields, "apply")?)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.transpose()?;
|
||||
if variants.is_none() && multipart.is_none() {
|
||||
return Err(error("no variants or multipart"));
|
||||
}
|
||||
Ok(Self {
|
||||
variants,
|
||||
multipart,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Variant {
|
||||
fn decode(raw: &RawValue) -> Result<Self> {
|
||||
let fields = object(raw)?;
|
||||
let mut rotations = [0; 3];
|
||||
for (target, name) in rotations.iter_mut().zip(["x", "y", "z"]) {
|
||||
let angle = optional(&fields, name)
|
||||
.map(integer)
|
||||
.transpose()?
|
||||
.unwrap_or(0)
|
||||
.rem_euclid(360);
|
||||
if angle % 90 != 0 {
|
||||
return Err(error("model rotation must be a multiple of 90"));
|
||||
}
|
||||
*target = (angle / 90) as u8;
|
||||
}
|
||||
Ok(Self {
|
||||
model: identifier(&string(required(&fields, "model")?)?)?,
|
||||
rotations,
|
||||
uv_lock: optional(&fields, "uvlock")
|
||||
.map(|v| serde_json::from_str(v.get()))
|
||||
.transpose()?
|
||||
.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl VariantSet {
|
||||
fn decode(raw: &RawValue) -> Result<Self> {
|
||||
if !raw.get().starts_with('[') {
|
||||
return Ok(Self::Single(Variant::decode(raw)?));
|
||||
}
|
||||
let values = array(raw)?;
|
||||
if values.is_empty() {
|
||||
return Err(error("empty weighted variants"));
|
||||
}
|
||||
let mut total = 0i32;
|
||||
let entries = values
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
let fields = object(v)?;
|
||||
let weight = optional(&fields, "weight")
|
||||
.map(integer)
|
||||
.transpose()?
|
||||
.unwrap_or(1);
|
||||
if weight <= 0 {
|
||||
return Err(error("nonpositive variant weight"));
|
||||
}
|
||||
total = total
|
||||
.checked_add(weight)
|
||||
.ok_or_else(|| error("variant weight overflow"))?;
|
||||
Ok((Variant::decode(v)?, weight))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(Self::Weighted(entries))
|
||||
}
|
||||
}
|
||||
impl Condition {
|
||||
fn decode(raw: &RawValue) -> Result<Self> {
|
||||
let fields = object(raw)?;
|
||||
if fields.len() == 1 {
|
||||
for name in ["OR", "AND"] {
|
||||
if let Some(raw) = fields.get(name) {
|
||||
if let Ok(values) = array(raw).and_then(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(Self::decode)
|
||||
.collect::<Result<Vec<_>>>()
|
||||
}) {
|
||||
return Ok(if name == "OR" {
|
||||
Self::Or(values)
|
||||
} else {
|
||||
Self::And(values)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if fields.is_empty() {
|
||||
return Err(error("empty property conditions"));
|
||||
}
|
||||
let tests = fields
|
||||
.into_iter()
|
||||
.map(|(key, raw)| {
|
||||
let value = match raw.get().as_bytes().first() {
|
||||
Some(b'"') => string(raw)?,
|
||||
Some(b't' | b'f') => raw.get().to_owned(),
|
||||
_ => integer(raw)?.to_string(),
|
||||
};
|
||||
let terms = value
|
||||
.split('|')
|
||||
.map(|v| {
|
||||
let negated = v.starts_with('!');
|
||||
let value = v.strip_prefix('!').unwrap_or(v);
|
||||
if value.is_empty() {
|
||||
return Err(error("empty property term"));
|
||||
}
|
||||
Ok(Term {
|
||||
value: value.to_owned(),
|
||||
negated,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok((key, terms))
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
Ok(Self::Properties(tests))
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
pub mod animation;
|
||||
pub mod bake;
|
||||
pub mod blockstate;
|
||||
mod json;
|
||||
pub mod model;
|
||||
pub mod resolve;
|
||||
|
||||
@@ -129,6 +129,20 @@ transparency classification, actual tint/light evaluation and GPU rendering
|
||||
remain separate from the geometry result. Other operating systems still need
|
||||
their native validation runs.
|
||||
|
||||
Blockstate resources now decode into ordered variant selectors and multipart
|
||||
conditions. The [native blockstate comparison](../benchmarks/rust-blockstate-model-decoding-2026-10-06/scoped-parity.json)
|
||||
covers all 1,288 bundled definitions and 1,351 synthetic cases (2,639 total,
|
||||
468 expected rejections). It uses the same `StrictJsonParser` and
|
||||
`BlockStateModelDispatcher.CODEC` as the pinned loader. The Rust representation
|
||||
preserves single versus weighted variants, all three quarter-turn axes, UV-lock
|
||||
flags, positive weights and total-weight limits, ordered selectors, nested
|
||||
AND/OR conditions and per-term negation. Registry binding, property selection,
|
||||
random draw consumption and transformed baking remain separate work.
|
||||
|
||||
```sh
|
||||
python3 tools/test_blockstate_model_parity.py
|
||||
```
|
||||
|
||||
The sprite animation module reproduces the pinned metadata codec's integer
|
||||
conversion, strict primitive types, optional null fields and frame dimensions.
|
||||
It preserves repeated frames, filters out-of-image frame indices, and treats
|
||||
|
||||
+3
-1
@@ -19,7 +19,9 @@ block-model JSON files and 2,457 synthetic cases compared against the actual
|
||||
client parser. [Decoder scope and validation](native-client.md#validation)
|
||||
also include parent/texture resolution over 5,648 roots and cuboid baking over
|
||||
7,100 roots with identity blockstate transforms. Blockstate selection, transformed
|
||||
models and UV locking remain unfinished. The native renderer now draws an
|
||||
models and UV locking remain unfinished. Blockstate resource decoding now
|
||||
matches all 1,288 bundled definitions and 1,351 synthetic cases; registry
|
||||
binding and model selection are not yet connected to the renderer. The native renderer now draws an
|
||||
inspection gallery from the decoded/resolved/baked models, including binary
|
||||
cutouts and stepped lantern animation; these are not yet gameplay blocks.
|
||||
Animation metadata, frame sequences and tick-state selection now have native
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.renderer.block.dispatch.*;
|
||||
import net.minecraft.client.renderer.block.dispatch.multipart.*;
|
||||
import net.minecraft.util.StrictJsonParser;
|
||||
import static dev.horizonlab.parity.VanillaCuboidModelOracle.map;
|
||||
|
||||
/** Original adapter around the actual blockstate resource parser and codec. */
|
||||
public final class VanillaBlockStateModelOracle {
|
||||
private static final Gson JSON = new GsonBuilder().serializeNulls().create();
|
||||
private static Object variant(Variant value) {
|
||||
var state=value.modelState();
|
||||
return map("model",value.modelLocation().toString(),"rotations",new int[]{state.x().ordinal(),state.y().ordinal(),state.z().ordinal()},"uvlock",state.uvLock());
|
||||
}
|
||||
private static Object model(BlockStateModel.Unbaked value) {
|
||||
if(value instanceof SingleVariant.Unbaked single)return map("single",variant(single.variant()));
|
||||
var weighted=(WeightedVariants.Unbaked)value;
|
||||
return map("weighted",weighted.entries().unwrap().stream().map(e->Arrays.asList(variant(((SingleVariant.Unbaked)e.value()).variant()),e.weight())).toList());
|
||||
}
|
||||
private static Object condition(Condition value) {
|
||||
if(value instanceof CombinedCondition combined)return map(combined.operation().name().toLowerCase(Locale.ROOT),combined.terms().stream().map(VanillaBlockStateModelOracle::condition).toList());
|
||||
var tests=new TreeMap<String,Object>();
|
||||
((KeyValueCondition)value).tests().forEach((key,terms)->tests.put(key,terms.entries().stream().map(t->map("value",t.value(),"negated",t.negated())).toList()));
|
||||
return map("properties",tests);
|
||||
}
|
||||
private static Object definition(BlockStateModelDispatcher value) {
|
||||
var variants=value.simpleModels().map(m->m.models().entrySet().stream().map(e->Arrays.asList(e.getKey(),model(e.getValue()))).toList()).orElse(null);
|
||||
var multipart=value.multiPart().map(m->m.selectors().stream().map(s->map("when",s.condition().map(VanillaBlockStateModelOracle::condition).orElse(null),"apply",model(s.variant()))).toList()).orElse(null);
|
||||
return map("variants",variants,"multipart",multipart);
|
||||
}
|
||||
public static void main(String[] args) throws Exception {
|
||||
net.minecraft.SharedConstants.tryDetectVersion();
|
||||
try(var input=Files.newBufferedReader(Path.of(args[0]));var output=Files.newBufferedWriter(Path.of(args[1]))) {
|
||||
for(String line;(line=input.readLine())!=null;) {
|
||||
var row=JsonParser.parseString(line).getAsJsonArray();Object result;
|
||||
BlockStateModelDispatcher decoded;
|
||||
try { decoded=BlockStateModelDispatcher.CODEC.parse(JsonOps.INSTANCE,StrictJsonParser.parse(row.get(1).getAsString())).getOrThrow(); }
|
||||
catch(RuntimeException error) {output.write(JSON.toJson(map("name",row.get(0).getAsString(),"error",true)));output.newLine();continue;}
|
||||
result=map("name",row.get(0).getAsString(),"definition",definition(decoded));
|
||||
output.write(JSON.toJson(result));output.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare blockstate resource decoding with the pinned client loader's codec."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import random
|
||||
import subprocess
|
||||
import zipfile
|
||||
try:
|
||||
from . import lab, test_rust_parity as base, test_cuboid_model_parity as decoding
|
||||
except ImportError:
|
||||
import lab, test_rust_parity as base, test_cuboid_model_parity as decoding
|
||||
|
||||
|
||||
def corpus():
|
||||
model={'model':'block/stone'}
|
||||
def root(value): return {'variants':{'':value}}
|
||||
for value in [None,{},[],False,0,'x',{'variants':{}},{'multipart':[]},{'variants':None},{'variants':None,'multipart':[{'apply':model}]},root(model),root([model]),root([])]:yield json.dumps(value)
|
||||
values=[None,True,False,0,1,-1,90,90.9,-90,270,360,450,4294967296,4294967386,1e100,'90','true','',[],{},[model]]
|
||||
for key in ['model','x','y','z','uvlock','weight']:
|
||||
for value in values:
|
||||
variant=dict(model);variant[key]=value
|
||||
yield json.dumps(root(variant));yield json.dumps(root([variant]))
|
||||
for weights in [[2147483647],[2147483647,1],[2147483646,1],[0,1],[-1,2],[1,1],[1.9,2.9]]:
|
||||
yield json.dumps(root([dict(model,weight=w) for w in weights]))
|
||||
for name in ['block/stone',':block/stone',':','minecraft:','a:b:c','A:a','a:é','../a']:
|
||||
yield json.dumps(root({'model':name}))
|
||||
for x in [-360,-90,0,90,180,270,360,450]:
|
||||
for y in [0,90,180,270]:
|
||||
for z in [0,90,180,270]:yield json.dumps(root(dict(model,x=x,y=y,z=z,uvlock=True)))
|
||||
terms=[None,True,False,0,1,1.9,-1,4294967297,'true','false','!true','true|false','!true|false','!!true','!','', '|','true|','|true','a||b',' a ',[],{},['true']]
|
||||
conditions=[{},None,True,[],{'OR':[]},{'AND':[]},{'OR':[{'a':'b'}]},{'AND':[{'a':'b'},{'c':'d'}]},{'OR':'true'},{'OR':[],'AND':[]},{'OR':[],'a':'b'}]
|
||||
for key in ['a','OR','AND','']:
|
||||
conditions.extend({key:t} for t in terms)
|
||||
for condition in conditions:
|
||||
for apply in [model,[model],[],None]:yield json.dumps({'multipart':[{'when':condition,'apply':apply}]})
|
||||
for raw in ['{} trailing','{} {}','\ufeff'+json.dumps(root(model)), '{"variants":{"b":{"model":"b"},"a":{"model":"a"},"b":{"model":"c"}}}', '{"variants":{"":{"model":"a"}},"multipart":[{"apply":{"model":"b"}}]}']:
|
||||
yield raw
|
||||
rng=random.Random(0x424c4f434b)
|
||||
for _ in range(500):
|
||||
selectors=[]
|
||||
for _ in range(rng.randrange(1,8)):
|
||||
condition={rng.choice(['a','b','c']):rng.choice(['x','y','!x','x|y'])}
|
||||
if rng.randrange(2):condition={rng.choice(['OR','AND']):[condition,{'a':'x'}]}
|
||||
variants=[dict(model,weight=rng.randrange(1,10),x=rng.randrange(-4,5)*90,y=rng.randrange(4)*90,z=rng.randrange(4)*90,uvlock=bool(rng.randrange(2))) for _ in range(rng.randrange(1,6))]
|
||||
selectors.append({'when':condition,'apply':variants if rng.randrange(2) else variants[0]})
|
||||
yield json.dumps({'multipart':selectors})
|
||||
for number in ['1844674407370955161601','18446744073709551616.1','1e10000','1e9999','1' * 1024]:
|
||||
yield '{"variants":{"":{"model":"block/stone","x":'+number+'}}}'
|
||||
yield '{"variants":{"":{"model":"block/stone"}},"unknown":'+number+'}'
|
||||
|
||||
|
||||
def main():
|
||||
lock,jars,libraries=base.verify_artifacts()
|
||||
build=lab.LOCAL/'rust-blockstate-model-parity';build.mkdir(parents=True,exist_ok=True)
|
||||
report_path=build/'report.json';report_path.unlink(missing_ok=True)
|
||||
oracle=lab.ROOT/'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockStateModelOracle.java';helper=oracle.with_name('VanillaCuboidModelOracle.java');crate=lab.ROOT/'crates/horizon-assets'
|
||||
sources=[oracle,helper,Path(__file__),lab.ROOT/'tools/test_cuboid_model_parity.py',lab.ROOT/'tools/lab.py',lab.ROOT/'tools/test_rust_parity.py',lab.ROOT/'Cargo.toml',lab.ROOT/'Cargo.lock',crate/'Cargo.toml',*sorted(crate.rglob('*.rs'))]
|
||||
tested=base.source_hashes(sources)
|
||||
with zipfile.ZipFile(jars['client']) as archive:
|
||||
names=sorted(n for n in archive.namelist() if n.startswith('assets/minecraft/blockstates/') and n.endswith('.json'))
|
||||
rows=[(n,archive.read(n).decode()) for n in names]
|
||||
synthetic=list(corpus());rows.extend((f'synthetic/{i}',raw) for i,raw in enumerate(synthetic))
|
||||
inputs=build/'inputs.jsonl';inputs.write_text(''.join(json.dumps(row)+'\n' for row in rows));fixtures=base.source_hashes([inputs]);classes=build/'classes';classes.mkdir(exist_ok=True)
|
||||
def run(command,name):
|
||||
with (build/(name+'.log')).open('w') as log:subprocess.run(list(map(str,command)),cwd=lab.ROOT,check=True,timeout=3600,stdout=log,stderr=subprocess.STDOUT)
|
||||
dependencies=os.pathsep.join(map(str,[jars['client'],*libraries]))
|
||||
run(['javac','--release',lock['minecraft']['java_major'],'-proc:none','-cp',dependencies,'-d',classes,oracle,helper],'compile-java')
|
||||
run(['cargo','build','--locked','-p','horizon-assets','--example','blockstate_model_fixture'],'compile-rust')
|
||||
metadata=json.loads(subprocess.check_output(['cargo','metadata','--locked','--no-deps','--format-version','1'],cwd=lab.ROOT));binary=Path(metadata['target_directory'])/'debug/examples'/('blockstate_model_fixture.exe' if os.name=='nt' else 'blockstate_model_fixture')
|
||||
run([binary,inputs,build/'rust.jsonl'],'rust');run(['java','-Xmx512M','-cp',str(classes)+os.pathsep+dependencies,'dev.horizonlab.parity.VanillaBlockStateModelOracle',inputs,build/'client.jsonl'],'client')
|
||||
rejected=decoding.canonicalize(build/'client.jsonl');decoding.canonicalize(build/'rust.jsonl')
|
||||
native=[json.loads(line) for line in (build/'client.jsonl').read_text().splitlines()];rust=[json.loads(line) for line in (build/'rust.jsonl').read_text().splitlines()]
|
||||
for left,right in zip(native,rust,strict=True):
|
||||
if left!=right:raise AssertionError('Blockstate model mismatch: '+left['name'])
|
||||
matched=base.compare(build/'client.jsonl',build/'rust.jsonl');assert all('error' not in row for row in native[:len(names)]),'Bundled blockstate rejected'
|
||||
base.ensure_sources_unchanged(sources,tested);base.ensure_sources_unchanged([inputs],fixtures)
|
||||
report={'schema':1,'minecraft':lock['minecraft']['version'],'manifest_sha1':lock['minecraft']['manifest_sha1'],'platform':platform.platform(),'java':subprocess.check_output(['java','--version'],text=True).splitlines()[0],'rustc':subprocess.check_output(['rustc','--version'],text=True).strip(),'resource_blockstates':len(names),'synthetic_definitions':len(synthetic),'rejected':rejected,'sources_sha256':tested,'fixtures_sha256':fixtures,'rust_output_sha256':lab.digest(build/'rust.jsonl','sha256'),'references':[{'side':'client','jar_sha256':lab.digest(jars['client'],'sha256'),'output_sha256':lab.digest(build/'client.jsonl','sha256'),'matched_output_lines':matched}],'scope':'Actual StrictJsonParser and BlockStateModelDispatcher codec used by the pinned resource loader. Every bundled blockstate JSON plus variants, XYZ quarter turns, UV-lock flags, weighted entries, source order, multipart conditions and malformed synthetic cases. No registry binding, property selection, random draw selection, transformed baking, rendered pixels or gameplay parity claim.'}
|
||||
lab.write_json(report_path,report);print(f'Native blockstate models: {matched:,} definitions match ({rejected:,} rejected); {report_path}',flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in new issue
Block a user