Files
2009scape/docs/ARCHITECTURE.md
2026-07-05 18:14:54 -04:00

6.0 KiB

Architecture

How the 2009scape game server is put together. See ../AGENTS.md for build/run instructions and the top-level rules, and CONTENT_GUIDE.md for adding gameplay.

The two halves: engine vs. content

The codebase is deliberately split:

  • Server/src/main/core/ — the engine. Networking, the RS cache reader, the game loop/pulse scheduler, entities (players, NPCs), the world map, storage/auth, and the content API (core.api). Engine code must not depend on any specific piece of content.
  • Server/src/main/content/ — the content. Quests, NPC behavior, minigames, skills, items, region-specific interactions. Content is built on top of core.api and is discovered reflectively at startup.

Keeping this boundary clean is the main architectural discipline of the project.

Startup and plugin discovery

Entry point: core.Server (core.Server.main). During boot the server:

  1. Loads configuration from worldprops/default.conf (TOML) into GameWorld.settings.
  2. Loads the RuneScape cache (map, item/npc/object definitions) via core.cache.
  3. Runs core.plugin.ClassScanner, which uses the classgraph library to scan the classpath and reflectively instantiate every class implementing a known ContentInterface sub-type (see the table in AGENTS.md). This is how content "registers" itself — there is no central manifest to edit.
  4. Starts the world / game loop and opens the network listener(s).

Because discovery is reflective, adding a new content class is enough to activate it — just implement the right interface and put the file under content/.

ClassScanner also builds the registry of player-runnable bot scripts (see BOT_SCRIPTING.md) by picking up classes annotated with @PlayerCompatible.

The game loop / pulse system

The world advances in discrete ticks (RuneScape's canonical tick is 600 ms). Timed and repeating work is modeled as Pulse objects submitted to GameWorld.Pulser. A Pulse overrides pulse(): Boolean and returns true when it is finished. Movement, combat swings, skill actions, and bot scripts are all implemented as pulses.

TickListener content runs once per tick globally; MovementPulse handles walking a player/NPC to a destination.

Coroutines (kotlinx-coroutines) back parts of the worker/pulse infrastructure.

Entities and nodes

Everything interactable in the world is a Node (core.game.node):

  • EntityPlayer and NPC (living things with combat, skills, movement).
    • Player also has an AI subclass, AIPlayer, used for bots.
  • Item / GroundItem — items in containers or on the floor.
  • Scenery — world objects (trees, doors, altars…).

Players carry a lot of linked state under core.game.node.entity.player.link (quests, diaries, prayers, emotes, teleport manager, hint icons, etc.) and use Containers (inventory, equipment, bank) for items.

The content API (core.api)

core.api.ContentAPI.kt is a large file of top-level helper functions that form the intended, stable surface for content code — e.g. sendMessage, animate, addItem, removeItem, getStatLevel, rewardXP, teleport, getVarp/setVarp, lock, sendDialogue. Content should call these rather than reaching into engine internals.

Key interfaces in core.api (all extend the ContentInterface marker so ClassScanner finds them): InteractionListener, InterfaceListener, Commands, LoginListener, LogoutListener, TickListener, StartupListener, ShutdownListener, PersistPlayer, PersistWorld, MapArea.

Networking

The server speaks the original RS2 build-530 binary protocol.

  • TCP is the primary transport.
  • An optional WebSocket listener (org.java-websocket) carries the same raw binary protocol inside binary frames, enabling browser/PWA clients. It is off by default; enable via websocket_enabled/websocket_port in default.conf (port defaults to 53594 + world_id). Plain ws:// for local testing; wss:// (with a PKCS12 keystore) for production/PWA. See the README for keystore setup.
  • Client and server must share secret_key or the connection is refused.

A separate management interface is defined via Protobuf (Proto/Management.proto, src/main/proto/).

Data, config, and storage

  • Configuration: worldprops/default.conf (TOML), parsed by core.game.system.config.ServerConfigParser into GameWorld.settings (core.game.world.GameSettings). Paths (cache, saves, data, logs, scripts) are held in core.ServerConstants.
  • Static game data (npc spawns, shops, drop tables, item configs) lives in JSON files loaded at runtime. Edit these with the Thanos tool (needs Java 11), not by hand.
  • Item/NPC/object/animation IDs come from the external ConstLib dependency (org.rs09.consts.*, bundled in Server/libs/) — reference them as Items.FEATHER_314, NPCs.CHICKEN_..., etc.
  • Persistence: player saves + account data via MySQL (mysql-connector-java) or SQLite (sqlite-jdbc). Controlled by use_auth / persist_accounts in config; when both are false, no database is needed (ideal for local content work). Both MUST be true in production.

Testing & static analysis

  • JUnit 5 tests in Server/src/test/kotlin/ (e.g. APITests, ExchangeTests, skill/region tests). Run with ./run -t or sh mvnw test.
  • detekt (Server/detekt.yml) runs in the Maven verify phase and in CI (.gitlab-ci.yml); keep new Kotlin clean against it.

Build specifics worth knowing

  • Maven, JVM target 11 (JDK 11 required — not newer).
  • mvn clean installs the bundled ConstLib and PrimitiveExtensions jars from Server/libs/ into the local Maven repo, so a fresh checkout won't compile until clean has run once.
  • The package build produces a fat jar (…-with-dependencies.jar) whose main class is core.Server; the build script renames it to builddir/server.jar.