# How to write a phBot plugin: the Python API, hooks and a working example

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](https://www.mohamedgado.com/projects/phbot-plugins), 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".

```python
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:

- `pName` and `pVersion` are module-level names. My plugins also set `pAuthor` and `pUrl`, 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 reads `hp`, `mp`, `name`, `region` and `x`/`y` from it.
- `get_inventory()`, `get_pets()`, `get_pet_inventory()` and `get_item(model_id)` for items.
- `get_party()`, `get_monsters()` and `get_position()` for the world around you.
- `get_config_dir()`: the folder where phBot keeps its configs. Plugins store their own JSON there, for example `get_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()` and `stop_bot()` toggle botting. `set_training_position(region, x, y, z)` moves the training area, and `set_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`, `.Global` and `.Stall` send chat. `phBotChat.Private(name, msg)` and `phBotChat.Note(name, msg)` take a recipient first.
- `inject_joymax(opcode, data, encrypted)` sends a raw packet to the server, and `inject_silkroad` sends one to the client. `data` is a `bytearray`.

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:

```text
inject,3091,0
```

phBot 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_loop` runs on phBot's own thread. A `time.sleep` or a slow HTTP call there freezes the bot. xNotify, which sends Telegram and Discord alerts, queues them for a background `threading.Thread` instead.
- **Reacting to your own messages.** `handle_chat` also 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 `QtBind` methods are not present on every phBot build. My plugins look a method up with `getattr` and 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](https://www.mohamedgado.com/projects/phbot-plugins), with the source on [GitHub](https://github.com/Vette1123/phbot-plugins).

