Keep a move behind a dig and its arm swing for the next tick

The hub hands a dig (handlePlayerAction) to the game after its pass, and
a move queued right behind it waited for the next tick so it was not
checked against the block the player had just broken. A creative break
sends the arm swing (Punch) between the dig and the move, so the move
was handled in the same pass: the block was still there, the move was
refused and the player sent back. Digging straight down, the player
sank into the block, was put back on top of it a tick later, then fell.

Any move left in the queue now waits, with what is queued before it,
as vanilla handles the packets in order. Traced with horizon-ctl in a
creative world: the player rose back after 6 to 7 of 8 breaks before,
after none now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
RVZO6andClaude Opus 5.5 committed 2026-10-27 15:47:14 -04:00
1 parent 27c3559421
commit 32a0a11fed
2 files changed
+56 -5

No files matched your search

+8 -5
View File
@@ -2216,13 +2216,16 @@ impl Hub {
// game's, run after this pass. A move queued behind it is checked
// against the block the player just broke and falls through, so it
// would be sent back; it waits for the next tick, when the block is
// gone, as vanilla handles the two packets in order.
// gone, as vanilla handles the packets in order. So does everything
// queued before that move: the arm swing (Punch) comes between the dig
// and the move.
S::PlayerAction(p) if p.action <= 3 => {
events.push(HostEvent::Packet(conn, Box::new(S::PlayerAction(p))));
if matches!(
self.guests[i].inbox.front(),
Some(S::MovePlayerPos(_) | S::MovePlayerPosRot(_))
) {
if self.guests[i]
.inbox
.iter()
.any(|p| matches!(p, S::MovePlayerPos(_) | S::MovePlayerPosRot(_)))
{
break;
}
}
+48
View File
@@ -407,6 +407,54 @@ fn sprint_commands_survive_keyboard_input_changes() {
);
}
#[test]
fn a_move_behind_a_dig_and_its_swing_waits_for_the_next_tick() {
let Some(resources) = resources() else {
return;
};
let mut s = server(&resources);
let port = s.hub.lan.port;
let _bot = run(&mut s, move || connect(port, "Digger", true));
let deadline = Instant::now() + Duration::from_secs(5);
while s.hub.guests.is_empty() {
s.tick(&[]);
assert!(Instant::now() < deadline, "guest did not join");
thread::yield_now();
}
s.hub.guests[0].loaded = true;
let at = s.hub.guests[0].position;
let below = [
at[0].floor() as i32,
at[1].floor() as i32 - 1,
at[2].floor() as i32,
];
// A creative break as the client sends it in one tick: the dig, the arm swing,
// then the move into the block it predicted gone.
let inbox = &mut s.hub.guests[0].inbox;
inbox.push_back(S::PlayerAction(play::PlayerAction {
action: 0,
pos: below,
direction: 1,
sequence: 1,
}));
inbox.push_back(S::Punch(Empty));
inbox.push_back(S::MovePlayerPos(play::MovePlayerPos {
x: at[0],
y: at[1] - 0.0784,
z: at[2],
flags: 0,
}));
s.tick(&[]);
// The game breaks the block after the hub's pass: the move waits, or it would be
// checked against the block and sent back.
let g = &s.hub.guests[0];
assert_eq!(g.position, at, "the move was handled before the dig");
assert!(
matches!(g.inbox.back(), Some(S::MovePlayerPos(_))),
"the move left the queue"
);
}
#[test]
fn status_and_join() {
let Some(resources) = resources() else {