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):
| Command | Effect |
|---|---|
::leagues | Open league_side_panel (interface 656) |
::relics | Open league_relics (interface 655) — the relic selector |
::ltut | Open league_tutorial_main (interface 677) |
| `::iface <name | id>` |
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:
::relicsExpected: the relic-selector interface opens. It may render empty or partial at first because
its cs2 onload (league_relics_init → league_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 wiresleague_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. (fromosrs-dumps/config/dump.varp+dump.varbit). - Next: read
league_relics_draw_selections.cs2to 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-buttonIF3_BUTTONserver-side and the selection animation.
Verification channels
- Visual:
scripts/shot.ps1screenshot of the client. - Packets: RSProx logs every packet; its transcriber (
tools/rsproxtranscribetask / GUI Logging menu) converts the binary session log to readable form — use this to confirm theIF_OPENTOP/SUBand varp packets actually went out.
VERIFIED in-game (2026-06-29) ✅
On a live client (rev-233 native via RSProx), logged into our server:
::myposworks → command system + admin access confirmed (dev player =ownermod level, which includes admin, perConnectionHandler.devModeModLevel).::leaguesopensleague_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 viaifOpenMain; it's really a sidebar interface).::relicsopensleague_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 theloadinglayer (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), procleague_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_button656:40,tasks_button656:36,mastery_button656:32);league_relics:close_button= 655:4.
Next-session target:
- Capture the relic-grid click op: close RSProx (flushes its binary; it buffers ~5 min), then
cd tools/rsprox && ./gradlew transcribe→ read the.txtnext to the latest~/.rsprox/binary/OSRS Leagues Dev/*.binfor theif_button*/clickzone op on a relic. - 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.
- (Polish) open
league_side_panelas a sidebar andleague_relicswith 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 ping→OK pong;dbg.ps1 relics(live player) → opens the relic selector;dbg.ps1 varbit league_type 4→ sets the varbit (confirmed in packet tracevarbit 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, …) plusiface/leagues/relics/ltut. Loaded at runtime (Loaded 120 scripts). Commit onleaguesbranch.
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_selectionsline ~227). On click it runsleague_relics_loadingclient-side, which ONLY unhides the loading layer (655:24) + spins aloading_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 viarunclientscript. So the server must complete the selection.league_relic_expanded_viewearly-returns unlessenum(int, struct, enum_2670, %league_type)is non-null.enum_2670:league_type1..6 → struct (4→struct_4699 = Trailblazer/Leagues IV, matches the relics shown);0→null. Soleague_typemust 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
- Set the Leagues state a logged-in player needs (
league_type, and any "tier available" bits) — ideally a::leaguesetupconvenience command, or fold into login for the dev realm. - Handle the relic-click server op (once identified): respond with
runClientScript(league_relic_expanded_view, <args>). The args mirror the component list passed toleague_relics_init(header, icons, outlines, names, view_one, confirm, loading, …) plus the clicked relic'sstruct(from the relic enum/struct table) + colour/string params. - 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_initdoesif_setonvartransmit("script3040(...){fossil_storage11, league_general, fsw_perm_int_1}", universe)— the journal's League tab is driven by theleague_generalvarbit. (The journal sidebarside_journalalready containsleague_listalongside Quests / Character Summary / Tasks / Adventure Path.)league_generalbit 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
openLeagueTab → ifOpenOverlay(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).
- 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). - Verify the journal sidebar's League tab appears and opens the league panel in its proper
sidebar slot (no more
ifOpenMain). - 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.mesoutput back through the channel (currently returns exec status only; effects verified via packet trace + screenshot).
Combat notes — NPC attackability & the "Attack" option (A3 findings)
How player-initiated PvM is offered to the client, and why the "Attack" option sometimes appeared
Unattended Client-Driving Harness (VM / headless) — research & plan
the user's desktop? Answer: yes — best via a headless Linux box running RuneLite under