Skip to content

Add BotPrePhysicsTick event for movement-assist plugins#313

Open
aquariusnetwork9 wants to merge 1 commit into
rfresh2:1.21.4from
aquariusnetwork9:feature/pre-physics-tick-hook
Open

Add BotPrePhysicsTick event for movement-assist plugins#313
aquariusnetwork9 wants to merge 1 commit into
rfresh2:1.21.4from
aquariusnetwork9:feature/pre-physics-tick-hook

Conversation

@aquariusnetwork9

Copy link
Copy Markdown

Summary

Adds BotPrePhysicsTick, a new plugin-facing event posted once per bot tick from Bot.tick(), at the exact point after the server's latest entity metadata sync for that tick has been applied (fall-flying included, via updateFallFlying()) but before the local physics/movement step runs.

This supports plugins that implement autonomous flight/movement modules — for example, a long-distance elytra autopilot — which need tick-precise control over physics-affecting state like fall-flying. If a plugin sets that state even one tick off from this exact seam, client-side prediction falls out of sync with what the server's own movement validation confirms a moment later, and the server corrects/rubberbands the player. Today the only way to get this ordering right is to fork Bot.java directly and splice private logic into the tick method, which means every plugin author who needs it has to maintain their own fork of the whole proxy. This PR replaces that with a proper, general extension point through the existing event-bus API (EventConsumer.of(...)), following the same pattern as the existing ClientBotTick event.

Changes

  • src/main/java/com/zenith/event/client/BotPrePhysicsTick.java (new): a zero-field record event, mirroring ClientBotTick's style/shape.
  • src/main/java/com/zenith/feature/player/Bot.java: posts BotPrePhysicsTick.INSTANCE via EVENT_BUS.post(...) at the seam described above (immediately after the existing jump/fall-flying handling block, before the if (isFlying) physics block).
  • src/main/java/com/zenith/feature/player/Bot.java: adds public void setFallFlying(boolean value). There was previously no public way to set this state — startFallFlying()/stopFallFlying() are package-private — so a plugin subscribed to the new event would otherwise have nothing to call. This is a thin wrapper around the two existing package-private methods; no new logic.

No existing behavior changes when nothing subscribes to the new event — this is purely an additive extension point.

Test plan

  • ./gradlew compileJava — compiles clean
  • ./gradlew shadowJar — builds successfully
  • Downstream plugin integration test (author-side, not included in this PR)

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.
@rfresh2

rfresh2 commented Jul 24, 2026

Copy link
Copy Markdown
Owner

why can't you just submit a jump input to start flying:

INPUTS.submit(InputRequest.builder()
    .owner(this)
    .input(Input.builder()
        .jumping(true)
        .build())
    .priority(priority)
    .build());

@aquariusnetwork9

aquariusnetwork9 commented Jul 24, 2026

Copy link
Copy Markdown
Author

Good question — the short answer is a plain jump input isn't enough, for two separate, code-level reasons:

  1. The vanilla-mirroring deploy check is edge-triggered. The existing movementInput.jumping && !didUpdateFlyState && !wasJumpPressed && ... && tryToStartFallFlying() check only fires on the jump press edgewasJumpPressed gets set to movementInput.jumping unconditionally at the end of every tick, so it can't re-arm while jump is held continuously across repeated cycles. It mirrors vanilla's "press jump once to deploy" behavior by design, which is the right default, but it structurally can't do a continuous per-tick redeploy.

  2. Even a direct redeploy call from a module's own tick handler races updateFallFlying(). Per the event-priority ordering, module ClientBotTick subscribers run before InputManager.handleTick and Bot.tick(). Inside Bot.tick(), updateFallFlying() unconditionally overwrites isFallFlying from the current entity-metadata cache. If the server's own clear packet for that flag lands in the window between a module's redeploy attempt and updateFallFlying() running, the redeploy gets silently stomped that same tick — no exception thrown, it just doesn't stick. We measured this directly: it produces a one-tick client/server divergence that caps sustained flight at a hard speed wall well before it should physically top out.

What's actually missing isn't a way to submit a jump — it's a seam to act after updateFallFlying() has consumed that tick's server state but before the physics step runs. That doesn't exist on the public API today. BotPrePhysicsTick is exactly that seam, and setFallFlying() is the paired setter, since by definition this needs to bypass tryToStartFallFlying()'s canGlide()/!isFallFlying() gating — the whole point is overriding what that gate would otherwise block.

Happy to narrow scope if useful (e.g. trim the setter to the minimum surface). But this is solving a real tick-ordering gap, not standing in for input submission.

I'm looing to port the ebounce capabilities of my zenith fork to a straight plugin instead of using something totally different

@rfresh2

rfresh2 commented Jul 24, 2026

Copy link
Copy Markdown
Owner
  1. so this sequence?

vanilla (possible with current zenith)
tick 1: forward and jump from ground
tick 2: forward
tick 3: forward and jump to takeoff

and what you want to do:
tick 1: forward and jump from ground
tick 2: forward and jump to takeoff

  1. i dont think this is entirely true. packets and ClientBotTick are handled on the client tick thread. all ClientBotTick subs are invoked sequentially on the tick thread, no server packet will be handled during that sequence. event subs can choose order with priority, so they could be called after Bot.tick() if set to.

a server packet to stop flying (metadata or teleport) would be handled either after or before the ClientBotTick. which may be what you're getting confusing about?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants