Fluid player correction contract — approved target, not implemented #
Status: This is an approved design target for the Fluid HEXWORK player correction. It is not an implementation report, a release note, or an acceptance result. No Fluid runtime, dependency, save migration, or schema change is delivered by this document.
Annotated baseline: local
legacy-grid-finalat the exact commit3f81e656bead175606b31a87cb15c9068f0d1c2b.Current executable: until implementation advances,
cargo buildstill produces the legacy executable. It does not implement the Fluid contracts below.
This document freezes the correction boundary so implementation can proceed in small, reviewable slices. “Approved target” means the contract is the intended behavior. “Implemented” means the current source and executable demonstrably provide that behavior; this document does not turn one status into the other.
1. Target versus actual status #
| Area | Approved Fluid target | Current local status |
|---|---|---|
| Actor authority | State::actors is the single authority for all actor transforms, in signed i64 millimetres |
Legacy grid authority remains in the executable |
| Simulation | 10 Hz, exactly 3 physics substeps per authoritative tick |
Legacy timing/runtime path remains active |
| Player geometry | 3000 mm/s; half-width/depth 300 mm, height 1800 mm; FULL AABB 600 × 600 × 1800 mm; 1 mm skin; 250 mm step |
Fluid geometry is not delivered |
| Heading | Integer heading with 4096 units per full turn |
Fluid heading contract is not delivered |
| Pacing | Action-paced default; realtime Tactical and realtime Continuous are independent policies, orthogonal to death | Existing executable is not a Fluid implementation |
| Effects and Inn | Explicit durations; pending descriptor final-tick ordering; resumable Inn with exact debt rules | Not delivered |
| Observations | Lit integer LOS terrain faces only, stale remembered terrain, unknown/blank presentation, and a north-up compact map | Not delivered |
| Persistence | Intentional future v5 schema break; no silent migration | Current formats and readers remain legacy |
| Local presentation | Optional local SDL2 0.38/fontdue 0.9.3 path is intended, not required for SSH |
Dependencies and local client are not delivered |
A build, unit test, screenshot, or successful legacy play session must not be reported as Fluid acceptance. The target remains open until the implementation and its human/runtime checks actually land.
2. Coordinate and actor authority contract #
2.1 One authority: State::actors #
State::actors is the one authoritative collection for all actors: the
player, NPCs, monsters, and every other actor kind. It owns each actor's
canonical transform and liveness. The player is not a special second authority.
Collision, movement, fall resolution, action effects, save/replay state, and
observations read State::actors or a checked snapshot of it. No player-only
position, NPC shadow transform, floating-point transform, or independently
mutable grid location may silently become authoritative.
The implementation may derive voxel candidates, render coordinates, projection DTOs, and broad-phase bounds, but those are projections. A projection must never write back a rounded or truncated position without an explicit checked operation. There is no wraparound, saturation, or “nearest valid cell” recovery for an invalid authoritative position.
2.2 Signed millimetres and Euclidean voxel floor #
Authoritative actor feet positions are signed i64 millimetres. One voxel is
1000 mm. Converting millimetres to a voxel index uses Euclidean floor,
never language integer truncation toward zero:
voxel(mm) = floor(mm / 1000)
Thus -1 mm maps to voxel -1, -1000 mm maps to -1, -1001 mm maps to
-2, 0 mm maps to 0, and 999 mm maps to 0. The implementation must
use an explicitly Euclidean operation such as div_euclid and test negative
boundaries.
The voxel index and its permitted margin are signed i32 with a one-cell guard
margin at each end: the usable canonical index domain is
[i32::MIN + 1, i32::MAX - 1]
The reserved edge cells are not valid occupancy/query targets. This margin is part of the v5 domain, not an implementation convenience. All actors and all voxel projections obey it, including negative coordinates and chunk edges.
2.3 Checked projection ordering #
A movement, collision, fall, or observation query computes its complete body,
skin, step, and one-cell-margin envelope in checked i128 intermediates before
narrowing to voxel indices:
- Start with the signed
i64millimetre authority inState::actors. - Add/subtract the full AABB,
1 mmskin, movement/fall step envelope, and required one-cell voxel margin ini128. - Reject arithmetic overflow/underflow and every envelope that could project
outside the usable
i32domain before indexing, allocation, or a world read. - Apply Euclidean floor and perform deterministic voxel queries/collision checks. Never wrap, truncate toward zero, or clamp an invalid coordinate.
A valid voxel projection is a checked read/validation result, never a second actor authority. The same rule applies at the minimum and maximum world edges, across negative chunks, during three-substep movement, and for LOS targets.
3. Fixed simulation and player geometry #
The Fluid simulation uses these exact constants:
| Contract | Value |
|---|---|
| Authoritative simulation rate | 10 Hz (100 ms per tick) |
| Physics substeps | Exactly 3 per authoritative tick |
| Player speed | 3000 mm/s |
| Player body half-width | 300 mm |
| Player body half-depth | 300 mm |
| Player body height | 1800 mm |
| Player full AABB | 600 × 600 × 1800 mm (full width × full depth × height) |
| Collision skin | 1 mm |
| Step height | 250 mm |
| Heading resolution | 4096 integer units per full turn |
At the nominal speed, one authoritative tick permits 300 mm of travel and
its three equal substeps permit 100 mm each before collision constraints.
Movement preserves the speed cap and makes collision/step decisions at
substep boundaries. Render cadence, terminal latency, and input-arrival timing
must not alter authoritative displacement.
Heading is a canonical integer with 4096 units per full turn. It is not a
floating-point render angle and is not inferred exclusively from movement.
Presentation yaw/compass state is separately defined in the map section below.
4. Pacing, actions, and death #
4.1 The three pacing policies #
The following table is normative. A policy changes when simulation work runs; it does not select or rewrite a death policy.
| Policy | Idle, look, and browse | Held/blocked movement and debt | Other exact behavior |
|---|---|---|---|
| Action (default) | Idle is frozen. Look and browse are free observational work. | A held or blocked move is still a 100 ms move attempt in all phases. Pending-action and fall debt continue their cadence. |
Accepted actions run according to their declared duration. |
| Tactical | Idle is live. An explicit pause is deferred until existing debt is settled. Browsing does not autopause. | A held or blocked move is still a 100 ms move attempt in all phases; pending-action and fall debt continue their cadence. |
Step is one wait (1 tick), while a chosen action runs for its whole declared duration. |
| Continuous | Idle is live. Menus request a system pause; noninteractive presentation does not pause the simulation. | A held or blocked move is still a 100 ms move attempt in all phases; pending-action and fall debt continue their cadence outside a system-paused menu. |
Realtime menu/system pause is a pacing behavior, not a death-policy choice. |
Look and browse never mutate the authoritative world merely because a view is open. In Tactical, “pause” is an explicit request with the stated debt barrier; opening a browser is not an implicit pause. In Continuous, only the explicit system pause for an interactive menu stops noninteractive work; ordinary noninteractive presentation remains live.
Death is orthogonal to all three policies. Story, Classic, and any later death policy retain their own lifecycle and recovery rules in every pacing policy. Death is an authoritative boundary: queued player work stops while the applicable death/lifecycle transition is resolved. A pacing mode must not hide, postpone, or resume a dead-branch action.
4.2 Exact action durations #
Durations are authoritative simulation ticks (100 ms each), not wall-clock
animation lengths, terminal frames, key-repeat intervals, or guessed defaults.
The approved table is:
| Action | Exact duration | Timing class |
|---|---|---|
| Walk / move | 1 tick |
World |
| Wait | 1 tick |
World |
| Pickup | 10 ticks |
World |
| Drop | 10 ticks |
World |
| Wield | 10 ticks |
World |
| Accept contract | 10 ticks |
World |
| Turn in | 10 ticks |
World |
| Service (every service other than Inn) | 10 ticks |
World |
| Transition | 10 ticks |
World |
| Attack | 10 ticks |
World |
| Identify | 20 ticks |
World |
| Inn/rest | authored current 8640 ticks |
Unattended |
| InspectBoard | observational; no committed action duration | Observation |
InspectBoard is an observational request, not a committed action. It records
no action event, consumes no simulation duration, changes no canonical hash,
and does not enter pending-action history. It is not a zero-duration committed
action. There is no zero-duration default: every committed action must
match an explicit positive row in the table (or a separately approved future
row), and an unknown/missing/negative/overflowing duration is invalid rather
than zero.
The v5 implementation must make action timing exhaustive: no wildcard timing arm, implicit fallback, or service-wide default may accidentally assign a new action a duration. Save/replay records carry enough explicit timing data to reproduce the same tick boundaries.
4.3 Pending descriptors and final-tick ordering #
A pending effect stores a small immutable descriptor: action/effect kind,
source identity, exact target identity/coordinates/instance ID as applicable,
start/final tick, and the approved parameters. It does not clone State, a
world image, or a target's mutable object. The descriptor is an intent to
revalidate, not permission to apply a stale effect.
At the descriptor's final tick, process the final current state in this order:
- Advance the world and all required Fluid substeps for that tick.
- Resolve world hazards, actor liveness, and death at the world boundary.
- If the actor/lifecycle is still permitted to continue, revalidate the pending descriptor against the final current State and the exact target identity/coordinates/instance ID.
- Apply the effect exactly once only after that revalidation, then commit the state/history/observation boundary.
A missing, moved, changed, dead, or otherwise stale exact target rejects or aborts the pending effect; it never retargets to another actor, item, cell, or service. Revalidation must not use a cloned State. If the world/death step reaches a death boundary, the dead branch's pending effect is aborted and is never applied or resumed; lifecycle handling cannot be bypassed by leaving a descriptor around.
4.4 Inn admission, unattended work, and debt #
Inn/rest is the authored-current 8640-tick unattended action and is
resumable, but it is not an unbounded fast-forward:
- Debit the Inn cost exactly once at successful action admission. A rejected admission does not debit; a later interruption, death, save failure, or reload does not refund or debit it again.
- Each unattended-work iteration advances at most 5 unattended ticks.
- The unattended whitelist is exactly: NPC intent clocks, canonical NPC locomotion, item clocks, corpse clocks, and gossip clocks.
- Normal hazards are excluded from unattended Inn work. Do not run normal hazard simulation as an accidental side effect of resting.
- Apply the rest effect exactly once, only when the full admitted action completes. Partial iterations do not rest repeatedly.
- Persist the remaining Inn debt/pending descriptor at the normal save boundary. Save/reload preserves that debt exactly: reload performs no refund, re-debit, repayment, or debt clearing, and resumes only the remaining work.
The final Inn completion follows the same world-then-death-then-final-effect ordering. If a death boundary aborts the dead branch, its pending Inn effect does not resume on resurrection; only pending state that was already part of the accepted Story checkpoint can be restored by Story rollback.
5. Terrain observation and lighting contract #
Fluid observation is a bounded, deterministic projection of terrain only. The observation memory contains no remembered actors, items, services, hazards, actor identities, item identities, service availability, or other gameplay facts. Such information must not be smuggled into a terrain observation cache.
5.1 Positive integer light and first-hit LOS #
A current terrain face is observable only through positive local light represented
as an integer and a first-hit integer LOS test. The LOS range is at most
48 m (48,000 mm). The canonical ray origin at an observation boundary is
the authoritative actor feet position plus 1600 mm on the vertical axis:
origin = actor_feet + [0 mm, 0 mm, 1600 mm]
At every canonical observation boundary, including spawn and arrival, perform
this observation omnidirectionally and independently of the camera. Camera
orientation cannot hide a required boundary sample or make unlit geometry
current. A first terrain-face hit within 48 m is the only terrain geometry
eligible for that sample.
Face targets are sampled 1 mm into the air from the terrain face. This
one-millimetre air offset makes the target side of a face unambiguous and is
part of the integer boundary/LOS contract. All range, boundary, light, and LOS
arithmetic is checked; floating-point camera/render values are not authority.
There is no live-geometry handle crossing into the projection. The renderer and map receive detached, bounded observation values; they cannot retain a pointer, reference, mutable handle, or lazy query into live geometry.
5.2 Observation states and stale memory #
| State | Required meaning and presentation |
|---|---|
| Lit | A current terrain face passed positive integer local light and first-hit LOS. It may be rendered as current terrain. |
| Remembered / stale | A terrain face observed earlier but not currently visible. It remains explicitly stale and is not refreshed by hidden world changes. |
| Unknown | No valid terrain observation has ever been obtained. Do not infer terrain, occupancy, light, safety, actors, items, or services. |
| Blank | No terrain face is returned for this presentation cell. Render blank; do not turn blank into guessed floor or an omniscient dump. |
Hidden changes stay stale. If terrain, light, occupancy, or a boundary changes while a face is hidden, its remembered value remains the old stale value until a new valid observation replaces it. Stale/unknown/blank values cannot authorize movement, collision, targeting, service use, or a current-world claim. The observation cache is bounded and is cleared or restamped only at its explicit lifecycle boundary.
6. North-up map and compass presentation #
The tactical map is NORTH-UP. North remains at the top; the map does not rotate with player heading, camera yaw, or compass yaw. The approved map sizes are:
| Terminal/layout | Map viewport |
|---|---|
Normal 80 × 24 terminal |
15 × 9 |
| Compact layout | 9 × 5 |
The map shows only permitted current/stale/unknown/blank terrain observations; it is not an omniscient geometry view and never becomes movement authority.
The compass's presentation yaw is separate from canonical actor heading. It may be changed freely and immediately for presentation, independently of any captured canonical heading. Canonical heading is not derived from movement exclusively, and a presentation compass change does not mutate actor authority, world direction, map north-up orientation, or replay state.
7. Story checkpoint rollback and epochs #
A Story checkpoint is a complete accepted rollback boundary. Story death rolls back all pacing state, all knowledge, and all pending state to the checkpoint, not merely player position and HP. This includes the selected pacing policy, cadence/debt/accumulator state, learned observation/knowledge state, and any pending descriptors that were part of the accepted checkpoint. After rollback, restamp a new epoch so a descriptor, target, observation, or queued input from the dead branch cannot pass as current.
Every action/effect admitted after that checkpoint on the now-dead branch is aborted and does not resume. In particular, a pending effect that was in flight on the dead branch is not replayed after resurrection. Only pending state that was already present in the checkpoint may be restored, and it must still obey final-current-State exact-target revalidation before any effect can apply.
8. Persistence, schema, and optional local presentation #
8.1 Future v5 schema break #
The Fluid authority, timing, pending descriptor, Inn debt, pacing, observation, and epoch contracts require a deliberate future v5 schema break. v5 is not emitted by the current executable, and no v4-to-v5 migration is authorized by this document. There must be no silent reinterpretation of legacy positions, actions, ticks, pending state, observations, hashes, or map orientation.
When implementation advances, incompatible prior artifacts must receive a clear version/compatibility result before their bodies are interpreted. Exact v5 fields, action tags, timing data, canonical ordering, bounds, epoch rules, limits, and old-format behavior must be reviewed and frozen together. A placeholder version bump is not a migration plan.
8.2 Optional local SDL2/fontdue path #
An optional local presentation path using SDL2 0.38 and fontdue 0.9.3
is intended for a later local development/client slice. It is not delivered,
not a dependency change in this documentation-only correction, and not a
requirement for the ordinary terminal/SSH path.
Ordinary terminal SSH remains the intended constrained-client route. It must not depend on SDL2, fontdue, a GPU, or a local graphical session. Conversely, terminal/SSH evidence does not prove that the optional local path exists or that Fluid implementation is complete.
9. Safe isolated baseline checkout/build #
Use a separate detached worktree. Do not replace the current checkout, alter its branch, migrate saves, push a branch, or treat a build as Fluid delivery. The annotated local baseline is the exact commit shown below, not a moving branch name:
# Run from the repository root; choose a disposable sibling path.
git worktree add --detach ../hexwork-fluid-player-legacy-grid-final \
3f81e656bead175606b31a87cb15c9068f0d1c2b
cd ../hexwork-fluid-player-legacy-grid-final
git rev-parse HEAD # must print 3f81e656bead175606b31a87cb15c9068f0d1c2b
cargo build --locked --release
./target/release/hexwork --help
The resulting binary is still the legacy executable until Fluid implementation
advances. Keep the worktree isolated and disposable; do not run git push,
git merge, git rebase, or a save/schema migration as part of this checkout
or build check. Remove only the disposable worktree when finished:
cd -
git worktree remove ../hexwork-fluid-player-legacy-grid-final
10. Acceptance boundary and open human check #
The earlier first-person acceptance failure remains preserved: the first-session observation reported that the advertised Contracts key terminated play. It is not reclassified as a pass by the Fluid target. The README normal-brightness first-person route is still pending human execution; screenshots, terminal identity/dimensions, color mode, and an explicit human pass/fail remain required. Automated tests and SVG/terminal artifacts cannot certify font or monitor readability or the complete HEXWORK experience.
No Fluid acceptance, v5 compatibility result, SDL2/fontdue delivery, ordinary SSH certification, or HEXWORK completion claim is made here. A pending effect that encounters death is aborted on the dead branch; it is never applied or resumed, including after Story resurrection or a later load.