244 lines
12 KiB
Markdown
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 |
|