Skip to content

Commit

Permalink
Merge pull request #453 from hanzi/plugin-and-stats-docs
Browse files Browse the repository at this point in the history
Add Wiki pages for plugins and the stats database
  • Loading branch information
hanzi authored Oct 18, 2024
2 parents f1e4bbc + 9dd363a commit f694e3e
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 6 deletions.
128 changes: 128 additions & 0 deletions modules/plugin_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,168 @@

class BotPlugin:
def get_additional_bot_modes(self) -> Iterable[type["BotMode"]]:
"""
This hook can return an iterable (i.e. a list, tuple, or generator) of bot modes
that should be added to the default ones.
It can be used to add custom bot modes without modifying the regular bot code
(which would get replaced during updates.)
:return: Iterable of bot mode _types_. Note that this must be a reference to the
class itself and not an instance of it. So do `yield MyMode` instead of
`yield MyMode()`.
"""
return []

def get_additional_bot_listeners(self) -> Iterable["BotListener"]:
"""
This hook returns a list of bot listeners that should be loaded.
Bot listeners are classes implementing `BotListener`, and these have a
`handle_frame()` method that gets called -- you guessed it -- every frame.
This can be useful to wait for certain in-game events to happen and then
act upon it without having to do it within a bot mode.
Bot listeners can also be added in other hooks (by calling
`context.bot_listeners.append(MyListener())`), in case they don't have to
run all the time.
:return: List of _instances_ of bot listeners.
"""

return []

def on_profile_loaded(self, profile: "Profile") -> None:
"""
This is called after a profile has been selected via the GUI or a command-line
option and the emulation has started.
:param profile: The profile selected by the user.
"""
pass

def on_battle_started(self, encounter: "EncounterInfo | None") -> Generator | None:
"""
This is called once the game entered battle mode, so when the screen has faded
to black.
:param encounter: Information about the encounter if this is a wild Pokémon
encounter, otherwise (in trainer battles) None.
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""

pass

def on_wild_encounter_visible(self, encounter: "EncounterInfo") -> Generator | None:
"""
This is called once a wild encounter is fully visible, i.e. the sliding-in
animation has completed, the Pokémon has done it's cry and the 'Wild XYZ appeared!'
message is visible.
:param encounter: Information about the wild encounter.
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""
pass

def on_battle_ended(self, outcome: "BattleOutcome") -> Generator | None:
"""
This is called once a battle has ended. At this point, the game is still in battle
mode and not yet in the overworld. It's just the point at which the outcome of the
battle is known.
:param outcome: How the battle ended, e.g. won, lost, ran away, ...
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""
pass

def on_logging_encounter(self, encounter: "EncounterInfo") -> None:
"""
This is called whenever an encounter is being logged. This _may_ happen because of a
wild encounter battle, but it also gets triggered by hatching eggs. Bot modes can
trigger this too by calling `log_encounter()`, which is done for gift Pokémon.
:param encounter: Information about the wild encounter.
"""
pass

def on_pokemon_evolved(self, evolved_pokemon: "Pokemon") -> Generator | None:
"""
This is called when a Pokémon has evolved. It is not called if an evolution has been
interrupted by pressing `B`.
:param evolved_pokemon: Data of the Pokémon _after_ evolution.
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""
pass

def on_egg_starting_to_hatch(self, hatching_pokemon: "EncounterInfo") -> Generator | None:
"""
This is called when the egg hatching cutscene starts.
:param hatching_pokemon: Data of the egg that is about to hatch.
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""

pass

def on_egg_hatched(self, hatched_pokemon: "EncounterInfo") -> Generator | None:
"""
This is called during the egg-hatching cutscene once the egg has hatched and the
Pokémon is visible.
:param hatched_pokemon: Data of the Pokémon that has hatched.
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""
pass

def on_whiteout(self) -> Generator | None:
"""
This is called when the player has whited out (due to being defeated in battle, or
the last party Pokémon fainting due to poison.)
When this is called, the white-out dialogue has already been completed and the player
is standing in front of the last Pokémon Center.
:return: This _may_ return a Generator (so you can use `yield` inside here), in
which case the current bot mode is suspended and this generator function
takes control.
"""
pass

def on_judge_encounter(self, opponent: "Pokemon") -> str | bool:
"""
This is called during `judge_encounter()`, which is supposed to decide whether a
Pokémon is worth catching or not.
Shiny and Roamer Pokémon are matched automatically, but this can be used to add custom
filter rules for Pokémon.
:param opponent: Information about the encountered Pokémon.
:return: `False` is this Pokémon is considered to NOT be of interest, otherwise a string
describing why it has value. This string is displayed in some log messages.
"""
return False

def on_should_nickname_pokemon(self, pokemon: "Pokemon") -> str | None:
"""
This is called when the player is asked whether to give a nickname to a newly
acquired Pokémon.
:param pokemon: The newly received Pokémon.
:return: The nickname (max. 10 characters) to give to the Pokémon, or `None` to
not give a nickname.
"""
return None
10 changes: 4 additions & 6 deletions modules/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,12 @@ def load_plugins():
inspect.getmembers(getattr(imported_module, module_name), inspect.isclass),
)
)

if len(classes) == 0:
raise RuntimeError(f"Could not load plugin `{file.name}`: It did not contain any class.")
if len(classes) > 1:
raise RuntimeError(f"Could not load plugin `{file.name}`: It contained more than one class.")
if not issubclass(classes[0][1], BotPlugin):
raise RuntimeError(f"Could not load plugin `{file.name}`: Class did not inherit from `BotPlugin`.")
raise RuntimeError(f"Could not load plugin `{file.name}`: It did not contain any plugin class.")

plugins.append(classes[0][1]())
for class_name, plugin_class in classes:
plugins.append(plugin_class())


def plugin_get_additional_bot_modes() -> Iterable["BotMode"]:
Expand Down
5 changes: 5 additions & 0 deletions wiki/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ For quick help and support, reach out in Discord [#pokebot-gen3-support❔](http
- 🥅 [Custom Catch Filters](pages/Configuration%20-%20Custom%20Catch%20Filters.md)
- 💎 [Cheats](pages/Configuration%20-%20Cheats.md)
- 📡 [HTTP Server](pages/Configuration%20-%20HTTP%20Server.md)

### Customisation

- 🧩 [Bot Plugins](pages/Customisation%20-%20Plugins.md)
- 📊 [Statistics Database](pages/Customisation%20-%20Statistics%20Database.md)
47 changes: 47 additions & 0 deletions wiki/pages/Customisation - Plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
🏠 [`pokebot-gen3` Wiki Home](../Readme.md)

# 🧩 Plugins

If you know Python and want to customise the behaviour of the bot, you can create a plugin.

Plugins are little scripts that get called for specific events during the game (such as a
battle starting, an egg hatching, etc.) Take a look at
[modules/plugin_interface.py](../../modules/plugin_interface.py) for a list of events that
a plugin may be called for.


## Creating a plugin

To make a plugin, create a Python file inside the `plugins/` directory. This must have the
`.py` file extension and it must be placed directly in the `plugins/` directory, not in a
subdirectory.

In this file, create a class that inherits from `BotPlugin`. So the most basic implementation
of a plugin would be:

```python
from modules.plugin_interface import BotPlugin

class MyFirstPlugin(BotPlugin):
pass
```

Of course, this doesn't do anything yet. You can choose some method from the parent `BotPlugin`
class to overwrite (see [modules/plugin_interface.py](../../modules/plugin_interface.py) for
a list.)


## Why write a plugin and not just edit the bot's code?

The `plugins/` directory is excluded from the Git repository and will also not be touched by
the automatic updater. So code in that directory won't fall victim to future updates --
whereas if you edit the bot's code directly, this might get removed again when the bot updates
and you're not careful.


## Example plugins

While not meant to be just an example, there are some features that use the bot's plugin
infrastructure to work.

You can find those 'default plugins' in [modules/built_in_plugins/](../../modules/built_in_plugins/).
21 changes: 21 additions & 0 deletions wiki/pages/Customisation - Statistics Database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
🏠 [`pokebot-gen3` Wiki Home](../Readme.md)

# 📊 Statistics Database

The bot stores its statistics in an sqlite3 database. This file can be found in the
profile directory, at `profiles/<profile name>/stats.db`.

This contains 4 main tables:

- **encounters** contains information about encountered Pokémon. If `log_encounters` is
enabled (see [the Wiki page on logging](Console,%20Logging%20and%20Image%20Config.md)),
this will contain _all_ encountered Pokémon. Otherwise it just contains shinies,
roaming Pokémon as well as Pokémon that matched a custom catch filter.
- **shiny_phases** contains information about Shiny Phases, etc. the time periods between
two shiny encounters.
- **encounter_summaries** contains information for each species (and in case of Unown, for
each single letter) the bot has encountered in this profile and so can answer questions
like 'How many Seedot have we encountered in total?' By summing all those individual
species entries you get the total stats.
- **pickup_items** contains a list of items that have been acquired using the Pickup ability,
and how many of them have been picked up so far.

0 comments on commit f694e3e

Please sign in to comment.