RKClient Core Systems
Design for the next layer of RKClient itself — input, per-entity state, assets, and UI — as opposed to the feature plugins (Character/Stats/Skills, Currency, etc.) that will consume them. All still design stage, brainstorming, subject to change.
RKDev → RKInput (keys/mouse) → Asset loader → Base UI → Toast. Sub-systems like PlayerController/NpcController aren't their own numbered step — they'll get built whenever the first thing that actually needs per-entity state comes up, likely during the RKInput step.Framework vs. plugin: the decision rule
Before scoping any of the systems below, a test for where each one belongs: does it arbitrate a single shared resource that multiple plugins would otherwise independently collide over? If yes, it belongs in RKClient core, the same tier as the already-core packet registry, event pipeline, and settings system — splitting it into its own plugin buys no real isolation, since every feature plugin would still need a hard dependency on it, identical in practice to depending on RKClient directly. If no — it's self-contained business logic that consumes the shared infrastructure rather than arbitrating it — it should be its own standalone plugin.
| Core (RKClient) | Why |
|---|---|
| RKInput (keys/mouse) | Key registration is a genuinely global, accumulating native resource — confirmed against the real Plugin API, not assumed (see below). |
| PlayerController / NpcController | RKClient itself already needs this for its own state (verified/redirect-locked/NavigationController). Foundational attachment point, not a feature with its own data. |
| Base UI dock | One dock, one cursor, one arbitrator — same reasoning as Input. |
| Toast notifications | Thin display utility riding on the same UI root the dock owns — no independent business logic or update cycle. |
| Asset loader | A shared texture/asset registry queried by name — two plugins registering the same name would silently collide or double-load. |
| Standalone plugin | Why |
|---|---|
RKDev | By definition — its whole purpose is proving the framework's plugin-facing API works from outside RKClient's own source. |
| Character, Stats, Skills, Currency | Real, separable business data/logic. See Character & Skills. |
| NPC role behaviors (e.g. a Vendor plugin) | Plugs into NpcController's interaction dispatch, doesn't arbitrate it. |
| Cities & Portals | Self-contained feature, see Plans. |
PlayerController & NpcController
One consolidated object per connected player (and, mirrored, per relevant NPC), attached via Rising World's own native generic attribute storage rather than a parallel lookup map:
player.setAttribute("Player_Controller", playerController);
npc.setAttribute("Npc_Controller", npcController);
Confirmed both Player and Npc support this natively (setAttribute/getAttribute/hasAttribute/deleteAttribute/getAttributes) — checked directly against the Plugin API, and against an old prior project's code which already used exactly this pattern (player.setAttribute("PlayerController", playerController)).
Current reality, not a strawman: RKClient today has no unified per-player object. PlayerStateTracker is a set of parallel maps, all independently keyed by player UID — online players, verified players, redirect-locked players, needs-default-spawn, NavigationControllers, cached player data. Nothing consolidates these, and there's no extension point for a new plugin to attach its own per-player data. PlayerController replaces this with one object other systems attach sub-objects to instead of maintaining their own independent map.
Generalizes beyond static data to per-player recurring state too — counters, cooldowns, timers. The current pattern in places like DataSyncManager.syncLoop() is a manager-owned loop over every online player with a per-iteration filter. The shift is toward that state living on the controller itself, so a check becomes "does this player's controller say it's due" rather than "loop everyone and filter."
Per-player settings flow
Mirrors the two-tier model an old prior project already used for its theme system (see below): on first join, the player picks character/race/avatar info, and a copy of the current UI defaults (loaded from JSON, not hardcoded) gets attached to their controller alongside it. That combined data — character info + UI appearance settings + keybind assignments (RKInput's combos are player-remappable, not just plugin-assigned defaults) — gets saved to the backend through the normal player-data sync path. The player can customize any of it later (colors, fonts, keybinds), same changes-buffer-then-apply shape the old theme system used.
RKInput — key/mouse combo registry
Same "one owner exposing a resolved callback" principle already used for cancelable player-lifecycle events, applied to input.
Verified against the real Plugin API (not assumed):
PlayerKeyEvent.getKey()/isPressed()— a key does nothing untilPlayer.registerKeys(Key...)is called andPlayer.setListenForKeyInput(true)is set.Player.isKeyPressed(Key)— the API's own documented way to check a second key's live state from inside a handler; their own javadoc example is literally "check if player pressed shift + c." No need to hand-roll modifier tracking.- Critical finding, the real justification for centralizing this: registered keys accumulate globally across every plugin, and any plugin with listening enabled receives
PlayerKeyEventfor every key any plugin has registered — not just its own, confirmed directly in the javadoc. Without one owner, every plugin wanting a hotkey has to filter out every other plugin's key events from the same shared stream. PlayerMouseButtonEvent.getButton()/isPressed()— only needssetListenForMouseInput(true), no separate per-button registration step like keys have (asymmetry worth designing around).
Design: config holds one or more named modifier channels as a JSON array, e.g. [{"name": "primary", "key": "LeftShift"}] — starts with one entry, structurally ready for more later. Plugins register a trailing Key against a named channel (defaulting to "primary"), not a full combo themselves. RKClient owns the actual registerKeys()/setListenForKeyInput()/isKeyPressed() calls and resolves the combo internally, handing each plugin only its own resolved "this fired" callback.
Open, not decided: what happens when two plugins request the same trailing key under the same channel.
Distinct from the already-planned chat command registry (registerCommand(...), slash-command text parsing) — RKInput is raw keyboard/mouse hotkeys during live gameplay, a separate complementary system.
Asset loader
A shared texture/asset registry in RKClient, queried by name. Has to exist before UI/theme work — the old theme system (below) depended entirely on a shared-asset name lookup for backgrounds and icons.
Two distinct sources, both need to land in the same name-keyed registry:
- Centrally-distributed assets — the existing, already-planned-but-never-built large-asset system (manifest-hash + HTTP range requests, on-demand, not blocking boot), assumed to live under
Updates/Assets/and pulled similarly to howUpdateHttpServeralready serves theUpdates/tree with byte-range support. - Plugin-bundled assets — a plugin ships its own images/textures directly inside its own package, not distributed centrally at all. The loader needs to discover and register these too.
Base UI dock
User's stated target: a dock on the right side of the screen; other plugins register a button into it, and registering the button is what adds their window/panel to the system — the button registration is the panel registration, one action. This matches an old prior project's pattern exactly (see below) — it just needs generalizing into one reusable RKClient-owned component instead of each plugin hand-rolling its own copy the way the old code did.
Mouse cursor visibility likely doesn't need its own arbitration system — it can be derived from the dock's state (visible iff any plugin's UI element is currently showing) rather than tracked separately, avoiding the exact collision the old code actually had (two independent systems each calling setMouseCursorVisible() with nothing arbitrating between them).
Confirmed real UI API (checked against the Plugin API, not assumed): a genuine hierarchical, CSS-flexbox-like toolkit — UIElement base class (subclassed by UILabel, UITextField, UIScrollView, UIMesh, UIPainter2D), real tree structure (addChild/removeChild/getChilds/getParent), a Style object per element plus USS stylesheet assets and CSS-style class lists for shared theming, and — importantly — UI elements are already natively isolated per plugin (Player.getAllUIElements(includeFromOtherPlugins), false returns only your own plugin's elements). Unlike keys, the engine already prevents one plugin from seeing or clobbering another's UI by default, which changes why a shared dock is needed — it's not fixing a collision risk the way Input is, it's about a shared page-switching container and consistent look, not native safety.
Prior art from an old project
Full scan of 4 old zips (RPCore, RPCoreCurrency, RPCoreNPC, MobSpawner) turned up a consistent, real precedent — not a fresh design:
- Icon dock + single content-swap panel, applied recursively. A shared
UIScrollViewdock holds one icon per feature; a single shared panel is the one active content area, swapped by clicking an icon (same icon twice closes it, a different icon swaps content). The same shape recurses one level deeper inside individual panels for their own sub-sections. Never generalized into a reusable base class in the old code — every feature hand-rolled its own copy. - Standalone dedicated UI, no dock at all. An old mob-spawner admin tool is the clear precedent for a UI that isn't registered into any shared dock — built independently via
player.addUIElement()directly, its own explicit Close button, opened programmatically rather than via icon click. This is the pattern future NPC merchant/quest interaction UIs will likely follow. - Listing/refresh idiom. Scrollable lists (currency listings, transaction logs, active-mob lists) all use the same shape:
removeAllChilds()then re-populate from current data in a loop. - Styling was 100% manual percentage positioning — none of the old code used the flexbox-like
Style/stylesheet system now confirmed to exist. Worth a deliberate choice before writing new UI code.
Old theme system
File-based (.thm Properties files), loaded at startup into named Theme presets — background image, header/text/button/menu fonts and colors, border styling, author. Two-tier design: a player's live UI state is their own personal customized copy (loaded from saved DB settings on connect), not a reference to a preset — browsing the theme gallery and clicking a swatch copies that theme's values onto a live preview + a pending-changes buffer, only becoming the real saved state on Apply. A "Share" button let a player package their current customization into a brand-new named theme file for others to pick — player-generated themes, not just admin-authored, gated by a permission check.
Settings/*.json convention) — any equivalent data files rebuilt for RK (themes, flavor-text pools, etc.) should be JSON instead of introducing a second file format.Rough edge, not to repeat: some Apply changes in the old code required kicking the player to force a reconnect before fully taking effect — the code's own comment flagged it as unfinished. Worth designing around properly this time.
NPC manager — aspirational scope reference
Explicitly not for verbatim reuse — "not in its entirety, we're open to improve... just a look at what I would like the end result to be."
- Role-based interaction dispatch via the same native attribute mechanism as
PlayerController—npc.getAttribute("Role")determined behavior (e.g. a Vendor role routed to a vendor interaction module). - Extensible role-plugin registration — a manager exposed
addNpcPlugin(name, plugin)so role-specific behavior (a Vendor module) could register itself rather than being hardcoded into the manager. The old code still hardcoded the actual dispatch (if (role.equals(VENDOR))) inside the manager itself — with the subscriber convention below, a plugin registers "I handle interactions for role X" andNpcControllerresolves to the right subscriber instead, so no central class needs to know about every role in advance. - Admin tooling used a native context menu (
player.showContextMenu(String[], callback)) for a simple admin pick-one-role case, rather than building custom UI — a real precedent for when not to build custom UI. - Also present: procedural name generation, loot tables on NPC death, NPC movement control, and a flat random flavor-text line picker (not a real dialogue-tree system, despite the name).
One subscriber convention, applied everywhere
Most new registration-style systems should use the same shape already established for packets and events (registerPacketHandler/registerEventHandler — one owner, plugins register a handler, the owner resolves and calls out) rather than each system inventing its own registration mechanism. Applies to: RKInput's key-combo registration, the base UI dock (registering a button is registering a panel), and NPC interaction dispatch on NpcController (a plugin registers "I handle interactions for role X," instead of a central class hardcoding role checks).
Considered and declined: embedded FTP for updates
Raised as a way to get faster parallel downloads for Updates/. Assessment: declined for this reason. Parallel chunked downloading isn't a protocol feature — it's a client behavior (opening several concurrent range requests for the same file and reassembling them), and UpdateHttpServer already supports byte-range requests that this would ride on for free, no new server needed. FTP would actually work against the stated multi-server-over-the-internet scale (other people's servers linking in via portal travel): separate control/data channels and passive/active mode are notoriously NAT/firewall-unfriendly for exactly that kind of remote-operator network, and FTP has no TLS by default unlike the HTTP path (which gets TLS for free from the reverse proxy work). Recommended alternative: parallelize range requests client-side in RKUpdater against the existing HTTP endpoint. Not fully closed — if a different, non-speed motivation comes up later, worth reopening on its own merits.