126 lines
5.2 KiB
Markdown
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`).
|