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
//! V4300 (25w02a + 2) — schematic-relevant subset of `V4300.java`.
//!
//! The saddle item moved out of the legacy mob fields into the new `saddle`
//! slot (later folded under ENTITY_EQUIPMENT by V4301). The ENTITY structure
//! converter (V4300.java:162-218):
//! * "saddle item" mobs (horse family, camel, llama, trader_llama): rename
//! `SaddleItem` -> `saddle` and, if it existed, set a guaranteed saddle drop
//! chance (`drop_chances.saddle = 2.0`).
//! * "saddle flag" mobs (pig, strider): the boolean `Saddle` flag becomes an
//! actual `saddle` item (`{id:minecraft:saddle, count:1}`) plus the
//! guaranteed drop chance, when set.
//!
//! Plus walkers: llama/trader_llama/donkey/mule keep an `Items` item-list walker
//! (their inventory), and horse/skeleton_horse/zombie_horse get an explicit
//! no-op walker (V4300.java:221-228).
//!
//! VERSION = V25W02A(4298) + 2. Everything here is schematic-relevant (ENTITY).
use std::sync::Arc;
use crate::nbt::NbtMap;
use super::super::loss::{report_loss, LossKind, Severity};
use super::super::registry::RegistryBuilder;
use super::super::types::MapExt;
use super::super::walker::item_lists;
const VERSION: i32 = 4300;
/// Mobs that stored the saddle as a `SaddleItem` compound (V4300.java:163-174).
const SADDLE_ITEM_ENTITIES: &[&str] = &[
"minecraft:horse",
"minecraft:skeleton_horse",
"minecraft:zombie_horse",
"minecraft:donkey",
"minecraft:mule",
"minecraft:camel",
"minecraft:llama",
"minecraft:trader_llama",
];
/// Mobs that stored the saddle as a boolean `Saddle` flag (V4300.java:175-180).
const SADDLE_FLAG_ENTITIES: &[&str] = &["minecraft:pig", "minecraft:strider"];
/// `setGuaranteedSaddleDropChance` — `drop_chances.saddle = 2.0f`
/// (V4300.java:182-184). `getOrCreateMap` semantics: reuse an existing
/// `drop_chances` compound if present, else create one.
fn set_guaranteed_saddle_drop_chance(data: &mut NbtMap) {
if data.get_map("drop_chances").is_none() {
data.set_map("drop_chances", NbtMap::new());
}
data.get_map_mut("drop_chances")
.unwrap()
.set_f32("saddle", 2.0);
}
/// Inverse of `set_guaranteed_saddle_drop_chance`: remove the `saddle` entry the
/// forward added to `drop_chances`, and drop the `drop_chances` compound itself
/// if that leaves it empty (i.e. the forward had created it from scratch). The
/// old format had no saddle drop-chance slot, so this is exact (bucket D).
fn remove_guaranteed_saddle_drop_chance(data: &mut NbtMap) {
if let Some(drop_chances) = data.get_map_mut("drop_chances") {
if let Some(chance) = drop_chances.get_f64("saddle") {
if chance != 2.0 {
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
"saddle drop_chances.saddle has no pre-4300 representation; dropping it",
);
}
}
drop_chances.take("saddle");
if drop_chances.is_empty() {
data.take("drop_chances");
}
}
}
fn is_canonical_saddle_item(item: &NbtMap) -> bool {
item.get_string("id") == Some("minecraft:saddle") && item.get_i64("count").unwrap_or(1) == 1
}
pub fn register(reg: &mut RegistryBuilder) {
reg.entity.add_structure_converter(
VERSION,
0,
Box::new(|data, _from, _to| {
let id = match data.get_string("id") {
Some(s) => s.to_string(),
None => return,
};
if SADDLE_ITEM_ENTITIES.contains(&id.as_str()) {
// renameSingle returns false (no-op) when the source key is absent.
if !data.has_key("SaddleItem") {
return;
}
data.rename_key("SaddleItem", "saddle");
set_guaranteed_saddle_drop_chance(data);
} else if SADDLE_FLAG_ENTITIES.contains(&id.as_str()) {
let saddle = data.get_bool("Saddle").unwrap_or(false);
data.take("Saddle");
if !saddle {
return;
}
let mut saddle_item = NbtMap::new();
saddle_item.set_string("id", "minecraft:saddle");
saddle_item.set_i32("count", 1);
data.set_map("saddle", saddle_item);
set_guaranteed_saddle_drop_chance(data);
}
}),
);
// Reverse of the saddle structure converter (new -> old). The entity id is
// unchanged by the forward, so we branch on it exactly like the forward.
reg.entity.add_reverse_converter(
VERSION,
0,
Box::new(|data, _from, _to| {
let id = match data.get_string("id") {
Some(s) => s.to_string(),
None => return,
};
if SADDLE_ITEM_ENTITIES.contains(&id.as_str()) {
// Forward renamed `SaddleItem` -> `saddle` and added the saddle
// drop chance only when `SaddleItem` existed. Undo both: rename
// `saddle` back and drop the added drop-chance. Lossless — the
// new `saddle` slot uniquely encodes the old `SaddleItem`.
if !data.has_key("saddle") {
return;
}
data.rename_key("saddle", "SaddleItem");
remove_guaranteed_saddle_drop_chance(data);
} else if SADDLE_FLAG_ENTITIES.contains(&id.as_str()) {
// Forward replaced the boolean `Saddle` flag with a `saddle` item
// (`{id:minecraft:saddle, count:1}`) + drop chance when set, and
// always removed `Saddle`. Undo: presence of the `saddle` item
// means `Saddle=true`, absence means `Saddle=false`. Pigs/striders
// could only ever wear a vanilla saddle, so the item maps back to
// the boolean exactly (the old format always carried `Saddle`).
let had_saddle = data.has_key("saddle");
if let Some(saddle) = data.get_map("saddle") {
if !is_canonical_saddle_item(saddle) {
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
"pig/strider saddle item data has no legacy boolean representation; restoring Saddle=true",
);
}
} else if had_saddle {
report_loss(
VERSION,
LossKind::UnsupportedInTarget,
Severity::Loss,
"non-compound pig/strider saddle value has no legacy boolean representation; restoring Saddle=true",
);
}
data.take("saddle");
remove_guaranteed_saddle_drop_chance(data);
data.set_bool("Saddle", had_saddle);
}
}),
);
// Inventory walkers (saddle moved out; `Items` is the carried inventory).
reg.entity
.add_walker(VERSION, 0, "minecraft:llama", item_lists(&["Items"]));
reg.entity
.add_walker(VERSION, 0, "minecraft:trader_llama", item_lists(&["Items"]));
reg.entity
.add_walker(VERSION, 0, "minecraft:donkey", item_lists(&["Items"]));
reg.entity
.add_walker(VERSION, 0, "minecraft:mule", item_lists(&["Items"]));
// Explicit no-op walkers (replace any earlier inventory walker).
let noop: super::super::engine::Walker = Arc::new(|_reg, _data, _from, _to| {});
reg.entity
.add_walker(VERSION, 0, "minecraft:horse", noop.clone());
reg.entity
.add_walker(VERSION, 0, "minecraft:skeleton_horse", noop.clone());
reg.entity
.add_walker(VERSION, 0, "minecraft:zombie_horse", noop);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dataconverter::registry::convert_entity_reverse;
#[test]
fn reverse_pig_custom_saddle_and_drop_chance_reports_losses() {
let mut data = NbtMap::new();
data.set_string("id", "minecraft:pig");
let mut saddle = NbtMap::new();
saddle.set_string("id", "minecraft:diamond");
saddle.set_i32("count", 2);
data.set_map("saddle", saddle);
let mut chances = NbtMap::new();
chances.set_f32("saddle", 0.5);
data.set_map("drop_chances", chances);
let report = convert_entity_reverse(&mut data, VERSION, VERSION - 1);
assert_eq!(report.loss_count(), 2);
assert_eq!(data.get_bool("Saddle"), Some(true));
assert!(data.get_map("drop_chances").is_none());
}
}