Merge pull request #9992 from UMCEKO/fix/tnt-owner-saved

Primed TNT saves and loads its owner
This commit is contained in:
Kacper authored and GitHub committed 2026-11-06 09:15:03 +01:00
commit 9f282e42ea
5 files changed
+108 -45

No files matched your search

+42 -1
View File
@@ -2260,7 +2260,9 @@ pub fn tnt_power(tag: &Tag) -> f32 {
}
/// `PrimedTnt.addAdditionalSaveData`: its `fuse` and, when it is not the
/// default, its `explosion_power`, and the encoded live block state.
/// default, its `explosion_power`, the encoded live block state, and its
/// `owner` (`EntityReference.store`). With no live owner, a loaded `owner`
/// stays as the raw compound has it.
#[allow(clippy::too_many_arguments)]
pub fn tnt_to_nbt(
position: Vec3,
@@ -2271,6 +2273,7 @@ pub fn tnt_to_nbt(
block_state: Tag,
rotation: [f32; 2],
uuid: [i32; 4],
owner: Option<[i32; 4]>,
raw: Option<&Tag>,
) -> Tag {
let mut tag = Base {
@@ -2288,8 +2291,15 @@ pub fn tnt_to_nbt(
put(&mut tag, "explosion_power", Tag::Float(power.to_bits()));
}
put(&mut tag, "block_state", block_state);
if let Some(owner) = owner {
put(&mut tag, "owner", Tag::IntArray(owner.to_vec()));
}
tag
}
/// `PrimedTnt.readAdditionalSaveData`: `EntityReference.read(input, "owner")`.
pub fn tnt_owner(tag: &Tag) -> Option<[i32; 4]> {
uuid_from_tag(tag.get("owner"))
}
/// `EvokerFangs.addAdditionalSaveData`: always save the live warmup counter.
/// Known Owner UUIDs use `EntityReference.store` encoding; unresolved
/// loaded Owner data remains in the raw compound.
@@ -3666,6 +3676,7 @@ mod tnt_power_tests {
[0.0, 0.0],
[1, 2, 3, 4],
None,
None,
);
assert_eq!(tnt_power(&saved), 10.0);
let plain = tnt_to_nbt(
@@ -3678,9 +3689,38 @@ mod tnt_power_tests {
[0.0, 0.0],
[1, 2, 3, 4],
None,
None,
);
assert!(plain.get("explosion_power").is_none());
}
/// `EntityReference.store(owner, output, "owner")`: the igniter's UUID as
/// an int array, read back by `EntityReference.read`; an owner the save
/// could not name keeps the loaded one from the raw compound.
#[test]
fn the_owner_round_trips_as_a_uuid() {
let save = |owner, raw: Option<&Tag>| {
let t = string("minecraft:tnt");
tnt_to_nbt(
Vec3::ZERO,
Vec3::ZERO,
false,
80,
4.0,
t,
[0.0; 2],
[1; 4],
owner,
raw,
)
};
let saved = save(Some([5, 6, 7, 8]), None);
assert_eq!(saved.get("owner"), Some(&Tag::IntArray(vec![5, 6, 7, 8])));
assert_eq!(tnt_owner(&saved), Some([5, 6, 7, 8]));
assert_eq!(tnt_owner(&save(None, None)), None);
assert_eq!(tnt_owner(&save(None, Some(&saved))), Some([5, 6, 7, 8]));
assert_eq!(tnt_owner(&save(Some([9; 4]), Some(&saved))), Some([9; 4]));
}
}
#[cfg(test)]
@@ -4504,6 +4544,7 @@ mod tests {
[90.0, 10.0],
[1; 4],
None,
None,
);
assert_eq!(rotation(&tnt), [90.0, 10.0]);
let mut stand = ArmorStand::new(1, Vec3::ZERO, 0.0);
@@ -201,19 +201,8 @@ impl Game<'_> {
crate::game::persist::with_raw(tag, self.persist.raw.get(&id))
}
Target::Tnt(id) => {
let uuid = self.uuid_for(id);
let t = self.tnts.iter().find(|t| t.id == id)?;
entity_nbt::tnt_to_nbt(
t.position,
t.velocity,
t.on_ground,
t.fuse,
t.power,
t.block_state_nbt(self.pack.assets.registry),
t.rotation,
uuid,
self.persist.raw.get(&id),
)
let i = self.tnts.iter().position(|t| t.id == id)?;
self.tnt_nbt(i)
}
Target::Falling(id) => {
let f = self.falling.iter().find(|f| f.id == id)?.clone();
@@ -967,19 +967,7 @@ impl Game<'_> {
if !self.may_enter(plan.dim) {
return;
}
let uuid = self.uuid_for(id);
let t = &self.tnts[i];
let tag = horizon_client::entity_nbt::tnt_to_nbt(
t.position,
t.velocity,
t.on_ground,
t.fuse,
t.power,
t.block_state_nbt(self.pack.assets.registry),
t.rotation,
uuid,
self.persist_raw(id),
);
let tag = self.tnt_nbt(i);
self.tnts.remove(i);
self.forget_entity(id);
self.portals.arrivals.push(Arrival { plan, tag });
+3 -17
View File
@@ -1799,23 +1799,9 @@ impl Game<'_> {
i: usize,
keys: &HashSet<ChunkKey>,
) -> Option<(ChunkKey, Tag)> {
let (id, at) = (self.tnts[i].id, self.tnts[i].position);
keys.contains(&chunk_of(at)).then(|| {
let uuid = self.uuid_for(id);
let t = &self.tnts[i];
let tag = entity_nbt::tnt_to_nbt(
t.position,
t.velocity,
t.on_ground,
t.fuse,
t.power,
t.block_state_nbt(self.pack.assets.registry),
t.rotation,
uuid,
self.persist.raw.get(&id),
);
(chunk_of(at), tag)
})
let at = self.tnts[i].position;
keys.contains(&chunk_of(at))
.then(|| (chunk_of(at), self.tnt_nbt(i)))
}
pub(super) fn save_boat(
&mut self,
+60 -1
View File
@@ -3,6 +3,7 @@
//! (flint and steel / fire charge).
use super::{Game, entity::Entity};
use horizon_client::entity_rules::EntityRules;
use horizon_client::lan::playerdata::uuid_ints;
use horizon_client::{
Result, collisions::SceneCollisions, items::ItemStack, mesher::Vertex, mobs::Who, text::Text,
};
@@ -282,7 +283,13 @@ impl Game<'_> {
t.position.z,
];
let bounds = t.bounds();
let (owner, guest, power) = (t.owner, t.guest, t.power);
let (id, owner, guest, power) = (t.id, t.owner, t.guest, t.power);
let (owner, guest) = match owner {
Some(_) => (owner, guest),
None => self
.loaded_tnt_owner(id)
.map_or((None, None), |(w, g)| (Some(w), g)),
};
let in_water = self.bounds_in_water(bounds);
self.tnts.remove(i);
// PrimedTnt.tick: with `tnt_explodes` off it goes away without exploding.
@@ -313,6 +320,57 @@ impl Game<'_> {
}
Ok(())
}
/// The saved compound of primed TNT `i`: chunk saves, `/data get`, portals.
pub(super) fn tnt_nbt(&mut self, i: usize) -> horizon_world::nbt::Tag {
let id = self.tnts[i].id;
let uuid = self.uuid_for(id);
let owner = self.tnt_owner_uuid(self.tnts[i].owner, self.tnts[i].guest);
let t = &self.tnts[i];
horizon_client::entity_nbt::tnt_to_nbt(
t.position,
t.velocity,
t.on_ground,
t.fuse,
t.power,
t.block_state_nbt(self.pack.assets.registry),
t.rotation,
uuid,
owner,
self.persist.raw.get(&id),
)
}
/// `EntityReference.of(owner)`: the UUID of the player or mob that lit it.
fn tnt_owner_uuid(&mut self, owner: Option<Who>, guest: Option<u32>) -> Option<[i32; 4]> {
match owner? {
Who::Player => match guest {
Some(conn) => self
.lan
.as_ref()?
.hub
.guest(conn)
.map(|g| uuid_ints(g.uuid)),
None => self.level.player_uuid,
},
Who::Mob(id) if self.mobs.iter().any(|m| m.id == id) => Some(self.uuid_for(id)),
Who::Mob(id) => self.persist.uuids.get(&id).copied(),
}
}
/// `PrimedTnt.getOwner` for TNT loaded with an `owner`: looked up when
/// asked, so a player who joined or a mob loaded since is found.
fn loaded_tnt_owner(&self, id: u32) -> Option<(Who, Option<u32>)> {
let uuid = horizon_client::entity_nbt::tnt_owner(self.persist.raw.get(&id)?)?;
let guest = self.lan.as_ref().and_then(|host| {
let mut guests = host.hub.guests.iter();
guests.find(|g| uuid_ints(g.uuid) == uuid).map(|g| g.conn)
});
if guest.is_some() {
return Some((Who::Player, guest));
}
if self.level.player_uuid == Some(uuid) {
return Some((Who::Player, None));
}
self.mob_by_uuid(uuid).map(|m| (Who::Mob(m), None))
}
/// `TntBlock.useItemOn`: flint and steel or a fire charge primes TNT.
pub(super) fn ignite_tnt(&mut self, stack: &ItemStack) -> Result<bool> {
let item = &*stack.item;
@@ -594,6 +652,7 @@ mod tests {
tnt.block_state_nbt(&registry),
[0.0, 0.0],
[221, 0, 0, 1],
None,
Some(&raw),
);
assert_eq!(saved.get("block_state"), Some(&expected));