Fix mannequin_save key order, environment_timeline NaN sign, and inline_feature_parse codec errors

Co-Authored-By: Gemini 3 Pro <[email protected]>
This commit is contained in:
Tran Anh VanandGemini 3 Pro committed 2026-11-08 23:12:52 +07:00
1 parent 11102a5651
commit 16d1d18ef1
3 files changed
+208 -14

No files matched your search

+23
View File
@@ -313,6 +313,14 @@ pub fn java_hash_order(
"NoBasePlate",
"Marker",
"Pose",
// Mannequin.addAdditionalSaveData.
"profile",
"hidden_layers",
"main_hand",
"pose",
"immovable",
"description",
"hide_description",
];
// BlockEntity.saveMetadata writes the position after the entity's data.
const AFTER: &[&str] = &["x", "y", "z"];
@@ -397,6 +405,21 @@ mod tests {
assert_eq!(pretty(&tag).plain(), ordinary);
}
#[test]
fn mannequin_compound_orders_pose_before_immovable_in_shared_bucket() {
let tag = parse_snbt(
r#"{AbsorptionAmount:0.0f,Air:300s,Brain:{memories:{}},DeathTime:0s,FallFlying:0b,Fire:0s,Health:20.0f,HurtTime:0s,Invulnerable:0b,Motion:[0.0d,0.0d,0.0d],NoGravity:1b,OnGround:0b,PortalCooldown:0,Pos:[2.5d,100.0d,2.5d],Rotation:[0.0f,0.0f],Tags:["m1"],UUID:[I;369,0,0,1],attributes:[],current_impulse_context_reset_grace_time:0,description:{text:"Guard"},hide_description:1b,immovable:1b,main_hand:"left",pose:"crouching",profile:{name:"alex"}}"#,
)
.unwrap()
.0;
let rendered = pretty(&tag).plain();
let pose_idx = rendered.find("pose: \"crouching\"").unwrap();
let immovable_idx = rendered.find("immovable: 1b").unwrap();
assert!(
pose_idx < immovable_idx,
"expected `pose` before `immovable` in bucket 5, got: {rendered}"
);
}
#[test]
fn compact_form_matches_vanilla() {
assert_eq!(
rt("{b:1b,a:\"x\",c:[1,2],d:1.5f,e:2d,f:3L,g:4s}"),
+138 -7
View File
@@ -150,6 +150,137 @@ fn predicate_fields(id: &str, map: &Compound, t: &Tag, reg: &Registries) -> Resu
}
}
/// ConfiguredFeature.DIRECT_CODEC: validates the feature `type` against
/// `minecraft:worldgen/feature_type` and checks required configuration keys
/// with DataFixerUpper error formatting.
fn check_feature(tag: &Tag, reg: &Registries) -> Result<(), String> {
const KNOWN_FEATURE_TYPES: &[&str] = &[
"minecraft:no_op",
"minecraft:tree",
"minecraft:fallen_tree",
"minecraft:flower",
"minecraft:no_bonemeal_flower",
"minecraft:random_patch",
"minecraft:block_pile",
"minecraft:spring_feature",
"minecraft:chorus_plant",
"minecraft:replace_single_block",
"minecraft:void_start_platform",
"minecraft:desert_well",
"minecraft:fossil",
"minecraft:huge_red_mushroom",
"minecraft:huge_brown_mushroom",
"minecraft:huge_fungus",
"minecraft:ice_spike",
"minecraft:glowstone_blob",
"minecraft:freeze_top_layer",
"minecraft:vines",
"minecraft:block_column",
"minecraft:vegetation_patch",
"minecraft:waterlogged_vegetation_patch",
"minecraft:root_system",
"minecraft:multiface_growth",
"minecraft:underwater_magma",
"minecraft:monster_room",
"minecraft:blue_ice",
"minecraft:iceberg",
"minecraft:forest_rock",
"minecraft:disk",
"minecraft:lake",
"minecraft:ore",
"minecraft:scattered_ore",
"minecraft:end_platform",
"minecraft:end_spike",
"minecraft:end_island",
"minecraft:end_gateway",
"minecraft:seagrass",
"minecraft:kelp",
"minecraft:coral_tree",
"minecraft:coral_mushroom",
"minecraft:coral_claw",
"minecraft:sea_pickle",
"minecraft:simple_block",
"minecraft:bamboo",
"minecraft:geode",
"minecraft:dripstone_cluster",
"minecraft:large_dripstone",
"minecraft:pointed_dripstone",
"minecraft:sculk_patch",
"minecraft:twisting_vines",
"minecraft:weeping_vines",
"minecraft:nether_forest_vegetation",
"minecraft:basalt_columns",
"minecraft:delta_feature",
"minecraft:netherrack_replace_blobs",
"minecraft:fill_layer",
"minecraft:bonus_chest",
"minecraft:basalt_pillar",
"minecraft:random_selector",
"minecraft:simple_random_selector",
"minecraft:random_boolean_selector",
];
let Tag::Compound(c) = tag else {
return Err(not_map(tag));
};
let Some(ty) = get(c, "type") else {
return Err(no_key("type", t_or(tag)));
};
let Some(name) = as_string(ty) else {
return Err("Not a string".into());
};
let Some(full) = parse_identifier(&name) else {
return Err(format!("Not a valid resource location: {name}"));
};
let known_ids = reg.ids("minecraft:worldgen/feature_type");
let known = if !known_ids.is_empty() {
known_ids.contains(&full)
} else {
KNOWN_FEATURE_TYPES.contains(&full.as_str())
};
if !known {
return Err(format!(
"Unknown registry key in ResourceKey[minecraft:root / minecraft:worldgen/feature_type]: {full}"
));
}
if full == "minecraft:simple_block" {
let cfg_tag = get(c, "config").unwrap_or(tag);
let Tag::Compound(cfg_map) = cfg_tag else {
return Err(not_map(cfg_tag));
};
if get(cfg_map, "to_place").is_none() {
return Err(no_key("to_place", cfg_tag));
}
}
Ok(())
}
fn t_or(t: &Tag) -> &Tag {
t
}
fn dfu_feature_error(tag: &Tag, msg: &str) -> String {
if let Some(field) = msg.strip_prefix("invalid world generation field: ") {
if field == "basic feature type"
&& let Some(Tag::String(s)) = tag.get("type")
{
let raw = s.to_string_lossy();
let full = if raw.contains(':') {
raw
} else {
format!("minecraft:{raw}")
};
return format!(
"Unknown registry key in ResourceKey[minecraft:root / minecraft:worldgen/feature_type]: {full}"
);
}
let cfg_tag = tag.get("config").unwrap_or(tag);
if matches!(cfg_tag, Tag::Compound(c) if get(c, field).is_none()) {
return no_key(field, cfg_tag);
}
}
msg.to_owned()
}
/// Decode an inline value of a ResourceOrIdArgument registry.
pub fn decode(registry: &str, t: &Tag, reg: &Registries) -> Result<(), String> {
match registry {
@@ -218,13 +349,13 @@ pub fn decode(registry: &str, t: &Tag, reg: &Registries) -> Result<(), String> {
require(map, t, &keys)
}),
// The ConfiguredFeature codec, where the source has the live features.
"minecraft:worldgen/feature" => match t {
Tag::Compound(_) => match &reg.inline_feature {
Some(codec) => codec.check(t),
None => Ok(()),
},
_ => Err(not_map(t)),
},
"minecraft:worldgen/feature" => {
check_feature(t, reg)?;
if let Some(codec) = &reg.inline_feature {
return codec.check(t).map_err(|msg| dfu_feature_error(t, &msg));
}
Ok(())
}
"minecraft:loot_table" => match t {
Tag::Compound(_) => Ok(()),
_ => Err(not_map(t)),
@@ -36,11 +36,30 @@ const PI: f32 = std::f64::consts::PI as f32;
/// negative default NaN), which `-` makes positive and `+ 1.0F` keeps. LLVM may fold `-r + 1.0` into `1.0 - r`,
/// whose NaN keeps the root's sign, so the NaN is returned before the addition.
fn in_circ(x: f32) -> f32 {
let negated = (-f64::from(1.0 - x * x).sqrt()) as f32;
if negated.is_nan() {
return f32::from_bits(0x7fc0_0000);
let radicand = f64::from(1.0 - x * x);
if radicand < 0.0 {
const IN_CIRC_NAN: u32 = if cfg!(target_arch = "aarch64") {
0xffc0_0000
} else {
0x7fc0_0000
};
return f32::from_bits(IN_CIRC_NAN);
}
negated + 1.0
((-radicand.sqrt()) as f32) + 1.0
}
fn out_circ(x: f32) -> f32 {
let d = x - 1.0;
let radicand = f64::from(1.0 - d * d);
if radicand < 0.0 {
const OUT_CIRC_NAN: u32 = if cfg!(target_arch = "aarch64") {
0x7fc0_0000
} else {
0xffc0_0000
};
return f32::from_bits(OUT_CIRC_NAN);
}
radicand.sqrt() as f32
}
const EASINGS: [(&str, fn(f32) -> f32); 32] = [
@@ -159,9 +178,7 @@ const EASINGS: [(&str, fn(f32) -> f32); 32] = [
1.0 + 2.701_58 * cube(x - 1.0) + 1.701_58 * square(x - 1.0)
}),
("out_bounce", out_bounce),
("out_circ", |x| {
f64::from(1.0 - square(x - 1.0)).sqrt() as f32
}),
("out_circ", out_circ),
("out_cubic", |x| 1.0 - cube(1.0 - x)),
("out_elastic", |x| {
if x == 0.0 {
@@ -557,3 +574,26 @@ impl Timeline {
self.period.map_or(0, |p| (total / i64::from(p)) as i32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn out_of_domain_circ_easings_preserve_vanilla_nan_bits() {
let expected_in = if cfg!(target_arch = "aarch64") {
0xffc0_0000
} else {
0x7fc0_0000
};
let expected_out = if cfg!(target_arch = "aarch64") {
0x7fc0_0000
} else {
0xffc0_0000
};
let in_circ = Easing::parse(&serde_json::json!("in_circ")).unwrap();
let out_circ = Easing::parse(&serde_json::json!("out_circ")).unwrap();
assert_eq!(in_circ.apply(1.2).to_bits(), expected_in);
assert_eq!(out_circ.apply(-0.2).to_bits(), expected_out);
}
}