From c37b120e8843d821839e744caf66a5ee8ab00057 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 24 Jul 2026 13:40:00 -0500 Subject: [PATCH] Add BotPrePhysicsTick event for movement-assist plugins Adds a new plugin-facing event, BotPrePhysicsTick, posted once per bot tick from Bot.tick() at the exact point after the server's latest entity metadata sync for this tick has been applied (fall-flying included, via updateFallFlying()) but before the local physics/ movement step runs. This supports plugins implementing autonomous flight/movement modules (e.g. a long-distance elytra autopilot) that need tick-precise control over physics-affecting state like fall-flying, so client-side prediction doesn't fall out of sync with the server's own movement validation by even one tick. Currently the only way to get this ordering right is to fork Bot.java directly and splice logic into the tick method; this event gives plugins a proper, general extension point through the existing event-bus API instead. Also adds Bot#setFallFlying(boolean), a small public wrapper around the existing package-private startFallFlying()/stopFallFlying(), since there was previously no public way for a plugin to set this state from an event handler. --- .../zenith/event/client/BotPrePhysicsTick.java | 9 +++++++++ .../java/com/zenith/feature/player/Bot.java | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/java/com/zenith/event/client/BotPrePhysicsTick.java diff --git a/src/main/java/com/zenith/event/client/BotPrePhysicsTick.java b/src/main/java/com/zenith/event/client/BotPrePhysicsTick.java new file mode 100644 index 000000000..699cffc25 --- /dev/null +++ b/src/main/java/com/zenith/event/client/BotPrePhysicsTick.java @@ -0,0 +1,9 @@ +package com.zenith.event.client; + +// Posted once per bot tick, after the bot has applied the server's latest entity metadata sync +// (e.g. fall-flying/pose/abilities) for this tick, but before the bot's local physics/movement step runs. +// Intended for plugins that need to influence tick-local physics-affecting state (e.g. fall-flying) at this +// exact point, so that client-side prediction stays consistent with what the server confirms a moment later. +public record BotPrePhysicsTick() { + public static final BotPrePhysicsTick INSTANCE = new BotPrePhysicsTick(); +} diff --git a/src/main/java/com/zenith/feature/player/Bot.java b/src/main/java/com/zenith/feature/player/Bot.java index 0547f5570..610a68e33 100644 --- a/src/main/java/com/zenith/feature/player/Bot.java +++ b/src/main/java/com/zenith/feature/player/Bot.java @@ -6,6 +6,7 @@ import com.zenith.cache.data.entity.EntityLiving; import com.zenith.cache.data.entity.EntityPlayer; import com.zenith.cache.data.inventory.Container; +import com.zenith.event.client.BotPrePhysicsTick; import com.zenith.event.client.ClientBotTick; import com.zenith.event.client.ClientTickEvent; import com.zenith.feature.spectator.SpectatorSync; @@ -374,6 +375,12 @@ && canPlayerFitWithinBlocksAndEntitiesWhen(getCollisionBox(Pose.SNEAKING)) if (movementInput.jumping && !didUpdateFlyState && !wasJumpPressed && !onClimbable() && tryToStartFallFlying()) { sendClientPacketAsync(new ServerboundPlayerCommandPacket(CACHE.getPlayerCache().getEntityId(), PlayerState.START_ELYTRA_FLYING)); } + // Extension point for plugins: the server's latest entity metadata for this tick (fall-flying included) + // has just been applied above via updateFallFlying(), but the physics/movement step below hasn't run yet. + // Plugins implementing their own movement/flight logic can hook this to adjust tick-local physics-affecting + // state (e.g. via setFallFlying) at the one point in the tick where doing so stays consistent with what + // the server will confirm next, instead of racing the next metadata sync. + EVENT_BUS.post(BotPrePhysicsTick.INSTANCE); if (isFlying) { int verticalDirection = 0; if (this.movementInput.isSneaking()) { @@ -1746,6 +1753,16 @@ boolean tryToStartFallFlying() { } } + // Public entry point for plugins (e.g. via a BotPrePhysicsTick subscriber) to set local fall-flying state + // directly, without going through tryToStartFallFlying()'s canGlide() gating. + public void setFallFlying(boolean value) { + if (value) { + startFallFlying(); + } else { + stopFallFlying(); + } + } + void startFallFlying() { var metadata0 = CACHE.getPlayerCache().getThePlayer().getMetadata().get(0); if (metadata0 instanceof ByteEntityMetadata bmd0) {