Skip to content

fix(eloot): v2.11.9 Hinterwilds sell run no longer walks you to Icemule for the pawnshop - #2473

Merged
mrhoribu merged 3 commits into
elanthia-online:masterfrom
Nisugi:fix/eloot-hinterwilds-pawnshop-trip
Sep 14, 2026
Merged

mrhoribu merged 3 commits into
elanthia-online:masterfrom
Nisugi:fix/eloot-hinterwilds-pawnshop-trip

Conversation

@Nisugi

@Nisugi Nisugi commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Reported: a character hunting out of Coldriver Village (the Hinterwilds) is now taken to Icemule Trace to sell, where eloot used to just skip the pawnshop because the town doesn't have one.

The Hinterwilds town (uid 7503205) has exactly three shop tags across its 246 rooms: a gemshop, a furrier, and the locksmith pool. Sell.go_sell has always known this and skipped pawnshop|collect|consignment|chronomage there unless "Sell in FWI" is on — that's the skip being remembered, and it still works.

The problem is that go_sell is not the only way to reach a shop. Several paths find one with their own find_nearest_by_tag(shop) call, which searches the entire map graph with no town or distance scoping, and then call ELoot.go2 directly. From the Hinterwilds, the nearest pawnshop is Icemule's room 2464.

Affected paths

Path Added How it's reached
Sell.retry_wrong_shop_jewelry_at_pawnshop v2.11.7 (#2453) every sell run
Sell.recheck_refused_at_pawnshop v2.10.0 (#2365) opt-in "Recheck gemshop-refused items"
Loot.sell_box_contents v2.11.8 (#2455) locksmith pool full-box recovery
Sell.custom_type ;eloot type <x>
Sell.custom_list ;eloot sell <item>

The first is the one being reported. Its trigger fits the location: the Hinterwilds gemshop is the only gem buyer in town, so any jewelry-tagged item the jeweler answers "that's not quite my field" to gets queued and dragged to Icemule. Before v2.11.7 the jewelry + Pawnshop guard in Sell.appraise stopped those items cold and they were simply stowed.

sell_box_contents is worth calling out: the Hinterwilds has a locksmith pool, so that recovery runs exactly where the bug lives. A pool box holding a pawnshop-only item walks you out of the area mid-hunt.

What changed

Guarding call sites one at a time is what caused this in the first place — v2.11.7 added a shop trip and forgot the check — so the guard is folded into the lookup rather than bolted onto each caller:

  • ELoot.shop_unavailable_in_town?go_sell's inline town check, extracted.
  • ELoot.nearest_shop(shop) — applies that guard, then find_nearest_by_tag. Returns nil for "don't go". Every path that travels to a shop now finds it through here, so a new sell path gets the town scoping by construction instead of having to remember a separate check.

The guard was deliberately not put inside ELoot.go2. That method is also called with bare room ids for banks, start rooms and locksmith pools, so a check there would silently skip legitimate travel while callers carried on as if they'd arrived.

Behaviour notes:

  • The two pawnshop retries keep two distinct outcomes. No pawnshop in town clears the queue, since it can never succeed there. No reachable pawnshop leaves it queued for a later run, which is the pre-existing behaviour.
  • "Sell in FWI" still overrides everywhere, so anyone who has opted into travelling to sell is unaffected.
  • Sell.collectibles was not reachable unguarded (both callers already filter), but it had its own unguarded lookup, so it is now safe from any future caller. It also returns instead of calling go2(nil) when nothing is reachable.
  • Incidental: go_sell is now nil-safe. The old inline check evaluated Room[nil].uid when no town could be found.

Test plan

  • ruby -c scripts/eloot.lic — Syntax OK
  • rubocop scripts/eloot.lic spec/scripts/eloot_spec.rb — no offenses
  • rspec spec/scripts/eloot_spec.rb — 165 examples, 0 failures (134 on master)
  • Mutation-checked both ways: reverting nearest_shop's guard fails the helper specs, and reverting sell_box_contents to its old lookup fails three of the new box examples. The new tests are not vacuous.
  • Verified against the shipped map data that the Hinterwilds carries only gemshop, furrier and locksmith pool tags, and that the nearest pawnshop is Icemule room 2464
  • Verified in-game: a live sell in the Hinterwilds stayed in the Hinterwilds. This is the reported bug, confirmed fixed on a real character.
  • Verified in-game: retrieving boxes from the locksmith pool in the Hinterwilds also stayed in the Hinterwilds. This exercises Loot.sell_box_contents, a separate code path from the sell run above, and it is the one the Hinterwilds actually triggers since the town has a locksmith pool.

New coverage: the shop_unavailable_in_town? predicate (each missing shop skipped, each present shop still routed, the FWI opt-in, no-town, argument handling), the nearest_shop lookup, four cases on the jewelry retry, and four on sell_box_contents confirming it no longer leaves town and still sells what the town can buy.

🤖 Generated with Claude Code

…or the pawnshop

The Hinterwilds town (Coldriver Village, uid 7503205) has only a gemshop, a
furrier and the locksmith pool. Sell.go_sell has always known this and skipped
pawnshop/collectibles/consignment/chronomage there, but the pawnshop retries
added in v2.11.7 do their own find_nearest_by_tag("pawnshop") lookup and call
go2 directly, bypassing that guard. find_nearest_by_tag searches the whole map
graph with no town or distance scoping, so the nearest pawnshop is Icemule
Trace's -- and anyone whose gemshop declined a jewelry-tagged item got walked
out of the Hinterwilds mid-hunt. Their only bail-out was pawn.nil?, which never
fires while any pawnshop is reachable anywhere.

Extracts go_sell's inline condition into ELoot.shop_unavailable_in_town? and
routes all three paths through it, so a future shop trip can't quietly
reintroduce the cross-continent walk. When it trips, the queued items are left
with your loot and the queue is cleared, matching the pre-2.11.7 behaviour.

Both retries are affected: retry_wrong_shop_jewelry_at_pawnshop always runs, so
it is the one being reported; recheck_refused_at_pawnshop has the same defect
behind the opt-in "Recheck gemshop-refused items at the pawnshop" setting.

Also makes go_sell nil-safe. The old inline check called Room[nil].uid when no
town could be found; the predicate returns false instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4b9a53a1-7796-41b5-86d1-4cbcaec7572e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…retries

Review caught that the first commit fixed the reported symptom and missed
three sibling paths with the identical defect. Guarding call sites one at a
time is what caused this bug in the first place -- v2.11.7 added a shop trip
and forgot the check -- so fold the guard into the lookup instead.

Adds ELoot.nearest_shop, which applies shop_unavailable_in_town? and then
find_nearest_by_tag, returning nil for "don't go". A new sell path now gets
the town scoping by construction rather than having to remember a separate
check.

Newly covered:

- Loot.sell_box_contents (v2.11.8, the locksmith pool full-box recovery).
  Picks shops via its own lookup and sorts them by dijkstra distance, never
  passing through go_sell. The Hinterwilds has a locksmith pool, so this
  fires exactly where the bug lives: a pool box holding a pawnshop-only item
  walked the character to Icemule mid-hunt.
- Sell.custom_type (;eloot type <x>). Dispatches to the shop methods
  directly, bypassing go_sell's filter entirely.
- Sell.custom_list (;eloot sell <item>). Called go2 on every location
  check_items returned, with no filtering at all.
- Sell.collectibles, which did its own unguarded lookup. Both callers
  already filter, so this was not reachable, but it is safe from any future
  caller now. Also returns instead of calling go2(nil) when nothing is
  reachable.

The two pawnshop retries keep their two distinct outcomes, which an earlier
pass in this branch had collapsed into one: no pawnshop in town clears the
queue (it can never succeed here), while no reachable pawnshop leaves it
queued for a later run, as it did before this branch.

go_sell now expresses its skip through the same lookup rather than a
separate guard line above it.

Specs: 165 examples, 0 failures (134 on master). Mutation-checked both ways
-- reverting nearest_shop's guard fails the helper specs, and reverting
sell_box_contents to its old lookup fails three of the new box examples.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread scripts/eloot.lic Outdated
Use ||= so re-running the script does not log "already initialized
constant" warnings (review feedback on elanthia-online#2473). The spec mirrors the
constant from source, so its extraction pattern follows suit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mrhoribu
mrhoribu merged commit 096a093 into elanthia-online:master Sep 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants