You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Interpolate tile-authoritative movement for actors and projectiles #13
This preserved product issue is now part of the fresh MIT replacement program. Final implementation owner: atrinik/client. Legacy C/SDL2, packet, global-state, and file-path details below are historical evidence only.
Replacement implementation contract
Preserve tile authority, interpolation, snap, latency, projectile, and disclosure rules. Use stable entity generations plus authoritative simulation ticks in Game Protocol 1; interpolation is presentation state and never changes committed session position.
New implementation and tests are independent MIT work unless an exact contribution by an approved MIT provenance grantor is admitted through the recorded file-level MIT grant. Preserve every player-facing, accessibility, disclosure, and performance design decision below.
Required verification
Test pure state/geometry/material/UI behavior headlessly where possible and run supported Linux/Windows integration paths.
Add bounded malformed, stale, lifecycle, resource-loss, and recovery cases appropriate to the owner.
Add shared Go/Rust protocol fixtures for authoritative fields; presentation never reconstructs hidden rules.
Use released shared renderer/protocol/toolkit contracts and wrapper-managed replacement scenarios.
Preserved product/design specification and historical implementation notes
Summary
Keep Atrinik's server-authoritative tile simulation and existing integer x/y/z object coordinates, but render players, monsters, and moving
projectiles at interpolated sub-tile positions between authoritative MAP
snapshots. Drive their walk/projectile animation from elapsed time and motion
progress so an 8 Hz server can look fluid at the client's 30/60/120 Hz present
rate without changing collision, pathfinding, combat, LOS, or map semantics.
This should be a classic client/server vertical slice, not client-authoritative
movement. Initial local movement begins when the server confirms the step;
client prediction and rollback are deliberately separate work.
Why the current client snaps
The codebase already has the two cadences needed for interpolation, but not the
entity model:
The server's default tick is 125 ms (MAX_TIME), or 8 simulation ticks per
second. socket_server_post_process() calls draw_client_map() and draw_client_map2() sends a MAP packet even when its level blocks contain no
changed tiles.
The client main loop can present at 30, 60, 120, or unlimited FPS, but map_animate() advances map animation counters only every 125 ms.
CLIENT_CMD_MAP is a delta of tile/layer appearance. The client MapCell
stores faces, animation flags/state, names, target data, and lighting at an
integer cell. display_mapscroll() immediately rotates the caches when the
local player changes tile, and map_draw_map() projects every object from
that cell's integer anchor.
There is no stable render identity for all moving objects. A living object's head->count is sent only through MAP2_FLAG2_TARGET, the local player's ID
is not sent, and effect-layer projectiles do not carry an ID. Matching a
clear on tile A to an appearance on tile B by face/layer would fail when two
identical monsters cross, entities swap tiles, a projectile changes facing,
or an object is replaced by another with the same art.
Projectiles remain ordinary authoritative objects. For example, common_object_projectile_process() and common_object_projectile_move() move arrows/spells by one integer tile via object_move_to(), so they can use the same presentation pipeline as actors.
The map widget caches a complete software surface and redraws it only when map_redraw_flag is set. Correct moving sprites also participate in the
unified cross-depth painter queue, lighting, walls/roof occlusion, names,
target HP, and effects; drawing them as a late overlay would be visibly
wrong.
The missing abstraction is therefore a short-lived client render entity with
stable identity and a time-ordered authoritative tile history. Fractional
coordinates belong only to that render state.
Target behavior
The server continues to resolve movement atomically on integer map tiles.
At render time, an actor/projectile has an interpolated foot/ground anchor
between its previous and current authoritative tiles. Eight-directional
movement follows the existing isometric projection.
The local player remains at the camera anchor during ordinary walking while
the world/camera offset eases by one confirmed tile. Remote actors and
projectiles move through the same interval relative to that camera.
Continuous movement is visually continuous. Stopping settles exactly on the
authoritative destination. A blocked move does not start a visual segment.
Teleports, new maps, authorization/visibility changes, incompatible physical
depth changes, large discontinuities, and stale histories snap instead of
animating through unknown or hidden space.
Walk/projectile animation is time-based and synchronized to segment progress.
Existing discrete sprite frames remain pixel art; this issue does not invent
frame morphing or new artwork.
Names, status icons, target bars/outlines, damage/kill annotations, glow,
lighting, hit regions, and debug bounds follow the interpolated render
transform.
The minimap, region map, pathfinding, target authority, FOW/LOS, and all
gameplay coordinates remain tile-based.
Protocol contract
Make one intentional classic protocol break and update all in-repository
producers/consumers rather than inferring identity client-side.
MAP snapshot timing
Add these fixed fields to every CLIENT_CMD_MAP header after the existing
player position/sub-layer fields:
simulation_tick uint32
tick_duration_us uint32
simulation_tick is the authoritative global_round_tag for the state being
serialized. tick_duration_us reflects the current server tick duration, so
the existing configurable --speed (and runtime speed changes) do not leave
the client assuming 125 ms. Repeated empty/delta MAP packets at the same tick
must not advance the snapshot timeline. Tick comparisons must be wrap-safe.
Define and enforce sensible duration bounds and reject truncated or invalid
timing fields before applying the packet.
Stable render entity identity
Add an extended MAP2 layer field, for example MAP2_FLAG2_ENTITY, containing:
entity_id uint32
motion_flags uint8
Use the server object's existing connection-visible head->count/tag_t as entity_id. Send it for every disclosed player, monster, and moving
projectile, including the local player. motion_flags should be a bounded bit
contract that at minimum identifies an interpolatable entity and the
client-controlled camera anchor; reserve explicit semantics for projectile or
snap-only presentation only if the renderer needs them. Unknown bits are an
error until defined.
Multi-part objects use the head ID and the existing part/quick_pos identity,
so every visible part receives one group transform. Add entity ID/flags to the
server's per-socket MapCell cache: replacing an object with another object
that has the same face and animation data must still emit a delta.
Layer clears and appearances for one simulation tick must be staged before
updating render histories. A clear at A plus the same entity ID at adjacent B
is one movement segment, not a despawn/spawn. An unmatched clear retires the
entity; an unmatched appearance creates it at the authoritative position
without flying in from guessed coordinates.
Specify direction, framing, byte order, field order, identity lifetime,
multi-part semantics, valid flags, bounds, clearing, map/depth transitions,
tick wrap, and malformed/trailing-data behavior in doc/ADS/ADS-2. Bump SOCKET_VERSION and update common/toolkit/socket.h, the server serializer and
cache, client parser/cache, tests, and any bot/protocol consumer together. Do
not retain a parallel identity-free MAP path.
Client architecture
1. Add a render-entity registry beside the tile cache
Keep MapCell as the latest authoritative disclosed tile state. Add a bounded
registry keyed by entity ID (and part where applicable) containing the current
visual properties plus at least two timestamped authoritative transforms:
The MAP parser should stage a complete framed update, validate it, then commit
tile changes and entity-history changes atomically. Shift histories with display_mapscroll() and map_level_scroll() just as map animations are
shifted today; clear them on new maps, teleports, disconnects, hard visibility
loss, and incompatible level transitions. Bound entries to disclosed entities
and retire absent histories promptly.
2. Use a small interpolation buffer, never unbounded extrapolation
Map monotonic client time to the server snapshot timeline and normally render
about one simulation tick behind the newest authoritative state. Interpolate
the projected foot anchor and elevation between two known states using a
well-defined linear or smoothstep curve. A small adaptive allowance for
measured arrival jitter is reasonable, but cap it tightly.
If the next snapshot is late, finish at the known endpoint and hold. Do not
keep extrapolating actors/projectiles through walls, undisclosed cells, or
future collisions. Snap on non-adjacent displacement, invalid history,
authorization loss, or a buffer overrun.
For the local player, a confirmed same-map step creates the equivalent inverse
camera/world offset after display_mapscroll() rotates the authoritative
cache, then interpolates that offset to zero. This makes the camera glide
without changing the player's tile or predicting whether a requested move will
succeed.
3. Integrate fractional anchors into the existing painter
Extend map_render_command_t (or its successor) with a fractional/fixed-point
world anchor and derive screen position, projected bounds, and painter keys
from the interpolated foot point rather than always from the destination
cell. Interpolate elevation on stairs/sloped floors from already authorized
source/destination support samples. Preserve the documented unified
back-to-front order across physical depths; an actor must pass behind/in front
of walls and roofs at the correct point in the segment.
Lighting may interpolate the two disclosed tile samples cosmetically, but it
must never infer visibility or a light source the server did not send. Target
and pointer hit testing should prefer the interpolated entity bounds and send
the stable ID plus its latest authoritative tile; the server continues to
validate identity, visibility, position, and range.
The current software renderer may initially rebuild the painter queue while
motion is active, provided profiling shows the supported client can maintain
its selected cadence. Set redraw/present continuously only while an entity,
camera segment, or time-based animation is active; idle scenes must retain the
current no-redraw behavior. atrinik/renderer#17 and atrinik/renderer#16 can later move the same transforms to
GPU/retained commands without changing simulation or protocol semantics.
4. Make moving animation elapsed-time based
Move actor/projectile playback away from the global 125 ms anim_last/anim_state scan. Keep the existing animation ID, facing, and bank
selection, but select frames from monotonic elapsed time:
movement-bank phase derives from distance/progress across consecutive
segments, so walk cycles do not restart on every tile;
a stop settles into idle cleanly, while a direction change selects the new
facing without changing authoritative position;
existing anim_speed can initially mean anim_speed * tick_duration for
idle/non-motion clips, removing the hard-coded client assumption;
projectile facing/spin/clip timing remains independent of render FPS;
Animation speed determines which discrete authored frame is shown, while the
movement transform updates every presented frame. More sprite frames or a new
clip metadata format can be separate content work.
Implementation sequence
Add deterministic tests/diagnostics around current MAP deltas, player cache
scrolling, moving identical actors, and projectiles; record packet,
map-render, and frame-time baselines at the default 8 Hz server rate.
Define snapshot timing and entity identity in ADS-2, bump the protocol, and
update the server cache/serializer and client parser transactionally.
Introduce the bounded render-entity history and cover spawn, adjacent move,
swap/crossing, removal, same-art replacement, multi-part, depth, map-scroll,
and discontinuity cases without fractional rendering.
Add remote actor/projectile interpolation, fractional painter keys,
elevation/lighting handling, annotations, and visual hit testing.
Add confirmed local-player camera interpolation and verify cardinal,
diagonal, blocked, continuous, path, connected-map, and teleport behavior.
Run protocol malformed-input tests, both native builds, server checks, live
high-jitter testing, and video/screenshot comparison; document the final
timing, registry, and authority boundaries.
Acceptance criteria
With the server at its default 8 Hz and the client at 60/120 Hz,
continuous players, monsters, and projectiles show monotonic intermediate
render positions between adjacent authoritative tiles instead of
teleporting once per MAP update.
Server object coordinates, collision, LOS/FOW, pathfinding, targeting,
combat, projectile hits/reflection/stopping, and map disclosure remain
integer-tile authoritative and behaviorally unchanged.
Local cardinal/diagonal walking and click paths glide the camera after
confirmation; rejected/blocked movement produces no false step, and
teleports/new maps snap cleanly.
Two identical entities can cross or swap tiles without changing identity,
merging, duplicating, flickering, or borrowing each other's animation.
Arrows and spell projectiles interpolate until the authoritative impact,
reflection, stop, removal, or conversion to a dropped item; no client
visual continues through a collision.
Names, target bars/outlines, status icons, damage text, glow, occlusion,
painter order, elevation, and lighting remain attached to the moving
entity and correct across stacked depths.
Clicking/targeting a visually interpolated actor resolves its stable ID
while the server still validates its latest authoritative tile and
visibility.
Long stalls, tick-duration changes, tick wrap, malformed deltas,
non-adjacent moves, visibility loss, connected maps, and depth changes
have deterministic bounded hold/snap behavior with no extrapolation into
undisclosed state.
Walk/projectile motion is render-FPS independent; changing among
30/60/120 FPS changes smoothness, not travel time or animation speed.
Idle scenes do not redraw continuously. Motion scenes meet the selected
cadence on a representative ordinary map and a +1/+2 stacked scene, or
measured renderer work is reduced through the retained/GPU path before
merge.
Packet size/rate, render-entity count, interpolation delay/underruns,
snap reasons, map traversal/sort time, drawn FPS, and frame time are
visible in diagnostics or the render profiler.
Round-trip and malformed-packet tests cover the new fields and atomic
commit behavior; both legacy C targets build warning-free, prepared
server checks pass, and live video demonstrates actor and projectile
motion under both low jitter and injected 50/150 ms jitter.
Out of scope
Client-authoritative movement, collision, projectile hits, input prediction,
rollback, or reconciliation beyond snapping to confirmed state.
Increasing the simulation tick rate as a substitute for interpolation.
Changing tile coordinates, save/map formats, movement balance, or actor and
projectile speed rules.
Skeletal animation, optical-flow/frame morphing, or authoring additional art
frames.
Important
This preserved product issue is now part of the fresh MIT replacement program. Final implementation owner:
atrinik/client. Legacy C/SDL2, packet, global-state, and file-path details below are historical evidence only.Replacement implementation contract
Preserve tile authority, interpolation, snap, latency, projectile, and disclosure rules. Use stable entity generations plus authoritative simulation ticks in Game Protocol 1; interpolation is presentation state and never changes committed session position.
New implementation and tests are independent MIT work unless an exact contribution by an approved MIT provenance grantor is admitted through the recorded file-level MIT grant. Preserve every player-facing, accessibility, disclosure, and performance design decision below.
Required verification
Preserved product/design specification and historical implementation notes
Summary
Keep Atrinik's server-authoritative tile simulation and existing integer
x/y/zobject coordinates, but render players, monsters, and movingprojectiles at interpolated sub-tile positions between authoritative MAP
snapshots. Drive their walk/projectile animation from elapsed time and motion
progress so an 8 Hz server can look fluid at the client's 30/60/120 Hz present
rate without changing collision, pathfinding, combat, LOS, or map semantics.
This should be a classic client/server vertical slice, not client-authoritative
movement. Initial local movement begins when the server confirms the step;
client prediction and rollback are deliberately separate work.
Why the current client snaps
The codebase already has the two cadences needed for interpolation, but not the
entity model:
MAX_TIME), or 8 simulation ticks persecond.
socket_server_post_process()callsdraw_client_map()anddraw_client_map2()sends a MAP packet even when its level blocks contain nochanged tiles.
map_animate()advances map animation counters only every 125 ms.CLIENT_CMD_MAPis a delta of tile/layer appearance. The clientMapCellstores faces, animation flags/state, names, target data, and lighting at an
integer cell.
display_mapscroll()immediately rotates the caches when thelocal player changes tile, and
map_draw_map()projects every object fromthat cell's integer anchor.
head->countis sent only throughMAP2_FLAG2_TARGET, the local player's IDis not sent, and effect-layer projectiles do not carry an ID. Matching a
clear on tile A to an appearance on tile B by face/layer would fail when two
identical monsters cross, entities swap tiles, a projectile changes facing,
or an object is replaced by another with the same art.
common_object_projectile_process()andcommon_object_projectile_move()move arrows/spells by one integer tile viaobject_move_to(), so they can use the same presentation pipeline as actors.map_redraw_flagis set. Correct moving sprites also participate in theunified cross-depth painter queue, lighting, walls/roof occlusion, names,
target HP, and effects; drawing them as a late overlay would be visibly
wrong.
The missing abstraction is therefore a short-lived client render entity with
stable identity and a time-ordered authoritative tile history. Fractional
coordinates belong only to that render state.
Target behavior
between its previous and current authoritative tiles. Eight-directional
movement follows the existing isometric projection.
the world/camera offset eases by one confirmed tile. Remote actors and
projectiles move through the same interval relative to that camera.
authoritative destination. A blocked move does not start a visual segment.
depth changes, large discontinuities, and stale histories snap instead of
animating through unknown or hidden space.
Existing discrete sprite frames remain pixel art; this issue does not invent
frame morphing or new artwork.
lighting, hit regions, and debug bounds follow the interpolated render
transform.
gameplay coordinates remain tile-based.
Protocol contract
Make one intentional classic protocol break and update all in-repository
producers/consumers rather than inferring identity client-side.
MAP snapshot timing
Add these fixed fields to every
CLIENT_CMD_MAPheader after the existingplayer position/sub-layer fields:
simulation_tickis the authoritativeglobal_round_tagfor the state beingserialized.
tick_duration_usreflects the current server tick duration, sothe existing configurable
--speed(and runtime speed changes) do not leavethe client assuming 125 ms. Repeated empty/delta MAP packets at the same tick
must not advance the snapshot timeline. Tick comparisons must be wrap-safe.
Define and enforce sensible duration bounds and reject truncated or invalid
timing fields before applying the packet.
Stable render entity identity
Add an extended MAP2 layer field, for example
MAP2_FLAG2_ENTITY, containing:Use the server object's existing connection-visible
head->count/tag_tasentity_id. Send it for every disclosed player, monster, and movingprojectile, including the local player.
motion_flagsshould be a bounded bitcontract that at minimum identifies an interpolatable entity and the
client-controlled camera anchor; reserve explicit semantics for projectile or
snap-only presentation only if the renderer needs them. Unknown bits are an
error until defined.
Multi-part objects use the head ID and the existing part/
quick_posidentity,so every visible part receives one group transform. Add entity ID/flags to the
server's per-socket
MapCellcache: replacing an object with another objectthat has the same face and animation data must still emit a delta.
Layer clears and appearances for one simulation tick must be staged before
updating render histories. A clear at A plus the same entity ID at adjacent B
is one movement segment, not a despawn/spawn. An unmatched clear retires the
entity; an unmatched appearance creates it at the authoritative position
without flying in from guessed coordinates.
Specify direction, framing, byte order, field order, identity lifetime,
multi-part semantics, valid flags, bounds, clearing, map/depth transitions,
tick wrap, and malformed/trailing-data behavior in
doc/ADS/ADS-2. BumpSOCKET_VERSIONand updatecommon/toolkit/socket.h, the server serializer andcache, client parser/cache, tests, and any bot/protocol consumer together. Do
not retain a parallel identity-free MAP path.
Client architecture
1. Add a render-entity registry beside the tile cache
Keep
MapCellas the latest authoritative disclosed tile state. Add a boundedregistry keyed by entity ID (and part where applicable) containing the current
visual properties plus at least two timestamped authoritative transforms:
The MAP parser should stage a complete framed update, validate it, then commit
tile changes and entity-history changes atomically. Shift histories with
display_mapscroll()andmap_level_scroll()just as map animations areshifted today; clear them on new maps, teleports, disconnects, hard visibility
loss, and incompatible level transitions. Bound entries to disclosed entities
and retire absent histories promptly.
2. Use a small interpolation buffer, never unbounded extrapolation
Map monotonic client time to the server snapshot timeline and normally render
about one simulation tick behind the newest authoritative state. Interpolate
the projected foot anchor and elevation between two known states using a
well-defined linear or smoothstep curve. A small adaptive allowance for
measured arrival jitter is reasonable, but cap it tightly.
If the next snapshot is late, finish at the known endpoint and hold. Do not
keep extrapolating actors/projectiles through walls, undisclosed cells, or
future collisions. Snap on non-adjacent displacement, invalid history,
authorization loss, or a buffer overrun.
For the local player, a confirmed same-map step creates the equivalent inverse
camera/world offset after
display_mapscroll()rotates the authoritativecache, then interpolates that offset to zero. This makes the camera glide
without changing the player's tile or predicting whether a requested move will
succeed.
3. Integrate fractional anchors into the existing painter
Extend
map_render_command_t(or its successor) with a fractional/fixed-pointworld anchor and derive screen position, projected bounds, and painter keys
from the interpolated foot point rather than always from the destination
cell. Interpolate elevation on stairs/sloped floors from already authorized
source/destination support samples. Preserve the documented unified
back-to-front order across physical depths; an actor must pass behind/in front
of walls and roofs at the correct point in the segment.
Lighting may interpolate the two disclosed tile samples cosmetically, but it
must never infer visibility or a light source the server did not send. Target
and pointer hit testing should prefer the interpolated entity bounds and send
the stable ID plus its latest authoritative tile; the server continues to
validate identity, visibility, position, and range.
The current software renderer may initially rebuild the painter queue while
motion is active, provided profiling shows the supported client can maintain
its selected cadence. Set redraw/present continuously only while an entity,
camera segment, or time-based animation is active; idle scenes must retain the
current no-redraw behavior. atrinik/renderer#17 and atrinik/renderer#16 can later move the same transforms to
GPU/retained commands without changing simulation or protocol semantics.
4. Make moving animation elapsed-time based
Move actor/projectile playback away from the global 125 ms
anim_last/anim_statescan. Keep the existing animation ID, facing, and bankselection, but select frames from monotonic elapsed time:
segments, so walk cycles do not restart on every tile;
facing without changing authoritative position;
anim_speedcan initially meananim_speed * tick_durationforidle/non-motion clips, removing the hard-coded client assumption;
clip while still inheriting the entity's interpolated transform.
Animation speed determines which discrete authored frame is shown, while the
movement transform updates every presented frame. More sprite frames or a new
clip metadata format can be separate content work.
Implementation sequence
scrolling, moving identical actors, and projectiles; record packet,
map-render, and frame-time baselines at the default 8 Hz server rate.
update the server cache/serializer and client parser transactionally.
swap/crossing, removal, same-art replacement, multi-part, depth, map-scroll,
and discontinuity cases without fractional rendering.
elevation/lighting handling, annotations, and visual hit testing.
diagonal, blocked, continuous, path, connected-map, and teleport behavior.
integrate the action-state precedence from Replace post-action cooldowns with server-authoritative wind-ups and timed animations server#25 if that work has landed.
Build the shared SDL3 and wgpu GPU renderer renderer#17's GPU renderer and Retain and incrementally invalidate GPU scene data renderer#16's retained representation when available.
high-jitter testing, and video/screenshot comparison; document the final
timing, registry, and authority boundaries.
Acceptance criteria
continuous players, monsters, and projectiles show monotonic intermediate
render positions between adjacent authoritative tiles instead of
teleporting once per MAP update.
combat, projectile hits/reflection/stopping, and map disclosure remain
integer-tile authoritative and behaviorally unchanged.
confirmation; rejected/blocked movement produces no false step, and
teleports/new maps snap cleanly.
merging, duplicating, flickering, or borrowing each other's animation.
reflection, stop, removal, or conversion to a dropped item; no client
visual continues through a collision.
painter order, elevation, and lighting remain attached to the moving
entity and correct across stacked depths.
while the server still validates its latest authoritative tile and
visibility.
non-adjacent moves, visibility loss, connected maps, and depth changes
have deterministic bounded hold/snap behavior with no extrapolation into
undisclosed state.
30/60/120 FPS changes smoothness, not travel time or animation speed.
cadence on a representative ordinary map and a +1/+2 stacked scene, or
measured renderer work is reduced through the retained/GPU path before
merge.
snap reasons, map traversal/sort time, drawn FPS, and frame time are
visible in diagnostics or the render profiler.
commit behavior; both legacy C targets build warning-free, prepared
server checks pass, and live video demonstrates actor and projectile
motion under both low jitter and injected 50/150 ms jitter.
Out of scope
rollback, or reconciliation beyond snapping to confirmed state.
projectile speed rules.
frames.
and retained-map work should consume this issue's render transforms when
available.
Related work
state; its retained packets are the natural efficient consumer of fractional
entity/camera transforms.
effects. This issue should use its monotonic timing owner if it lands first.
action animation can travel with the interpolated entity without coupling
action authority to movement presentation.
Protocol-epoch coordination
Coordinate this wire change through atrinik/atrinik#168's next classic protocol epoch with atrinik/server#26, atrinik/server#25, atrinik/atrinik#156, #13, #9, #7, and atrinik/server#6 where practical. Do not reserve an isolated numeric version in advance. Land atrinik/atrinik#190's bounded packet primitives first where this payload uses them, then update all current producers, consumers, bots, fixtures, tests, and ADS-2 together.