2021-10-07 17:41:29 +00:00
|
|
|
# Archipelago API
|
|
|
|
|
|
|
|
This document tries to explain some internals required to implement a game for
|
2021-10-08 22:49:47 +00:00
|
|
|
Archipelago's generation and server. Once a seed is generated, a client or mod is
|
|
|
|
required to send and receive items between the game and server.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
Client implementation is out of scope of this document. Please refer to an
|
2021-10-08 23:15:35 +00:00
|
|
|
existing game that provides a similar API to yours.
|
|
|
|
Refer to the following documents as well:
|
|
|
|
* [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
|
|
|
|
* [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
Archipelago will be abbreviated as "AP" from now on.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
## Language
|
|
|
|
|
2021-10-09 09:06:41 +00:00
|
|
|
AP worlds are written in python3.
|
2021-10-09 00:05:55 +00:00
|
|
|
Clients that connect to the server to sync items can be in any language that
|
|
|
|
allows using WebSockets.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
## Coding style
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
AP follows all the PEPs. When in doubt use an IDE with coding style
|
|
|
|
linter, for example PyCharm Community Edition.
|
|
|
|
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
## Docstrings
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
Docstrings are strings attached to an object in Python that describe what the
|
|
|
|
object is supposed to be. Certain docstrings will be picked up and used by AP.
|
|
|
|
They are assigned by writing a string without any assignment right below a
|
|
|
|
definition. The string must be a triple-quoted string.
|
|
|
|
Example:
|
2021-10-07 17:41:29 +00:00
|
|
|
```python
|
|
|
|
class MyGameWorld(World):
|
|
|
|
"""This is the description of My Game that will be displayed on the AP
|
|
|
|
website."""
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Definitions
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### World Class
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
A `World` class is the class with all the specifics of a certain game to be
|
2021-10-07 17:41:29 +00:00
|
|
|
included. It will be instantiated for each player that rolls a seed for that
|
|
|
|
game.
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### MultiWorld Object
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
The `MultiWorld` object references the whole multiworld (all items and locations
|
|
|
|
for all players) and is accessible through `self.world` inside a `World` object.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### Player
|
|
|
|
|
|
|
|
The player is just an integer in AP and is accessible through `self.player`
|
|
|
|
inside a World object.
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Player Options
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
Players provide customized settings for their World in the form of yamls.
|
2021-10-08 22:49:47 +00:00
|
|
|
Those are accessible through `self.world.<option_name>[self.player]`. A dict
|
|
|
|
of valid options has to be provided in `self.options`. Options are automatically
|
|
|
|
added to the `World` object for easy access.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### World Options
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
Any AP installation can provide settings for a world, for example a ROM file,
|
|
|
|
accessible through `Utils.get_options()['<world>_options']['<option>']`.
|
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
Users can set those in their `host.yaml` file.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
2021-10-09 09:06:41 +00:00
|
|
|
Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
|
|
|
|
in a Region and has access rules.
|
2021-10-08 22:49:47 +00:00
|
|
|
The name needs to be unique in each game, the ID needs to be unique across all
|
|
|
|
games and is best in the same range as the item IDs.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
Special locations with ID `None` can hold events.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
2021-10-09 00:05:55 +00:00
|
|
|
Each item has a `name`, an `id` (can be known as "code"), and an `advancement`
|
|
|
|
flag. An advancement item is an item which a player may require to advance in
|
|
|
|
their world. Advancement items will be assigned to locations with higher
|
|
|
|
priority and moved around to meet defined rules and accomplish progression
|
|
|
|
balancing.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
Special items with ID `None` can mark events (read below).
|
|
|
|
|
|
|
|
### Events
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
Events will mark some progress. You define an event location, an
|
|
|
|
event item, strap some rules to the location (i.e. hold certain
|
2021-10-07 17:41:29 +00:00
|
|
|
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.
|
2021-10-08 22:49:47 +00:00
|
|
|
Events will show up in the spoiler playthrough but they do not represent actual
|
|
|
|
items or locations within the game.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
There is one special case for events: Victory. To get the win condition to show
|
|
|
|
up in the spoiler log, you create an event item and place it at an event
|
2021-10-08 22:49:47 +00:00
|
|
|
location with the `access_rules` for game completion. Once that's done, the
|
2021-10-07 22:25:20 +00:00
|
|
|
world's win condition can be as simple as checking for that item.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2021-10-09 09:06:41 +00:00
|
|
|
Regions have a list called `exits` which are `Entrance` objects representing
|
|
|
|
transitions to other regions.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
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
|
2021-10-07 17:41:29 +00:00
|
|
|
resetting the game ("Save and quit").
|
|
|
|
|
|
|
|
### Entrances
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
An `Entrance` connects to a region, is assigned to region's exits and has rules
|
2021-10-07 17:41:29 +00:00
|
|
|
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).
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Access Rules
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-09 09:06:41 +00:00
|
|
|
An access rule is a function that returns `True` or `False` for a `Location` or
|
|
|
|
`Entrance` based on the the current `state` (items that can be collected).
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Item Rules
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
An item rule is a function that returns `True` or `False` for a `Location` based
|
2021-10-07 17:41:29 +00:00
|
|
|
on a single item. It can be used to reject placement of an item there.
|
|
|
|
|
|
|
|
|
|
|
|
## Implementation
|
|
|
|
|
|
|
|
### Your World
|
|
|
|
|
2021-10-09 09:28:15 +00:00
|
|
|
All code for your world implementation should be placed in a python package in
|
|
|
|
the `/worlds` directory. The starting point for the package is `__init.py__`.
|
|
|
|
Conventionally, your world class is placed in that file.
|
|
|
|
|
|
|
|
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
|
|
|
|
which can be imported as `..AutoWorld.World` from your package.
|
|
|
|
|
|
|
|
AP will pick up your world automatically due to the `AutoWorld` implementation.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### 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)
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Relative Imports
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
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`.
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Your Item Type
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
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"
|
2021-10-10 12:03:33 +00:00
|
|
|
|
|
|
|
# override constructor to automatically mark event locations as such
|
|
|
|
def __init__(self, player: int, name = '', code = None, parent = None):
|
|
|
|
super(MyGameLocation, self).__init__(player, name, code, parent)
|
|
|
|
self.event = code is None
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
in your `__init__.py` or your `Locations.py`.
|
|
|
|
|
|
|
|
### Options
|
|
|
|
|
|
|
|
By convention options are defined in `Options.py` and will be used when parsing
|
|
|
|
the players' yaml files.
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
Each option has its own class, inherits from a base option type, has a docstring
|
2021-10-09 11:00:50 +00:00
|
|
|
to describe it and a `displayname` property for display on the website and in
|
|
|
|
spoiler logs.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-09 00:05:55 +00:00
|
|
|
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
|
|
|
|
assigned to the world under `self.options`.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-09 09:06:41 +00:00
|
|
|
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
|
|
|
|
For more see `Options.py` in AP's base directory.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
#### 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
|
2021-10-08 22:49:47 +00:00
|
|
|
more natural. These games typically have been edited to 'bake in' the items.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### A World Class Skeleton
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
```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
|
2021-10-09 11:00:50 +00:00
|
|
|
topology_present: bool = True # show path to required location checks in spoiler
|
2021-10-07 17:41:29 +00:00
|
|
|
remote_items: bool = False # True if all items come from the server
|
2021-10-09 11:00:50 +00:00
|
|
|
remote_start_inventory: bool = False # True if start inventory comes from the server
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-09 12:29:52 +00:00
|
|
|
# data_version is used to signal that items, locations or their names
|
|
|
|
# changed. Set this to 0 during development so other games' clients do not
|
|
|
|
# cache any texts, then increase by 1 for each release that makes changes.
|
|
|
|
data_version = 0
|
|
|
|
|
|
|
|
# ID of first item and location, could be hard-coded but code may be easier
|
|
|
|
# to read with this as a propery.
|
|
|
|
base_id = 1234
|
|
|
|
# Instead of dynamic numbering, IDs could be part of data.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
# 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
|
2021-10-09 12:29:52 +00:00
|
|
|
id, name in enumerate(mygame_items, base_id)}
|
2021-10-07 17:41:29 +00:00
|
|
|
location_name_to_id = {name: id for
|
2021-10-09 12:29:52 +00:00
|
|
|
id, name in enumerate(mygame_locations, base_id)}
|
2021-10-09 11:00:50 +00:00
|
|
|
|
|
|
|
# Items can be grouped using their names to allow easy checking if any item
|
|
|
|
# from that group has been collected. Group names can also be used for !hint
|
|
|
|
item_name_groups = {
|
|
|
|
"weapons": {"sword", "lance"}
|
|
|
|
}
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
### 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
|
2021-10-09 12:35:08 +00:00
|
|
|
* applying `self.world.precollected_items` for plando/start inventory
|
|
|
|
if not using a `remote_start_inventory`
|
2021-10-07 17:41:29 +00:00
|
|
|
* a `def generate_output(self, output_directory: str)` that creates the output
|
2021-10-09 11:00:50 +00:00
|
|
|
if there is output to be generated. If only items are randomized and
|
|
|
|
`remote_items = True` it is possible to have a generic mod and output
|
|
|
|
generation can be skipped. In all other cases this is required. When this is
|
2021-10-07 17:41:29 +00:00
|
|
|
called, `self.world.get_locations()` has all locations for all players, with
|
|
|
|
properties `item` pointing to the item and `player` identifying the player.
|
2021-10-07 22:25:20 +00:00
|
|
|
`self.world.get_filled_locations(self.player)` will filter for this world.
|
|
|
|
`item.player` can be used to see if it's a local item.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
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
|
2021-10-09 11:00:50 +00:00
|
|
|
from .Items import is_progression # this is just a dummy
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
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)
|
2021-10-07 22:25:20 +00:00
|
|
|
|
|
|
|
def create_event(self, event: str):
|
|
|
|
# while we are at it, we can also add a helper to create events
|
|
|
|
return MyGameItem(event, True, None, self.player)
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
#### 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.
|
2021-10-10 11:08:23 +00:00
|
|
|
# Having an item in the start inventory won't remove it from the pool.
|
|
|
|
# If an item can't have duplicates it has to be excluded manually.
|
2021-10-10 16:39:03 +00:00
|
|
|
|
|
|
|
# List of items to exclude, as a copy since it will be destroyed below
|
|
|
|
exclude = [item for item in self.world.precollected_items[self.player]]
|
2021-10-10 11:08:23 +00:00
|
|
|
|
|
|
|
for item in map(self.create_item, mygame_items):
|
|
|
|
if item in exclude:
|
|
|
|
exclude.remove(item) # this is destructive. create unique list above
|
|
|
|
self.world.itempool.append(self.create_item('nothing'))
|
|
|
|
else:
|
|
|
|
self.world.itempool.append(item)
|
|
|
|
|
|
|
|
# itempool and number of locations should match up.
|
|
|
|
# If this is not the case we want to fill the itempool with junk.
|
|
|
|
junk = 0 # calculate this based on player settings
|
|
|
|
self.world.itempool += [self.create_item('nothing') for _ in range(junk)]
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
#### 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.
|
2021-10-07 22:25:20 +00:00
|
|
|
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))
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
# If setting location access rules from data is easier here, set_rules can
|
|
|
|
# possibly omitted.
|
|
|
|
```
|
|
|
|
|
|
|
|
#### generate_basic
|
|
|
|
|
|
|
|
```python
|
|
|
|
def generate_basic(self):
|
2021-10-07 22:25:20 +00:00
|
|
|
# place "Victory" at "Final Boss" and set collection as win condition
|
|
|
|
self.world.get_location("Final Boss", self.player)\
|
|
|
|
.place_locked_item(self.create_event("Victory"))
|
|
|
|
self.world.completion_condition[self.player] = \
|
|
|
|
lambda state: state.has("Victory", self.player)
|
|
|
|
|
|
|
|
# place item Herb into location Chest1 for some reason
|
|
|
|
item = self.create_item("Herb")
|
|
|
|
self.world.get_location("Chest1", self.player).place_locked_item(item)
|
|
|
|
# in most cases it's better to do this at the same time the itempool is
|
|
|
|
# filled to avoid accidental duplicates:
|
|
|
|
# manually placed and still in the itempool
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Setting Rules
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
```python
|
2021-10-07 22:25:20 +00:00
|
|
|
from ..generic.Rules import add_rule, set_rule, forbid_item
|
|
|
|
from Items import get_item_type
|
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
def set_rules(self):
|
|
|
|
# For some worlds this step can be omitted if either a Logic mixin
|
2021-10-07 22:25:20 +00:00
|
|
|
# (see below) is used, it's easier to apply the rules from data during
|
|
|
|
# location generation or everything is in generate_basic
|
|
|
|
|
|
|
|
# set a simple rule for an region
|
2021-10-07 22:39:16 +00:00
|
|
|
set_rule(self.world.get_entrance("Boss Door", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Boss Key", self.player))
|
|
|
|
# combine rules to require two items
|
2021-10-07 22:39:16 +00:00
|
|
|
add_rule(self.world.get_location("Chest2", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Sword", self.player))
|
2021-10-07 22:39:16 +00:00
|
|
|
add_rule(self.world.get_location("Chest2", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Shield", self.player))
|
|
|
|
# or simply combine yourself
|
2021-10-07 22:39:16 +00:00
|
|
|
set_rule(self.world.get_location("Chest2", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Sword", self.player) and
|
|
|
|
state.has("Shield", self.player))
|
|
|
|
# require two of an item
|
2021-10-07 22:39:16 +00:00
|
|
|
set_rule(self.world.get_location("Chest3", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Key", self.player, 2))
|
2021-10-09 09:06:41 +00:00
|
|
|
# require one item from an item group
|
|
|
|
add_rule(self.world.get_location("Chest3", self.player),
|
|
|
|
lambda state: state.has_group("weapons", self.player))
|
|
|
|
# state also has .item_count() for items, .has_any() and.has_all() for sets
|
|
|
|
# and .count_group() for groups
|
2021-10-07 22:25:20 +00:00
|
|
|
# set_rule is likely to be a bit faster than add_rule
|
|
|
|
|
|
|
|
# disallow placing a specific local item at a specific location
|
|
|
|
forbid_item(self.world.get_location("Chest4", self.player), "Sword")
|
|
|
|
# disallow placing items with a specific property
|
2021-10-07 22:39:16 +00:00
|
|
|
add_item_rule(self.world.get_location("Chest5", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda item: get_item_type(item) == "weapon")
|
|
|
|
# get_item_type needs to take player/world into account
|
|
|
|
# if MyGameItem has a type property, a more direct implementation would be
|
2021-10-07 22:39:16 +00:00
|
|
|
add_item_rule(self.world.get_location("Chest5", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda item: item.player != self.player or\
|
|
|
|
item.my_type == "weapon")
|
|
|
|
# location.item_rule = ... is likely to be a bit faster
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Logic Mixin
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
While lambdas and events could do pretty much anything, by convention we
|
2021-10-09 00:05:55 +00:00
|
|
|
implement more complex logic in logic mixins, even if there is no need to add
|
2021-10-07 22:25:20 +00:00
|
|
|
properties to the `BaseClasses.CollectionState` state object.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-09 00:05:55 +00:00
|
|
|
When importing a file that defines a class that inherits from
|
2021-10-07 22:25:20 +00:00
|
|
|
`..AutoWorld.LogicMixin` the state object's class is automatically extended by
|
2021-10-09 00:05:55 +00:00
|
|
|
the mixin's members. These members should be prefixed with underscore following
|
|
|
|
the name of the implementing world. This is due to sharing a namespace with all
|
|
|
|
other logic mixins.
|
2021-10-07 22:25:20 +00:00
|
|
|
|
|
|
|
Typical uses are defining methods that are used instead of `state.has`
|
|
|
|
in lambdas, e.g.`state._mygame_has(custom, world, player)` or recurring checks
|
|
|
|
like `state._mygame_can_do_something(world, player)` to simplify lambdas.
|
|
|
|
|
|
|
|
More advanced uses could be to add additional variables to the state object,
|
|
|
|
override `World.collect(self, state, item)` and `remove(self, state, item)`
|
|
|
|
to update the state object, and check those added variables in added methods.
|
|
|
|
Please do this with caution and only when neccessary.
|
|
|
|
|
|
|
|
#### Sample
|
|
|
|
|
|
|
|
```python
|
|
|
|
# Logic.py
|
|
|
|
|
|
|
|
from ..AutoWorld import LogicMixin
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
class MyGameLogic(LogicMixin):
|
|
|
|
def _mygame_has_key(self, world: MultiWorld, player: int):
|
|
|
|
# Arguments above are free to choose
|
|
|
|
# it may make sense to use World as argument instead of MultiWorld
|
2021-10-07 22:39:16 +00:00
|
|
|
return self.has('key', player) # or whatever
|
2021-10-07 22:25:20 +00:00
|
|
|
```
|
|
|
|
```python
|
|
|
|
# __init__.py
|
|
|
|
|
|
|
|
from ..generic.Rules import set_rule
|
|
|
|
import .Logic # apply the mixin by importing its file
|
|
|
|
|
|
|
|
class MyGameWorld(World):
|
|
|
|
# ...
|
|
|
|
def set_rules(self):
|
2021-10-07 22:39:16 +00:00
|
|
|
set_rule(self.world.get_location("A Door", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lamda state: state._myworld_has_key(self.world, self.player))
|
|
|
|
```
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Generate Output
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
```python
|
2021-10-07 22:25:20 +00:00
|
|
|
from .Mod import generate_mod
|
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
def generate_output(self, output_directory: str):
|
2021-10-07 22:25:20 +00:00
|
|
|
# How to generate the mod or ROM highly depends on the game
|
|
|
|
# if the mod is written in Lua, Jinja can be used to fill a template
|
|
|
|
# if the mod reads a json file, `json.dump()` can be used to generate that
|
|
|
|
# code below is a dummy
|
|
|
|
data = {
|
|
|
|
"seed": self.world.seed_name, # to verify the server's multiworld
|
|
|
|
"slot": self.world.player_name[self.player], # to connect to server
|
|
|
|
"items": {location.name: location.item.name
|
|
|
|
if location.item.player == self.player else "Remote"
|
|
|
|
for location in self.world.get_filled_locations(self.player)},
|
2021-10-09 12:29:52 +00:00
|
|
|
# store start_inventory from player's .yaml
|
2021-10-10 16:39:03 +00:00
|
|
|
"starter_items": [item.name for item
|
|
|
|
in self.world.precollected_items[self.player]],
|
2021-10-07 22:25:20 +00:00
|
|
|
"final_boss_hp": self.final_boss_hp,
|
|
|
|
# store option name "easy", "normal" or "hard" for difficuly
|
|
|
|
"difficulty": self.world.difficulty[self.player].current_key,
|
|
|
|
# store option value True or False for fixing a glitch
|
|
|
|
"fix_xyz_glitch": self.world.fix_xyz_glitch[self.player].value
|
|
|
|
}
|
|
|
|
# point to a ROM specified by the installation
|
|
|
|
src = Utils.get_options()["mygame_options"]["rom_file"]
|
|
|
|
# or point to worlds/mygame/data/mod_template
|
|
|
|
src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
|
|
|
# generate output path
|
|
|
|
mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}"
|
|
|
|
out_file = os.path.join(output_directory, mod_name + ".zip")
|
|
|
|
# generate the file
|
|
|
|
generate_mod(src, out_file, data)
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|