# Content Development Guide How to add gameplay to 2009scape. Assumes you've read [`../AGENTS.md`](../AGENTS.md) and [`ARCHITECTURE.md`](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//` (misthalin, asgarnia, kandarin, karamja, fremennik, morytania, desert, tirranwn, wilderness, misc) | | A minigame / activity | `content/minigame//` | | Skill training logic | `content/global/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`](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. ```kotlin 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. ```kotlin 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//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`).