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
This issue is implemented in the fresh MIT-licensed Go server under the replacement program. Its gameplay and content-design decisions remain authoritative. C, CPython, classic packet, file-path, and enum details in the preserved specification are historical evidence only; do not copy, translate, or structurally port GPL implementation code.
Replacement implementation contract
Implement the central Go action scheduler with pure preflight, captured stable identities, atomic commit, cancellation, movement policy, and deterministic time. Generated action state drives Rust animation.
The server remains authoritative, consumes versioned compiled content, and exposes bounded generated Game Protocol 1 messages. Pure rules may use a specifically approved typed CEL environment. Starlark is not part of this issue unless the separate residual-scripting decision explicitly approves it.
Required verification
Preserve every observable rule, balance decision, disclosure boundary, and anti-exploit invariant from the specification below.
Add deterministic Go unit/property tests and wrapper-managed scenario coverage at the appropriate integration boundary.
Add bounded malformed-input and persistence-failure cases where this feature accepts content, network, or stored data.
Add Go/Rust protocol conformance fixtures for every new cross-process field; the client must not reconstruct authoritative rules from prose.
Demonstrate that implementation and tests contain no copied GPL source/test material and execute no runtime Python.
Preserved product/design specification and historical implementation notes
Summary
Replace the legacy player's post-action recovery timer with a server-authoritative pending-action/wind-up model.
Today, melee damage, spells, projectiles, and fired skills resolve immediately, and only then does player.action_attack prevent another skill-based action for the weapon/skill duration. This makes the same timer feel inconsistent: a two-second weapon locks out spellcasting for two seconds after the hit, while a roughly one-second spell can resolve immediately and be followed by another action about one second later.
The interval should instead precede the effect:
accept and validate an action request;
begin its wind-up and visible animation;
allow or prohibit movement according to the action type;
revalidate and resolve the effect exactly once when the wind-up completes; and
allow the next action immediately, unless that action explicitly defines a separate recovery phase.
The first required coverage is every player combat/skill path currently sharing action_attack: targeted melee plus SERVER_CMD_FIRE actions (direct spells, bows, thrown weapons, rods/wands, and fired skill objects). Ordinary movement, chat, inventory rearrangement, and NPC dialogue are not combat actions and should not be swept into this timer merely because some of them use speed_left or command delays.
Current behavior and code path
The current implementation is a cooldown, not a wind-up:
server/src/types/player.c checks global_round_tag >= pl->action_attack, calls skill_attack() immediately, and then sets action_attack = global_round_tag + weapon_speed.
server/src/socket/request.c:socket_command_fire() rejects the request while action_attack is in the future, calls object_ranged_fire() immediately, and then advances the deadline by the chosen skill time plus the returned weapon/spell delay.
A directly cast SPELL resolves through server/src/types/spell.c and cast_spell() before socket_command_fire() starts the timer. Wizardry currently contributes one tick and most basic spell entries contribute their spell_struct.time; at the default 125 ms server tick, a total of nine ticks is 1.125 seconds.
Weapon speed is already expressed in server ticks and is reported to the client as seconds (weapon_speed / MAX_TICKS). A 16-tick weapon therefore produces the observed two-second post-hit lockout.
CS_STAT_ACTION_TIME sends only a float countdown. The server does not decrement it; client/src/gui/widgets/playerinfo.c predicts the countdown locally and describes it as time before another skill action is allowed.
socket_command_fire() silently returns when the deadline is active. There is no explicit action phase, queued action, cancellation reason, or acknowledgement.
Movement is on a different mechanism:
incoming player commands are globally gated by object.speed_left, not action_attack;
direct movement and run-on consume speed_left, and path movement loops while speed remains;
none of socket_command_move(), run-on, or player_path_handle() consults action_attack, so movement remains possible during the current post-action delay;
a spell can be fired while run-on/path movement is active because firing has no stationary precondition.
The animation system cannot accurately present the intended behavior today:
attack_object() sets ANIM_FLAG_ATTACKING only when the hit is already being resolved;
server animation maintenance clears or retains that flag according to generic animation cadence and melee-range/enemy state, not the action deadline;
CLIENT_CMD_MAP carries animation speed, facing, flags, and movement state, but no action identity, type, duration, remaining time, or completion phase;
client/src/gui/widgets/map.c gives the attack bank priority over the movement bank and loops frames using the archetype's fixed anim_speed;
player animations under arch/intern/player/ use facings 25, which provides idle, movement, and attack banks. There is no casting bank, and server/src/server/anim.c currently accepts only 9 or 25 facings.
Consequently, the existing attack pose can loop independently of actual hits, cannot be stretched once across an arbitrary weapon duration, and cannot distinguish casting from melee.
Target gameplay semantics
Use one pending combat action per actor. Starting a second combat action while one is pending must be rejected explicitly; it must never replace the first action or execute early.
Action
Wind-up duration
Movement policy
Completion
Basic melee
Existing computed weapon_speed
Allowed
Revalidate actor, captured target, equipped source, PvP/friendship, and melee range; then call the normal attack path once
Direct spell
Existing skill time + spell_struct.time
Stationary; beginning the cast stops run-on/path movement, and later intentional or forced displacement interrupts it
Revalidate source, captured target, range, map restrictions, mana, and spell requirements; then spend mana and resolve once
Wand/rod magic
Existing skill time + device delay
Stationary by default, using the same configurable policy as spells
Revalidate the captured device and charge state; consume a charge only when the action successfully completes
Bow/thrown/fired skill
Existing computed skill + source delay
Preserve current mobile behavior initially; represent this as action metadata rather than hard-coding it
Revalidate the captured source/ammunition/target or direction; then create the projectile/effect once
Important rules:
Preserve current start-to-start/impact-to-impact balance intervals initially. This change moves the effect from the start of the interval to its end; it should not accidentally add the old cooldown after the new wind-up.
Auto-melee begins a wind-up when idle and a valid hostile target is in range. At completion, an invalid or out-of-range target produces no hit. If combat remains active and the target is still eligible, the next wind-up can begin immediately.
Melee movement remains fully accepted during the animation. Moving does not restart, shorten, or cancel the swing. The completion range check remains authoritative, so following a target can preserve the hit while moving away can cause it to miss/cancel.
Beginning a stationary cast clears run-on and the current click-to-move path. A later movement command intentionally interrupts the cast before moving; displacement caused by knockback, teleportation, map transfer, or another actor also interrupts it. Interrupted actions do not spend mana, charges, or ammunition and do not produce effects.
Capture stable object identities (source and target counts/tags), direction, timing, and any required action parameters at begin time. Never retain an unchecked raw object pointer across ticks. Changing targets after initiation must not retarget an in-progress action.
Revalidate mutable facts at completion. Equipment/source removal, death, disconnect, map transition, target destruction, loss of range, insufficient resources, or a newly forbidden map condition must cancel cleanly without an effect.
Confusion/randomized direction needs one documented sampling point. Prefer selecting and displaying the final direction at action start so the telegraph matches the eventual effect.
Action cancellation and completion must be idempotent. A command burst, tick-boundary race, or repeated map update must not resolve the effect twice.
The pending-action API should carry a movement policy (allowed, interrupts, and room for future blocked) so later techniques from atrinik/atrinik#138 can reuse the mechanism without duplicating timer logic.
Proposed server architecture
Introduce an explicit runtime-only action state and lifecycle, for example:
start and completion tick using wrap-safe comparisons;
total duration;
movement policy and start map/coordinates;
captured source identity, target identity, and direction;
only the bounded, typed parameters required to replay that action at completion.
Replace player.action_attack and its manually maintained action_timer with this state rather than leaving two competing authorities. Keep ordinary movement speed (speed_left) independent.
socket_command_fire() and auto-melee should become action producers, not immediate effect executors. The current APIs mix validation, mutation, resource spending, event dispatch, and effect creation, so split them into explicit preflight and commit stages where necessary:
preflight must be side-effect-free and produce the duration plus a bounded pending-action payload;
commit re-resolves captured identities, performs final validation, spends resources, and invokes the existing authoritative damage/spell/projectile machinery exactly once;
plugin/map spell events fire at commit time, not when the wind-up begins;
if commit validation fails, cancellation is observable to the initiating client and no resource is consumed.
Apply stationary-cast interruption at every movement seam, not just socket_command_move(): direct movement, run-on, click paths, and server-caused displacement/map changes must converge on the same cancellation helper. Do not freeze speed_left globally, because that would also prevent the explicitly mobile melee animation.
Monster melee and spellcasting currently use separate post-effect timers (weapon_speed_left and last_grace). The action state should be actor-capable rather than player-shaped. Migrating NPC actions to visible wind-ups is desirable for consistent telegraphs, but player paths are the required vertical slice; if NPC migration is split out, do not couple the new player authority back to the old monster cooldown fields.
Protocol and client animation
The server must replicate action state rather than asking clients to infer it from an attack flag.
Local player state
Replace the scalar CS_STAT_ACTION_TIME contract with a typed action-state stat containing:
kind = none clears the state and requires zero durations. Bound duration/remaining values, require remaining_ms <= total_ms, and reject malformed/truncated states. The client can interpolate between authoritative updates but must reset on a new sequence, cancellation, or completion. Update the HUD tooltip from “time before the next action” to the current action name and wind-up time/progress.
Visible actor state
Add an extended living-layer MAP2 field, for example MAP2_FLAG2_ACTION, carrying the same kind, sequence, total, and remaining values for an active action. Add the corresponding values to the server's per-socket map cache and the client's MapCell; a sequence/kind transition must force a layer delta. A viewer who first observes an already-running action receives its current remaining time. Do not send a countdown delta every tick merely to animate it.
This is an intentional classic protocol break: bump SOCKET_VERSION, update common/toolkit/socket.h, the server serializer/cache, client parser/storage, all fixtures/consumers, and doc/ADS/ADS-2 together. Specify field order, widths, limits, byte order, clearing behavior, and truncation/trailing-byte handling.
Animation playback
Action playback must be non-looping and progress-based:
select the clip from action kind, not from ANIM_FLAG_ATTACKING alone;
compute the frame from normalized authoritative action progress so all frames span the complete wind-up, regardless of the archetype's ordinary anim_speed;
a late observer starts at the matching progress instead of replaying from frame zero;
the action clip has presentation priority over movement, but actor coordinates continue updating, so a melee swing visibly travels with the actor;
cancellation returns immediately to movement or idle; completion reaches the final/impact frame once and then returns;
local expiry is a visual fallback only. The server remains authoritative for damage, cancellation, and the ability to start another action.
Retire the migrated ANIM_FLAG_STOP_ATTACKING/enemy-range heuristic as the source of player attack duration. It may remain temporarily for unmigrated NPC animation only, but should not compete with explicit action state.
Extend the authored extended-animation layout from 25 to 33 facings to add eight directional casting banks:
Update server validation, client clip selection, collectors/tests, and every playable character animation. Add properly licensed casting frames for the player races; do not claim the visual requirement is complete by silently looping idle frames. Animations that lack a casting bank need an explicit bounded fallback (for example the physical-action bank) until their authored art is upgraded, without changing action timing or authority.
Feedback and command handling
Starting an action should update the HUD and visible map animation immediately on the next server update.
Rejected overlapping action inputs should provide rate-limited feedback or an explicit busy result instead of disappearing silently.
Interrupted casts should show a concise reason (movement, displacement, source lost, target invalid, map transition, etc.) without chat spam during held-input repetition.
The client may suppress obviously redundant held-fire packets while its local authoritative action state is active, but the server must still enforce exclusivity.
Combat mode and click-path/run-on UI state must reflect the server clearing movement when a cast starts.
Implementation sequence
Add focused tests around the existing cooldown behavior and duration calculations to lock down the starting balance values.
Introduce the runtime action state, begin/interrupt/update/complete lifecycle, and typed preflight/commit payloads without changing visuals.
Migrate targeted melee and all SERVER_CMD_FIRE sources; remove action_attack/manual action-timer authority.
Enforce and test movement policy across direct, run-on, path, forced movement, transitions, death, and disconnect.
Add local and visible-actor protocol state, bump the socket version, update ADS-2, and add malformed-packet/late-observer tests.
Make client playback progress-based and non-looping while preserving movement under melee actions.
Add the 33-facing casting bank and playable-character casting art, then perform live visual/timing validation.
Decide and document NPC migration; remove any remaining player dependency on the legacy attack-animation heuristic.
Acceptance criteria
A two-second melee weapon begins a two-second swing immediately, deals no damage before completion, resolves at completion if the captured target is still valid/in range, and can begin the next swing without an additional two-second cooldown.
The player can move normally throughout that melee swing; the action animation remains active and follows the actor rather than switching to the movement bank or restarting.
A direct spell begins a visible wind-up of its existing calculated duration, produces no spell effect before completion, and spends mana only on successful completion.
Starting a cast stops run-on/path movement. Direct movement, resumed run-on/path movement, forced displacement, teleport, or map transition interrupts the cast with no effect or resource consumption.
Direct spells, bows, thrown items, rods, wands, and fired skill objects all use the common action lifecycle; none resolves immediately and then sets a post-effect action_attack deadline.
A pending action captures its source/target/direction, rejects overlap, revalidates at completion, and cannot execute twice under repeated input or tick-boundary conditions.
Target destruction, range loss, source removal/equipment change, death, logout, and insufficient completion-time resources cancel safely.
The local HUD identifies the current action and displays wind-up progress/time rather than post-action lockout time.
Nearby clients see one non-looping melee/cast animation spanning the authoritative duration. Late observers join at current progress, and cancellation clears the pose promptly.
Every playable character animation has a validated directional casting bank with preserved asset attribution; older 9/25-facing non-player animations follow the documented fallback.
The updated MAP/STATS parsers reject invalid kind values, zero/impossible durations, remaining values larger than total, truncation at every new field, and trailing malformed payload without partially applying state.
SOCKET_VERSION, doc/ADS/ADS-2, shared constants, server producer/cache, client parser/render state, tests, and any bot/protocol consumers are updated in one coherent breaking change.
Focused server tests pass, both legacy C targets build warning-free, content collection validates the new animation layout, and live screenshots/video demonstrate one roughly one-second cast and one roughly two-second mobile melee wind-up.
Out of scope
Rebalancing every weapon and spell duration as part of the timer inversion. Preserve current values first, then tune from playtesting.
Client-authoritative hit prediction or spell effects.
Turning ordinary movement, chat, inventory rearrangement, or NPC dialogue into wind-up actions.
Adding a permanent compatibility path for old socket versions or retaining action_attack beside the new authority.
A general skeletal animation system; this proposal extends the current directional sprite-bank model only as far as required for timed melee and casting clips.
Related work
atrinik/atrinik#138 can use the same action lifecycle and movement-policy metadata for active combat techniques and explicit recovery phases.
Important
This issue is implemented in the fresh MIT-licensed Go server under the replacement program. Its gameplay and content-design decisions remain authoritative. C, CPython, classic packet, file-path, and enum details in the preserved specification are historical evidence only; do not copy, translate, or structurally port GPL implementation code.
Replacement implementation contract
Implement the central Go action scheduler with pure preflight, captured stable identities, atomic commit, cancellation, movement policy, and deterministic time. Generated action state drives Rust animation.
The server remains authoritative, consumes versioned compiled content, and exposes bounded generated Game Protocol 1 messages. Pure rules may use a specifically approved typed CEL environment. Starlark is not part of this issue unless the separate residual-scripting decision explicitly approves it.
Required verification
Preserved product/design specification and historical implementation notes
Summary
Replace the legacy player's post-action recovery timer with a server-authoritative pending-action/wind-up model.
Today, melee damage, spells, projectiles, and fired skills resolve immediately, and only then does
player.action_attackprevent another skill-based action for the weapon/skill duration. This makes the same timer feel inconsistent: a two-second weapon locks out spellcasting for two seconds after the hit, while a roughly one-second spell can resolve immediately and be followed by another action about one second later.The interval should instead precede the effect:
The first required coverage is every player combat/skill path currently sharing
action_attack: targeted melee plusSERVER_CMD_FIREactions (direct spells, bows, thrown weapons, rods/wands, and fired skill objects). Ordinary movement, chat, inventory rearrangement, and NPC dialogue are not combat actions and should not be swept into this timer merely because some of them usespeed_leftor command delays.Current behavior and code path
The current implementation is a cooldown, not a wind-up:
server/src/types/player.cchecksglobal_round_tag >= pl->action_attack, callsskill_attack()immediately, and then setsaction_attack = global_round_tag + weapon_speed.server/src/socket/request.c:socket_command_fire()rejects the request whileaction_attackis in the future, callsobject_ranged_fire()immediately, and then advances the deadline by the chosen skill time plus the returned weapon/spell delay.SPELLresolves throughserver/src/types/spell.candcast_spell()beforesocket_command_fire()starts the timer. Wizardry currently contributes one tick and most basic spell entries contribute theirspell_struct.time; at the default 125 ms server tick, a total of nine ticks is 1.125 seconds.weapon_speed / MAX_TICKS). A 16-tick weapon therefore produces the observed two-second post-hit lockout.CS_STAT_ACTION_TIMEsends only a float countdown. The server does not decrement it;client/src/gui/widgets/playerinfo.cpredicts the countdown locally and describes it as time before another skill action is allowed.socket_command_fire()silently returns when the deadline is active. There is no explicit action phase, queued action, cancellation reason, or acknowledgement.Movement is on a different mechanism:
object.speed_left, notaction_attack;speed_left, and path movement loops while speed remains;socket_command_move(), run-on, orplayer_path_handle()consultsaction_attack, so movement remains possible during the current post-action delay;The animation system cannot accurately present the intended behavior today:
attack_object()setsANIM_FLAG_ATTACKINGonly when the hit is already being resolved;CLIENT_CMD_MAPcarries animation speed, facing, flags, and movement state, but no action identity, type, duration, remaining time, or completion phase;client/src/gui/widgets/map.cgives the attack bank priority over the movement bank and loops frames using the archetype's fixedanim_speed;arch/intern/player/usefacings 25, which provides idle, movement, and attack banks. There is no casting bank, andserver/src/server/anim.ccurrently accepts only 9 or 25 facings.Consequently, the existing attack pose can loop independently of actual hits, cannot be stretched once across an arbitrary weapon duration, and cannot distinguish casting from melee.
Target gameplay semantics
Use one pending combat action per actor. Starting a second combat action while one is pending must be rejected explicitly; it must never replace the first action or execute early.
weapon_speedspell_struct.timeImportant rules:
The pending-action API should carry a movement policy (
allowed,interrupts, and room for futureblocked) so later techniques from atrinik/atrinik#138 can reuse the mechanism without duplicating timer logic.Proposed server architecture
Introduce an explicit runtime-only action state and lifecycle, for example:
The state should include at least:
Replace
player.action_attackand its manually maintainedaction_timerwith this state rather than leaving two competing authorities. Keep ordinary movement speed (speed_left) independent.socket_command_fire()and auto-melee should become action producers, not immediate effect executors. The current APIs mix validation, mutation, resource spending, event dispatch, and effect creation, so split them into explicit preflight and commit stages where necessary:Apply stationary-cast interruption at every movement seam, not just
socket_command_move(): direct movement, run-on, click paths, and server-caused displacement/map changes must converge on the same cancellation helper. Do not freezespeed_leftglobally, because that would also prevent the explicitly mobile melee animation.Monster melee and spellcasting currently use separate post-effect timers (
weapon_speed_leftandlast_grace). The action state should be actor-capable rather than player-shaped. Migrating NPC actions to visible wind-ups is desirable for consistent telegraphs, but player paths are the required vertical slice; if NPC migration is split out, do not couple the new player authority back to the old monster cooldown fields.Protocol and client animation
The server must replicate action state rather than asking clients to infer it from an attack flag.
Local player state
Replace the scalar
CS_STAT_ACTION_TIMEcontract with a typed action-state stat containing:kind = noneclears the state and requires zero durations. Bound duration/remaining values, requireremaining_ms <= total_ms, and reject malformed/truncated states. The client can interpolate between authoritative updates but must reset on a new sequence, cancellation, or completion. Update the HUD tooltip from “time before the next action” to the current action name and wind-up time/progress.Visible actor state
Add an extended living-layer MAP2 field, for example
MAP2_FLAG2_ACTION, carrying the same kind, sequence, total, and remaining values for an active action. Add the corresponding values to the server's per-socket map cache and the client'sMapCell; a sequence/kind transition must force a layer delta. A viewer who first observes an already-running action receives its current remaining time. Do not send a countdown delta every tick merely to animate it.This is an intentional classic protocol break: bump
SOCKET_VERSION, updatecommon/toolkit/socket.h, the server serializer/cache, client parser/storage, all fixtures/consumers, anddoc/ADS/ADS-2together. Specify field order, widths, limits, byte order, clearing behavior, and truncation/trailing-byte handling.Animation playback
Action playback must be non-looping and progress-based:
ANIM_FLAG_ATTACKINGalone;anim_speed;Retire the migrated
ANIM_FLAG_STOP_ATTACKING/enemy-range heuristic as the source of player attack duration. It may remain temporarily for unmigrated NPC animation only, but should not compete with explicit action state.Extend the authored extended-animation layout from 25 to 33 facings to add eight directional casting banks:
Update server validation, client clip selection, collectors/tests, and every playable character animation. Add properly licensed casting frames for the player races; do not claim the visual requirement is complete by silently looping idle frames. Animations that lack a casting bank need an explicit bounded fallback (for example the physical-action bank) until their authored art is upgraded, without changing action timing or authority.
Feedback and command handling
Implementation sequence
SERVER_CMD_FIREsources; removeaction_attack/manual action-timer authority.Acceptance criteria
action_attackdeadline.SOCKET_VERSION,doc/ADS/ADS-2, shared constants, server producer/cache, client parser/render state, tests, and any bot/protocol consumers are updated in one coherent breaking change.Out of scope
action_attackbeside the new authority.Related work
Protocol-epoch coordination
Coordinate this wire change through atrinik/atrinik#168's next classic protocol epoch with #26, #25, atrinik/atrinik#156, atrinik/client#13, atrinik/client#9, atrinik/client#7, and #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.