add api.md work-in-progress v3

This commit is contained in:
black-sliver 2021-10-07 19:41:29 +02:00
parent 1985423a97
commit 76d1460d0f
1 changed files with 489 additions and 0 deletions

489
docs/api.md Normal file
View File

@ -0,0 +1,489 @@
# Archipelago API
This document tries to explain some internals required to implement a game for
Archipelago's generation and server. Once generated a client or mod is
required to send and receive items between game and the server.
Client implementation is out of scope of this document. Please refer to an
existing game that provides a similar API to yours or read the
[network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
Archipelago will be abbreviated as "AP" below.
## Language
AP uses python3 for generation, server and web host. The seed generation will be
written in python3, the client that connects to the server to sync items can be
in any language that allows using websockets.
## Codeing style
AP follows all the PEPs. When in doubt use an IDE with coding style
linter, for example PyCharm Community Edition.
## Doc strings
Doc strings are strings attached to an object in python that describe what the
object is supposed to be. Certain doc strings will be picked up and used by AP.
They are assigned by writing a string without any assigment right below a
defition. e.g.
```python
class MyGameWorld(World):
"""This is the description of My Game that will be displayed on the AP
website."""
```
## Definitions
### World class
A world class is the class with all the specifics of a certain game to be
included. It will be instantiated for each player that rolls a seed for that
game.
### MultiWorld object
The multiworld object references the whole multiworld (all items and locations
for all players) and is accessible through `self.world` inside a World object.
### Player
The player is just an integer in AP and is accessible through `self.player`
inside a World object.
### Player options
Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.world.option_name[self.player]`. A dict
of valid options has to be provided in `self.options`. More on that later.
### World options
Any AP installation can provide settings for a world, for example a ROM file,
accessible through `Utils.get_options()['<world>_options']['<option>']`.
User can set those in their `host.yaml` file.
### Locations
Locations are places where items can be located in your game. This may be chests
or boss drops for RPG-like games but could also be progress in a research tree.
Each location has a name and an ID (also "code", also "address"), is placed in a
Region and has (access) rules.
The name needs to be unique in each game, the ID needs to be unique accross all
games and is best in the same range as the item IDs (see below).
Special locations with ID `None` can hold events (read below).
### Items
Items are all things that can "drop" for your game. This may be RPG items like
weapons, could as well be technologies you normally research in a research tree.
Each item has a name and an ID (also "code"), a `progression` flag and will
be assigned to a location when rolling a seed. Items with `progression` will be
placed early and moved around to produce some balancing. Name has to be unique
in each game, the ID needs to be unique accross all games.
Special items with ID `None` can mark events (read below).
### Events
Events will mark some progress. You define a event location (see above), an
event item (see above), strap some rules to the location (i.e. hold certain
items) and manually place the event item at the event location.
Events can be used to either simplify the logic or to get better spoiler logs.
Events will show up in the play through.
There is one special case for events: Victory. The get the win condition to show
up in the spoiler log, you create an event item for, and place it at an event
location with the access_rules for game completion. Once that's done, the
world's win condition can be a simple check for that item.
By convention the victory event is called `"Victory"`. It can be placed at one
or more event locations based on player options.
### Regions
Regions are logical groups of locations that share some common access rules. If
location logic is written from scratch, using regions greatly simplifies the
definition and allow to somewhat easily implement things like entrance
randomizer in logic.
Regions have a list exits that are Entrances (see below) to other regions.
There has to be one special region `"Menu"` from which the logic unfolds. AP
assumes that a player will always be able to return to the `"Menu"` region by
resetting the game ("Save and quit").
### Entrances
An entrance connects to a region, is assigned to region's exits and has rules
to define if it and thus the connected region is accessible.
They can be static (regular logic) or be defined/connected during generation
(entrance randomizer).
### Access rules
An access rule is a function that returns `True` or `False` for a `spot` based
on the the current `state` (items that can be collected).
`spot` is either Location, Entrance
### Item rules
An item rule is a function that returns `True` or `False` for a Location based
on a single item. It can be used to reject placement of an item there.
### Plando
Plando allows a player to place certain items in certain locations through their
player options. While specifics are not covered here, plando is automatically
possible by providing a complete world with a working create_item method.
## Implementation
### Your World
Your world lives in `world/[world_name]/__init__.py` and is a class that
inherits from `..AutoWorld.World`. The generation progress will automatically
pick it up.
### Requirements
If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format)
### Relative imports
AP will only import the `__init__.py`. Depending on code size it makes sense to
use multiple files and use relative imports to access them.
e.g. `from .Options import mygame_options` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `mygame_options` accesible.
When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
### Your item type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
overridden to attach additional data to it, e.g. "price in shop".
Since the constructor is only ever called from your code, you can add whatever
arguments you like to the constructor.
In its simplest form we only set the game name and use the default constuctor
```python
from BaseClasses import Item
class MyGameItem(Item):
game: str = "My Game"
```
By convention this class definition will either be placed in your `__init__.py`
or your `Items.py`. For a more elaborate example see `worlds/oot/Items.py`.
### Your location type
The same we have done for items above, we will do for locations
```python
from BasClasses import Location
class MyGameLocation(Location):
game: str = "My Game"
```
in your `__init__.py` or your `Locations.py`.
**FIXME**: Is setting Location.event actually required? Minecraft and OoT
do that. Factorio does not. What's the goal of doing that? When factorio places
a locked item this will do Location.event = item.advancement.
### Options
By convention options are defined in `Options.py` and will be used when parsing
the players' yaml files.
Each option has its own class, inherits from a base option type, has a doc
string to describe it and a displayname property for display on the website.
The actual name as used in the yaml is defined in a dict[str, Option], that is
assigned to the world.
Common option types are Toggle, DefaultOnToggle, Choice, Range. For more see
`Options.py` in AP's base directory.
#### Toggle, DefaultOnToggle
Those don't need any additional properties defined. After parsing the option,
its `value` will either be True or False.
#### Range
Define properties `range_start`, `range_end` and `default`. Ranges will be
displayed as sliders on the website and can be set to random in the yaml.
#### Choice
Choices are like toggles, but have more options than just True and False.
Define a property `option_<name> = <number>` per selectable value and
`default = <number>` to set the default selection. Aliases can be set by
defining a property `alias_<name> = <same number>`.
One special case where aliases are required is when option name is `yes`, `no`,
`on` or `off` because they parse to `True` or `False`:
```python
option_off = 0
option_on = 1
option_some = 2
alias_false = 0
alias_true = 1
default = 0
```
#### Sample
```python
# Options.py
from Options import Toggle, Range, Choice
import typing
class Difficulty(Choice):
"""Sets overall game difficulty."""
displayname = "Difficulty"
option_easy = 0
option_normal = 1
option_hard = 2
alias_beginner = 0 # same as easy
alias_expert = 2 # same as hard
default = 1 # default to normal
class FinalBossHP(Range):
"""Sets the HP of the final boss"""
displayname = "Final Boss HP"
range_start = 100
range_end = 10000
default = 2000
class FixXYZGlitch(Toggle):
"""Fixes ABC when you do XYZ"""
displayname = "Fix XYZ Glitch"
# By convention we call the options dict variable `<world>_options`.
mygame_options: typing.Dict[str, type(Option)] = {
"difficulty": Difficulty,
"final_boss_hp": FinalBossHP,
"fix_xyz_glitch": FixXYZGlitch
}
```
```python
# __init__.py
from ..AutoWorld import World
from .Options import mygame_options # import the options dict
class MyGameWorld(World):
#...
options = mygame_options # assign the options dict to the world
#...
```
### Local or Remote
A world with `remote_items` set to `True` gets all items items from the server
and no item from the local game. So for an RPG opening a chest would not add
any item to your inventory, instead the server will send you what was in that
chest. The advantage is that a generic mod can be used that does not need to
know anything about the seed.
A world with `remote_items` set to `False` will locally reward its local items.
For console games this can remove delay and make script/animation/dialog flow
more natural.
### A World class skeleton
```python
# world/mygame/__init__.py
from .Options import mygame_options # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from ..AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item
from Utils import get_options, output_path
class MyGameItem(Item): # or from Items import MyGameItem
game = "My Game" # name of the game/world this item is from
class MyGameLocation(Location): # or from Locations import MyGameLocation
game = "My Game" # name of the game/world this location is in
class MyGameWorld(World):
"""Insert description of the world/game here."""
game: str = "My Game" # name of the game/world
options = mygame_options # options the player can set
topology_present: bool = True # show path to victory in spoiler
remote_items: bool = False # True if all items come from the server
# ID of first item and location, can be hard-coded but code may be easier
# to read with this as a propery
start_id = 1234
# The following two dicts are required for the generation to know which
# items exist. They could be generated from json or something else. They can
# include events, but don't have to since events will be placed manually.
item_name_to_id = {name: id for
id, name in enumerate(mygame_items, start_id)}
location_name_to_id = {name: id for
id, name in enumerate(mygame_locations, start_id)}
```
### Generation
The world has to provide the following things for generation
* the properties mentioned above
* additions to the item pool
* additions to the regions list: at least one called "Menu"
* locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` for plando/manual placing
* a `def generate_output(self, output_directory: str)` that creates the output
if there is output to be generated (i.e. `remote_items = False`). When this is
called, `self.world.get_locations()` has all locations for all players, with
properties `item` pointing to the item and `player` identifying the player.
In addition the following methods can be implemented
* `def generate_early(self)`
called per player before any items or locations are created. You can set
properties on your world here. Already has access to player options and RNG.
* `def create_regions(self)`
called to place player's regions into the MultiWorld's regions list. If it's
hard to separate, this can be done during `generate_early` or `basic` as well.
* `def create_items(self)`
called to place player's items into the MultiWorld's itempool.
* `def set_rules(self)`
called to set access and item rules on locations and entrances.
* `def generate_basic(self)`
called after the previous steps. Some placement and player specific
randomizations can be done here. After this step all regions and items have
to be in the MultiWorld's regions and itempool.
* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
before, during and after the regular fill process, before `generate_output`.
* `fill_slot_data` and `modify_multidata` can be used to modify the data that
will be used by the server to host the MultiWorld.
* `def get_required_client_version(self)`
can return a tuple of 3 ints to make sure the client is compatible to this
world (e.g. item IDs) when connecting.
#### generate_early
```python
def generate_early(self):
# read player settings to world instance
self.final_boss_hp = self.world.final_boss_hp[self.player].value
```
#### create_item
```python
# we need a way to know if an item provides progress in the game ("key item")
# this can be part of the items definition, or depend on recipe randomization
from .Items import is_pregression # this is just a dummy
def create_item(self, item: str):
# This is called when AP wants to create an item by name (for plando) or
# when you call it from your own code.
return MyGameItem(item, is_progression(item), self.item_name_to_id[item],
self.player)
```
#### create_items
```python
def create_items(self):
# Add items to the Multiworld.
# If there are two of the same item, the item has to be twice in the pool.
# Which items are added to the pool may depend on player settings,
# e.g. custom win condition like triforce hunt.
for item in mygame_items:
self.world.itempool += self.create_item(item)
```
#### create_regions
```python
def create_regions(self):
# Add regions to the multiworld. "Menu" is the required starting point.
# Arguments to Region() are name, type, human_readable_name, player, world
r = Region("Menu", None, "Menu", self.player, self.world)
# Set Region.exits to a list of entrances that are reachable from region
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
# Append region to MultiWorld's regions
self.world.regions.append(r) # or use += [r...]
r = Region("Main Area", None, "Main Area", self.player, self.world)
# Add main area's locations to main area (all but final boss)
r.locations = [MyGameLocation(self.player, location.name,
self.location_name_to_id[location.name], r)]
r.exits = [Entrance(self.player, "Boss Door", r)]
self.world.regions.append(r)
r = Region("Boss Room", None, "Boss Room", self.player, self.world)
# add event to Boss Room
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
self.world.regions.append(r)
# If entrances are not randomized, they should be connected here, otherwise
# they can also be connected at a later stage.
self.world.get_entrance('New Game', self.player)\
.connect(self.world.get_region('Main Area', self.player))
self.world.get_entrance('Boss Door', self.player)\
.connect(self.world.get_region('Boss Room', self.player))
# If setting location access rules from data is easier here, set_rules can
# possibly omitted.
```
#### generate_basic
```python
def generate_basic(self):
# Place item Herb into location Chest1
self.world.get_location("Chest1", self.player)\
.place_locked_item(self.create_item("Herb"))
```
### Setting rules
```python
def set_rules(self):
# For some worlds this step can be omitted if either a Logic mixin
# (see below) is used or it's easier to apply the rules from data during
# location generation.
pass
```
**TODO:** actually set some rules
### Logic mixin
**TODO:** features of and conventions for logic mixin
### Generate output
```python
def generate_output(self, output_directory: str):
# How to generate the mod or ROM highly depends on the game, here is a dummy
from .Mod import generate_mod
# TODO: data = {...}
generate_mod(data)
```
**TODO:** generate data dict from locations and settings