From c1bd7eb0450d31777cfce6eb2a06f2ad33ece60d Mon Sep 17 00:00:00 2001 From: yzinchuk Date: Sun, 5 Jul 2026 18:14:54 -0400 Subject: [PATCH] LLM Agent files --- AGENTS.md | 188 +++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 122 +++++++++++++++++++ docs/BOT_SCRIPTING.md | 243 +++++++++++++++++++++++++++++++++++++ docs/CONTENT_GUIDE.md | 125 +++++++++++++++++++ docs/GLOSSARY.md | 75 ++++++++++++ docs/SERVER_SETUP_ARCH.md | 247 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 1000 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/BOT_SCRIPTING.md create mode 100644 docs/CONTENT_GUIDE.md create mode 100644 docs/GLOSSARY.md create mode 100644 docs/SERVER_SETUP_ARCH.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c8033af34 --- /dev/null +++ b/AGENTS.md @@ -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//` — 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//` — self-contained minigames (Barrows, Castle Wars, Pest + Control, MTA, etc.). +- `content/global/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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..a24826bf7 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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`. diff --git a/docs/BOT_SCRIPTING.md b/docs/BOT_SCRIPTING.md new file mode 100644 index 000000000..cdd929eb5 --- /dev/null +++ b/docs/BOT_SCRIPTING.md @@ -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 ` 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 `** — 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 ` 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/.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 | diff --git a/docs/CONTENT_GUIDE.md b/docs/CONTENT_GUIDE.md new file mode 100644 index 000000000..4c5b4a68f --- /dev/null +++ b/docs/CONTENT_GUIDE.md @@ -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//` (misthalin, asgarnia, kandarin, karamja, fremennik, morytania, desert, tirranwn, wilderness, misc) | +| A minigame / activity | `content/minigame//` | +| Skill training logic | `content/global/skill//` | +| A cross-cutting item/interface/scenery handler | `content/global/handlers/` | +| Shared data tables / enums | `content/data/` | +| A player-runnable bot script | `content/global/bots/` (see [`BOT_SCRIPTING.md`](BOT_SCRIPTING.md)) | + +Follow the structure of the nearest existing example — the region tree alone has ~850 +files, so there is almost always precedent to copy. + +## Reacting to interactions — `InteractionListener` + +The workhorse for "player clicks/uses something." Implement `InteractionListener` and +override `defineListeners()`. The handler returns `true` if it handled the interaction. + +```kotlin +class AttackListener : InteractionListener { + override fun defineListeners() { + on(IntType.NPC, "attack") { player, npc -> + player.attack(npc) + return@on true + } + } +} +``` + +Common builders (see `core.game.interaction.InteractionListener`): + +- `on(id/ids, IntType.NPC|SCENERY|ITEM|GROUNDITEM, vararg options) { player, node -> ... }` +- `onUseWith(IntType, used, vararg with) { player, used, with -> ... }` — "use X on Y" +- `onUseAnyWith(IntType, vararg with)` / `onUseWithWildcard(...)` — broader matches + (wildcards cost overhead on every use-with; use sparingly) +- `onEquip(id) { player, node -> ... }` / `onUnequip(...)` +- `flagInstant()` — mark listeners that should fire without the walk-to delay + +`IntType` values: `NPC`, `SCENERY`, `ITEM`, `GROUNDITEM`, `PLAYER`. + +## Interface/widget clicks — `InterfaceListener` + +Implement `InterfaceListener` / `defineInterfaceListeners()` to handle button presses on +a specific interface (component) id, open/close hooks, etc. + +## Chat commands — `Commands` + +Implement `Commands` / `defineCommands()`; register with `define("name") { player, args -> }`. +Gate access with `Privilege` (STANDARD / ADMIN / …). Use the provided `reject(player, msg)` +for usage errors and `notify(player, msg)` for output — `reject` colors the text red and +aborts the command. + +```kotlin +class MyCommands : Commands { + override fun defineCommands() { + define("heal", Privilege.ADMIN) { player, _ -> + player.skills.updateLevel(Skills.HITPOINTS, /*...*/) + } + } +} +``` + +Command *sets* in the engine subclass `CommandSet` and are `@Initializable`; content-side +commands just implement `Commands`. + +## Lifecycle hooks + +- `LoginListener` / `LogoutListener` — per-player login/logout logic. +- `TickListener` — runs every game tick globally (keep it cheap). +- `StartupListener` / `ShutdownListener` — server boot/shutdown. +- `PersistPlayer` / `PersistWorld` — serialize custom player/world state to the save data. + +## NPC behavior + +For per-NPC AI or combat overrides, extend `NPCBehavior` keyed by NPC id +(`core.game.node.entity.npc.NPCBehavior`). Used for custom aggression, combat scripts, +tick behavior, and death handling. + +## Dialogue + +Dialogue lives beside its region/NPC (e.g. `content/region//dialogue/`). Use the +dialogue system (`DialogueFile` / `dialogueInterpreter`) to send NPC lines, player lines, +and option menus. Look at existing `*Dialogue.kt` files for the current idiom. + +## Using the content API + +Prefer the top-level helpers in `core.api.ContentAPI.kt` over engine internals: +`sendMessage`, `sendDialogue`, `animate`, `visualize`, `addItem`, `removeItem`, +`hasAnItem`, `getStatLevel`, `rewardXP`, `teleport`, `getVarp`/`setVarp`, +`getAttribute`/`setAttribute`, `lock`/`unlock`, `playAudio`, etc. These are the intended, +stable surface and keep content decoupled from the engine. + +Player attributes support a `/save:` prefix (e.g. `setAttribute("/save:my_flag", true)`) +to persist across logins. + +## IDs and data + +- Reference game ids via `ConstLib`: `org.rs09.consts.Items`, `NPCs`, `Scenery`, + `Animations`, `Components`, `Sounds`, etc. — e.g. `Items.FEATHER_314`. +- Static data (spawns, shops, drop tables, item configs) is JSON loaded at runtime. Edit + with the **Thanos tool** (Java 11), not by hand. + +## Testing your content + +- Run locally with `./run` (no DB needed with default config). +- Add JUnit tests under `Server/src/test/kotlin/` for non-trivial logic — there's good + precedent in `content/skill`, `content/region`, and the core API tests. +- Run `sh mvnw verify` to execute tests **and** detekt before opening an MR. + +## Before you open a merge request + +- Kotlin only (unless fixing existing Java). +- Passes `mvn verify` (tests + detekt clean). +- Behavior is authentic to ~Jan 2009. +- Uses the default GitLab MR template (`.gitlab/issue_templates/Default.md`). diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md new file mode 100644 index 000000000..44ce2e8c1 --- /dev/null +++ b/docs/GLOSSARY.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). diff --git a/docs/SERVER_SETUP_ARCH.md b/docs/SERVER_SETUP_ARCH.md new file mode 100644 index 000000000..cc5aa3f84 --- /dev/null +++ b/docs/SERVER_SETUP_ARCH.md @@ -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 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`. |