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

244 lines
12 KiB
Markdown

# 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 |