Send a player's own shoulder parrots to its client

The integrated server synced DATA_SHOULDER_PARROT_LEFT/RIGHT only to the
players around, so the owner's own model (third person) drew no parrot in
singleplayer and on servers hosted by this game. sendDirtyEntityData sends
the owner its own entity data too.
This commit is contained in:
DUzzL committed 2026-11-02 13:50:22 +01:00
1 parent 6ad45198ee
commit 7090fdd5cc
2 files changed
+41

No files matched your search

+4
View File
@@ -276,6 +276,9 @@ pub struct Guest {
/// The shared flags (Entity.DATA_SHARED_FLAGS_ID) the player's client was
/// last sent about itself.
pub sent_shared: i8,
/// The shoulder parrots (Player.DATA_SHOULDER_PARROT_LEFT and _RIGHT) the
/// player's client was last sent about itself.
pub sent_shoulders: [Option<i32>; 2],
/// Where the player last died (Player.lastDeathLocation), sent with the respawn.
pub last_death: Option<(String, [i32; 3])>,
/// The last action sequence to acknowledge at the next tick (-1: none).
@@ -450,6 +453,7 @@ impl Guest {
sent_xp: None,
sent_air: (300, 0.0),
sent_shared: 0,
sent_shoulders: [None; 2],
last_death: None,
ack_up_to: std::cell::Cell::new(-1),
sent_selected: 0,
+37
View File
@@ -346,6 +346,19 @@ fn shoulder_meta(state: &mut EntityState, shoulders: [Option<i32>; 2]) {
}
}
/// The shoulder parrot slots that differ from what the player's client was
/// last sent, as entity data: 0 for none, else the variant's id plus one.
fn shoulder_changes(now: [Option<i32>; 2], sent: [Option<i32>; 2]) -> Vec<(u8, i32, Value)> {
(19..)
.zip(now.into_iter().zip(sent))
.filter(|(_, (now, sent))| now != sent)
.map(|(index, (now, _))| {
let id = now.map_or(0, |v| v + 1);
(index, serializer::OPTIONAL_UNSIGNED_INT, Value::Id(id))
})
.collect()
}
impl Hub {
/// Player.getDisplayName: the name formatted for the player's team
/// (PlayerTeam.formatNameForTeam), suggesting `/tell NAME `, showing the
@@ -2672,6 +2685,17 @@ impl Hub {
values: vec![(0, serializer::BYTE, Value::Byte(shared))],
}));
}
// ServerEntity.sendDirtyEntityData: the parrots on the player's
// shoulders go to the player too, whose own model draws them.
let g = &mut self.guests[i];
if g.shoulders != g.sent_shoulders {
let values = shoulder_changes(g.shoulders, g.sent_shoulders);
g.sent_shoulders = g.shoulders;
g.send(&C::SetEntityData(play::SetEntityData {
id: g.entity_id,
values,
}));
}
// Sent when the health or the food level changes, or the saturation
// reaches or leaves zero; a dead player's health of 0 too.
let g = &mut self.guests[i];
@@ -3804,6 +3828,7 @@ impl Hub {
g.sent_xp = None;
g.sent_air = (300, 0.0);
g.sent_shared = 0;
g.sent_shoulders = [None; 2];
let (cx, cz) = g.chunk();
g.send(&C::SetChunkCacheCenter(play::SetChunkCacheCenter {
x: cx,
@@ -3974,6 +3999,18 @@ mod tests {
use super::*;
use crate::fast_hash::FxHashSet;
/// The owner's own shoulder parrots are sent when a slot changes.
#[test]
fn the_owner_is_sent_the_shoulder_slots_that_changed() {
assert!(shoulder_changes([None; 2], [None; 2]).is_empty());
let landed = shoulder_changes([Some(1), None], [None; 2]);
assert_eq!(landed.len(), 1);
assert_eq!((landed[0].0, &landed[0].2), (19, &Value::Id(2)));
let left = shoulder_changes([None; 2], [Some(1), Some(4)]);
assert_eq!(left.len(), 2);
assert_eq!((left[1].0, &left[1].2), (20, &Value::Id(0)));
}
/// The counts of what is stuck in a player are sent when set, and a
/// count back at zero is sent as the default.
#[test]