Normalize saved boolean flags for boats, minecarts and cushions

Preserve live gravity and grounded state while encoding retained entity
flags with Entity.saveWithoutId's canonical bytes and optional defaults.

Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
This commit is contained in:
RLC248andCodex committed 2026-11-08 18:25:44 +01:00
1 parent c98a51a664
commit 63db5cdd81
7 files changed
+121 -4

No files matched your search

+34 -3
View File
@@ -59,13 +59,40 @@ pub fn loaded_motion(tag: &Tag) -> Vec3 {
pub fn rotation(tag: &Tag) -> [f32; 2] {
read_doubles::<2>(tag.get("Rotation")).map_or([0.0; 2], |r| [r[0] as f32, r[1] as f32])
}
/// `NumericTag.byteValue`'s nonzero test for entity NBT, defaulting to false.
/// `TagValueInput.getBooleanOr`: a numeric tag's byteValue is nonzero, with a false default.
pub fn flag(tag: &Tag, key: &str) -> bool {
// NBT floats floor before int narrowing; the final byte keeps the low bits.
tag.get(key)
.and_then(int_value)
.is_some_and(|value| value as i8 != 0)
}
/// `Entity.saveWithoutId`: write the movement flags and the flags retained
/// from `Entity.load` as booleans. Only OnGround and Invulnerable write false.
pub fn write_saved_flags(tag: &mut Tag, on_ground: bool, no_gravity: bool) {
let invulnerable = flag(tag, "Invulnerable");
let optional = [
("NoGravity", no_gravity),
("CustomNameVisible", flag(tag, "CustomNameVisible")),
("Silent", flag(tag, "Silent")),
("Glowing", flag(tag, "Glowing")),
("HasVisualFire", flag(tag, "HasVisualFire")),
];
let Tag::Compound(map) = tag else {
return;
};
map.insert("OnGround".into(), Tag::Byte(i8::from(on_ground)));
map.insert("Invulnerable".into(), Tag::Byte(i8::from(invulnerable)));
for (key, value) in optional {
let key = key.into();
if value {
map.insert(key, Tag::Byte(1));
} else {
map.remove(&key);
}
}
}
/// `Entity.saveWithoutId` fields every entity has, over the loaded compound.
struct Base<'a> {
id: &'a str,
@@ -3816,18 +3843,22 @@ pub fn painting_from_nbt(tag: &Tag) -> Option<([i32; 3], u8, String)> {
let facing = (number(tag.get("facing")).unwrap_or(0) & 3) as u8;
Some((pos, facing, text(tag.get("variant"))?))
}
/// Cushion.addAdditionalSaveData: the block it sits on and its colour.
/// `Cushion.addAdditionalSaveData`: the block it sits on and its colour.
pub fn cushion_to_nbt(c: &Cushion, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
// A block-attached cushion does not update these movement flags.
let on_ground = raw.is_some_and(|tag| flag(tag, "OnGround"));
let no_gravity = raw.is_some_and(|tag| flag(tag, "NoGravity"));
let base = Base {
id: "minecraft:cushion",
uuid,
position: c.position,
motion: Vec3::ZERO,
rotation: [c.yaw, 0.0],
on_ground: false,
on_ground,
fall_distance: 0.0,
};
let mut tag = base.tag(raw);
write_saved_flags(&mut tag, on_ground, no_gravity);
put(&mut tag, "block_pos", Tag::IntArray(c.pos.to_vec()));
put(&mut tag, "color", string(c.color_name()));
if let Some(name) = &c.custom_name {
+1
View File
@@ -253,6 +253,7 @@ impl Boat {
raw,
self.chest.then_some(&self.items[..]),
);
horizon_client::entity_nbt::write_saved_flags(&mut tag, self.on_ground, self.no_gravity);
if let Tag::Compound(map) = &mut tag {
map.insert("Fire".into(), Tag::Short(self.fire_ticks as i16));
}
@@ -478,7 +478,7 @@ impl Game<'_> {
}
Target::Boat(id) => {
// AbstractBoat.readAdditionalSaveData over Entity.load: the
// position, rotation, motion, NoGravity, Fire and a chest boat's items.
// position, rotation, motion, ground and gravity flags, Fire and chest items.
let (position, velocity, yaw) =
entity_nbt::boat_from_nbt(&tag).ok_or_else(stale)?;
let uuid = self.uuid_for(id);
@@ -494,6 +494,7 @@ impl Game<'_> {
b.previous_yaw = yaw;
b.pitch = entity_nbt::rotation(&tag)[1];
b.no_gravity = entity_nbt::flag(&tag, "NoGravity");
b.on_ground = entity_nbt::flag(&tag, "OnGround");
b.fire_ticks = crate::game::boats::saved_fire(&tag);
if b.chest {
b.items = save::decode_items(tag.get("Items"), crate::game::boats::CHEST_SLOTS);
@@ -2961,6 +2961,7 @@ impl Minecart {
uuid,
raw,
);
horizon_client::entity_nbt::write_saved_flags(&mut tag, self.on_ground, self.no_gravity);
if let Tag::Compound(map) = &mut tag {
// ContainerEntity.addChestVehicleSaveData belongs only to the
// container subclasses, never to a plain or specialized cart.
@@ -3059,6 +3060,7 @@ impl Minecart {
c.yaw = yaw;
c.previous_yaw = yaw;
c.no_gravity = horizon_client::entity_nbt::flag(tag, "NoGravity");
c.on_ground = horizon_client::entity_nbt::flag(tag, "OnGround");
c.pitch = horizon_client::entity_nbt::rotation(tag)[1];
c.previous_pitch = c.pitch;
// ValueInput's numeric getters take any NumericTag: getIntOr is its
@@ -855,6 +855,7 @@ impl Game<'_> {
let (wood, chest) = super::boats::any_boat_wood(k).unwrap_or(("oak", false));
let mut b = super::boats::Boat::new(id, std::sync::Arc::from(wood), position, yaw);
b.no_gravity = entity_nbt::flag(&tag, "NoGravity");
b.on_ground = entity_nbt::flag(&tag, "OnGround");
b.fire_ticks = super::boats::saved_fire(&tag);
b.pitch = entity_nbt::rotation(&tag)[1];
if chest {
+5
View File
@@ -6397,6 +6397,10 @@ def groups():
from .vehicle_container_corpus import vehicle_containers
except ImportError:
from vehicle_container_corpus import vehicle_containers
try:
from .vehicle_boolean_corpus import vehicle_boolean_flags
except ImportError:
from vehicle_boolean_corpus import vehicle_boolean_flags
try:
from .data_literal_corpus import storage_literals
except ImportError:
@@ -6467,6 +6471,7 @@ def groups():
('spectral saved duration', spectral_saved_duration(cmd, setup)),
('item Health short load', item_health_load(cmd, setup)),
('vehicle container saved fields', vehicle_containers(cmd)),
('vehicle boolean save flags', vehicle_boolean_flags(cmd)),
('macro source type error', macro_source_type_error()),
('potion custom effect icons', potion_effect_icons(cmd, setup)),
('stand saved fall distance', stand_saved_fall()),
+76
View File
@@ -0,0 +1,76 @@
"""Entity.load and Entity.saveWithoutId on boats, minecarts and cushions (#12523)."""
OPTIONAL_FLAGS = ('NoGravity', 'Silent', 'Glowing', 'HasVisualFire', 'CustomNameVisible')
COMMON_FLAGS = ('Invulnerable', 'OnGround', *OPTIONAL_FLAGS)
def vehicle_boolean_flags(cmd):
# A remote forced chunk can unload independently of the connected owner.
ops = [cmd('/forceload add 4096 0', expect_result=1),
{'op': 'wait_chunks', 'chunks': [[256, 0]]},
cmd('/fill 4096 299 0 4111 299 15 stone', expect_result=256)]
def read_flags(target, label, enabled, *, on_ground=None):
saved = [f'Invulnerable:{int(enabled)}b']
if on_ground is not None:
saved.append(f'OnGround:{int(on_ground)}b')
if enabled:
saved.extend(f'{field}:1b' for field in OPTIONAL_FLAGS)
ops.extend([cmd(f'/data get entity {target} Invulnerable', expect_result=int(enabled)),
cmd(f'/execute if entity @e[tag={label},nbt={{{",".join(saved)}}}]',
expect_result=1)])
if enabled:
ops.append(cmd(f'/data get entity {target} NoGravity', expect_result=1))
else:
absent = ' '.join(f'unless data entity {target} {field}' for field in OPTIONAL_FLAGS)
ops.append(cmd(f'/execute {absent}', expect_result=1))
actors = []
case_id = 0x12523
# The last case stays in the world for live edits and a real chunk reload.
cases = [(None, False), ('0b', False), ('-1b', True), ('127b', True),
('256s', False), ('-0.5f', True), ('"invalid"', False), ('2b', True)]
for index, kind in enumerate(('minecart', 'hopper_minecart', 'spruce_boat', 'cushion'), 1):
x = 4096 + index * 3
y = 300 if kind == 'cushion' else 302
pos = f'{x}.5 {y} 4.5'
saved_pos = f'[{x}.5d,{y}d,4.5d]'
for serial, (value, enabled) in enumerate(cases, 1):
label = f'h12523_{index}_{serial}'
target = f'{case_id:08x}-0000-0000-{index:04x}-{serial:012x}'
fields = [f'UUID:[I;{case_id},0,{index << 16},{serial}]', f'Tags:["{label}"]']
if kind == 'cushion':
fields.append(f'block_pos:[I;{x},{y},4]')
if value is not None:
fields.extend(f'{flag}:{value}' for flag in COMMON_FLAGS)
ops.append(cmd(f'/summon minecraft:{kind} {pos} {{{",".join(fields)}}}',
expect_result=1))
read_flags(target, label, enabled, on_ground=enabled)
if serial == len(cases):
actors.append((kind, target, label, saved_pos))
else:
ops.append(cmd(f'/kill {target}', expect_result=1))
vehicles = [(target, pos) for kind, target, _, pos in actors if kind != 'cushion']
ops.append({'op': 'tick', 'n': 2})
for target, pos in vehicles:
ops.extend([cmd(f'/execute if data entity {target} {{Pos:{pos}}}', expect_result=1),
cmd(f'/data merge entity {target} {{NoGravity:0b}}', expect_result=1),
cmd(f'/execute unless data entity {target} NoGravity', expect_result=1)])
ops.append({'op': 'tick', 'n': 1})
for target, pos in vehicles:
ops.extend([cmd(f'/execute unless data entity {target} {{Pos:{pos}}}', expect_result=1),
cmd(f'/data merge entity {target} {{Pos:{pos},Motion:[0d,0d,0d],NoGravity:-1b}}',
expect_result=1),
cmd(f'/data get entity {target} NoGravity', expect_result=1)])
ops.extend([cmd('/forceload remove 4096 0', expect_result=1),
{'op': 'tick', 'n': 60},
cmd('/execute unless loaded 4096 300 0', expect_result=1),
cmd('/forceload add 4096 0', expect_result=1),
{'op': 'wait_chunks', 'chunks': [[256, 0]]}])
for kind, target, label, _ in actors:
read_flags(target, label, True, on_ground=True if kind == 'cushion' else None)
ops.append(cmd(f'/kill {target}', expect_result=1))
ops.append(cmd('/forceload remove 4096 0', expect_result=1))
return ops