LLM Agent files

This commit is contained in:
yzinchuk
2026-07-05 18:14:54 -04:00
parent afeffc1b98
commit c1bd7eb045
6 changed files with 1000 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
# AGENTS.md — 2009scape
Guidance for LLM agents (and humans) working in this repository. Read this first,
then consult the deeper docs in [`docs/`](docs/) for architecture and content patterns.
## What this project is
2009scape is an **open-source MMORPG emulation server** — a from-scratch remake of
RuneScape 2 as it existed around **build 530 / January 2009**.
The server speaks the original RS2 binary protocol to a matching game client. Players
connect over TCP or (optionally) WebSocket. This repo is the **game server only** — the
client and cache tooling live in separate projects.
## The single most important rule
> **All new contributions MUST be written in Kotlin.** Only touch the legacy Java when
> fixing or updating existing Java code. Do not write new features in Java.
Everything else in this file elaborates on how to do that well.
## Repository layout (top level)
| Path | What it is |
|------|-----------|
| `Server/` | The game server Maven project — **almost all work happens here** |
| `Server/src/main/core/` | Engine: networking, cache, game loop, entities, the content **API** |
| `Server/src/main/content/` | Actual game content: quests, NPCs, minigames, skills, regions |
| `Server/src/test/kotlin/` | JUnit 5 tests |
| `Server/worldprops/default.conf` | TOML server configuration (log level, DB, websocket, etc.) |
| `Server/pom.xml` | Maven build definition |
| `Server/detekt.yml` | Kotlin static-analysis (detekt) ruleset — runs in the `verify` phase |
| `build`, `run` | Bash helper scripts (build / build+run the server) |
| `run-server.bat` | Windows run script |
| `Dockerfile`, `docker-compose.yml` | Containerized server + MySQL |
| `Proto/`, `Server/src/main/proto/` | Protobuf for the management interface |
| `Tools/` | Cache/data editing tools (RSDataSuite, drop-table tool, JSON diff) |
| `docs/` | Deeper docs generated for agents (see below) |
## Build, run, test
**Prerequisites:** JDK 11 (exactly — newer JDKs are not supported by this codebase),
and for JSON data editing, the Thanos tool which needs Java 11.
The Maven wrapper (`Server/mvnw`) is the source of truth; the root `build`/`run` scripts
wrap it. From the repo root:
```bash
./build -g # build the server jar into builddir/server.jar
./build -qgc # clean + build, skipping tests (-q)
./run # incremental build, then run the server
./run -r # force a clean rebuild, then run
./run -t # run tests only, don't start the server
./run -h # full option list
```
Directly with Maven (equivalent, from `Server/`):
```bash
sh mvnw clean # clean (also installs bundled libs from Server/libs/)
sh mvnw package # build the fat jar (…-with-dependencies.jar)
sh mvnw test # run the JUnit test suite
sh mvnw verify # build + run detekt static analysis
```
> If you build manually (not via the provided scripts/IntelliJ), you **must** run
> `mvn clean` at least once first, because the clean phase installs the bundled
> `ConstLib` and `PrimitiveExtensions` jars from `Server/libs/` into the local repo.
The server main class is `core.Server`. It runs headless and listens for client
connections. By default (`use_auth = false`, `persist_accounts = false` in
`default.conf`) **no database is required** — great for local content testing.
**Docker:** copy `mysql.env.example``mysql.env`, create `config/default.conf` from
`Server/worldprops/default.conf`, then `docker compose up --build`.
## Tech stack
- **Kotlin 1.8.20** (primary) + legacy **Java 11** (~roughly half the files, being phased out)
- **Maven** build; JVM target 11
- **kotlinx-coroutines** for the pulse/worker system
- **classgraph** for reflective plugin discovery (see below)
- **JSON-simple** + **toml4j** for config/data, **MySQL connector** + **SQLite** for storage
- **Java-WebSocket** for the optional browser transport
- **JUnit 5** for tests, **detekt** for static analysis
## How content is wired in — the plugin/listener model
This is the concept an agent most needs to understand. **There is no central registry
you edit to add content.** Instead, at startup `core.plugin.ClassScanner` reflectively
scans the classpath (via classgraph) and instantiates every class implementing certain
marker interfaces. You add content by creating a new class implementing the right
interface anywhere under `src/main/content/`.
The core marker interface is `core.api.ContentInterface`. The important sub-interfaces:
| Interface | Override | Purpose |
|-----------|----------|---------|
| `InteractionListener` | `defineListeners()` | React to player↔NPC/object/item/ground interactions (`on(...)`, `onUseWith(...)`, `onEquip(...)`) |
| `InterfaceListener` | `defineInterfaceListeners()` | Handle interface/widget button clicks |
| `Commands` | `defineCommands()` | Register `::command` chat commands, gated by `Privilege` |
| `LoginListener` / `LogoutListener` | | Run logic on player login/logout |
| `TickListener` | | Run logic every game tick |
| `StartupListener` / `ShutdownListener` | | Server lifecycle hooks |
| `PersistPlayer` / `PersistWorld` | | Serialize custom player/world data |
| `NPCBehavior` | | Per-NPC-id AI / combat behavior |
| `MapArea` / `ZoneBuilder` | | Define map zones and their rules |
| `WorldEvent` | | Global scheduled/world events |
| `ActivityPlugin` | | Minigames / activities |
Minimal example (`Server/src/main/content/global/skill/AttackListener.kt`):
```kotlin
class AttackListener : InteractionListener {
override fun defineListeners() {
on(IntType.NPC, "attack") { player, npc ->
player.attack(npc)
return@on true // true = interaction handled
}
}
}
```
Almost everything a content author needs is exposed as top-level helper functions in
`core.api.ContentAPI.kt` (e.g. `sendMessage`, `animate`, `addItem`, `getStatLevel`,
`teleport`, `setVarp`). **Prefer these API functions over reaching directly into engine
internals** — they are the stable, intended surface for content.
## Directory conventions for content
Put new content where similar content already lives:
- `content/region/<kingdom>/` — content tied to a geographic area (Misthalin, Asgarnia,
Kandarin, Karamja, Fremennik, Morytania, Desert, Tirannwn, Wilderness, misc). This is
by far the largest area (~850 files). Quest logic and area NPCs/dialogue go here.
- `content/minigame/<name>/` — self-contained minigames (Barrows, Castle Wars, Pest
Control, MTA, etc.).
- `content/global/skill/<skill>/` — skill training logic shared across the world.
- `content/global/handlers/` — cross-cutting item/interface/scenery handlers.
- `content/data/` — shared data tables and enums (consumables, jewellery, quests list).
Item/NPC/object/animation IDs come from the `ConstLib` dependency (`org.rs09.consts`,
bundled in `Server/libs/`). Static game data (spawns, shops, drop tables) lives in JSON
data files loaded at runtime — edit those with the **Thanos tool** (Java 11), not by hand.
## Coding standards & conventions
- **Kotlin for anything new.** Match the style of the surrounding file.
- Keep engine (`core/`) and content (`content/`) separate. Content depends on the API in
`core.api`; the engine must not depend on specific content.
- Use `ContentAPI` helpers and `colorize(...)` for player-facing messages.
- Author-tag classes with a KDoc `@author` where the surrounding code does.
- **detekt** runs in `mvn verify` and CI — keep new code clean against `Server/detekt.yml`.
- Write a JUnit test under `src/test/kotlin/` when adding non-trivial logic; the suite
has good precedent for API, exchange, and skill tests.
- **Authenticity matters.** This is a remake targeting the real Jan-2009 game. When
implementing behavior, match authentic RuneScape behavior of that era rather than
inventing new mechanics.
## Contributing / merge requests
- Upstream is **GitLab**, not GitHub. Development discussion is on Discord.
- All merge requests **must** use the default MR template
(`.gitlab/issue_templates/Default.md`) or they will be rejected.
- CI is defined in `.gitlab-ci.yml`.
- License is **AGPL-3.0** and will not be changed — keep all contributions compatible.
## Things to be careful about (agent gotchas)
- **Don't switch Java versions.** JDK 11 specifically. Higher versions break the build.
- **Don't write new Java.** New content must be Kotlin.
- **Don't hand-edit binary/JSON game data** — use the provided tools.
- **`mvn clean` installs bundled libs** — a fresh checkout won't compile until it's run.
- **No DB needed for local testing** — leave `use_auth`/`persist_accounts` false locally,
but note the config comments: both MUST be `true` in production.
- The `build`/`run` scripts are Bash (Linux/OSX); Windows uses `run-server.bat`.
- This repo has **no pre-existing CLAUDE.md/AGENTS.md**; the `docs/` folder here was
authored to help agents and is not upstream canon — treat the code and README as
ground truth if they ever disagree.
## Further reading
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — engine internals and the game loop
- [`docs/CONTENT_GUIDE.md`](docs/CONTENT_GUIDE.md) — how to add quests, NPCs, items, commands
- [`docs/BOT_SCRIPTING.md`](docs/BOT_SCRIPTING.md) — the player self-botting system (`::script`) and how to add new scripts
- [`docs/SERVER_SETUP_ARCH.md`](docs/SERVER_SETUP_ARCH.md) — compile & run the server on Arch Linux and connect from a LAN PC
- [`docs/GLOSSARY.md`](docs/GLOSSARY.md) — RuneScape/RSPS terms used throughout the code
- `README.md` — project history, values, and full setup instructions
+122
View File
@@ -0,0 +1,122 @@
# Architecture
How the 2009scape game server is put together. See [`../AGENTS.md`](../AGENTS.md) for
build/run instructions and the top-level rules, and [`CONTENT_GUIDE.md`](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 of `core.api` and
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:
1. Loads configuration from `worldprops/default.conf` (TOML) into `GameWorld.settings`.
2. Loads the RuneScape **cache** (map, item/npc/object definitions) via `core.cache`.
3. Runs **`core.plugin.ClassScanner`**, which uses the **classgraph** library to scan the
classpath and reflectively instantiate every class implementing a known
`ContentInterface` sub-type (see the table in `AGENTS.md`). This is how content
"registers" itself — there is no central manifest to edit.
4. 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`](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``Player` and `NPC` (living things with combat, skills, movement).
- `Player` also 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 `Container`s
(`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 via `websocket_enabled`/`websocket_port` in `default.conf` (port defaults to
`53594 + world_id`). Plain `ws://` for local testing; `wss://` (with a PKCS12 keystore)
for production/PWA. See the README for keystore setup.
- Client and server must share `secret_key` or 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 by
`core.game.system.config.ServerConfigParser` into `GameWorld.settings`
(`core.game.world.GameSettings`). Paths (cache, saves, data, logs, scripts) are held in
`core.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 **`ConstLib`** dependency
(`org.rs09.consts.*`, bundled in `Server/libs/`) — reference them as `Items.FEATHER_314`,
`NPCs.CHICKEN_...`, etc.
- **Persistence:** player saves + account data via MySQL (`mysql-connector-java`) or
SQLite (`sqlite-jdbc`). Controlled by `use_auth` / `persist_accounts` in config; when
both are false, **no database is needed** (ideal for local content work). Both MUST be
`true` in production.
## Testing & static analysis
- **JUnit 5** tests in `Server/src/test/kotlin/` (e.g. `APITests`, `ExchangeTests`,
skill/region tests). Run with `./run -t` or `sh mvnw test`.
- **detekt** (`Server/detekt.yml`) runs in the Maven `verify` phase 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 clean` installs the bundled `ConstLib` and `PrimitiveExtensions` jars from
`Server/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 is
`core.Server`; the `build` script renames it to `builddir/server.jar`.
+243
View File
@@ -0,0 +1,243 @@
# Bot Scripting Guide (player self-botting)
This documents the system that lets a **player automate their own account** — i.e. hand
their logged-in character over to an in-game script that plays for them. This is the area
to expand when adding new automation scripts.
## The two kinds of "bots" — don't confuse them
The `core.game.bots` package serves two related but distinct purposes:
1. **AI players (`AIPlayer`)** — fake, server-spawned characters used to populate the
world (`PvMBots`, `CombatBot`, the various assemblers/builders). They are *not* real
accounts.
2. **Player self-botting** — a **real** logged-in `Player` runs a `Script` on their own
character via the `::script` command. This guide is about #2.
Both reuse the same `Script` base class and `ScriptAPI`, which is why they live together.
## How a player runs a script on their own account
Implemented in `core.game.system.command.sets.BottingCommandSet` (a `STANDARD`-privilege
command set). All three commands early-return unless botting is enabled in config:
- **`::scripts`** — opens an interface listing every registered player-compatible script:
its name, description lines, and the `::script <identifier>` needed to start it. On
first use it shows a red warning that running a script removes the player from the
highscores; the player must acknowledge (sets the saved attribute
`botting:warning_shown`) before the list appears.
- **`::script <identifier>`** — looks up the script in `PlayerScripts.identifierMap`,
closes interfaces, and starts it on the calling player via
`GeneralBotCreator(script.newInstance() as Script, player, isPlayer = true)`. Also shows
the highscores warning on first use.
- **`::stopscript`** — retrieves the running pulse from the player's `botting:script`
attribute and stops it, closing the overlay. (Logging out also stops the script.)
Starting a script sets `/save:not_on_highscores = true` on the player (the permanent
highscore removal) and stashes the running `BotScriptPulse` under the `botting:script`
attribute.
## Enabling the feature
Off by default. In `Server/worldprops/default.conf`:
```properties
enable_botting = false # set true to expose ::scripts / ::script / ::stopscript
```
Config flow: `default.conf` `world.enable_botting`
`ServerConfigParser``GameSettings.enabled_botting` → read by `BottingCommandSet`.
(Note the config key is `enable_botting`; the parsed field is `enabled_botting`.)
## How scripts are discovered and registered
At startup `core.plugin.ClassScanner` finds every class annotated with
**`@PlayerCompatible`** and registers it into `PlayerScripts.identifierMap`, keyed by the
script's `@ScriptIdentifier`. Each entry is a
`PlayerScripts.PlayerScript(identifier, description, name, clazz)`.
So a script becomes available to players simply by existing with the right annotations —
**no registry edit needed.**
## Anatomy of a script
Player-runnable scripts live in **`Server/src/main/content/global/bots/`** and extend
`core.game.bots.Script`. Required annotations:
| Annotation | Meaning |
|-----------|---------|
| `@PlayerCompatible` | Marks the script as runnable on a real player account (required for `::script`) |
| `@ScriptName("...")` | Display name in the `::scripts` list |
| `@ScriptDescription("line1", "line2", ...)` | Description lines shown in the list |
| `@ScriptIdentifier("snake_case_id")` | The id used in `::script <id>` and the map key |
The base class (`Script.java`) gives you:
- `bot: Player` — the character the script controls (for self-botting, the real player).
- `scriptAPI: ScriptAPI` — the automation helper API (created in `init`).
- `inventory` / `equipment` / `skills` / `quests` — starting loadouts, **only applied for
AI players** (`isPlayer == false`); skipped when a real player runs the script.
- `abstract fun tick()` — called each pulse to advance the script. **This is where all
your logic goes.**
- `newInstance()` — currently required to compile but effectively unused (the transition
pulse that called it is dead code); return `this`.
### The execution model
`GeneralBotCreator` wraps the script in a `BotScriptPulse` (a `Pulse(1)`, i.e. runs each
tick) submitted to `GameWorld.Pulser`. Each tick, `pulse()`:
1. If a random delay is pending, decrements it and skips (simulates human hesitation).
2. If the bot is stuck mid-`MovementPulse` for >5 ticks, cancels the movement.
3. If a modal/dialogue is open and `endDialogue == true`, auto-closes it to avoid a
deadlock where the authentic interaction subsystem waits on input that never comes.
(Set `endDialogue = false` in your script if you need dialogue to persist — e.g. boat
travel.)
4. Only when the bot has **no pulse running and no active script action** does it call
your `tick()`. There's also a per-tick global cap (`botPulsesTriggeredThisTick`, max
75) and a ~10% chance to insert a random idle delay (skipped for `Idler`).
Practical implication: **write `tick()` as a state machine.** Kick off one action
(walk, attack, interact), let the resulting pulse run, and advance your state on the next
`tick()` when the bot is free again. See `ChickenKiller` below.
### Reference example — `ChickenKiller`
`content/global/bots/ChickenKiller.kt` is the canonical minimal example. Shape:
```kotlin
@PlayerCompatible
@ScriptName("Chicken Killer")
@ScriptDescription("Kills chickens and loots feathers. Start in any chicken area.")
@ScriptIdentifier("chicken_killer")
class ChickenKiller : Script() {
var state = State.INIT
var overlay: ScriptAPI.BottingOverlay? = null
override fun tick() {
when (state) {
State.INIT -> {
overlay = scriptAPI.getOverlay()
overlay!!.init(); overlay!!.setTitle("Chickens")
overlay!!.setTaskLabel("Chickens KO'd:"); overlay!!.setAmount(0)
// ask the player a config question via dialogue, then advance state
state = State.CONFIG
}
State.KILLING -> {
val chicken = scriptAPI.getNearestNode("Chicken")
if (chicken == null) scriptAPI.randomWalkTo(startLocation, 3)
else scriptAPI.attackNpcInRadius(bot, "Chicken", 10)
}
// ... LOOTFEATHER / LOOTBONES / BURYBONES states ...
}
}
override fun newInstance(): Script = this
enum class State { INIT, CONFIG, KILLING, IDLE, LOOTFEATHER, LOOTBONES, BURYBONES }
}
```
Other **player-compatible** scripts to crib from (all in `content/global/bots/`):
`CoalMiner`, `LobsterCatcher`, `SharkCatcher`, `NatureCrafter`, `LawCrafter`,
`CosmicCrafter`, `GnomeAgility`, `GnomeBowstring`, `SeersFlax`, `SeersMagicTrees`,
`VarrockEssenceMiner`, `DraynorWillows`, `CannonballSmelter`.
## AI-world bots vs. player self-bots (and converting between them)
The `content/global/bots/` directory holds **two kinds of scripts that share the exact
same `Script` base class, `tick()`, and `ScriptAPI`** — they differ only in how they're
launched and bodied:
- **AI-world bots** — server-spawned fake `AIPlayer`s used to make the world feel
populated. Launched at startup by `core.game.world.ImmerseWorld` (a `StartupListener`,
gated by `enable_bots` / `max_adv_bots` config) via
`GeneralBotCreator(script, location)` — the `bot` is a fabricated `AIPlayer` whose gear,
stats, and inventory are **injected** by `SkillingBotAssembler` / `CombatBotAssembler`.
These are the ones **without** `@PlayerCompatible` (e.g. `CowKiller`, `ManThiever`,
`FarmerThiever`, `VarrockSmither`, `GreenDragonKiller`, `DraynorFisher`, `Idler`,
`DoublingMoney`).
- **Player self-bots** — launched by `::script` via
`GeneralBotCreator(script, player, isPlayer = true)`. The `bot` **is the real player**,
and `Script.init()` **skips** the `inventory`/`equipment`/`skills`/`quests` injection
(that only runs when `isPlayer == false`).
**Converting an AI-world bot into a player self-bot** is usually straightforward because
the core logic is identical. Steps:
1. Add the four registration annotations (`@PlayerCompatible`, `@ScriptName`,
`@ScriptDescription`, `@ScriptIdentifier`). This alone lists it in `::scripts`.
2. Remove reliance on assembler-injected gear/stats — the real player brings their own.
Anything the AI version assumed it was handed (pickaxe, runes, armor, food) must be
checked / withdrawn from bank / bought on the GE, or stated as a start requirement in
the description lines.
3. Drop fixed spawn-location assumptions — a player runs from where they stand. The
`newInstance()` overrides that build a fresh `AIPlayer` at a spawn zone are irrelevant
in player mode (and that respawn path is dead code); just `return this`.
4. Sanity-check that the behavior makes sense on a real account. Clean candidates:
`VarrockSmither`, `NonBankingMiner`, `FarmerThiever`, `ManThiever`, and the
bankstander scripts. Poor candidates: `DoublingMoney` (a scammer NPC), `Idler`, and
world-immersion combat bots like `GreenDragonKiller`.
The reverse (making a player script also spawn as world AI) means giving it a
`newInstance()`/spawn that builds an `AIPlayer` via an assembler and adding a
`GeneralBotCreator(script, location)` call in `ImmerseWorld`.
## The `ScriptAPI` toolbox
`core.game.bots.ScriptAPI` (constructed as `scriptAPI` on your `Script`) is the automation
surface. Grouped highlights:
**Finding things**
- `getNearestNode(name)` / `getNearestNode(id, isObject)` / `getNearestNodeFromList(names, isObject)`
- `getNearestGameObject(loc, objectId)`, `getNearestObjectByPredicate { ... }`
- `distance(n1, n2)`
**Movement**
- `walkTo(loc)`, `walkArray(steps)`, `randomWalkTo(loc, radius)`
- `randomizeLocationInRanges(loc, xMin, xMax, yMin, yMax, z)`
- `teleport(loc)`, `teleportToGE()`
**Interacting / combat**
- `interact(bot, node, option)`, `useWith(bot, itemId, node)`
- `attackNpcInRadius(bot, name, radius)`, `attackNpcsInRadius(bot, radius)`
- `takeNearestGroundItem(id)`
**Items / banking / Grand Exchange**
- `bankItem(id)`, `bankAll { onComplete }`, `depositAtBank()`, `withdraw(id, amount)`
- `sellOnGE(id)`, `sellAllOnGe()`, `sellAllOnGeAdv()`, `buyFromGE(bot, id, amount)`
- `eat(foodId)`, `forceEat(foodId)`
- `equipAndSetStats(item(s))`, `loadAppearanceAndEquipment(json)`
**Feedback / UI**
- `sendChat(message)`
- `getOverlay(): BottingOverlay``init()`, `setTitle(...)`, `setTaskLabel(...)`,
`setAmount(n)` — the little progress panel scripts show while running.
## How to add a new self-bot script (checklist)
1. Create `Server/src/main/content/global/bots/<YourScript>.kt` extending `Script()`.
2. Annotate it with `@PlayerCompatible`, `@ScriptName`, `@ScriptDescription`,
`@ScriptIdentifier("unique_id")`.
3. Implement `tick()` as a state machine driving `scriptAPI` calls; add a
`BottingOverlay` for progress feedback if useful.
4. Implement `newInstance()` returning `this`.
5. Build and run with `enable_botting = true` in `default.conf`; test in-game with
`::scripts` then `::script unique_id`, and `::stopscript` to end.
6. Remember `inventory`/`equipment`/`skills`/`quests` fields are **ignored** when a real
player runs it — don't rely on them for player self-botting; instead work with whatever
the player already has (bank/withdraw/buy as needed).
## Key files at a glance
| File | Role |
|------|------|
| `core/game/system/command/sets/BottingCommandSet.kt` | The `::scripts` / `::script` / `::stopscript` commands |
| `core/game/bots/Script.java` | Base class every script extends |
| `core/game/bots/ScriptAPI.kt` | Automation helper API (`scriptAPI`) |
| `core/game/bots/GeneralBotCreator.kt` | Wraps a script in a per-tick `BotScriptPulse` |
| `core/game/bots/PlayerScripts.kt` | Registry (`identifierMap`) of player-runnable scripts |
| `core/game/bots/PlayerCompatible.kt` + `ScriptName`/`ScriptDescription`/`ScriptIdentifier` | The annotations `ClassScanner` reads |
| `core/plugin/ClassScanner.kt` | Discovers `@PlayerCompatible` scripts at startup |
| `content/global/bots/*.kt` | The actual scripts (add yours here) |
| `worldprops/default.conf``enable_botting` | Feature flag |
+125
View File
@@ -0,0 +1,125 @@
# 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`).
+75
View File
@@ -0,0 +1,75 @@
# Glossary
RuneScape, RSPS, and codebase-specific terms that show up throughout 2009scape.
## Project / RuneScape terms
- **RS2 / build 530** — the version of RuneScape 2 this project emulates, targeting
content as it existed around **January 2009**.
- **RSPS** — RuneScape Private Server; a third-party server emulating the game.
- **Authenticity** — a core value: behavior should match the real ~Jan-2009 game, not
invented mechanics.
- **Cache** — the RuneScape game data archive (maps, models, item/NPC/object definitions).
Read by `core.cache`. Edited with external tools (`Tools/`, Thanos, RSDataSuite).
- **Client** — the game client that connects to this server; lives in a separate repo.
- **World** — a running game instance. Multiple worlds can run with different ids; the
websocket port defaults to `53594 + world_id`.
## Engine concepts
- **Tick** — the fundamental time unit of the game loop (canonically 600 ms). Almost all
timed logic is expressed in ticks.
- **Pulse** — a unit of scheduled/repeating work (`core.game.system.task.Pulse`) submitted
to `GameWorld.Pulser`; `pulse()` returns `true` when done. Movement, combat, skilling,
and bot scripts are pulses.
- **Node** — base type for anything in the world (`core.game.node`): entities, items,
ground items, scenery.
- **Entity** — a living `Node`: `Player` or `NPC`.
- **AIPlayer** — a server-controlled fake player used to populate the world / for bots.
- **Scenery** — a world object (tree, door, altar, etc.).
- **Container** — an item collection (`inventory`, `equipment`, `bank`).
- **Varp / Varbit** — RuneScape client state variables ("player variables" / bit-packed
variables) used to drive interface and world state. Accessed via `getVarp`/`setVarp`,
`VarbitDefinition`.
- **Component / Interface / Widget** — an on-screen UI panel (ids from `Components`).
- **Attribute** — arbitrary key/value state stored on a `Player`; prefix a key with
`/save:` to persist it across logins.
## Content system
- **ContentInterface** — the marker interface (`core.api.ContentInterface`) whose
sub-types `ClassScanner` reflectively instantiates at startup. Implementing one is how
content self-registers.
- **ClassScanner** — startup component (`core.plugin.ClassScanner`) that uses **classgraph**
to discover and load all content/plugins.
- **InteractionListener** — content interface for reacting to player interactions with
NPCs / scenery / items / ground items.
- **InterfaceListener** — content interface for handling UI/widget button events.
- **Commands** — content interface for registering `::command` chat commands, gated by
**Privilege**.
- **CommandSet** — engine-side grouping of commands, marked `@Initializable`.
- **NPCBehavior** — per-NPC-id AI/combat behavior class.
- **Plugin** — historically any auto-loaded content class; in legacy Java code see
`core.plugin.Plugin` / `PluginManifest` / `@Initializable`.
- **ConstLib** — external dependency (`org.rs09.consts.*`) providing named id constants
(`Items`, `NPCs`, `Scenery`, `Animations`, `Components`, `Sounds`).
## Bots / self-botting
- **Script** (`core.game.bots.Script`) — a bot automation routine with a `tick()` method.
- **Self-botting** — a real player automating their own account via `::script`; permanently
removes them from the highscores. Gated by the `enable_botting` config flag.
- **`@PlayerCompatible`** — annotation marking a `Script` as runnable on a real player;
such scripts appear in the `::scripts` menu.
- **ScriptAPI** — automation helper API (`scriptAPI`) exposing movement, interaction,
banking, GE, and overlay helpers to scripts.
- **BottingOverlay** — the small progress panel a running script shows the player.
- See [`BOT_SCRIPTING.md`](BOT_SCRIPTING.md) for the full system.
## Tooling / infra
- **Thanos tool** — the required editor for the JSON game-data files; runs on **Java 11**.
- **RSDataSuite** — cache/data tool bundled in `Tools/`.
- **detekt** — Kotlin static analyzer run in `mvn verify` / CI (`Server/detekt.yml`).
- **mvnw** — the Maven wrapper; source of truth for building.
- **Thanos / Zaros jar** — same tool family for JSON editing (see README).
+247
View File
@@ -0,0 +1,247 @@
# Running a 2009scape Server on Arch Linux (LAN play)
A step-by-step guide to compiling and running the game server on an Arch Linux machine,
then connecting to it from another PC on your network (e.g. a gaming PC in the same house).
This targets a **private/LAN setup for yourself** — it deliberately skips the database
(`use_auth`/`persist_accounts` stay off), which is the simplest way to get playing. See
[the notes on persistence](#optional-persistence-and-accounts-mysql) if you want saved
accounts.
> Reminder: the upstream project only supports its own live server. This guide is for
> running your own copy locally and is assembled from how this repo actually works — see
> [`ARCHITECTURE.md`](ARCHITECTURE.md) and the root [`../README.md`](../README.md).
## 0. What you'll end up with
- The **game server** running on your Arch machine (headless, in a terminal).
- It listens on TCP port **`43594 + world_id`** → **43595** with the default `world_id = 1`.
- Your **gaming PC** runs the 2009scape **client** (a separate download) pointed at the
Arch machine's LAN IP.
## 1. Install prerequisites (Arch)
The build needs **JDK 11 specifically** — not a newer JDK (the project targets Java 11 and
newer versions break the build). You also need **git** and **git-lfs** (the game cache is
stored via Git LFS).
```bash
sudo pacman -S --needed jdk11-openjdk git git-lfs
# optional: tmux, if you want to use the run script's -x fancy session mode
sudo pacman -S --needed tmux
```
You do **not** need Maven installed — the repo ships the Maven wrapper (`Server/mvnw`).
If you have multiple JDKs installed, point the default at 11 for this shell:
```bash
archlinux-java status # list installed JVMs
sudo archlinux-java set java-11-openjdk
java -version # should report 11.x
```
Enable Git LFS once for your user:
```bash
git lfs install
```
## 2. Get the code and pull the cache
If you haven't cloned yet (upstream is on GitLab):
```bash
git clone https://gitlab.com/2009scape/2009scape.git
cd 2009scape
```
If you already have this repo, just make sure the LFS-backed cache is actually present
(this pulls the real binary cache files under `Server/data/cache/`, which are LFS pointers
until fetched):
```bash
git lfs pull
```
You can sanity-check the cache came down (files should be MB-sized, not tiny pointer text):
```bash
du -sh Server/data/cache
```
## 3. Configure the server for LAN play
The server config is `Server/worldprops/default.conf` (TOML). The defaults are already set
up for a no-database local run:
- `use_auth = false` — any password is accepted at login.
- `persist_accounts = false` — no database required.
- `noauth_default_admin = true` — you log in as an admin (handy for testing).
- `world_id = "1"` — so the game port is **43595**.
- `enable_bots = true` — the world spawns AI player bots (see [`BOT_SCRIPTING.md`](BOT_SCRIPTING.md)); set to `false` if you'd rather have an empty world.
**You generally do not need to change anything in the config for LAN play.** The one field
people assume they must change — `msip` — is the **management-server** address (used by the
separate world-list/management backend), *not* the address the game client connects to. For
a single self-hosted world you can leave `msip = "127.0.0.1"`.
Two things you *may* want to set:
- **`secret_key`** — the client sends this on login and it **must match** the server's
value or the connection is refused. The default is `"2009scape_development"`. If your
client uses a different key, make them match here.
- **`new_player_location` / `home_location`** — where you spawn; fine to leave default.
## 4. Build and run
From the repo root, the helper scripts wrap the Maven wrapper. The simplest path:
```bash
./run
```
`./run` does an incremental build and then starts the server. Other useful invocations:
```bash
./run -r # force a clean rebuild, then run (use after pulling updates)
./run -t # run the test suite only, don't start the server
./run -h # show all options
./build -qgc # clean build only (skip tests), no run
```
The **first build takes a while** (it compiles thousands of Kotlin/Java files). Grab a
coffee. Subsequent runs are fast.
Under the hood this runs the fat jar with:
```
cd Server && java -Dnashorn.args=--no-deprecation-warning -jar builddir/server.jar
```
The server runs headless in your terminal. You'll see log lines ending with something like
`2009Scape started in <n> milliseconds.` and `Starting networking...`. It reads simple
commands on stdin — type `stop` to shut it down cleanly (or `help` for the list).
> If you build manually with Maven instead of the scripts, run `sh mvnw clean` first — the
> clean phase installs bundled libraries (`ConstLib`, `PrimitiveExtensions`) from
> `Server/libs/` into your local Maven repo, without which compilation fails.
### Memory
The server is comfortable in default JVM memory for a small LAN world. If you enable
`preload_map = true` (smoother ticks) it needs ~2 GB more RAM; give the JVM more heap by
editing the `java` invocation in the `run` script, e.g. add `-Xmx4g`.
## 5. Find the server's LAN IP
On the Arch machine:
```bash
ip -4 addr show | grep inet
```
Note the LAN address (typically `192.168.x.y` or `10.x.y.z`). That's what the gaming PC
will connect to. Example used below: `192.168.1.50`.
## 6. Open the firewall (if one is running)
Arch has no firewall enabled by default, but if you run one, allow the game port
(**43595** for world 1). Examples:
```bash
# firewalld
sudo firewall-cmd --add-port=43595/tcp --permanent && sudo firewall-cmd --reload
# ufw
sudo ufw allow 43595/tcp
# nftables (add to your ruleset)
# tcp dport 43595 accept
```
If you also enabled the browser WebSocket transport, open its port too (default
`53594 + world_id` = **53595**).
## 7. Connect from the gaming PC
The **client is a separate program** from this server repo — download the launcher/client
from the 2009scape site or use whichever client you already have. In the client's server
configuration, point it at the Arch machine instead of the public server:
- **Server address / IP:** the Arch machine's LAN IP, e.g. `192.168.1.50`
- **Port:** `43595` (i.e. `43594 + world_id`)
- **Secret key:** must match `secret_key` in `default.conf` (default `2009scape_development`)
How you set these depends on the client build — commonly an in-launcher field, a settings
file, or a `worlds`/`serverlist` entry. Look for where the client stores the world IP/port.
Then log in with any username; with `use_auth = false` the password is not checked, and
with `noauth_default_admin = true` you'll have admin privileges (try `::` commands in
chat).
## 8. Quick verification
- On the server machine, confirm it's listening:
```bash
ss -tlnp | grep 43595
```
- From the gaming PC, confirm reachability (PowerShell):
```powershell
Test-NetConnection 192.168.1.50 -Port 43595
```
or from any Linux box: `nc -vz 192.168.1.50 43595`.
If the port test succeeds but login fails, the usual culprit is a **`secret_key` mismatch**
between client and server.
## Optional: persistence and accounts (MySQL)
The no-database setup above forgets account-level data (credits, playtime) on restart —
but note character save data (stats, inventory) is handled separately and still saves to
`Server/data/players/`. If you want real authenticated accounts and persisted account
data, set in `default.conf`:
```properties
use_auth = true
persist_accounts = true
```
…and provide a MySQL/MariaDB database matching the `[database]` block (`database_name`,
`_username`, `_password`, `_address`, `_port`). On Arch:
```bash
sudo pacman -S --needed mariadb
sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
sudo systemctl enable --now mariadb
```
Then create the database and import the schema shipped in the repo:
```bash
sudo mariadb -e "CREATE DATABASE global;"
sudo mariadb global < Server/db_exports/global.sql
# optional test account:
sudo mariadb global < Server/db_exports/testuser.sql
```
Adjust `database_username`/`database_password` in `default.conf` to match a MySQL user you
create. (For quick LAN use, keeping the database off is simpler.)
### Docker alternative
If you'd rather not manage a local MariaDB, the repo has a Docker path (server + MySQL) —
see the **Docker** section of the root [`../README.md`](../README.md). It uses
`mysql.env` and a `config/default.conf` you copy from `Server/worldprops/default.conf`.
## Troubleshooting
| Symptom | Likely cause / fix |
|---------|--------------------|
| Build fails immediately with weird Kotlin/Java errors | Wrong JDK. Ensure `java -version` is **11** (`sudo archlinux-java set java-11-openjdk`). |
| Build fails about missing `ConstLib`/`primextends` | You built without a clean. Run `./build -qgc` (or `sh mvnw clean` in `Server/`). |
| Cache errors / tiny cache files | LFS not pulled. Run `git lfs install` then `git lfs pull`. |
| `Port 43595 is already in use` | Another server instance is running, or change `world_id`. |
| Client can't reach the server | Firewall on the Arch box, wrong LAN IP, or client pointed at the wrong port. Verify with `ss`/`nc` (step 8). |
| Connects but login refused | `secret_key` mismatch between client and `default.conf`. |
| World feels crowded with bots | Set `enable_bots = false` (and/or `max_adv_bots = 0`) in `default.conf`. |