A phBot plugin is a single Python file dropped into phBot's Plugins/ folder. phBot imports it at startup. Your plugin talks to the bot through two modules, phBot and QtBind, and phBot calls functions in your file by name when something happens: a packet arrives, a chat message lands, half a second passes. That is the whole model. Everything below comes from the ten phBot plugins I maintain, so each call is one I have seen work in a live client.
What does a minimal phBot plugin look like?#
Here is a complete plugin. It adds a tab with a checkbox, and when it is ticked, it answers "ping" in party chat with "pong".
from phBot import *
import QtBind
pName = 'xHello'
pVersion = '1.0.0'
gui = QtBind.init(__name__, pName)
QtBind.createLabel(gui, 'Replies "pong" when a party member types "ping".', 10, 10)
cbxEnabled = QtBind.createCheckBox(gui, 'cbxEnabled_clicked', 'Enabled', 10, 40)
enabled = False
def cbxEnabled_clicked(checked):
global enabled
enabled = checked
def handle_chat(t, player, msg):
if enabled and t == 4 and msg.strip().lower() == 'ping':
phBotChat.Party('pong')
log('Plugin: ' + pName + ' v' + pVersion + ' loaded')Save it as xHello.py in Plugins/, then restart phBot or use Reload Plugins. A new xHello tab appears.
Three things in it apply to every plugin:
pNameandpVersionare module-level names. My plugins also setpAuthorandpUrl, the raw GitHub URL of the file.QtBind.init(__name__, pName)creates the plugin's tab and returns a handle. Every UI call takes that handle first.- Callbacks are passed by name, as a string:
'cbxEnabled_clicked'. phBot looks the function up in your module when the checkbox changes, so the name has to match exactly.
Which functions does phBot call in a plugin?#
You opt in to an event by defining a function with the right name. These are the ones my plugins use:
| Function | When phBot calls it |
|---|---|
handle_joymax(opcode, data) |
Every packet from the game server. Return True to pass it on to the client, False to drop it. |
handle_silkroad(opcode, data) |
Every packet from the game client. Same return rule, towards the server. |
handle_chat(t, player, msg) |
Every chat message. t is the chat type. |
handle_event(t, data) |
Bot events with a text payload. My Jupiter hunter reads it for the kill-credit message that names a unique. |
event_loop() |
Every 500 ms, as long as the plugin is loaded. |
joined_game() |
Once the character is in the game world. |
connected() / disconnected() |
When the connection to the server opens or drops. |
teleported() |
After every teleport. |
The return value of the two packet hooks is easy to get wrong. Every hook in my plugins ends with an explicit return True; a hook that falls off the end returns None, and you should not have to find out what phBot does with that.
The chat types in handle_chat are numbers. xControl maps them like this: 1 all, 2 private, 3 guild, 4 party, 5 global, 6 note, 7 stall, 11 union. Union messages arrive with the guild name in front, so xControl strips everything up to the first ": " before parsing.
How do I read the character and the world?#
The phBot module exposes getters that return plain dicts and lists:
get_character_data(): name, server, region, position, HP, MP and more. My xNotify plugin readshp,mp,name,regionandx/yfrom it.get_inventory(),get_pets(),get_pet_inventory()andget_item(model_id)for items.get_party(),get_monsters()andget_position()for the world around you.get_config_dir(): the folder where phBot keeps its configs. Plugins store their own JSON there, for exampleget_config_dir() + pName + '.json'.
Treat every getter as able to return nothing. Right after login, or during a teleport, get_character_data() can be empty, so xNotify reads it as get_character_data() or {} inside a try.
How do I make the bot do something?#
log(text)writes to phBot's log tab. It is the first debugging tool you have.start_bot()andstop_bot()toggle botting.set_training_position(region, x, y, z)moves the training area, andset_training_script(path)swaps the walk script.move_to(x, y, z)walks the character. xControl's follow command calls it every loop tick.phBotChat.All,.Party,.Guild,.Union,.Globaland.Stallsend chat.phBotChat.Private(name, msg)andphBotChat.Note(name, msg)take a recipient first.inject_joymax(opcode, data, encrypted)sends a raw packet to the server, andinject_silkroadsends one to the client.datais abytearray.
Packet injection is how most of the interesting plugins work. My Magic Pop spinner, the lightstone crafter and the Jupiter hunter all build packets by hand and send them with inject_joymax. To find the opcodes in the first place, I use xPacketTool, which logs client and server packets with an opcode filter.
How do I run plugin code from a walk script?#
Any plugin function that takes one args list can be called from a phBot training script by its name. xPacketTool defines inject(args), so a script line like this greets:
inject,3091,0phBot splits the line on commas and passes the list, so args[0] is the command name itself, and the values start at args[1]. The function returns 0, as every script command in my plugins does. xControl uses the same mechanism for its chat,party,<message> command.
What goes wrong most often?#
- Blocking the loop.
event_loopruns on phBot's own thread. Atime.sleepor a slow HTTP call there freezes the bot. xNotify, which sends Telegram and Discord alerts, queues them for a backgroundthreading.Threadinstead. - Reacting to your own messages.
handle_chatalso fires for what your character says. xControl remembers each message it sent and skips it once when it comes back. - UI method names that differ between builds. Some
QtBindmethods are not present on every phBot build. My plugins look a method up withgetattrand skip it when it is missing, rather than crashing the whole plugin on load.
The full plugins, including xControl's party commands and the Caravan runner with its own dashboard, are on the phBot plugins page, with the source on GitHub.