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

126 lines
5.2 KiB
Markdown

# 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/<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`](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/<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`).