Script Structure and Setup
Script Structure and Setup Guide
This comprehensive guide explains how to create well-structured Lua scripts for PowBot Desktop, covering everything from basic script scaffolding to advanced features like configuration schemas and paint overlays.
Table of Contents
- Project Structure
- Manifest File
- Script Lifecycle
- Configuration System
- Paint System
- API Invocation Pattern
- VSCode Setup
- Complete Working Example
- Best Practices
Project Structure
PowBot scripts can be organized as either single-file scripts or multi-file projects. Multi-file projects are recommended for complex scripts with multiple features.
Single-File Structure
my-script/
├── manifest.json # Script metadata and configuration
└── main.lua # Main script file
Multi-File Structure
my-script/
├── manifest.json # Script metadata
├── main.lua # Entry point
├── modules/ # Core logic modules
│ ├── config.lua # Configuration handling
│ ├── tasks.lua # Task implementations
│ └── ui.lua # Paint/UI setup
└── utils/ # Utility functions
└── helpers.lua # Helper functions
Creating Your Script Directory
- Navigate to your scripts folder (e.g.,
test-scripts/) - Create a new folder with your script name
- Create
manifest.jsonandmain.luafiles inside
mkdir my-script
cd my-script
touch manifest.json main.lua
Manifest File
The manifest.json file defines script metadata, dependencies, and structure.
Basic Manifest
{
"name": "My Script",
"version": "1.0.0",
"description": "A simple example script",
"author": "Your Name",
"main": "main.lua",
"min_api_version": "1.0.0"
}
Multi-File Manifest
For scripts with multiple modules:
{
"name": "Advanced Script",
"version": "2.0.0",
"description": "A complex multi-file script",
"author": "Your Name",
"main": "main.lua",
"modules": [
"modules/config.lua",
"modules/tasks.lua",
"modules/ui.lua",
"utils/helpers.lua"
],
"lua_paths": [
"modules",
"utils"
],
"min_api_version": "1.0.0"
}
Field Descriptions
| Field | Required | Description |
|---|---|---|
name | Yes | Display name shown in the UI |
version | Yes | Semantic version (e.g., "1.0.0") |
description | Yes | Brief description of what the script does |
author | Yes | Your name or username |
main | Yes | Entry point file (usually "main.lua") |
modules | No | List of additional Lua files to load |
lua_paths | No | Directories to add to Lua's require path |
min_api_version | Yes | Minimum required API version |
Script Lifecycle
Every script must return a table with lifecycle methods. The engine calls these methods at specific times during script execution.
Lifecycle Flow
on_start() → poll() → poll() → ... → on_stop()
↓
(on_pause/on_resume can interrupt)
Basic Script Template
local script = {
name = "MyScript",
version = "1.0.0"
}
-- Called once when script starts
-- Return true to continue, false to stop immediately
function script:on_start()
logger:info("Script starting...")
-- Initialize variables
self.state = "IDLE"
self.counter = 0
-- Validate prerequisites
local player = players:local_player()
if not player then
logger:error("No player found!")
return false
end
return true
end
-- Called repeatedly while script is running (main loop)
-- This is where your automation logic lives
function script:poll()
-- Check conditions
local player = players:local_player()
if not player then
return
end
-- Execute logic based on state
if self.state == "IDLE" then
-- Do something
self.counter = self.counter + 1
logger:info("Counter: " .. self.counter)
end
-- Always include a sleep to control polling rate
sleep(50) -- Poll every 50ms
end
-- Called when script is paused (optional)
function script:on_pause()
logger:info("Script paused")
end
-- Called when script resumes from pause (optional)
function script:on_resume()
logger:info("Script resumed")
end
-- Called when script stops (optional)
-- Use for cleanup and logging final stats
function script:on_stop()
logger:info("Script stopped. Total iterations: " .. self.counter)
end
return script
Why Use self?
The script table is passed as self to each lifecycle method. This allows you to:
- Store persistent state across method calls
- Track statistics and counters
- Share data between lifecycle methods
-- Store state in script table
function script:on_start()
self.kills = 0
self.state = "HUNTING"
end
-- Access state in poll
function script:poll()
if self.state == "HUNTING" then
-- ... attack logic ...
self.kills = self.kills + 1
end
end
-- Access state in cleanup
function script:on_stop()
logger:info("Total kills: " .. self.kills)
end
Important Notes
on_start()must returntrueto continue orfalseto abort startuppoll()is called continuously - always includesleep()to prevent excessive CPU usageon_stop()errors don't halt script cleanup - they're logged and ignored- Use
sleep(50)orsleep(100)as a baseline polling rate
Configuration System
PowBot uses a flexible UI configuration system where scripts define their UI layout and receive configuration values.
How Configuration Works
- Script defines a configuration schema via
on_config_request() - Engine renders UI based on the schema
- User interacts with UI controls
- Engine calls
on_config_changed()when values change - Script accesses values through the global
optionstable
Configuration Callbacks
-- Called when engine requests the configuration schema
-- Parameter: current values from the UI (may be nil on first call)
function script:on_config_request(values)
return {
version = "1.0",
layout = {
type = "vstack",
children = {
-- UI components here
}
}
}
end
-- Called when user changes configuration
-- Parameter: new values from the UI
-- Return: updated schema (can be same as on_config_request)
function script:on_config_changed(values)
logger:info("Config changed!")
return self:on_config_request(values)
end
Legacy Configuration (Backwards Compatibility)
Note: For backwards compatibility, PowBot still supports the legacy config_schema table property. However, the function-based on_config_request() approach is strongly recommended for new scripts as it provides:
- Dynamic UI generation based on current values
- Conditional UI elements
- Better validation and error handling
- Support for button clicks and advanced interactions
Legacy approach (still works, but not recommended):
local script = {
name = "LegacyScript",
version = "1.0.0",
-- Legacy: Static config schema as a table property
config_schema = {
{
key = "location",
label = "Mining Location",
type = "select",
default = "varrock_east",
options = {
{ value = "varrock_east", label = "Varrock East Mine" },
{ value = "lumbridge", label = "Lumbridge Mine" }
}
}
}
}
-- Access still uses 'options' table
function script:on_start()
logger:info("Location: " .. options.location)
return true
end
If a script defines on_config_request(), it takes precedence over config_schema. Only use config_schema for simple, static configurations or when maintaining older scripts.
Configuration Schema Example
function script:on_config_request(values)
return {
version = "1.0",
layout = {
type = "vstack", -- Vertical stack layout
children = {
{
type = "section",
label = "General Settings",
description = "Basic configuration options",
children = {
{
type = "select",
id = "location",
label = "Mining Location",
description = "Choose where to mine",
props = {
options = {
{ value = "varrock_east", label = "Varrock East Mine" },
{ value = "lumbridge", label = "Lumbridge Swamp Mine" },
{ value = "falador", label = "Falador Mine" }
},
default = "varrock_east"
}
},
{
type = "checkbox",
id = "power_mining",
label = "Power Mining",
description = "Drop ores instead of banking",
props = { default = false }
},
{
type = "number",
id = "min_health",
label = "Minimum Health %",
description = "Stop script if health falls below this",
props = {
default = 20,
min = 0,
max = 100
}
}
}
}
}
}
}
end
Available UI Components
| Type | Description | Props |
|---|---|---|
section | Groups related controls | label, description, children |
select | Dropdown menu | options, default |
checkbox | Boolean toggle | default |
number | Numeric input | default, min, max |
text | Text input | default, placeholder |
grid | Grid layout | columns, children |
button | Clickable button | (handled by on_button_clicked) |
Accessing Configuration Values
Use the global options table to read current configuration:
function script:on_start()
-- Access configuration values
local location = options.location or "varrock_east"
local power_mining = options.power_mining or false
local min_health = options.min_health or 20
logger:info("Location: " .. location)
logger:info("Power mining: " .. tostring(power_mining))
logger:info("Min health: " .. min_health .. "%")
return true
end
function script:poll()
-- Configuration values can be read anytime
if options.power_mining then
-- Drop ores
else
-- Bank ores
end
end
Creating a Config Module
For complex scripts, extract configuration into a separate module:
-- modules/config.lua
local config = {}
function config:get_schema(values)
return {
version = "1.0",
layout = {
-- ... schema definition ...
}
}
end
function config:get()
return {
location = options.location or "varrock_east",
power_mining = options.power_mining or false,
min_health = options.min_health or 20
}
end
return config
-- main.lua
local config = require("modules.config")
function script:on_config_request(values)
return config:get_schema(values)
end
function script:on_start()
local cfg = config:get()
logger:info("Location: " .. cfg.location)
return true
end
Paint System
The paint system creates visual overlays on the game client showing script status, statistics, and tracked skills.
How Paint Works
- Create a paint builder with
paint:builder() - Configure position, size, colors
- Add static text, tracked skills, and dynamic content
- Call
build()to register the paint - Engine automatically updates the overlay each frame
Basic Paint Example
function script:on_start()
-- Create paint overlay
local builder = paint:builder()
-- Configure appearance
builder:position(10, 50) -- X, Y position on screen
builder:size(300, 150) -- Width, height
builder:bg_color(0, 0, 0, 180) -- Background: black, 70% opacity
builder:text_color(255, 255, 255, 255) -- Text: white, 100% opacity
-- Add runtime tracker (shows how long script has been running)
builder:add_runtime()
-- Add static label
builder:add_text("My Awesome Script v1.0")
-- Build and register
self.paint_id = builder:build()
return true
end
function script:on_stop()
-- Clean up paint when script stops
if self.paint_id then
paint:remove(self.paint_id)
end
end
Tracking Skills
Track XP gains and levels for specific skills:
builder:track_skill("Mining")
builder:track_skill("Woodcutting")
builder:track_skill("Hitpoints")
The engine automatically displays:
- Current level
- XP gained since script started
- XP per hour
- Time to next level
Dynamic Content with Functions
Use functions to display values that change during script execution:
function script:on_start()
self.ores_mined = 0
self.trips = 0
local builder = paint:builder()
builder:position(10, 50)
builder:size(300, 200)
builder:bg_color(0, 0, 0, 180)
builder:text_color(255, 255, 255, 255)
builder:add_runtime()
builder:track_skill("Mining")
-- Add labeled dynamic values
builder:add_labeled("State:", function()
return self.state or "Unknown"
end)
builder:add_labeled("Ores mined:", function()
return tostring(self.ores_mined)
end)
builder:add_labeled("Bank trips:", function()
return tostring(self.trips)
end)
self.paint_id = builder:build()
return true
end
function script:poll()
-- Update counters - paint automatically refreshes
if self.state == "MINING" then
self.ores_mined = self.ores_mined + 1
end
end
Paint Builder API
| Method | Parameters | Description |
|---|---|---|
position(x, y) | x: number, y: number | Set paint position in pixels |
size(w, h) | w: number, h: number | Set paint dimensions |
bg_color(r, g, b, a) | RGBA (0-255) | Set background color |
text_color(r, g, b, a) | RGBA (0-255) | Set text color |
add_runtime() | - | Add script runtime display |
track_skill(name) | name: string | Track skill XP and level |
add_text(text) | text: string | Add static text line |
add_labeled(label, fn) | label: string, fn: function | Add dynamic value with label |
build() | - | Register paint and return ID |
Creating a UI Module
For cleaner code, extract paint setup into a module:
-- modules/ui.lua
local ui = {}
function ui:create_paint(script)
local builder = paint:builder()
builder:position(10, 50)
builder:size(350, 200)
builder:bg_color(0, 0, 0, 180)
builder:text_color(255, 255, 255, 255)
builder:add_runtime()
builder:track_skill("Mining")
builder:add_labeled("State:", function()
return script.state or "Unknown"
end)
builder:add_labeled("Ores mined:", function()
return tostring(script.ores_mined or 0)
end)
return builder:build()
end
return ui
-- main.lua
local ui = require("modules.ui")
function script:on_start()
self.paint_id = ui:create_paint(self)
return true
end
API Invocation Pattern
CRITICAL: PowBot APIs are UserData objects that must be invoked with the colon (:) operator, not the dot (.) operator.
Why Use Colons?
In Lua, the colon operator automatically passes the object as the first self parameter:
-- These are equivalent:
npcs:find_all(filters)
npcs:find_all(npcs, filters)
-- But only the colon version works with PowBot APIs!
Correct Usage
-- ✓ CORRECT - Use colons for API methods
local player = players:local_player()
local all_npcs = npcs:find_all({ name = "Cow" })
local rocks = game_objects:find({ name_contains = "Rock" })
inventory:count()
bank:opened()
-- ✓ CORRECT - Chain method calls with colons
if player:hp() < 20 then
logger:error("Low health!")
end
-- ✓ CORRECT - Use on returned objects too
local nearest_npc = npcs:find_nearest({ name = "Guard" })
if nearest_npc then
nearest_npc:interact({ action = "Attack" })
end
Common Mistakes
-- ✗ WRONG - Using dot operator
local player = players.local_player() -- ERROR!
npcs.find_all({ name = "Cow" }) -- ERROR!
inventory.count() -- ERROR!
-- ✗ WRONG - Mixing dots and colons
players.local_player():hp() -- ERROR!
-- ✗ WRONG - Calling without object
local_player() -- ERROR!
find_all({ name = "Cow" }) -- ERROR!
Why This Matters
PowBot's Lua API is implemented using mlua's UserData system. UserData methods require the object reference as the first parameter, which the colon operator provides automatically. Using the dot operator causes:
- Type errors: "attempt to call a nil value"
- Missing parameter errors: "expected 2 arguments, got 1"
- Crashes: if the native code expects a valid object reference
API Call Examples
-- Players API
local player = players:local_player()
if player then
local name = player:name()
local hp = player:hp()
local x = player:x()
local is_busy = player:is_busy()
end
-- NPCs API
local npcs_list = npcs:find_all({ name = "Cow", not_in_combat = true })
for _, npc in ipairs(npcs_list) do
logger:info(npc:name())
npc:interact({ action = "Attack" })
end
-- Game Objects API
local nearest_rock = game_objects:find_first({
name_contains = "Copper",
within_distance_of_local = 10
})
if nearest_rock then
nearest_rock:interact({ action = "Mine" })
end
-- Inventory API
if inventory:full() then
logger:info("Inventory is full")
end
local item_count = inventory:count({ name = "Iron ore" })
-- Bank API
if bank:opened() then
bank:deposit_all()
bank:close()
end
-- Movement API
movement:walk_to({ x = 3200, y = 3200, floor = 0 })
if movement:is_moving() then
logger:info("Walking...")
end
Remember
Always use colons (:) when calling PowBot API methods!
VSCode Setup
Enable auto-complete and IntelliSense for PowBot APIs in Visual Studio Code.
Step 1: Install Lua Extension
Install the Lua Language Server extension by sumneko:
- Open VSCode
- Press
Ctrl+Shift+X(orCmd+Shift+Xon Mac) - Search for "Lua"
- Install "Lua" by sumneko
Step 2: Configure Workspace
Create or edit .vscode/settings.json in your project root:
{
"Lua.workspace.library": [
"${workspaceFolder}/docs/api"
],
"Lua.diagnostics.globals": [
"logger",
"players",
"npcs",
"game_objects",
"ground_items",
"inventory",
"bank",
"bank_cache",
"deposit_box",
"grand_exchange",
"chat",
"menu",
"tabs",
"equipment",
"skills",
"viewport",
"keyboard",
"mouse",
"camera",
"components",
"combat",
"prayer",
"magic",
"quests",
"paint",
"stats",
"varpbits",
"item_prices",
"events",
"game",
"trade",
"store",
"worlds",
"movement",
"breaks",
"sleep",
"wait_until",
"wait_until_idle",
"wait_while_busy",
"options",
"Area",
"game_hwnd",
"scripts",
"daemons",
"print",
"script_manager"
],
"Lua.runtime.version": "Lua 5.4"
}
Step 3: Verify Setup
Open a Lua file and start typing an API name. You should see:
- Auto-complete suggestions
- Method signatures
- Parameter types
- Documentation from comments
Example:
-- Type "players:" and you'll see auto-complete with:
-- - local_player()
-- - find_all()
-- - find_nearest()
-- - etc.
local player = players:local_player()
-- Type "player:" and you'll see:
-- - name()
-- - hp()
-- - max_hp()
-- - x(), y(), floor()
-- - is_busy()
-- - etc.
Step 4: Add Type Annotations (Optional)
For better IntelliSense, add type annotations to your script:
---@type table
local script = {
name = "MyScript",
version = "1.0.0"
}
---@param self table
---@return boolean
function script:on_start()
return true
end
---@param self table
function script:poll()
local player = players:local_player()
if not player then return end
-- IntelliSense now knows player's methods
local hp = player:hp()
end
return script
Troubleshooting
Problem: No auto-complete appears
Solution:
- Ensure
docs/api/pow_api.d.luaexists in your project - Reload VSCode window (
Ctrl+Shift+P→ "Developer: Reload Window") - Check that Lua extension is enabled
Problem: "Undefined global" warnings
Solution: Add the global name to Lua.diagnostics.globals in settings.json
Complete Working Example
Here's a complete, production-ready script demonstrating all concepts:
manifest.json
{
"name": "Tutorial Miner",
"version": "1.0.0",
"description": "A complete example showing script structure, configuration, and paint",
"author": "Tutorial",
"main": "main.lua",
"min_api_version": "1.0.0"
}
main.lua
-- Tutorial Miner - Complete Example Script
-- Demonstrates: lifecycle, configuration, paint, and API usage
local script = {
name = "TutorialMiner",
version = "1.0.0",
-- State machine
state = "CHECK_INVENTORY",
-- Statistics
ores_mined = 0,
trips_made = 0,
-- UI
paint_id = nil
}
-- Configuration schema
function script:on_config_request(values)
return {
version = "1.0",
layout = {
type = "vstack",
children = {
{
type = "section",
label = "Mining Settings",
description = "Configure your mining behavior",
children = {
{
type = "select",
id = "ore_type",
label = "Ore Type",
description = "Which ore to mine",
props = {
options = {
{ value = "copper", label = "Copper ore" },
{ value = "tin", label = "Tin ore" },
{ value = "iron", label = "Iron ore" }
},
default = "copper"
}
},
{
type = "checkbox",
id = "power_mine",
label = "Power Mining",
description = "Drop ores instead of banking",
props = { default = false }
}
}
}
}
}
}
end
function script:on_config_changed(values)
logger:info("Configuration updated!")
return self:on_config_request(values)
end
-- Startup
function script:on_start()
logger:info("Tutorial Miner starting...")
-- Validate player exists
local player = players:local_player()
if not player then
logger:error("No player found!")
return false
end
-- Log configuration
local ore_type = options.ore_type or "copper"
local power_mine = options.power_mine or false
logger:info("Ore type: " .. ore_type)
logger:info("Power mining: " .. tostring(power_mine))
-- Initialize statistics
self.ores_mined = 0
self.trips_made = 0
-- Create paint overlay
self:create_paint()
logger:info("Tutorial Miner started successfully!")
return true
end
-- Create paint overlay
function script:create_paint()
local builder = paint:builder()
-- Configure appearance
builder:position(10, 50)
builder:size(300, 180)
builder:bg_color(0, 0, 0, 180)
builder:text_color(255, 255, 255, 255)
-- Add content
builder:add_runtime()
builder:track_skill("Mining")
builder:add_labeled("State:", function()
return self.state or "Unknown"
end)
builder:add_labeled("Ores mined:", function()
return tostring(self.ores_mined)
end)
builder:add_labeled("Bank trips:", function()
return tostring(self.trips_made)
end)
builder:add_labeled("Ore type:", function()
return options.ore_type or "copper"
end)
-- Build and store ID
self.paint_id = builder:build()
logger:info("Paint created with ID: " .. tostring(self.paint_id))
end
-- Main loop
function script:poll()
local player = players:local_player()
if not player then
return
end
-- State machine
if self.state == "CHECK_INVENTORY" then
self:handle_check_inventory()
elseif self.state == "DROP_ORES" then
self:handle_drop_ores()
elseif self.state == "WALK_TO_BANK" then
self:handle_walk_to_bank()
elseif self.state == "BANK_ORES" then
self:handle_bank_ores()
elseif self.state == "MINE_ORE" then
self:handle_mine_ore()
end
-- Control polling rate
sleep(50)
end
-- State: Check inventory
function script:handle_check_inventory()
if inventory:full() then
if options.power_mine then
self.state = "DROP_ORES"
else
self.state = "WALK_TO_BANK"
end
else
self.state = "MINE_ORE"
end
end
-- State: Drop ores (power mining)
function script:handle_drop_ores()
local ore_type = options.ore_type or "copper"
local ore_name = ore_type:sub(1,1):upper() .. ore_type:sub(2) .. " ore"
local ores = inventory:find_all({ name = ore_name })
for _, ore in ipairs(ores) do
ore:interact({ action = "Drop" })
sleep(600) -- Drop delay
end
self.state = "CHECK_INVENTORY"
end
-- State: Walk to bank
function script:handle_walk_to_bank()
-- This is a simplified example - real scripts would use proper pathfinding
logger:info("Walking to bank...")
-- Example bank tile (Lumbridge)
movement:walk_to({ x = 3208, y = 3220, floor = 2 })
-- Wait for movement
sleep(2000)
self.state = "BANK_ORES"
end
-- State: Bank ores
function script:handle_bank_ores()
-- Open bank if not open
if not bank:opened() then
local booth = game_objects:find_first({
name_contains = "Bank booth",
within_distance_of_local = 5
})
if booth then
booth:interact({ action = "Bank" })
sleep(1000)
end
return
end
-- Deposit ores using transaction API
local ore_type = options.ore_type or "copper"
local ore_name = ore_type:sub(1,1):upper() .. ore_type:sub(2) .. " ore"
if inventory:count({ name = ore_name }) > 0 then
-- Use transaction API for reliable banking
local result = bank:transaction()
:deposit({ name = ore_name }, -1) -- -1 means deposit all
:execute()
if result.success then
self.trips_made = self.trips_made + 1
logger:info("Deposited ores successfully")
end
sleep(600)
end
-- Close bank
bank:close()
sleep(600)
self.state = "CHECK_INVENTORY"
end
-- State: Mine ore
function script:handle_mine_ore()
local player = players:local_player()
if not player then return end
-- Wait if already mining
if player:is_animating() then
sleep(1000)
return
end
-- Find ore to mine
local ore_type = options.ore_type or "copper"
local ore_name = ore_type:sub(1,1):upper() .. ore_type:sub(2)
local rock = game_objects:find_first({
name_contains = ore_name,
within_distance_of_local = 10
})
if rock then
if rock:interact({ action = "Mine" }) then
logger:info("Started mining " .. ore_name)
self.ores_mined = self.ores_mined + 1
sleep(2000) -- Wait for ore to deplete
end
else
logger:warn("No " .. ore_name .. " rocks found nearby")
sleep(1000)
end
self.state = "CHECK_INVENTORY"
end
-- Cleanup
function script:on_stop()
logger:info("Tutorial Miner stopped")
logger:info("Total ores mined: " .. self.ores_mined)
logger:info("Total trips made: " .. self.trips_made)
-- Remove paint overlay
if self.paint_id then
paint:remove(self.paint_id)
end
end
return script
Best Practices
1. Use State Machines
Organize script logic into clear states:
-- ✓ GOOD - Clear state machine
if self.state == "CHECK_INVENTORY" then
-- ...
elseif self.state == "MINE_ORE" then
-- ...
end
-- ✗ BAD - Nested conditions
if inventory:full() then
if bank:opened() then
if has_ores then
-- Deeply nested logic
end
end
end
2. Always Use Sleep
Prevent excessive CPU usage and rate limiting:
-- ✓ GOOD - Controlled polling rate
function script:poll()
-- ... logic ...
sleep(50) -- 20 polls per second
end
-- ✗ BAD - Busy waiting
function script:poll()
-- ... logic ...
-- No sleep = thousands of polls per second!
end
3. Check for Nil
Always validate API responses:
-- ✓ GOOD - Nil check
local player = players:local_player()
if not player then
return
end
local hp = player:hp()
-- ✗ BAD - Assumes player exists
local player = players:local_player()
local hp = player:hp() -- Crashes if player is nil!
end
4. Use Wait Functions
PowBot provides wait utilities that check stop conditions:
-- ✓ GOOD - Use wait_until
wait_until(5000, function()
local player = players:local_player()
return player and player:is_idle()
end)
-- ✓ GOOD - Use wait_until_idle
wait_until_idle(5000)
-- ✗ BAD - Manual sleep loop
for i = 1, 100 do
sleep(50)
if players:local_player():is_idle() then
break
end
end
5. Extract Complex Logic
Use modules for better organization:
-- ✓ GOOD - Modular structure
local tasks = require("modules.tasks")
tasks:mine_ore("copper")
-- ✗ BAD - Everything in main.lua
function script:poll()
-- 500 lines of complex logic
end
6. Log Important Events
Help with debugging and monitoring:
-- ✓ GOOD - Informative logging
logger:info("Starting mining session")
logger:warn("No rocks found, waiting...")
logger:error("Failed to open bank after 3 attempts")
-- ✗ BAD - No logging
-- ... silent execution ...
7. Handle Configuration Changes
Respond to dynamic configuration updates:
function script:on_config_changed(values)
-- Reload location data if changed
if values.location ~= self.current_location then
self:load_location(values.location)
end
return self:on_config_request(values)
end
8. Clean Up Resources
Always clean up in on_stop():
function script:on_stop()
-- Remove paint
if self.paint_id then
paint:remove(self.paint_id)
end
-- Close interfaces
if bank:opened() then
bank:close()
end
-- Log final stats
logger:info("Final stats: " .. self.ores_mined .. " ores")
end
9. Use Colon Operator for APIs
Critical: Always use : for API methods:
-- ✓ CORRECT
players:local_player()
npcs:find_all({ name = "Cow" })
inventory:count()
-- ✗ WRONG
players:local_player()
npcs:find_all({ name = "Cow" })
10. Test Incrementally
Build and test your script in stages:
- Get basic lifecycle working (on_start, poll, on_stop)
- Add configuration schema
- Implement one state at a time
- Add paint overlay last
- Test each feature before moving on
Next Steps
Now that you understand the basic script structure:
- Explore the API: Read through
docs/api/pow_api.d.luato see all available methods - Study Examples: Look at existing scripts in
test-scripts/for patterns - Build Something: Start with a simple script (woodcutting, mining) and expand from there
- Read Advanced Guides: Check out the Progressive Miner Walkthrough for modular architecture
Additional Resources
- API Documentation:
docs/api/pow_api.d.lua - Basic Scripting Guide:
docs/guides/basic-scripting.md - Progressive Miner Walkthrough:
docs/guides/progressive-miner-walkthrough.md - Example Scripts:
test-scripts/directory
Common Issues
Issue: Script doesn't appear in UI
Cause: Invalid manifest.json or missing required fields
Solution: Validate JSON syntax and ensure all required fields are present
Issue: "attempt to call a nil value"
Cause: Using dot operator instead of colon for API methods
Solution: Change players.local_player() to players:local_player()
Issue: Paint doesn't update
Cause: Functions not used for dynamic values
Solution: Wrap dynamic values in functions:
-- Wrong
builder:add_labeled("Count:", self.count)
-- Correct
builder:add_labeled("Count:", function()
return tostring(self.count)
end)
Issue: Configuration changes not reflected
Cause: Not reading from options table
Solution: Always read from global options table, not cached values
Issue: Script stops unexpectedly
Cause: on_start() returning false or error in poll loop
Solution: Check logs for errors and ensure on_start() returns true
Summary
A well-structured PowBot script includes:
- manifest.json - Metadata and module declarations
- Lifecycle methods - on_start, poll, on_stop
- Configuration schema - Dynamic UI via on_config_request
- Paint overlay - Visual feedback with paint:builder()
- State machine - Clear, maintainable logic flow
- Proper API usage - Colon operator for all API methods
- Error handling - Nil checks and validation
- Resource cleanup - Removing paint and closing interfaces
Follow these patterns and best practices to create robust, maintainable automation scripts for Old School RuneScape.
Happy scripting!