1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//! V502 (16w20a+1) — schematic-relevant subset of
//! `DataConverterJava/.../versions/V502.java`.
//!
//! Ports both registrations from V502.java:
//! * the `cooked_fished` -> `cooked_fish` item rename (ITEM_NAME), and
//! * the `Zombie` entity converter that migrates the legacy `IsVillager`
//! boolean to the `ZombieType` profession index (ENTITY).
//!
//! Nothing in this version targets a non-schematic type, so nothing is skipped.
use rand::Rng;
use crate::nbt::NbtMap;
use super::super::helpers::{map_renamer, register_item_rename};
use super::super::loss::{report_loss, LossKind, Severity};
use super::super::registry::RegistryBuilder;
use super::super::types::MapExt;
const VERSION: i32 = 502;
pub fn register(reg: &mut RegistryBuilder) {
// `cooked_fished` -> `cooked_fish` (V502.java:18-20). The Java side uses a
// function rename, but it maps exactly one id, so a table renamer is
// equivalent and trivially reversible.
register_item_rename(
reg,
VERSION,
map_renamer(&[("minecraft:cooked_fished", "minecraft:cooked_fish")]),
);
// Villager-zombie migration (V502.java:21-44). Only the legacy `Zombie`
// entity id carried the `IsVillager` byte at this version.
reg.entity.add_converter_for_id(
"Zombie",
VERSION,
0,
Box::new(|data: &mut NbtMap, _from, _to| {
if data.get_bool("IsVillager") != Some(true) {
return;
}
data.take("IsVillager");
if data.has_key("ZombieType") {
return;
}
// Vanilla leaves the `VillagerProfession` tag in place, so we do too.
let mut profession = data.get_i32("VillagerProfession").unwrap_or(-1);
if !(0..6).contains(&profession) {
profession = rand::thread_rng().gen_range(0..6);
}
data.set_i32("ZombieType", profession);
}),
);
// Inverse of the villager-zombie migration. A modern `Zombie` carrying a
// `ZombieType` is a villager-zombie, so we restore the legacy encoding:
// `IsVillager = true`, and copy the profession index back into
// `VillagerProfession` (the forward sourced `ZombieType` from it), then drop
// `ZombieType`. `ZombieType` uniquely encodes the old profession, so this is
// an exact structural inverse — no loss. (The forward's random fill for an
// invalid profession isn't recoverable, but for genuine modern data
// `ZombieType` is the source of truth.)
reg.entity.add_reverse_converter_for_id(
"Zombie",
VERSION,
0,
Box::new(|data: &mut NbtMap, _from, _to| {
let Some(zombie_type) = data.get_i32("ZombieType") else {
return;
};
data.take("ZombieType");
match zombie_type {
1..=5 => {
data.set_bool("IsVillager", true);
data.set_i32("VillagerProfession", zombie_type);
}
0 => {
data.set_bool("IsVillager", false);
report_loss(
VERSION,
LossKind::EntityMergeAmbiguous,
Severity::Approximated,
"ZombieType=0 could be a plain zombie or a villager zombie with profession 0; restoring plain zombie",
);
}
6 => {
data.set_bool("IsVillager", false);
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
"Husk/ZombieType=6 has no pre-V502 representation; restoring plain zombie",
);
}
other => {
data.set_bool("IsVillager", false);
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
format!(
"ZombieType={other} has no pre-V502 villager-zombie representation; restoring plain zombie"
),
);
}
}
}),
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dataconverter::registry::convert_entity_reverse;
#[test]
fn reverse_plain_zombie_type_zero_restores_plain_zombie() {
let mut zombie = NbtMap::new();
zombie.set_string("id", "Zombie");
zombie.set_i32("ZombieType", 0);
let report = convert_entity_reverse(&mut zombie, 502, 501);
assert_eq!(zombie.get_bool("IsVillager"), Some(false));
assert!(!zombie.has_key("ZombieType"));
assert_eq!(report.len(), 1);
assert_eq!(report.loss_count(), 0);
}
}