19 Commits
Author SHA1 Message Date
max bittker fb26497163 Harden Miscellania transfers and add coin dupe regression coverage
Follow up kadeski’s fix in PR #73 with current-balance and inventory-capacity checks. Keep the original report and authorship credited in the vendor patch checklist. Exercise both reported exploit inputs in compiled-script tests, and build engine content for those tests in CI.
2026-10-11 19:37:42 -04:00
Kaden Staker ca0fe09cc8 fix: validate Managing Miscellania coffers deposit and withdraw amounts
Clamp a deposit to the coins held as well as the coffer space, and ignore a withdraw count of 0 or less. Listed in server/PATCHES.md until LostCity ships its own fix.
2026-10-11 17:29:39 -06:00
max bittkerandClaude Opus 5.5 68ff9ebf82 engine: add an optional live world feed for spectator viewers
A read-only /worldfeed websocket that streams the players and npcs in a
subscribed tile area every tick (positions, steps, anims, spotanims,
facing, hitmarks, chat, appearance), plus an optional online roster. It
drives the rs-world 3D viewer (github.com/MaxBittker/rs-world).

Off unless WORLD_FEED=true. WORLD_FEED_TOKEN requires ?token=, and
WORLD_FEED_MAX_CLIENTS caps connections (default 32). While no viewer is
subscribed the tick hook returns immediately. Hidden-visibility players
are never sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:10:56 -04:00
max bittkerandClaude Opus 5.5 93cd70b347 Tell clients on an old game revision how to update
After a revision upgrade, a lite runner from an old checkout fails login
with an opaque "Client out of date" and its scripts only report "No game
client is attached".

- Engine: on a revision mismatch, read the username from the login block
  (laid out the same across revisions) and record it for 30 minutes; a
  correct-revision login clears it. The management port serves it at
  /outdated-client.
- Gateway: when an SDK connects to a bot with no live client, ask the
  engine and answer sdk_error with the update steps. Old SDK checkouts
  print gateway errors, so this reaches people who haven't pulled yet.
- Public /api/version. The lite runner checks it before loading the
  cache and fails with OutdatedClientError (a LoginError, code 6) naming
  the fix; the runner and swarm exit 3 instead of retrying.
- CLAUDE.md troubleshooting entry for the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:53:12 -04:00
max bittkerandClaude Opus 5.5 b6714fa74e mapview: cover 289's map squares with non-overlapping bands
289 added Ape Atoll (rows 42-43), its dungeon (142-143) and squares at
raw row 69, which the old z remap either dropped or drew on top of the
underground. The bounds also skipped every edge square, so row 44 and
columns 55-56 were never drawn.

Replace the ad-hoc remap with a band table (overworld 42-62, underground
142-161 -> 63-82, misc 69-77 -> 83-91), widen the bounds to columns
29-56 with a padding square on each side, and leave rows outside every
band undrawn instead of placing them at their raw position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:41:36 -04:00
max bittkerandClaude Opus 5.5 882699169a Move custom GE interface and pvp_death_mark to reserved id ranges
Upstream pack ids jump at every revision bump, which is where ours were
appended, so each upgrade renumbered them. Move the two kinds that can
cheaply sit above any upstream revision (377-wip tops out at interface
18785, varp 724):

- GE interface components 11942-12437 -> 40000-40495. The interface
  format lists ids explicitly, so the gap costs nothing.
- pvp_death_mark varp 394 -> 1000. Saves store varps by id; a fixed id
  ends save migrations. The pre-289 358 migration targets it by name.

The GE booth loc and teller npc stay tail-appended: locs allocate an
engine object per id, and npc types are 11 bits in 289's NPC_INFO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:20:12 -04:00
max bittkerandClaude Opus 5.5 8601955cfb Upgrade LostCity from revision 274 to 289
Rebases the vendored engine, content and webclient onto upstream's 289
branches (34 engine, 165 content, 9 webclient commits past our 274 base).
Each vendor-289 branch in ../repos is upstream 289 plus one squashed
"rs-sdk local mods" commit, refreshed from this tree first.

289 adaptations:
- Protocol: 289 reshuffles every opcode. Custom MESSAGE_PUBLIC (server 255)
  and MARKET_SEARCH (client 243) are still free and kept.
- Lite client: CLIENT_VERSION 289, UPDATE_INV_FULL size g1->g2 and
  UPDATE_INV_PARTIAL slot g1->gsmart, mirroring Client.ts.
- Content ids: upstream appended configs where ours lived. GE interface
  moves 10984->11942 (through 12437), grand_exchange_booth 4671->5116,
  grand_exchange_teller 1359->1596 (m49_53.jm2 updated), pvp_death_mark
  varp 358->394. PlayerLoading moves a saved varp 358 value to
  pvp_death_mark (289's varp 358 is temp-scoped, so only pre-289 saves
  carry it).
- GE loc/npc configs use 289's renamed model debugnames.
- Engine: keep our Bun web split over upstream's new Fastify web.ts (no
  fastify/tsx deps), add OnDemand.onClientClosed to our WS close handler,
  keep our WSClientSocket API/batching and the 1s delayed close (TCP too).
  Adopt upstream's HashTable iterator fix, IPv6 player-loop key (with our
  NaN guard), chat effects 0-5, pointer-check removal.
- Razmire building store keeps our 650 sell price with upstream's 4% haggle.
- Tests: opcode assertions use ClientProt enums; hand-built inv packets
  use the 289 layout.

PATCHES.md documents the new per-upgrade steps (custom id renumbering,
lite decoder parity, opcode collision check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:53:05 -04:00
max bittkerandClaude Opus 5.5 415491a1cc fix(hiscores): stamp playtime only when a skill's level changes
Skill and overall rows rewrote playtime on every logout, even with no XP
gained. Playtime is the tiebreak (level desc, playtime asc), so maxed
players slid down the board for continuing to play. Now playtime records
when the current level was reached; XP-only changes still update value.
Matches the quest points board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:36:42 -04:00
max bittkerandClaude Opus 5.5 aa4bb5dbbf feat(hiscores): replace King of the Hill with a Lava Maze runite leaderboard
Rip out KOTH (Koth.ts, the per-minute World hook, koth_capture logging and
table, /hiscores/koth) for simplicity. In its place, the mining script calls
a new runite_mined(coord) command whenever it gives runite ore; the engine
counts only the two Lava Maze rocks (0_3059_3885, 0_3060_3884) and relays a
runite_mine row through the logger pipeline. /hiscores/runite ranks ore
mined (all time / week / today) under an in-game shot of the rocks. No
backfill. Migration 20260926000000_runite_mine drops koth_capture.

The server-side player sprites move to the Equipment board: the login
server stores each outfit's resolved appearance slots in the new
hiscore_outfit.appearance column, the top 10 rows show /sprite/player
images, and item icons use /sprite/item instead of the in-browser viewer.
Rows without an appearance get one on the player's next save.

The sidebar link list shared by the hiscores pages is now one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:56:17 -04:00
max bittker 69d5b709c8 feat: ship Grand Exchange with durable Fly market storage 2026-09-07 23:28:51 -04:00
max bittkerandClaude Fable 5 515f29ded5 feat(chat): observe-mode say, sdk/chat.ts CLI, 400-char messages
- Gateway: observe-mode sessions may send 'say' (and nothing else), so
  chat tools can talk through a bot without pre-empting its controller
- sdk/chat.ts: send / read / --watch chat over an observe connection
- maxMessageLength default 80 -> 400; engine WordPack caps follow the
  config instead of hardcoding 80
- 240-byte packed budget in both WordPack copies + byte-aware SDK
  chunking: all chat frames carry a 1-byte length and digit/punctuation
  chars pack at ~1 byte each, so long non-prose text would wrap the
  length byte and corrupt the stream
- chat handlers use Packet.alloc(1): the 100-byte alloc(0) tier threw
  RangeError on >~200-char messages and kicked the player with no log
- say() paces multi-chunk sends with wall-clock sleep in observe mode
  (a 'wait' action is rejected there and would occupy the bot executor)
- lite runner: GATEWAY_URL override (SERVER doubles as game origin and
  gateway address, which broke for localhost:8888)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 19:05:32 -04:00
max bittkerandClaude Opus 5 28acd81cb0 feat(sdk): surface server op rejections; reachability-filtered find*
Two silent failure modes cost bots a 10s timeout each and were
indistinguishable from "the action ran and did nothing".

Op rejections. When the server refuses an op (stunned, mid-action, target
out of view, modal open underneath) every Op*Handler answers with
UNSET_MAP_FLAG and nothing else - no message, no error. Client.ts and the
lite client now both count those packets, StateCollector turns unsets
that arrive while the player is standing still into
state.opFeedback.opRejectedCount, and ActionHelpers exposes the
snapshot/compare pair. interactNpc and pickpocketNpc use it to bail with
reason 'rejected' instead of waiting out the timeout. opFeedback is
optional throughout - an older connected client just goes quiet, and the
helpers report "no rejection".

Pickpocket acking. "You attempt to pick..." is printed by ~pick_pocket
before its first p_delay, so it is the earliest evidence the op actually
ran - an attempt earlier than xp or a stun. Without it, a lost op looked
identical to a slow one. Attempts that neither ack nor reject within 3
idle ticks (measured from the last step, since the client walks before
writing OPNPC) now return reason 'not_started'. Also fixes "you fail to
pick" being read as a timeout rather than a stun, and checks xp before
messages so a successful pick is never misreported.

find* reachability. All four find* lookups take {reachable: true}, which
returns null rather than a target the client cannot route to - the case
where the client silently writes no packet at all. Default behaviour is
unchanged: an unreachable match still wins if it is the only one.

PATCHES.md gains entries for the lite client, the ClientStream.isClosed
getter, the LoopCycle split, and the mapFlagUnsetCount counter, which is
now duplicated in both clients and would go quiet without a loud failure
if a sync reverted it. bun run test covers src/lite too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:55:45 -04:00
max bittkerandClaude Fable 5 31833be7cf fix(db): survive login storms - WAL + busy_timeout, compaction off the boot path
Post-deploy reconnect storms starved logins for minutes: every worker
thread opens its own connection to db.sqlite in rollback-journal mode
with no busy timeout, so the telemetry compactor's first run (2min after
boot, exactly mid-storm) blocked login queries into SQLITE_BUSY and
players saw "login server offline".

- db/query.ts: journal_mode=WAL + synchronous=NORMAL + busy_timeout=10s
  on every sqlite connection - reads no longer block on writes, and
  contending writers wait instead of erroring
- LoggerServer: first compaction 2min -> 15min after boot (env
  overridable), 10ms pause between per-group write transactions so other
  threads get lock windows mid-pass
- gateway: auth timeout 10s -> 15s so it outlasts the busy_timeout

Verified: reads complete in 1ms against an open write transaction
(previously "database is locked").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 00:42:36 -04:00
max bittkerandClaude Fable 5 61f7b79189 feat(koth): King of the Hill at the Demonic Ruins, tracked on hiscores
Every wall-clock minute, the highest combat level player standing inside
the Demonic Ruins wall polygon (traced from m51_60.jm2, incl. the western
annex; interior wall faces only) scores a point and is crowned; the
current king wins ties, an empty hill dethrones. Captures append to a new
koth_capture table via the logger pipeline, snapshotting the winner's
resolved appearance slots. /hiscores/koth shows all-time/week/day boards
with three kings side by side and median-loadout character sprites,
rendered in-browser by the viewer's new renderPlayerSpriteAsImageData
(ClientPlayer model assembly + offscreen Pix2D raster). Crown messages
are local (3 zones), never server-wide.

Also: split LoopCycle out of Client so the viewer bundle doesn't drag the
game client in, and fix the worker-killing once('error') ws handler in
InternalClient (the "engine dies ~40s into a bot session" local crash),
with per-worker error/exit logging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 23:49:15 -04:00
max bittkerandClaude Fable 5 2bf1f5ad90 Engine: cap tick catch-up at 2 ticks of backlog
The cycle scheduler tracked an unbounded wall-clock schedule: after
sustained overload (1000+ bots pushing cycle time past the tick rate),
the accumulated drift made the world run ticks back-to-back at max
speed until the entire backlog replayed - combat/XP/movement at 10-20x
for everyone. Clamp nextTick to 2 ticks of backlog so brief slow ticks
still smooth out but sustained backlog is dropped instead of replayed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:59:31 -04:00
max bittkerandClaude Fable 5 377652f2aa Remove dead files and dependencies; move README art out of vendored content
Dead code/files (zero references, verified):
- server/engine/RuneScriptCompiler.jar (12.7MB) — replaced by
  @lostcityrs/runescript TS package in the 274 upgrade
- server/engine/package-lock.json — bun-only repo; also gitignored so
  vendor syncs don't reintroduce it
- server/engine/.github/ — vendored CI, can never run from a nested path
- server/engine/public/maped/ + view/maped.ejs — unreachable map editor
- server/gateway/{rsbot,run-recorder.ts,bunfig.toml} + bin/cli/login
  package.json entries — all point at files/dirs that no longer exist
- server/webclient/.gitmodules + empty emsdk/tinymidipcm gitlinks —
  submodules never initialized; build uses committed prebuilt wasm
- sdk/package.json — publish manifest for a never-published package
- sdk/test/zanaris-pathfinding.ts — duplicate of zanaris-navigation.ts;
  its unique waypoint folded into the survivor

Dead dependencies (lockfiles regenerated):
- gateway: puppeteer + @anthropic-ai/sdk (~54MB + Chromium download per
  Docker build, zero gateway imports); puppeteer now a root devDependency
  since sdk/test/utils/browser.ts is its real consumer
- engine: @2004scape/rsmod-pathfinder (replaced by in-engine routefinder;
  server/vendor copy is the SDK's), tsx, redundant @jimp direct pins

Also: README art (promo.gif 31MB + svgs) moved from server/content/title/
to docs/media/ so it stops shipping in every Docker image; stale
gateway/agent-state gitignore entry dropped; DEVELOPERS.md stale rows fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 19:10:41 -04:00
max bittkerandClaude Fable 5 b30931f040 Pack: regenerate ondemand.zip so hiscores item icons stay fresh
The hiscores ItemViewer loads item models from /ondemand.zip, but the
254->274 upgrade dropped the step that built it, leaving a stale
pre-upgrade snapshot in the image. New items (e.g. Fremennik helm/cloak,
models 4211-4275) rendered as blank icons since the zip topped out at
model 3855. Re-add generation at the end of packAll(), matching the old
upstream packClient() behavior, so every build (incl. Docker) refreshes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:29:46 -04:00
max bittkerandClaude Fable 5 0e4aeb82a2 Engine: don't let a bare continue answer a pending choice dialog
RESUME_PAUSEBUTTON resumed any PAUSEBUTTON-suspended script, so a
continue click racing the state stream could land on a just-opened
p_choice menu and resolve it from the player's stale last_com —
whatever component they last clicked. Once a bot declined the Al Kharid
toll once, every later attempt auto-declined no matter which option it
clicked (opus48 team-benchmark bots sat at the gate for 20+ minutes
because of this, with their real IF_BUTTON clicks arriving late and
dropped by the visibility check).

- ResumePauseButtonHandler: ignore bare RESUME_PAUSEBUTTON while resume
  buttons are registered; a choice must be answered via IF_BUTTON on
  one of them
- IfButtonHandler: consume resumeButtons when a registered option
  resumes the script, so following narration pages accept plain
  continues again
- sdk/test/alkharid-gate-choice-resume.ts: regression test (decline
  once to seed last_com, probe the reopened choice with a bare
  continue, then pay and verify passage + 10gp deducted)
- PATCHES.md: record the monkeypatch in the vendored-tree checklist

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:04:34 -04:00
max bittkerandClaude Opus 4.8 b9f0168c70 Add server/PATCHES.md: vendored-tree monkeypatch checklist
Canonical list of every local modification to the vendored LostCity
trees (engine/webclient/content), each with a verification step, plus
the cross-boundary invariants derived from the bugs found during the
274-upgrade audits (one-sided custom packets cause T1 logouts, XP curve
duplication, walk-before-op, case-only renames on macOS, as-any field
accesses invisible to tsc). Includes a 5-minute post-sync verification
script. Linked from DEVELOPERS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:09:14 -04:00