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