LLM Agent files
This commit is contained in:
@@ -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
|
||||||
@@ -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`.
|
||||||
@@ -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 |
|
||||||
@@ -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`).
|
||||||
@@ -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).
|
||||||
@@ -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`. |
|
||||||
Reference in New Issue
Block a user