Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Design: travel and move hooks

Proposal for #1009, not an implemented API. This is a clean-room Horizon design from vanilla 26.3 and the requested use cases. There is no NeoForge counterpart. No other project's source informs this proposal. Code follows agreement on this note.

Moments and typed contract

Declare exactly two catalog moments: living_travel and entity_move. Both use HostSide with typed ServerHost or ClientHost access, never an unchecked side flag granting server writes. Entity reads and authoritative writes belong on typed entity/player handles; do not add entity-id movement methods to Host.

Travel fires once at the LivingEntity.travel boundary, after ordinary input and attribute resolution and player swimming preparation, before fluid/air/glide selection. Its owned snapshot contains a living-entity handle, optional player handle, level/entity epoch, input vector, velocity, orientation, grounded/fluid/ flying/gliding state, resolved attributes, simulation tick and action identity. The output is Continue, Cancel, or Replace { input, velocity }. Replacement supplies the branch's starting values; it does not replace the physics algorithm. Cancel skips this travel invocation, including its nested moves and flying velocity restoration, leaving its entry position and velocity intact. Earlier input/jump processing is not undone. Speed, jump, gravity and step remain attributes; neither hook adds multipliers or a parallel attribute system.

Move fires once per physical Entity.move invocation, including external pushes, before no-physics handling, piston limiting, stuck multipliers, edge backoff and collision. Snapshot includes entity handle, starting position/AABB, requested delta, velocity, typed cause (Self, Piston, ShulkerBox, Shulker or Vehicle), epoch and identity (connection epoch, tick, action sequence, invocation ordinal). Output is Continue, Cancel, or ReplaceDelta(Vec3). Cancel skips that invocation without changing position, velocity, collision flags or fall state and emits no displacement notice. Replacement still passes through all ordinary restrictions. Collision, step height, grounding, fall damage, restitution and movement emissions use the actual resulting displacement in vanilla order. Travel and move are distinct moments: a travel replacement can still encounter a move veto. No second hook is added for individual branches or replay.

Vectors use owned finite f64 components. Travel input components must lie in [-1,1]; replacement velocity and move delta components in [-64,64] blocks per tick or call. These are proposed mod-output budgets, not limits applied to vanilla Continue. Reject the entire malformed result (NaN, infinity, out-of-range, including negative magnitude budgets); signed negative vector components within range are valid. Reject rather than clamp, retain the preceding valid proposal, and diagnose once per listener/tick. Dependency-ordered listeners see the preceding valid proposal; Cancel is terminal. Panic or wasm trap rolls back that listener's writes, discards its result and disables it for the session; continue from the preceding proposal. Native abort cannot be recovered. Native and wasm otherwise share validation.

Ownership and route map

References verified at worktree HEAD 9556bd72a8; paths below are relative to crates/horizon-client/src/. These are current owners, not claims that hooks exist.

Route / ownerCurrent file:lineRequired integration
Player travel / air / glidemotion.rs:1487, :1167, :2187One travel decision before branch selection; water, lava, air, glide and creative flying covered
Physical player movementmotion.rs:2297, trait adapter :3039Shared move operation, retain cause through adapters
Shulker box push / piston limitmotion.rs:2292, :2492Same move moment before external-motion restrictions
Controlled window simulationgame/network/remote/mod.rs:7129Predict with client-side context
Local tick / replaymotion.rs:1367, lan/prediction.rs:900Same deterministic decision/apply path; replay is not a new action
Integrated, LAN and dedicated guest turnsgame/network/lan_actions/turns.rs:6, :18, :45Server context bound to acting guest, never host player identity
Action service and scoped fake actorsgame/modding/native_server.rs:538, :549Enter existing player action validation; physical motion subsequently uses shared hooks
Normal living mobsmobs/living.rs:1451, :1544Lift decision to game adapter; include mob air and fluid/glide equivalents, not just player motion
Shared air equations../horizon-world/src/living_air.rs:134Keep physics below game independent of Host

The last path is relative to the crates directory instead of the client source root: crates/horizon-world/src/living_air.rs:134 is the shared owner. Vehicle movement must use the entity move boundary for the vehicle itself; passenger positioning is not a second travel or physical move. Shulker pushes and other external physical displacement preserve their cause and invoke move even without travel. Teleports, packet corrections, remote interpolation and passenger seat assignment are position assignments, excluded from both hooks. Remote entities are not predicted locally; their authoritative server invokes the hooks. An action service request is not itself travel: do not manufacture movement on attack/use. Implementation must inventory vehicle and nonliving collision adapters before code lands and route their actual Entity.move equivalents through this boundary.

Transaction, prediction and replay

Use decide/fire/apply: lower physics prepares a bounded immutable decision, the game dispatches with Host, and apply revalidates entity existence, level epoch, position, velocity, attributes and collision/environment revision in the same transaction as listener writes. A changed precondition refuses the decision and rolls back that transaction; never recompute against changed state mid-callback. No listeners takes the existing path without snapshots, allocations or mod scans.

Integrate with the client action pipeline, preserving its input/action ordering and server validation. Negotiate movement-rule identity and deterministic configuration before enabling prediction. Server validates intent, attachment state, reach and environment independently; it never trusts a client's replacement delta. Server simulation is the sole authoritative result. Unmatched client rules disable custom prediction and await authoritative movement.

Record input, tick/action identity, mod rule version and deterministic attachment state with each prediction checkpoint. Corrections restore those values and rerun the same rules against corrected environment snapshots. Do not reuse outputs from a divergent environment. Predicted callbacks may update only rollback-owned state; server writes remain server-only. Sound, particles, packets, damage and other external effects commit through an identity-deduplicated effect journal, never inside replay callbacks. Server damage uses the authoritative collision result.

A grapple stores an anchor on the entity and contributes bounded velocity before travel; server checks anchor reach and obstruction. A glider replaces travel input and starting velocity while vanilla chooses the glide branch and applies gravity. Wall-running uses a recorded wall normal to remove velocity into the wall, then ordinary collision prevents penetration. Loss of anchor/wall invalidates the rule; client and server return Continue from the same rule rather than inventing a teleport. Each example uses attributes for speed/jump/gravity/step adjustments.

Acceptance matrix

These are implementation requirements, not tests claimed to have run. Geometric cases isolate move from travel acceleration and gravity.

FixtureConcrete expected result
Empty world, start (0,64,0), delta (1,0,0), ContinuePosition (1,64,0), one committed displacement notice
Same, CancelPosition (0,64,0), zero displacement notices
Same, ReplaceDelta (0,0,1)Position (0,64,1), one notice
AABB x extent [-0.3,0.3], solid wall plane x=1, delta (2,0,0), no step openingCenter x=0.7, right face x=1, zero penetration
Replacement NaN, infinity, 65 or -65 on one axisEntire result rejected; original delta (1,0,0) still yields (1,64,0)
Replacement (-1,0,0)Position (-1,64,0)
Listener panics after attachment writeWrite rolled back; original delta applied; listener disabled
Callback changes level epoch or starting positionStale decision refused; zero movement from that decision
Two identical corrected replays of fixed input sequenceFinal position/velocity and attachment bytes equal fresh authoritative simulation; exactly one external effect per action identity
Host window, LAN guest, dedicated player; native and wasmEach geometric case above has identical values and correct acting-player identity
No listeners: air, water, lava, glide, flying, piston, vehicleExact position/velocity/collision/fall-state equality with vanilla 26.3 oracle fixtures for the same inputs
Teleport/correction/interpolation/seat assignmentZero travel/move hook calls for the assignment itself

Vanilla was inspected with python3 tools/rea_vanilla.py method net.minecraft.world.entity.LivingEntity travel and the corresponding Entity move query using the prepared jars under /workspace/horizon. The former selects fluid, glide then air; the latter limits piston movement before collision and applies fall and emission behavior afterward. No vanilla implementation is copied here.

Open questions

  • Exact vehicle/nonliving adapter census is implementation work still needed. Recommend requiring a file:line route inventory and oracle fixtures before the hook implementation is approved; never ship player-only coverage under Entity.move.
  • Prediction checkpoint transport for mod attachment state is not specified by the existing action note. Recommend a negotiated, versioned bounded schema (64 KiB per entity checkpoint, reject overflow without prediction), with server snapshots and rule identity; agree its wire ownership before implementing prediction support.