Recode FallbackVehicleHandler

This commit is contained in:
Akkariin committed 2025-07-25 18:30:40 +08:00
1 parent 223726c90a
commit 7eb02ced23
1 file changed
+129 -78
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.common.fallback.verification;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
@@ -40,8 +41,9 @@ public final class FallbackVehicleHandler extends FallbackVerificationHandler {
private State state = State.WAITING;
private State nextState;
private int expectedKeepAliveId;
private int rotations, inputs, paddles, vehicleMoves;
private int checkedRound;
private double boatMotion, boatY = IN_AIR_Y_POSITION;
private @Nullable ExpectVehiclePacket expectVehiclePacket = null;
@RequiredArgsConstructor
private enum State {
@@ -54,6 +56,18 @@ public final class FallbackVehicleHandler extends FallbackVerificationHandler {
private final boolean inVehicle;
}
private enum ExpectVehiclePacket {
PADDLE_BOAT,
PLAYER_ROTATION,
PLAYER_INPUT,
VEHICLE_MOVE,
CLIENT_TICK_END;
public @NotNull String formattedName() {
return name().toLowerCase().replace('_', ' ');
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof KeepAlivePacket) {
@@ -68,60 +82,25 @@ public final class FallbackVehicleHandler extends FallbackVerificationHandler {
state = nextState;
nextState = null;
waitingForStateChange = false;
} else if (!waitingForStateChange) {
if (packet instanceof PaddleBoatPacket) {
if (state == State.IN_BOAT) {
paddles++;
}
} else if (packet instanceof VehicleMovePacket) {
if (state == State.IN_BOAT) {
final VehicleMovePacket vehicleMove = (VehicleMovePacket) packet;
// Check the Y position of the vehicle
checkState(vehicleMove.getY() <= IN_AIR_Y_POSITION, "bad vehicle y: " + vehicleMove.getY());
// Check the gravity of the vehicle
final double lastBoatMotion = boatMotion;
final double lastBoatY = boatY;
boatY = vehicleMove.getY();
boatMotion = boatY - lastBoatY;
final double predicted = lastBoatMotion - 0.03999999910593033D;
final double difference = Math.abs(boatMotion - predicted);
// Check if the difference between the predicted and actual motion is too large
checkState(difference < 1e-7, "bad vehicle gravity: " + predicted + "/" + boatMotion);
vehicleMoves++;
}
} else if (packet instanceof SetPlayerRotationPacket) {
if (state.inVehicle) {
rotations++;
// 1.21.2+ do not send PlayerInput packets when inside a vehicle.
// Handle it after SetPlayerRotationPacket to simulate vanilla behavior.
if (user.getProtocolVersion().greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21_2)) {
handlePlayerInput();
}
}
} else if (packet instanceof PlayerInputPacket) {
// 1.21.2+ send PlayerInput packets when the player starts sprinting, sneaking, etc.
if (state.inVehicle && user.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_2)) {
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
// Check if the player is sending invalid vehicle speed values
final float forward = Math.abs(playerInput.getForward());
final float sideways = Math.abs(playerInput.getSideways());
final float maxVehicleSpeed = /*user.isGeyser() ? 1 :*/ 0.98f;
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
handlePlayerInput();
}
} else if (packet instanceof SetPlayerPositionRotationPacket) {
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
handleMovement(posRot.getY(), posRot.isOnGround());
} else if (packet instanceof SetPlayerPositionPacket) {
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
handleMovement(position.getY(), position.isOnGround());
if (!state.inVehicle) {
expectVehiclePacket = null;
}
} else if (packet instanceof PaddleBoatPacket) {
onPaddleBoat((PaddleBoatPacket) packet);
} else if (packet instanceof VehicleMovePacket) {
onVehicleMove((VehicleMovePacket) packet);
} else if (packet instanceof SetPlayerRotationPacket) {
onPlayerRotation((SetPlayerRotationPacket) packet);
} else if (packet instanceof PlayerInputPacket) {
onPlayerInput((PlayerInputPacket) packet);
} else if (packet instanceof ClientTickEndPacket) {
onClientTickEnd((ClientTickEndPacket) packet);
} else if (packet instanceof SetPlayerPositionRotationPacket) {
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
handleMovement(posRot.getY(), posRot.isOnGround());
} else if (packet instanceof SetPlayerPositionPacket) {
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
handleMovement(position.getY(), position.isOnGround());
}
}
@@ -129,13 +108,14 @@ public final class FallbackVehicleHandler extends FallbackVerificationHandler {
user.delayedWrite(nextState == State.IN_BOAT ? SPAWN_BOAT_ENTITY : SPAWN_MINECART_ENTITY);
user.delayedWrite(SET_VEHICLE_PASSENGERS);
prepareForNextState(nextState);
expectVehiclePacket = isClientMovingVehicle() ? ExpectVehiclePacket.PADDLE_BOAT : ExpectVehiclePacket.PLAYER_ROTATION;
}
private void prepareForNextState(final @NotNull State nextState) {
this.nextState = nextState;
waitingForStateChange = true;
expectedKeepAliveId = RANDOM.nextInt();
rotations = inputs = paddles = vehicleMoves = 0;
checkedRound = 0;
user.delayedWrite(new KeepAlivePacket(expectedKeepAliveId));
user.channel().flush();
}
@@ -151,9 +131,12 @@ public final class FallbackVehicleHandler extends FallbackVerificationHandler {
private void handleMovement(final double y, final boolean isOnGround) {
// Make sure we're currently expecting movement
if (state.inVehicle || state == State.WAITING) {
if (state == State.WAITING) {
return;
}
checkState(!state.inVehicle && !waitingForStateChange,
"sending move packet while in vehicle");
expectVehiclePacket = null;
// Make sure the ground state and y position are correct
checkState(y <= boatY, "invalid y: " + y);
@@ -166,31 +149,99 @@ public final class FallbackVehicleHandler extends FallbackVerificationHandler {
}
}
private void handlePlayerInput() {
// 1.8 and below do not have PaddleBoat packets, so we simply exempt them from the PaddleBoat check.
// Clients also don't send PaddleBoat & VehicleMovePacket packets while riding minecarts.
if (user.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_9) || state == State.IN_MINECART) {
paddles++;
vehicleMoves++;
}
private void onPaddleBoat(final @NotNull PaddleBoatPacket packet) {
checkState(isClientMovingVehicle(), "received unexpected paddle boat");
checkPacketOrder(ExpectVehiclePacket.PADDLE_BOAT);
expectVehiclePacket = ExpectVehiclePacket.PLAYER_ROTATION;
}
// Check for packet order
checkState(rotations >= inputs,
"illegal packet order; i/r " + inputs + "/" + rotations);
checkState(paddles >= inputs,
"illegal packet order; i/p " + inputs + "/" + paddles);
checkState(vehicleMoves >= inputs,
"illegal packet order; i/v " + inputs + "/" + vehicleMoves);
private void onPlayerRotation(final @NotNull SetPlayerRotationPacket packet) {
checkPacketOrder(ExpectVehiclePacket.PLAYER_ROTATION);
inputs++;
// Check yaw/pitch?
// Check if we've received more than the minimum number of packets
final int minimumPackets = Sonar.get0().getConfig().getVerification().getVehicle().getMinimumPackets();
if (inputs > minimumPackets && rotations > minimumPackets
&& paddles > minimumPackets && vehicleMoves > minimumPackets) {
// Move on to the next stage
user.delayedWrite(REMOVE_VEHICLE);
prepareForNextState(state == State.IN_BOAT ? State.IN_AIR_AFTER_BOAT : State.IN_AIR_AFTER_MINECART);
if (user.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_2)) {
expectVehiclePacket = ExpectVehiclePacket.PLAYER_INPUT;
} else if (isClientMovingVehicle()) {
expectVehiclePacket = ExpectVehiclePacket.VEHICLE_MOVE;
} else {
expectVehiclePacket = ExpectVehiclePacket.CLIENT_TICK_END;
}
}
private void onPlayerInput(final @NotNull PlayerInputPacket packet) {
// 1.21.2+ send PlayerInput packets when the player starts sprinting, sneaking, etc.
if (user.getProtocolVersion().greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21_2)) {
return;
}
checkPacketOrder(ExpectVehiclePacket.PLAYER_INPUT);
// Check if the player is sending invalid vehicle speed values
final float forward = Math.abs(packet.getForward());
final float sideways = Math.abs(packet.getSideways());
final float maxVehicleSpeed = /*user.isGeyser() ? 1 :*/ 0.98f;
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
if (isClientMovingVehicle()) {
expectVehiclePacket = ExpectVehiclePacket.VEHICLE_MOVE;
} else {
finishRound();
}
}
private void onVehicleMove(final @NotNull VehicleMovePacket packet) {
checkState(isClientMovingVehicle(), "received unexpected vehicle move");
checkPacketOrder(ExpectVehiclePacket.VEHICLE_MOVE);
// Check the Y position of the vehicle
checkState(packet.getY() <= IN_AIR_Y_POSITION, "bad vehicle y: " + packet.getY());
// Check the gravity of the vehicle
final double lastBoatMotion = boatMotion;
final double lastBoatY = boatY;
boatY = packet.getY();
boatMotion = boatY - lastBoatY;
final double predicted = lastBoatMotion - 0.03999999910593033D;
final double difference = Math.abs(boatMotion - predicted);
// Check if the difference between the predicted and actual motion is too large
checkState(difference < 1e-7, "bad vehicle gravity: " + predicted + "/" + boatMotion);
if (user.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_2)) {
this.finishRound();
} else {
expectVehiclePacket = ExpectVehiclePacket.CLIENT_TICK_END;
}
}
private void onClientTickEnd(final @NotNull ClientTickEndPacket packet) {
if (expectVehiclePacket == ExpectVehiclePacket.CLIENT_TICK_END) {
finishRound();
} else if (!waitingForStateChange && state.inVehicle) { // Client can send end tick while not in vehicle
checkPacketOrder(ExpectVehiclePacket.CLIENT_TICK_END); // fail
}
}
private void finishRound() {
final int minimumPackets = Sonar.get0().getConfig().getVerification().getVehicle().getMinimumPackets();
if (++checkedRound > minimumPackets) {
// Move on to the next stage
expectVehiclePacket = null;
user.delayedWrite(REMOVE_VEHICLE);
prepareForNextState(state == State.IN_BOAT ? State.IN_AIR_AFTER_BOAT : State.IN_AIR_AFTER_MINECART);
} else {
expectVehiclePacket = isClientMovingVehicle() ? ExpectVehiclePacket.PADDLE_BOAT : ExpectVehiclePacket.PLAYER_ROTATION;
}
}
private boolean isClientMovingVehicle() {
return (waitingForStateChange && nextState.inVehicle ? nextState : state) == State.IN_BOAT
&& user.getProtocolVersion().greaterThan(ProtocolVersion.MINECRAFT_1_8);
}
private void checkPacketOrder(final @NotNull ExpectVehiclePacket current) {
checkState(expectVehiclePacket == current, expectVehiclePacket == null
? "sending vehicle packet while not in vehicle (" + current.formattedName() + ")"
: "expected " + expectVehiclePacket.formattedName() + " but got " + current.formattedName());
}
}