Conversation
Supervising scripts previously had only Script.running?('go2'), so a wedged
go2 (e.g. a search-gated exit while too wounded to search) looked identical
to one that was walking, and the restart loop never gave up.
- Go2.status struct: phase, blocked reason + failing command, destination,
rooms_left, last_room, last_progress_at, restarts, max_restarts, eta
- Go2.on_status / off_status hooks; Go2.last_result kept after exit
- --max-restarts=<N> (default 30, 0 = unlimited) with a clear give-up exit
- ;go2 status subcommand
- nested bank-detour go2 reports under the outer trip's status
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every status change (and the final :arrived/:failed/:aborted then :idle)
goes out as Events.emit('go2.status', Go2.status) when the Events primitive
exists (lich-5 PR elanthia-online#1619). Go2.on_status stays as the fallback for older Lich
and keeps working alongside it. Both routes share one notify path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…no idle emit - listeners (Events and on_status) receive st.dup.freeze, so a stashed payload keeps its emit-time state and no listener can alter go2's trip - the Events.emit call is rescued and logged, matching the local hooks, so a half-loaded or skewed Events cannot raise into the movement loop - status_finish emits only the terminal phase (:arrived/:failed/:aborted); the return to :idle is not a transition a supervisor can act on Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…esent Status gains a cause field. mark_blocked prefers the failure move() recorded during this step (exact line plus a classified :cause such as :injured, :encumbered, :position, :map; lich-5 PR elanthia-online#1622) and falls back to picking the line out of the buffer on a Lich whose move() predates that. A failure recorded before the trip, or already reported, is never reused. go2's own muckled wait reports cause :muckled. ;go2 status prints the cause. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mrhoribu
pushed a commit
to elanthia-online/lich-5
that referenced
this pull request
Sep 15, 2026
…ft it into Lich::Common::Move (#1622) ## Summary - bound every "fix the obstacle and re-send" branch in `move()`: `MAX_REMEDIES` (3) for remedies that should work first time (stand, unhide, retreat, empty hands, open, stow), `MAX_ROLLS` (20) for climb and swim skill rolls; roundtime waits stay uncapped since they always end - `Lich::Common::Move.last_failure` after a false or nil return: the direction sent, the game line that ended it, the attempt count, and a `:cause` from a small documented set (`:injured`, `:encumbered`, `:engaged`, `:position`, `:hidden`, `:hands`, `:closed`, `:map`, `:denied`, `:climb`, `:swim`, `:roundtime`, `:unknown`) so callers switch on it instead of matching text - a stand that keeps failing is attributed from character state (overburdened gives `:encumbered`, limb wounds give `:injured`), because the game says "You struggle, but fail to stand." for both - `move.failed` emitted on `Lich::Common::Events` when that module exists (#1619), rescued; nothing depends on it - `move` no longer mutates the caller's string: the climb/go swap and door renumbering used to rewrite wayto strings from the map database in place - the four exit paths share one finish lambda - remedies are sent through #1587's bounded `fput` (one resend, then a failure symbol), so `move` no longer carries its own copy of the refusal ladder - implementation lifted out of `global_defs.rb` into `Lich::Common::Move.move` (lib/common/move.rb) with a two-line top-level shim, the same carve-out pattern as ArgParser, OrderlyShutdown and Stash - first spec coverage for `move()`: 18 examples driving `Move.move` through a scripted fake stream ## Motivation Every recovery branch in `move()` follows one pattern: apply a remedy, re-send the direction, reset the give-up clock. That is right for obstacles the remedy actually fixes. It is wrong for stand: a stand that fails for wounds or encumbrance fails every time until something outside `move()` changes, and each try costs 10 seconds of roundtime. Seen in play: a prone, wounded character on a go2 trip. The game answered "You struggle, but fail to stand." then "Roundtime: 10 sec." on every attempt. `move()` never returned, so go2's step never failed, its restart counter never moved, and its status read `:moving` for as long as anyone cared to watch. The same shape exists in the unhide, retreat, empty-hands and climb branches; the stand case is the one with a reproduction. The failure reason is the second half. `move()` sees the exact line at the moment it matches. go2 (and any supervisor) currently has to scrape the buffer afterwards to guess why a step failed. Recording the line and a cause where it is matched means one classifier in one place, instead of every consumer regexing the same game text. ## What does not change - name, arguments, defaults - the tri-state return: true moved, false drop-the-exit, nil keep-the-exit; every branch returns what it returned before - every recognition regex, verbatim - the consumed stream is still restored to the script's downstream buffer on exit ## What does change, for callers that relied on it - a remedy that never works now returns nil after its budget instead of looping. A route that needed more than 20 consecutive failed climb rolls before one succeeded would now fail; `MAX_ROLLS` is the one constant to raise if such a route exists. - the caller's direction string is no longer rewritten. A script that read its own variable after `move` to learn the corrected verb would no longer see it. I found no such script in lich-5 or elanthia-online/scripts. - one new echo line when a budget runs out. ## Constant resolution after the lift Inside `Lich::Common::Move`, `XMLData` has no `Lich::Common::XMLData` to catch it and falls through to the top-level instance; `Script` and `Spell` resolve to the `Lich::Common` classes that `include Lich::Common` exposes at top level, so they are the same objects the old top-level method used. The spec found this the first time (the real `Lich::Common::Spell` was reached instead of a top-level stub) and now stubs both. ## Builds on #1587 (merged) Every remedy in `move` (stand, unhide, retreat, empty hands, open, stow, drag) goes through `fput(cmd, timeout: 3, max_resends: 1, failures: :symbol)` from #1587: one refusal-driven resend, then a failure symbol. On the pre-#1587 fput those options were silently ignored and the stand remedy recursed without limit, which is the P1 the first review of this PR found. Rebased onto `main` after #1587 landed; four commits. Independent of #1619. The `Events.emit` is behind a `defined?` guard. Once both land, go2's `mark_blocked` can read `Move.last_failure` instead of scraping the buffer (elanthia-online/scripts#2471). ## Review history - P1: the stand loop was still unbounded because remedies went through the old fput, which recurses on "You struggle, but fail to stand." Fixed by routing remedies through #1587's bounded fput. - P2: the shared skill-roll branch hardcoded `:climb` for swim, drag and guard lines. Fixed: the cause is taken from the matched line (`:swim`, `:drag`, `:denied`, else `:climb`). - P2: a remedy reply persisted across remedy kinds, so a stand that succeeded could be reported as the line for a later swim that failed. Fixed: replies are kept per remedy kind. ## Verification - `bundle exec rspec spec/lib/common/move_spec.rb`: 23 examples, 0 failures; with #1587's fput spec and `spec/lib/gemstone/`: 1022 examples, 0 failures - `bundle exec rubocop`: 1172 files inspected, no offenses - full suite: the same 14 environmental files (frontend, GTK, wine, Windows launcher) fail identically on pristine `main`, so none are introduced here - not yet exercised on a live character on this exact branch; the prone-and-wounded case above is the intended manual test 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Nisugi <nisugi-gs4@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
go2 v2.5.0: make a trip's progress observable to supervising scripts, and stop the restart loop from running forever.
Go2.statusstruct:phase, blockedreason, itscausesymbol, the failingcommand,destination,rooms_left,last_room,last_progress_at,restarts,max_restarts,eta,started_atgo2.statusonLich::Common::Eventswhen that primitive exists;Go2.on_statusis the fallback on older Lich, and both receive a frozen snapshotGo2.last_resultkeeps the final:arrived/:failed/:abortedstatus after the script exits--max-restarts=<N>(default 30, 0 = unlimited) bounds the restart loop with a clear give-up message naming the last blocker;go2 statusprints a one-line summaryMotivation
A script driving go2 had one signal,
Script.running?('go2'). Travelling out of [Hinterwilds, Beaten Path - 29882] while wounded, the exit's search failed with "You are in far too much agony to do that",go pathfailed, and go2 restarted itself roughly fifteen times over several minutes. Throughout, the supervisor saw "trip underway". A wedged go2 was indistinguishable from a walking one, and the loop had no exit other than success:error_countwas never compared to a ceiling and most$go2_restart = truesites never touched it.The blocked reason is captured from the game lines since the last room title, first failure line wins, so the root cause is reported rather than the cascade of "You can't go there" that typeahead sends afterwards.
Depends on
Draft until elanthia-online/lich-5#1619 (the
Lich::Common::Eventsprimitive) lands. The script runs fine without it: theEvents.emitis behind adefined?guard and rescued, andGo2.on_statusworks on any Lich. Once 1619 is released, supervisors should preferbecause that subscription is cleaned up automatically when the listening script dies.
And elanthia-online/lich-5#1622 for the
causefield: when that Lich'smove()recordsLich::Common::Move.last_failure, go2 takes the exact failing line and its classified cause (:injured,:encumbered,:position,:map, ...) from there instead of scraping the buffer, and;go2 statusprints it. On an older Lichcauseis nil and the buffer scrape still suppliesreason. go2 itself callsmovethrough the unchanged top-level name, so the bounded retries in #1622 apply to every go2 step with no change here.Behavior notes
status_finishemits only the terminal phase; the reset to:idleis silent since it is not a transition a supervisor can act on.error_countand its lag-check /timetologic are unchanged; the new ceiling counts restart cycles.Verification
ruby -candrubocop --only Lintcleanevents.rb, and a raisingEvents.emitbeing logged rather than propagated🤖 Generated with Claude Code