mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 17:19:15 -10:00
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:
1 parent
c98a51a664
commit
63db5cdd81
7 files changed
+121
-4
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user