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 ofcore.apiand 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:
- Loads configuration from
worldprops/default.conf(TOML) intoGameWorld.settings. - Loads the RuneScape cache (map, item/npc/object definitions) via
core.cache. - Runs
core.plugin.ClassScanner, which uses the classgraph library to scan the classpath and reflectively instantiate every class implementing a knownContentInterfacesub-type (see the table inAGENTS.md). This is how content "registers" itself — there is no central manifest to edit. - 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):
Entity→PlayerandNPC(living things with combat, skills, movement).Playeralso 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 viawebsocket_enabled/websocket_portindefault.conf(port defaults to53594 + world_id). Plainws://for local testing;wss://(with a PKCS12 keystore) for production/PWA. See the README for keystore setup. - Client and server must share
secret_keyor 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 bycore.game.system.config.ServerConfigParserintoGameWorld.settings(core.game.world.GameSettings). Paths (cache, saves, data, logs, scripts) are held incore.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
ConstLibdependency (org.rs09.consts.*, bundled inServer/libs/) — reference them asItems.FEATHER_314,NPCs.CHICKEN_..., etc. - Persistence: player saves + account data via MySQL (
mysql-connector-java) or SQLite (sqlite-jdbc). Controlled byuse_auth/persist_accountsin config; when both are false, no database is needed (ideal for local content work). Both MUST betruein production.
Testing & static analysis
- JUnit 5 tests in
Server/src/test/kotlin/(e.g.APITests,ExchangeTests, skill/region tests). Run with./run -torsh mvnw test. - detekt (
Server/detekt.yml) runs in the Mavenverifyphase 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 cleaninstalls the bundledConstLibandPrimitiveExtensionsjars fromServer/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 iscore.Server; thebuildscript renames it tobuilddir/server.jar.