ClockworkRS Docs

Leagues Slice — runbook & findings

The slice MVP: prove the interface pipeline by opening Jagex's in-cache Leagues interfaces and

The slice MVP: prove the interface pipeline by opening Jagex's in-cache Leagues interfaces and picking a relic with its animation. No Leagues game logic — just drive the existing league_* interfaces. See DESIGN.md Phase 3.

Dev commands added (server-side, content/.../commands/LeagueDevCommands.kt)

In-game admin chat commands (type in the client chat; dev realm player has admin):

CommandEffect
::leaguesOpen league_side_panel (interface 656)
::relicsOpen league_relics (interface 655) — the relic selector
::ltutOpen league_tutorial_main (interface 677)
`::iface <nameid>`

All open as a main modal via rsmod's ifOpenMain. Confirmed: the script loads at runtime (server log: Loaded 119 scripts). The interfaces resolve by name through the cache's gameval symbols (NameMapping.interfaces) → InterfaceTypeList.

How to verify the slice (when at the machine)

bash scripts/dev-up.sh        # start server
bash scripts/client-up.sh     # launch RSProx GUI -> select 'OSRS Leagues Dev' + Native -> Launch
# log in (any name/pass), then in the client chat type:
::relics

Expected: the relic-selector interface opens. It may render empty or partial at first because its cs2 onload (league_relics_initleague_relics_draw_selections) reads Leagues varps/varbits that aren't set yet — that's expected and is the next step.

Findings for populating the relic UI (next P3 work)

The relic interface is driven by cache varps/varbits that exist in rev 233:

  • league_relics_init(...) (cs2 3188) borders the panel and wires league_relics_draw_selections(...) on resize — the draw routine reads game state.
  • Relevant vars present in cache: league_general, league_temp, leagues_5_relic_int, league_task_filter_tier, league_points_currency, league_general_tasks*, etc. (from osrs-dumps/config/dump.varp + dump.varbit).
  • Next: read league_relics_draw_selections.cs2 to find exactly which varbits gate "a relic tier is available / show 3 options", set them via ::varbit <name> <val> / ::varp, and iterate until the 3 relic choices render. Then handle the confirm-button IF3_BUTTON server-side and the selection animation.

Verification channels

  • Visual: scripts/shot.ps1 screenshot of the client.
  • Packets: RSProx logs every packet; its transcriber (tools/rsprox transcribe task / GUI Logging menu) converts the binary session log to readable form — use this to confirm the IF_OPENTOP/SUB and varp packets actually went out.

VERIFIED in-game (2026-06-29) ✅

On a live client (rev-233 native via RSProx), logged into our server:

  • ::mypos works → command system + admin access confirmed (dev player = owner mod level, which includes admin, per ConnectionHandler.devModeModLevel).
  • ::leagues opens league_side_panel (656) — renders the real Leagues V panel (Areas / Tasks / Masteries / Relics / Rank / Summary, progress bar, "Tasks completed / Total points"). Missing its background (opened as a plain modal via ifOpenMain; it's really a sidebar interface).
  • ::relics opens league_relics (655) — renders the full relic grid from cache (Power Miner, Golden God, Fairy's Flight, Banker's Note, …) with background.
  • Interface pipeline fully proven. Confirmed in the RSProx packet transcript: [if_opensub] com=toplevel_osrs_stretch:mainmodal (161:16), id=league_relics (655), type=modal.

The relic PICK — the one remaining slice beat

Clicking a relic in the grid shows "Loading…" and hangs. Mechanism (from cs2 + trace):

  • league_relics_loading (cs2 3191) unhides the loading layer (655:24) + plays a spinner on click. Nothing hides it again because that's gated on a server response we don't send.
  • The client is waiting for the server to complete the selection (transition to the relic-detail "view_one" + confirm). Relevant cs2: league_relic_expanded_view, league_relics_loading (3191), proc league_relics_draw_selections (3189), league_relic_hover. Relevant vars (exist in cache): leagues_5_relic_int, league_general.
  • Confirmed button packets the client sends (so we know the op format): side panel uses if_buttonx (e.g. league_side_panel:relics_button = 656:44 op=1, areas_button 656:40, tasks_button 656:36, mastery_button 656:32); league_relics:close_button = 655:4.

Next-session target:

  1. Capture the relic-grid click op: close RSProx (flushes its binary; it buffers ~5 min), then cd tools/rsprox && ./gradlew transcribe → read the .txt next to the latest ~/.rsprox/binary/OSRS Leagues Dev/*.bin for the if_button*/clickzone op on a relic.
  2. Write a server handler for that op that responds so the cs2 un-sticks (hide loading, populate the relic detail / confirm) and the selection animation plays.
  3. (Polish) open league_side_panel as a sidebar and league_relics with proper backing so the side panel isn't missing its background.

Packet-trace observation layer — PROVEN

RSProx logs every packet to ~/.rsprox/binary/<target>/*.bin; ./gradlew transcribe (in tools/rsprox) decodes each to a readable .txt next to it. This is the Phase-2 observation channel working — use it to assert exactly what the server sent / client sent.

GUI-free debug channel — BUILT & VERIFIED ✅ (2026-06-29)

content/.../commands/DebugChannelScript.kt + scripts/dbg.ps1. A localhost-only TCP listener on 127.0.0.1:43595 accepts one command line per request (same syntax as :: cheats, :: optional), enqueues it, and drains it on the game thread via onEvent<GameLifecycle.StartCycle>, running it against the online player(s) through the existing CheatCommandMap.execute. Returns a one-line result.

  • Verified: dbg.ps1 pingOK pong; dbg.ps1 relics (live player) → opens the relic selector; dbg.ps1 varbit league_type 4 → sets the varbit (confirmed in packet trace varbit id=league_type (10032) new=4). The AI can now drive the server without touching the game window.
  • Reaches all :: admin commands (tele, varp, varbit, master, npcadd, …) plus iface/leagues/ relics/ltut. Loaded at runtime (Loaded 120 scripts). Commit on leagues branch.

Relic PICK — mechanism fully mapped (implementation = next task)

Clicking a relic still hangs on "Loading…". Root cause is NOT a single varbit — it's a missing server-driven completion:

  • The relic clickzone has cc_setop(1, "View") + cc_setonop("league_relics_loading(...)") (proc league_relics_draw_selections line ~227). On click it runs league_relics_loading client-side, which ONLY unhides the loading layer (655:24) + spins a loading_icon + league_relic_hover. It does not send a request or call the detail view.
  • league_relic_expanded_view (cs2 3193) is what HIDES the loading (if_sethide(true, loading)) and draws the relic detail/confirm. No cs2 anywhere calls it → it is invoked by the server via runclientscript. So the server must complete the selection.
  • league_relic_expanded_view early-returns unless enum(int, struct, enum_2670, %league_type) is non-null. enum_2670: league_type 1..6 → struct (4→struct_4699 = Trailblazer/Leagues IV, matches the relics shown); 0→null. So league_type must be set (we set it to 4 via the channel — necessary but not sufficient alone).

Open question (resolve first next session)

Does the relic click send a server op (an if_button on a league_relics component), or is the trigger elsewhere? The flushed RSProx transcript shows only league_relics:close_button (655:4) and side-panel buttons — the grid-click packet is still in RSProx's ~5-min buffer. To capture it: click a relic, then close the OSRS client (or RSProx) to flush the binary (or wait 5 min), then cd tools/rsprox && ./gradlew transcribe and read the newest ~/.rsprox/binary/OSRS Leagues Dev/*.txt for an if_button* on a 655:* component.

Implementation plan

  1. Set the Leagues state a logged-in player needs (league_type, and any "tier available" bits) — ideally a ::leaguesetup convenience command, or fold into login for the dev realm.
  2. Handle the relic-click server op (once identified): respond with runClientScript(league_relic_expanded_view, <args>). The args mirror the component list passed to league_relics_init (header, icons, outlines, names, view_one, confirm, loading, …) plus the clicked relic's struct (from the relic enum/struct table) + colour/string params.
  3. Handle the confirm button → set the relic-selection varbit (leagues_5_relic_int / per-tier), play the selection animation, redraw. That completes the slice's "pick a relic with animation".

KEY REFRAME (2026-06-29): the Leagues UI is VARP-DRIVEN

New-session capture confirmed the relic click sends nothing to the server (cc_setonop op 1 "View" is client-side only; from relic-open to session end the client sent zero ops). And league_relic_expanded_view (hides loading + draws detail) is invoked by no cs2/timer. Conclusion: we were opening interfaces the wrong way. The real Leagues UI is activated by setting state varbits, and the client's own cs2 wires everything up:

  • journal_list_init does if_setonvartransmit("script3040(...){fossil_storage11, league_general, fsw_perm_int_1}", universe) — the journal's League tab is driven by the league_general varbit. (The journal sidebar side_journal already contains league_list alongside Quests / Character Summary / Tasks / Adventure Path.)
  • league_general bit layout: league_account (bit 0 = is-league-account), league_type (bits 1–5; 4 = Trailblazer/Leagues IV, matching the relics shown), league_task_filter_* (6–10), league_tutorial_completed (13–18).

So the force-modal ::leagues/::relics were a debugging shortcut, not the real flow. The relic pick hung because the interface was opened cold/out-of-context. The proper path: set the player's Leagues state varps, and the journal League tab + panels appear and behave correctly.

IMPLEMENTATION PLAN (next)

Phase A finding (2026-06-29): Leagues is a WORLD flag, not just player varbits

The journal's League tab is gated by ~script9148~league_world = league_world_fromflags( %map_flags_cached) = testbit(map_flags_cached, 30) && !testbit(…, 29), AND league_tutorial_completed >= 3. map_flags_cached is a player varp (a world-flag bitfield the server sends; other bits: high-risk=10, beta=25/23, etc.). So setting league_account/league_type alone did NOT show the tab — the world must advertise map-flag bit 30. Implemented in LeagueRealmScript (onPlayerLogin): set map_flags_cached bit 30, league_account=1, league_type=5, league_tutorial_completed=6 (skip tutorial for now), + a "Welcome to Leagues V" banner. Behind LEAGUES_REALM (→ promote to realm config later). To verify the tab: set league_tutorial_completed (part of league_general, which the journal transmits on) to force a journal redraw after the map-flag is set. Open question: league_type=5 (Raging Echoes) vs the Trailblazer/IV relics the cold interface rendered — confirm which edition's content is clean in rev233.

Phase A.2 finding (2026-06-29): journal League tab CONTENT

The League tab now appears (world flag works) but shows no content. Mechanism: rsmod's content/interfaces/journal-tab (JournalTabScript + PlayerExtensions.openJournalTab) handles each tab by ifOpenOverlay(<contentInterface>, journal_components.tab_container, eventBus): Summary→account_summary_sidepanel, Quests→questlist, Tasks→area_task. There is no handler for the Leagues tab (side_journal:league_list = 629:34). Implementation: add SideJournalTab.Leagues(varValue=4), an onIfOverlayButton(league_list) handler, and openLeagueTabifOpenOverlay(league_side_panel [656], journal_components.tab_container, eventBus). Component ids: summary=629:2, quest=629:10, task=629:18, adventurepath=629:26, league=629:34; content slot = side_journal:tab_container. Note: league_side_panel/league_list aren't predefined interfaces.*/components.* refs — resolve via InterfaceTypeList/NameMapping (like LeagueDevCommands) or add refs. After the panel shows, its Areas/Tasks/Masteries/Relics buttons (656:40/36/32/44, server if_buttonx) open the sub-panels — the relic pick continues from there.

Phase A — Activate Leagues mode via state varps (the proper entry).

  1. Set league_account=1, league_type=4, league_tutorial_completed (skip intro) on the player (via the debug channel first: dbg.ps1 varbit league_account 1, ... league_type 4).
  2. Verify the journal sidebar's League tab appears and opens the league panel in its proper sidebar slot (no more ifOpenMain).
  3. Wrap it in a dev command ::leaguemode (sets all needed state) and/or auto-apply on dev login.

Phase B — Relic pick, in the proper flow. 4. Open the relic selector through the real flow (journal → League → Relics), and packet-trace a relic click in context — the behavior should differ from the cold open. Resolve the loading→detail handshake empirically (likely the server must runClientScript( league_relic_expanded_view, <args mirroring league_relics_init>) once the interface is opened with proper server-side event wiring). 5. Handle confirm → set the relic-selection varbit + animation. That completes the slice.

Phase C — Server-side Leagues state model (the real engine foundation). 6. Model league state server-side (points, selected relics per tier, unlocked regions, task progress) and push the varps that the cache UI reads (per RESEARCH.md §5). The varps are the client view; the server owns the truth. 7. Deprecate the force-modal helpers (keep ::iface for ad-hoc debugging).

Still TODO (Phase 2 remainder)

  • The debug channel covers GUI-free triggering. Remaining nice-to-have: capture player.mes output back through the channel (currently returns exec status only; effects verified via packet trace + screenshot).

On this page