A retired fishing spot leaves its Content.Fish timer running for good #244
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
When a fishing spot retires under a player, its
World.Content.Fishtimer isleft running on that player for good. The loop never fires again, the skilling
clock stays pinned at
0— permanently due — nothing animates, andTimers.has?(player.timers, World.Content.Fish)answerstrueforever.Measured
A fly fisher at Barbarian Village, polled every four seconds:
World.TickSnapshot.npc(17914)isnil:World.FishSpotshad retired thatspot and stood its replacement at another tile under a new index. The state
above then held, unchanged, for eight minutes until the sitting was
stopped.
Why it should not be possible
World.Content.Fish.pass/3's first clause isnot standing?(action) -> halt(player), andhalt/1reachesstop/1, which isPlayer.clear_timer(player, World.Content.Fish). A loop that fired even onceafter the spot went would have cleared its own timer within a tick.
So the loop is not firing. The clock sitting at
0says the same thing fromthe other side:
Contract.Skilling.due?/1isclock == 0, so hadpass/3run it would have taken the
due?branch, rolled, and either landed a fish orbeen refused — and every refusal path halts. Nothing rolled, and nothing
animated, which
resume/2would have done.The suspicion is that the timer is driven from the pending interaction and the
engine drops that interaction when the NPC is retired, leaving the timer with
nothing to run it. Not confirmed — that is the first thing to check.
Why it matters beyond fishing
skilling?on the bot snapshot is exactlyTimers.has?(player.timers, Content.Fish)and friends, so a stale timer is abody that reports working while doing nothing.
Bot.Goals.FishandBot.Goals.GatherAndBankno longer depend on the answer — both now askwhether the target is still there before believing
skilling?— but thatis a consumer working around it, not a fix. Anything else that reads
skilling?, now or later, inherits the lie.The same shape should be checked for
World.Content.ChopandWorld.Content.Mine, whose nodes go by changing id rather than by vanishingfrom an index; it is not known whether their teardown has the same hole.
Fixed at the engine on
fix/long-legs-and-unreachable-banks(dea398d). The diagnosis in the issue was half right — the timer was left standing, but not because the interaction was dropped.The cause is the level-up box.
World.Player.runnable?/1refused to run a:normaltimer while any modal was open, and interface233is the one screen that arrives with no click behind it, so a body with nobody to press Continue held its gathering loop at zero for good. That also explains the clock:begin_tick/1holds the skilling clock under the same condition, which is why it read as permanently due rather than idle.The gate was wrong on its own terms. Every
:normaltimer in this world is a skilling loop —Chop,Mine,Fish,Farm,Light,Productionarm one and nothing else does — and every client input reachesinterrupt/1, which halts them. So by the time any screen a player opened is up, the loop it interrupted is already gone: the modal half of the gate could only ever be holding a loop the player never stopped, and the level-up box is the one screen that reaches it. Which is the screeninterrupt/1deliberately does not end the action on — "it would take a roll away as a reward for earning one" — and then the gate took every roll in the window anyway. Pausing a gathering loop is not a softer version of stopping it.So an unbidden modal no longer holds a timer. A delay still does, and so does every screen the player opened — the case that cannot arise.
The trap worth recording:
begin_tick/1had to move with it. Left behind, the timer would fire every tick against a clock nothing was spending — the loop animates,due?is never true, and the body fishes and catches nothing, which is a worse bug that looks like working. Both ask one predicate now, andWorld.SkillingTestasserts the cadence either side of it:[3, 3, 3, 3]under a bank screen,[2, 1, 0, 3]under the box.Chop and Mine, which the issue asked about: identical teardown (
not standing?(..) -> halt), identical:normaltimer, so they had the same hole and this fix covers them. No separate defect.Three existing tests had encoded the old rule as a property and were rewritten — the honest signal that this was behaviour rather than an oversight. The new end-to-end one is driven through
run_clocks/2rather thanFish.run/2on purpose: the loop has always halted itself the first time it runs against an empty slot, so calling the script directly passes against the bug. The hole was in the caller.Bot.Session.dismiss/1(#242) stays and is still needed: the box holds a:normalqueue entry, which is untouched and correct.