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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! V102 (15w32a+2) — legacy numeric item ids -> string item names; cites V102.java.
//! Ports ITEM_NAME numeric->string converter, ITEM_STACK numeric-id->string-name
//! structure converter, and the minecraft:potion id converter. The schema's
//! ITEM_STACK string-only-id walker change needs no Rust update because ITEM_NAME
//! is already generic (int or String), per V102.java:21-22.
use std::collections::HashMap;
use std::sync::LazyLock;
use super::super::flattening::{get_name_from_id, get_potion_name_from_id};
use super::super::loss::{report_loss, LossKind, Severity};
use super::super::registry::RegistryBuilder;
use super::super::types::{MapExt, ValueExt};
use crate::nbt::{NbtMap, NbtValue};
const VERSION: i32 = 102;
// --- reverse (new -> old) lookup tables, inverted from the forward helpers ---
//
// HelperItemNameV102.ITEM_NAMES is 1:1 (no two ids share a name — verified
// against flattening::data::ITEM_NAMES_BY_ID), so name -> id is exact. Built by
// scanning the full id range 0..=2267 (max table id is the record_wait = 2267)
// and inverting `get_name_from_id`.
static ITEM_ID_BY_NAME: LazyLock<HashMap<&'static str, i32>> = LazyLock::new(|| {
let mut m = HashMap::new();
for id in 0..=2267i32 {
if let Some(name) = get_name_from_id(id) {
m.entry(name).or_insert(id);
}
}
m
});
// HelperItemNameV102.POTION_NAMES is heavily many-to-one (e.g. minecraft:water,
// minecraft:regeneration map from many damage values), so name -> damage is
// genuinely ambiguous. We keep the *lowest* base id (0..=127) per name as the
// canonical preimage; the splash bit (16384) is restored separately from the id.
static POTION_BASE_ID_BY_NAME: LazyLock<HashMap<&'static str, i16>> = LazyLock::new(|| {
let mut m: HashMap<&'static str, i16> = HashMap::new();
for id in 0..=127i16 {
if let Some(name) = get_potion_name_from_id(id) {
m.entry(name)
.and_modify(|e| {
if id < *e {
*e = id;
}
})
.or_insert(id);
}
}
m
});
pub fn register(reg: &mut RegistryBuilder) {
// ITEM_NAME: numeric legacy id -> string item name (default id 0 when unknown).
reg.item_name.add_converter(
VERSION,
0,
Box::new(|val: &mut NbtValue, _from, _to| {
// Java returns null (no change) when data isn't a Number.
if let Some(id) = val.as_number_i64() {
let id = id as i32;
let remap = get_name_from_id(id)
.or_else(|| get_name_from_id(0))
.unwrap_or("minecraft:air");
*val = NbtValue::String(remap.to_string());
}
}),
);
// REVERSE of ITEM_NAME: string name -> numeric legacy id. ITEM_NAMES is 1:1,
// so any name the forward could have produced reverses exactly (bucket B,
// lossless). A name with no legacy integer id (a modern item that never had
// one) cannot be represented numerically: leave the string and report loss.
reg.item_name.add_reverse_converter(
VERSION,
0,
Box::new(|val: &mut NbtValue, _from, _to| {
if let NbtValue::String(name) = val {
if let Some(&id) = ITEM_ID_BY_NAME.get(name.as_str()) {
*val = NbtValue::Int(id);
} else {
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
"item name has no legacy numeric id (V102 ITEM_NAME reverse)",
);
}
}
}),
);
// ITEM_STACK: when id is numeric, replace it with the string name (default id 0).
reg.item_stack.add_structure_converter(
VERSION,
0,
Box::new(|data: &mut NbtMap, _from, _to| {
// Java guard: hasKey("id", NUMBER) — only act when id is present and numeric.
let numeric_id = match data.get("id") {
Some(v) => v.as_number_i64(),
None => None,
};
if let Some(id) = numeric_id {
let id = id as i32;
let remap = get_name_from_id(id)
.or_else(|| get_name_from_id(0))
.unwrap_or("minecraft:air");
data.set_string("id", remap);
}
}),
);
// REVERSE of ITEM_STACK structure: string id -> numeric legacy id. Registered
// after the potion for-id reverse below, so under the descending sweep
// (reverse_converters run in .rev() order) the potion reverse runs FIRST and
// restores id to the "minecraft:potion" string; THEN this turns the string id
// back into the numeric id (e.g. 373). Lossless for any name the forward could
// have emitted (bucket B); a modern-only name with no legacy id is left as a
// string and reported (a pre-V102 reader expects a number).
reg.item_stack.add_reverse_converter(
VERSION,
0,
Box::new(|data: &mut NbtMap, _from, _to| {
let name = match data.get_string("id") {
Some(s) => s.to_string(),
None => return, // already numeric or absent — nothing to undo.
};
if let Some(&id) = ITEM_ID_BY_NAME.get(name.as_str()) {
data.set_i32("id", id);
} else {
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
"item stack id has no legacy numeric id (V102 ITEM_STACK reverse)",
);
}
}),
);
// ITEM_STACK minecraft:potion: zero Damage, set tag.Potion, splash on bit 16384.
reg.item_stack.add_converter_for_id(
"minecraft:potion",
VERSION,
0,
Box::new(|data: &mut NbtMap, _from, _to| {
let damage = data.get_i32("Damage").unwrap_or(0) as i16;
if damage != 0 {
data.set_short("Damage", 0);
}
if data.get_map("tag").is_none() {
data.set_map("tag", NbtMap::new());
}
// Only set Potion if not already a string.
let has_potion_string = data
.get_map("tag")
.and_then(|t| t.get_string("Potion"))
.is_some();
if !has_potion_string {
let converted = get_potion_name_from_id(damage).unwrap_or("minecraft:water");
let tag = data.get_map_mut("tag").unwrap();
tag.set_string("Potion", converted);
if (damage & 16384) == 16384 {
data.set_string("id", "minecraft:splash_potion");
}
}
}),
);
// REVERSE of the minecraft:potion converter. The forward read the legacy
// numeric `Damage` (a potion discriminator + splash bit 16384), zeroed it,
// stored the resolved name in tag.Potion, and split the splash variant into
// the id "minecraft:splash_potion". The reverse reconstructs `Damage` from
// tag.Potion and the id:
// * id "minecraft:splash_potion" -> base id | 16384, id back to "minecraft:potion".
// * id "minecraft:potion" -> base id.
// POTION_NAMES is many-to-one, so the exact original Damage cannot be
// recovered from the name — we use the lowest matching base id as the
// canonical preimage (Approximated). Runs BEFORE the ITEM_STACK structure
// reverse (it is registered later), so `id` is still the string here and we
// hand back "minecraft:potion" for that converter to renumber.
//
// Matches the NEW ids the forward produced (cheatsheet rule 4).
for new_id in ["minecraft:potion", "minecraft:splash_potion"] {
reg.item_stack.add_reverse_converter_for_id(
new_id,
VERSION,
0,
Box::new(move |data: &mut NbtMap, _from, _to| {
// The forward only wrote tag.Potion when there was no Potion string
// already; if it is absent we cannot reconstruct Damage — leave as is.
let potion_name = data
.get_map("tag")
.and_then(|t| t.get_string("Potion"))
.map(|s| s.to_string());
let potion_name = match potion_name {
Some(p) => p,
None => return,
};
let base = POTION_BASE_ID_BY_NAME
.get(potion_name.as_str())
.copied()
.unwrap_or(0);
let is_splash = new_id == "minecraft:splash_potion";
let damage = if is_splash { base | 16384 } else { base };
// The legacy Damage is the canonical preimage, not the true original
// (the name lost the exact id within its equivalence class).
report_loss(
VERSION,
LossKind::FingerprintCollapse,
Severity::Approximated,
"legacy potion Damage approximated from tag.Potion (many ids share one potion name)",
);
data.set_short("Damage", damage);
// Undo the id split so the ITEM_STACK structure reverse renumbers it.
data.set_string("id", "minecraft:potion");
// Remove the modern tag.Potion the forward introduced; drop tag if
// it is now empty.
if let Some(tag) = data.get_map_mut("tag") {
tag.take("Potion");
if tag.is_empty() {
data.take("tag");
}
}
}),
);
}
}