feat(skilling): one shared clock for every skilling action #100

Merged
sickday merged 2 commits from feat/skilling-clock into main 2026-08-02 16:16:41 +00:00
Owner

Each gathering loop owned a private countdown -- Chop carried until_roll in
its own timer arguments, Mine the same, Production used the recipe's ticks as
a timer interval -- and timers are keyed by script module, so no two could see
each other. Tick manipulation was not merely unimplemented by that shape, it
was inexpressible: a delay item had nothing to shorten.

Contract.Skilling now owns a single per-player clock and World.Delay holds it,
as a second counter that locks nothing and gates nothing. The loops become
three-answer passes -- wait, complete, re-arm -- and the two arming rules
differ by exactly one tick, which is the whole mechanism: on the tick an
action completes, an inventory action may claim the clock and a gathering
action may not. A resource re-clicked into that window finds the clock already
counting and completes on the shorter count.

A "four-tick" action arms three, because the tick spent falling past zero is
the tick the re-arm happens on. World.Skilling enforces the other half, that a
player is doing one of these at a time, hung off Player.interrupt/1 -- with
being hit and levelling up deliberately routed to the narrower
interrupt_queue/1, since a hit that stops skilling breaks every flinch-based
rotation.

Production no longer makes its first item inline. A make that completes
instantly consumes no ticks, so nothing can be timed against it.

Two ordering differences from the reference engine had to be repaired, both
found by sitting with a real client and neither visible to the suite:

  • a timer armed after the tick's timer pass spends that pass on the fresh
    mark, and in a rotation that is the pass the clock is due on.
    Timers.set_late/4 arms without it.
  • the roll is taken in the timer phase where the reference takes it in the
    interaction phase, so a click landing on the due tick lost that roll.
    take_due_roll/3 takes it on arrival.

Also settles the animation handover. A tool sequence is priority 0 and a
fletching sequence is 5; the client refuses anything of strictly lower
priority than what it is showing, only a stop is unconditional, and a tick
carries one animation mask. Stop this tick and swing the next is therefore the
only order that shows anything at all.

Verified live: three-tick woodcutting, consecutive rolls 1.803s apart against
2.404s unmanipulated.

Each gathering loop owned a private countdown -- Chop carried until_roll in its own timer arguments, Mine the same, Production used the recipe's ticks as a timer interval -- and timers are keyed by script module, so no two could see each other. Tick manipulation was not merely unimplemented by that shape, it was inexpressible: a delay item had nothing to shorten. Contract.Skilling now owns a single per-player clock and World.Delay holds it, as a second counter that locks nothing and gates nothing. The loops become three-answer passes -- wait, complete, re-arm -- and the two arming rules differ by exactly one tick, which is the whole mechanism: on the tick an action completes, an inventory action may claim the clock and a gathering action may not. A resource re-clicked into that window finds the clock already counting and completes on the shorter count. A "four-tick" action arms three, because the tick spent falling past zero is the tick the re-arm happens on. World.Skilling enforces the other half, that a player is doing one of these at a time, hung off Player.interrupt/1 -- with being hit and levelling up deliberately routed to the narrower interrupt_queue/1, since a hit that stops skilling breaks every flinch-based rotation. Production no longer makes its first item inline. A make that completes instantly consumes no ticks, so nothing can be timed against it. Two ordering differences from the reference engine had to be repaired, both found by sitting with a real client and neither visible to the suite: - a timer armed after the tick's timer pass spends that pass on the fresh mark, and in a rotation that is the pass the clock is due on. Timers.set_late/4 arms without it. - the roll is taken in the timer phase where the reference takes it in the interaction phase, so a click landing on the due tick lost that roll. take_due_roll/3 takes it on arrival. Also settles the animation handover. A tool sequence is priority 0 and a fletching sequence is 5; the client refuses anything of strictly lower priority than what it is showing, only a stop is unconditional, and a tick carries one animation mask. Stop this tick and swing the next is therefore the only order that shows anything at all. Verified live: three-tick woodcutting, consecutive rolls 1.803s apart against 2.404s unmanipulated.
feat(skilling): one shared clock for every skilling action
All checks were successful
ci / gates (pull_request) Successful in 43s
5a0721914d
Each gathering loop owned a private countdown -- Chop carried until_roll in
its own timer arguments, Mine the same, Production used the recipe's ticks as
a timer interval -- and timers are keyed by script module, so no two could see
each other. Tick manipulation was not merely unimplemented by that shape, it
was inexpressible: a delay item had nothing to shorten.

Contract.Skilling now owns a single per-player clock and World.Delay holds it,
as a second counter that locks nothing and gates nothing. The loops become
three-answer passes -- wait, complete, re-arm -- and the two arming rules
differ by exactly one tick, which is the whole mechanism: on the tick an
action completes, an inventory action may claim the clock and a gathering
action may not. A resource re-clicked into that window finds the clock already
counting and completes on the shorter count.

A "four-tick" action arms three, because the tick spent falling past zero is
the tick the re-arm happens on. World.Skilling enforces the other half, that a
player is doing one of these at a time, hung off Player.interrupt/1 -- with
being hit and levelling up deliberately routed to the narrower
interrupt_queue/1, since a hit that stops skilling breaks every flinch-based
rotation.

Production no longer makes its first item inline. A make that completes
instantly consumes no ticks, so nothing can be timed against it.

Two ordering differences from the reference engine had to be repaired, both
found by sitting with a real client and neither visible to the suite:

  - a timer armed after the tick's timer pass spends that pass on the fresh
    mark, and in a rotation that is the pass the clock is due on.
    Timers.set_late/4 arms without it.
  - the roll is taken in the timer phase where the reference takes it in the
    interaction phase, so a click landing on the due tick lost that roll.
    take_due_roll/3 takes it on arrival.

Also settles the animation handover. A tool sequence is priority 0 and a
fletching sequence is 5; the client refuses anything of strictly lower
priority than what it is showing, only a stop is unconditional, and a tick
carries one animation mask. Stop this tick and swing the next is therefore the
only order that shows anything at all.

Verified live: three-tick woodcutting, consecutive rolls 1.803s apart against
2.404s unmanipulated.
Merge branch 'main' into feat/skilling-clock
All checks were successful
ci / gates (pull_request) Successful in 46s
ci / gates (push) Successful in 43s
0423a8018a
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Revenant/Server!100
No description provided.