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

5.2 KiB

Content Development Guide

How to add gameplay to 2009scape. Assumes you've read ../AGENTS.md and ARCHITECTURE.md.

Golden rules: new content is Kotlin, lives under Server/src/main/content/, implements a ContentInterface sub-type so ClassScanner auto-loads it, and should match authentic January-2009 RuneScape behavior.

Where things go

You're adding… Put it under…
A quest / area NPC / area dialogue content/region/<kingdom>/ (misthalin, asgarnia, kandarin, karamja, fremennik, morytania, desert, tirranwn, wilderness, misc)
A minigame / activity content/minigame/<name>/
Skill training logic content/global/skill/<skill>/
A cross-cutting item/interface/scenery handler content/global/handlers/
Shared data tables / enums content/data/
A player-runnable bot script content/global/bots/ (see BOT_SCRIPTING.md)

Follow the structure of the nearest existing example — the region tree alone has ~850 files, so there is almost always precedent to copy.

Reacting to interactions — InteractionListener

The workhorse for "player clicks/uses something." Implement InteractionListener and override defineListeners(). The handler returns true if it handled the interaction.

class AttackListener : InteractionListener {
    override fun defineListeners() {
        on(IntType.NPC, "attack") { player, npc ->
            player.attack(npc)
            return@on true
        }
    }
}

Common builders (see core.game.interaction.InteractionListener):

  • on(id/ids, IntType.NPC|SCENERY|ITEM|GROUNDITEM, vararg options) { player, node -> ... }
  • onUseWith(IntType, used, vararg with) { player, used, with -> ... } — "use X on Y"
  • onUseAnyWith(IntType, vararg with) / onUseWithWildcard(...) — broader matches (wildcards cost overhead on every use-with; use sparingly)
  • onEquip(id) { player, node -> ... } / onUnequip(...)
  • flagInstant() — mark listeners that should fire without the walk-to delay

IntType values: NPC, SCENERY, ITEM, GROUNDITEM, PLAYER.

Interface/widget clicks — InterfaceListener

Implement InterfaceListener / defineInterfaceListeners() to handle button presses on a specific interface (component) id, open/close hooks, etc.

Chat commands — Commands

Implement Commands / defineCommands(); register with define("name") { player, args -> }. Gate access with Privilege (STANDARD / ADMIN / …). Use the provided reject(player, msg) for usage errors and notify(player, msg) for output — reject colors the text red and aborts the command.

class MyCommands : Commands {
    override fun defineCommands() {
        define("heal", Privilege.ADMIN) { player, _ ->
            player.skills.updateLevel(Skills.HITPOINTS, /*...*/)
        }
    }
}

Command sets in the engine subclass CommandSet and are @Initializable; content-side commands just implement Commands.

Lifecycle hooks

  • LoginListener / LogoutListener — per-player login/logout logic.
  • TickListener — runs every game tick globally (keep it cheap).
  • StartupListener / ShutdownListener — server boot/shutdown.
  • PersistPlayer / PersistWorld — serialize custom player/world state to the save data.

NPC behavior

For per-NPC AI or combat overrides, extend NPCBehavior keyed by NPC id (core.game.node.entity.npc.NPCBehavior). Used for custom aggression, combat scripts, tick behavior, and death handling.

Dialogue

Dialogue lives beside its region/NPC (e.g. content/region/<kingdom>/dialogue/). Use the dialogue system (DialogueFile / dialogueInterpreter) to send NPC lines, player lines, and option menus. Look at existing *Dialogue.kt files for the current idiom.

Using the content API

Prefer the top-level helpers in core.api.ContentAPI.kt over engine internals: sendMessage, sendDialogue, animate, visualize, addItem, removeItem, hasAnItem, getStatLevel, rewardXP, teleport, getVarp/setVarp, getAttribute/setAttribute, lock/unlock, playAudio, etc. These are the intended, stable surface and keep content decoupled from the engine.

Player attributes support a /save: prefix (e.g. setAttribute("/save:my_flag", true)) to persist across logins.

IDs and data

  • Reference game ids via ConstLib: org.rs09.consts.Items, NPCs, Scenery, Animations, Components, Sounds, etc. — e.g. Items.FEATHER_314.
  • Static data (spawns, shops, drop tables, item configs) is JSON loaded at runtime. Edit with the Thanos tool (Java 11), not by hand.

Testing your content

  • Run locally with ./run (no DB needed with default config).
  • Add JUnit tests under Server/src/test/kotlin/ for non-trivial logic — there's good precedent in content/skill, content/region, and the core API tests.
  • Run sh mvnw verify to execute tests and detekt before opening an MR.

Before you open a merge request

  • Kotlin only (unless fixing existing Java).
  • Passes mvn verify (tests + detekt clean).
  • Behavior is authentic to ~Jan 2009.
  • Uses the default GitLab MR template (.gitlab/issue_templates/Default.md).