2021-10-07 17:41:29 +00:00
|
|
|
# Archipelago API
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
This document tries to explain some aspects of the Archipelago World API used when implementing the generation logic of
|
|
|
|
a game.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Client implementation is out of scope of this document. Please refer to an existing game that provides a similar API to
|
|
|
|
yours, and the following documents:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* [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
|
|
|
|
2024-01-30 20:42:33 +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.
|
2024-01-30 20:42:33 +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
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
AP follows a [style guide](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md).
|
|
|
|
When in doubt, use an IDE with a code-style linter, for example PyCharm Community Edition.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
## Docstrings
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +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, and should
|
|
|
|
follow [reST style](https://peps.python.org/pep-0287/).
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
Example:
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
```python
|
2021-11-04 12:23:13 +00:00
|
|
|
from worlds.AutoWorld import World
|
2024-01-30 20:42:33 +00:00
|
|
|
|
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
class MyGameWorld(World):
|
2024-01-30 20:42:33 +00:00
|
|
|
"""This is the description of My Game that will be displayed on the AP website."""
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
## Definitions
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
This section covers various classes and objects you can use for your world. While some of the attributes and methods
|
|
|
|
are mentioned here, not all of them are, but you can find them in
|
|
|
|
[`BaseClasses.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py).
|
2022-03-24 13:21:08 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### World Class
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
A `World` is the class with all the specifics of a certain game that is to be included. A new instance will be created
|
|
|
|
for each player of the game for any given generated multiworld.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2022-03-24 13:21:08 +00:00
|
|
|
### WebWorld Class
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
A `WebWorld` class contains specific attributes and methods that can be modified for your world specifically on the
|
|
|
|
webhost:
|
2022-04-12 21:37:05 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `options_page` can be changed to a link instead of an AP-generated options page.
|
2022-04-12 21:37:05 +00:00
|
|
|
|
2024-06-14 22:53:42 +00:00
|
|
|
* `rich_text_options_doc` controls whether [Option documentation] uses plain text (`False`) or rich text (`True`). It
|
|
|
|
defaults to `False`, but world authors are encouraged to set it to `True` for nicer-looking documentation that looks
|
|
|
|
good on both the WebHost and the YAML template.
|
|
|
|
|
|
|
|
[Option documentation]: /docs/options%20api.md#option-documentation
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `theme` to be used for your game-specific AP pages. Available themes:
|
2023-07-09 16:04:24 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|
|
|
|
|--------------------------------------------|---------------------------------------------|----------------------------------------------------|-------------------------------------------|----------------------------------------------|---------------------------------------------|-------------------------------------------------|---------------------------------------------|
|
|
|
|
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> |
|
2022-03-24 13:21:08 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be
|
|
|
|
placed by the site to help users report bugs.
|
2022-04-12 21:37:05 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost.
|
2022-05-18 19:23:27 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `game_info_languages` (optional) list of strings for defining the existing game info pages your game supports. The
|
|
|
|
documents must be prefixed with the same string as defined here. Default already has 'en'.
|
2022-05-18 19:23:27 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `options_presets` (optional) `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values
|
|
|
|
are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names
|
|
|
|
of the options and the values are the values to be set for that option. These presets will be available for users to
|
|
|
|
select from on the game's options page.
|
2023-11-16 10:37:06 +00:00
|
|
|
|
|
|
|
Note: The values must be a non-aliased value for the option type and can only include the following option types:
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end`
|
|
|
|
values.
|
|
|
|
* If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the
|
2023-11-16 10:37:06 +00:00
|
|
|
`special_range_names` keys.
|
2024-01-30 20:42:33 +00:00
|
|
|
* If you have a `Choice` option, the value should be a `str` that is one of the `option_<name>` values.
|
|
|
|
* If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`.
|
|
|
|
* `random` is also a valid value for any of these option types.
|
2023-11-16 10:37:06 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on
|
|
|
|
the webhost at this time.
|
2023-11-16 10:37:06 +00:00
|
|
|
|
|
|
|
Here is an example of a defined preset:
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2023-11-16 10:37:06 +00:00
|
|
|
```python
|
|
|
|
# presets.py
|
|
|
|
options_presets = {
|
|
|
|
"Limited Potential": {
|
|
|
|
"progression_balancing": 0,
|
|
|
|
"fairy_chests_per_zone": 2,
|
|
|
|
"starting_class": "random",
|
|
|
|
"chests_per_zone": 30,
|
|
|
|
"vendors": "normal",
|
|
|
|
"architect": "disabled",
|
|
|
|
"gold_gain_multiplier": "half",
|
|
|
|
"number_of_children": 2,
|
|
|
|
"free_diary_on_generation": False,
|
|
|
|
"health_pool": 10,
|
|
|
|
"mana_pool": 10,
|
|
|
|
"attack_pool": 10,
|
|
|
|
"magic_damage_pool": 10,
|
|
|
|
"armor_pool": 5,
|
|
|
|
"equip_pool": 10,
|
|
|
|
"crit_chance_pool": 5,
|
|
|
|
"crit_damage_pool": 5,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2023-11-16 10:37:06 +00:00
|
|
|
# __init__.py
|
|
|
|
class RLWeb(WebWorld):
|
|
|
|
options_presets = options_presets
|
|
|
|
# ...
|
|
|
|
```
|
|
|
|
|
2024-05-23 07:08:08 +00:00
|
|
|
* `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations
|
|
|
|
or location groups.
|
|
|
|
|
|
|
|
```python
|
|
|
|
# locations.py
|
|
|
|
location_descriptions = {
|
|
|
|
"Red Potion #6": "In a secret destructible block under the second stairway",
|
|
|
|
"L2 Spaceship": """
|
|
|
|
The group of all items in the spaceship in Level 2.
|
|
|
|
|
|
|
|
This doesn't include the item on the spaceship door, since it can be
|
|
|
|
accessed without the Spaceship Key.
|
|
|
|
"""
|
|
|
|
}
|
|
|
|
|
|
|
|
# __init__.py
|
|
|
|
from worlds.AutoWorld import WebWorld
|
|
|
|
from .locations import location_descriptions
|
|
|
|
|
|
|
|
|
|
|
|
class MyGameWeb(WebWorld):
|
|
|
|
location_descriptions = location_descriptions
|
|
|
|
```
|
|
|
|
|
|
|
|
* `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item
|
|
|
|
groups.
|
|
|
|
|
|
|
|
```python
|
|
|
|
# items.py
|
|
|
|
item_descriptions = {
|
|
|
|
"Red Potion": "A standard health potion",
|
|
|
|
"Spaceship Key": """
|
|
|
|
The key to the spaceship in Level 2.
|
|
|
|
|
|
|
|
This is necessary to get to the Star Realm.
|
|
|
|
""",
|
|
|
|
}
|
|
|
|
|
|
|
|
# __init__.py
|
|
|
|
from worlds.AutoWorld import WebWorld
|
|
|
|
from .items import item_descriptions
|
|
|
|
|
|
|
|
|
|
|
|
class MyGameWeb(WebWorld):
|
|
|
|
item_descriptions = item_descriptions
|
|
|
|
```
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### MultiWorld Object
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible
|
|
|
|
through `self.multiworld` from your `World` object.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### Player
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
The player is just an `int` in AP and is accessible through `self.player` from your `World` object.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Player Options
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Options are provided by the user as part of the generation process, intended to affect how their randomizer experience
|
|
|
|
should play out. These can control aspects such as what locations should be shuffled, what items are in the itempool,
|
|
|
|
etc. Players provide the customized options for their World in the form of yamls.
|
|
|
|
|
|
|
|
By convention, options are defined in `options.py` and will be used when parsing the players' yaml files. Each option
|
|
|
|
has its own class, which inherits from a base option type, a docstring to describe it, and a `display_name` property
|
|
|
|
shown on the website and in spoiler logs.
|
|
|
|
|
|
|
|
The available options are defined by creating a `dataclass`, which must be a subclass of `PerGameCommonOptions`. It has
|
|
|
|
defined fields for the option names used in the player yamls and used for options access, with their types matching the
|
|
|
|
appropriate Option class. By convention, the strings that define your option names should be in `snake_case`. The
|
|
|
|
`dataclass` is then assigned to your `World` by defining its `options_dataclass`. Option results are then automatically
|
|
|
|
added to the `World` object for easy access, between `World` creation and `generate_early`. These are accessible through
|
|
|
|
`self.options.<option_name>`, and you can get a dictionary with option values
|
|
|
|
via `self.options.as_dict(<option_names>)`,
|
|
|
|
passing the desired option names as strings.
|
|
|
|
|
|
|
|
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, and `Range`.
|
|
|
|
For more information, see the [options api doc](options%20api.md).
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2023-07-09 16:04:24 +00:00
|
|
|
### World Settings
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Settings are set by the user outside the generation process. They can be used for those settings that may affect
|
|
|
|
generation or client behavior, but should remain static between generations, such as the path to a ROM file.
|
|
|
|
These settings are accessible through `self.settings.<setting_name>` or `cls.settings.<setting_name>`.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Users can set these in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing.
|
2023-07-05 20:39:35 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) for details.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### Locations
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
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, or even something more abstract like a level up.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
|
|
|
|
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
|
|
|
|
letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
|
2024-02-28 18:55:55 +00:00
|
|
|
Locations and items can share IDs, so typically a game's locations and items start at the same ID.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2022-10-30 23:47:23 +00:00
|
|
|
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
2023-05-20 18:04:26 +00:00
|
|
|
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
|
|
|
required, and will prevent progression and useful items from being placed at excluded locations.
|
2022-10-30 23:47:23 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
### Items
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally
|
|
|
|
research in a research tree.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Each item has a `name`, a `code` (hereafter referred to as `id`), and a classification.
|
|
|
|
The most important classification is `progression`. Progression items are items which a player *may* require to progress
|
|
|
|
in their world. If an item can possibly be considered for logic (it's referenced in a location's rules) it *must* be
|
|
|
|
progression. Progression items will be assigned to locations with higher priority, and moved around to meet defined rules
|
|
|
|
and satisfy progression balancing.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
|
|
|
|
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
|
2022-09-20 06:08:43 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Other classifications include:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2023-07-09 16:04:24 +00:00
|
|
|
* `filler`: a regular item or trash item
|
2024-01-30 20:42:33 +00:00
|
|
|
* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
|
2023-07-09 16:04:24 +00:00
|
|
|
* `trap`: negative impact on the player
|
2023-07-22 14:56:00 +00:00
|
|
|
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
|
|
|
|
combined with `progression`; see below)
|
|
|
|
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
2024-01-30 20:42:33 +00:00
|
|
|
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
|
2022-06-19 13:19:46 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
### Events
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to
|
|
|
|
track certain logic interactions, with the Event Item being required for access in other locations or regions, but not
|
|
|
|
being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is
|
|
|
|
never made aware of them and these locations can never be checked, nor can the items be received during play.
|
|
|
|
They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that
|
|
|
|
is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last
|
|
|
|
relevant Item. Events function just like any other Location, and can still have their own access rules, etc.
|
|
|
|
By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement.
|
|
|
|
They must not exist in the `name_to_id` lookups, as they have no ID.
|
|
|
|
|
|
|
|
The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
```python
|
|
|
|
from worlds.AutoWorld import World
|
|
|
|
from BaseClasses import ItemClassification
|
|
|
|
from .subclasses import MyGameLocation, MyGameItem
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
class MyGameWorld(World):
|
|
|
|
victory_loc = MyGameLocation(self.player, "Victory", None)
|
|
|
|
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
|
|
|
```
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### Regions
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Regions are logical containers that typically hold locations that share some common access rules. If location logic is
|
|
|
|
written from scratch, using regions greatly simplifies the requirements and can help with implementing things
|
|
|
|
like entrance randomization in logic.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
There must 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").
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### Entrances
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
An `Entrance` has a `parent_region` and `connected_region`, where it is in the `exits` of its parent, and the
|
|
|
|
`entrances` of its connected region. The `Entrance` then has rules assigned to it to determine if it can be passed
|
|
|
|
through, making the connected region accessible. They can be static (regular logic) or be defined/connected during
|
|
|
|
generation (entrance randomization).
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Access Rules
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
|
|
|
(items that have been 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
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
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 the placement of an item there.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
## Implementation
|
|
|
|
|
|
|
|
### Your World
|
|
|
|
|
2024-01-30 20:42:33 +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.
|
2021-10-09 09:28:15 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, which can be imported as
|
|
|
|
`from worlds.AutoWorld import World` from your package.
|
2021-10-09 09:28:15 +00:00
|
|
|
|
|
|
|
AP will pick up your world automatically due to the `AutoWorld` implementation.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
### Requirements
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
If your world needs specific python packages, they can be listed in `worlds/<world_name>/requirements.txt`.
|
|
|
|
ModuleUpdate.py will automatically pick up and install them.
|
2022-09-20 07:09:13 +00:00
|
|
|
|
|
|
|
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Relative Imports
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
AP will only import the `__init__.py`. Depending on code size, it may make sense to use multiple files and use relative
|
|
|
|
imports to access them.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
e.g. `from .options import MyGameOptions` from your `__init__.py` will load `world/[world_name]/options.py` and make
|
|
|
|
its `MyGameOptions` accessible.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
When imported names pile up, it may be easier to use `from . import options` and access the variable as
|
|
|
|
`options.MyGameOptions`.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is
|
|
|
|
required for zipped worlds to function, see [apworld specification.md](apworld%20specification.md).
|
2022-09-17 22:00:54 +00:00
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Your Item Type
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Each world uses its own subclass of `BaseClasses.Item`. The constructor 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 constructor:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
```python
|
|
|
|
from BaseClasses import Item
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
class MyGameItem(Item):
|
|
|
|
game: str = "My Game"
|
|
|
|
```
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
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`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py).
|
|
|
|
|
|
|
|
### Your Location Type
|
|
|
|
|
|
|
|
The same thing we did for items above, we will now do for locations:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
```python
|
2021-11-04 12:23:13 +00:00
|
|
|
from BaseClasses import Location
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
class MyGameLocation(Location):
|
|
|
|
game: str = "My Game"
|
|
|
|
```
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
in your `__init__.py` or your `locations.py`.
|
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
|
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
import settings
|
|
|
|
import typing
|
2023-11-15 16:07:42 +00:00
|
|
|
from .options import MyGameOptions # 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
|
2022-09-17 22:00:54 +00:00
|
|
|
from worlds.AutoWorld import World
|
2022-06-19 13:19:46 +00:00
|
|
|
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
class MyGameItem(Item): # or from Items import MyGameItem
|
|
|
|
game = "My Game" # name of the game/world this item is from
|
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
class MyGameLocation(Location): # or from Locations import MyGameLocation
|
|
|
|
game = "My Game" # name of the game/world this location is in
|
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
|
|
|
|
class MyGameSettings(settings.Group):
|
|
|
|
class RomFile(settings.SNESRomPath):
|
|
|
|
"""Insert help text for host.yaml here."""
|
|
|
|
|
|
|
|
rom_file: RomFile = RomFile("MyGame.sfc")
|
|
|
|
|
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
class MyGameWorld(World):
|
|
|
|
"""Insert description of the world/game here."""
|
2023-03-20 16:01:08 +00:00
|
|
|
game = "My Game" # name of the game/world
|
2023-10-10 20:30:20 +00:00
|
|
|
options_dataclass = MyGameOptions # options the player can set
|
|
|
|
options: MyGameOptions # typing hints for option results
|
2023-07-05 20:39:35 +00:00
|
|
|
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
|
2023-03-20 16:01:08 +00:00
|
|
|
topology_present = True # show path to required location checks in spoiler
|
2021-10-09 12:29:52 +00:00
|
|
|
|
|
|
|
# ID of first item and location, could be hard-coded but code may be easier
|
2023-07-09 16:04:24 +00:00
|
|
|
# to read with this as a property.
|
2021-10-09 12:29:52 +00:00
|
|
|
base_id = 1234
|
2024-01-30 20:42:33 +00:00
|
|
|
# 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 = {
|
2023-07-09 16:04:24 +00:00
|
|
|
"weapons": {"sword", "lance"},
|
2021-10-09 11:00:50 +00:00
|
|
|
}
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
### Generation
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
The world has to provide the following things for generation:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* the properties mentioned above
|
2021-10-07 17:41:29 +00:00
|
|
|
* additions to the item pool
|
|
|
|
* additions to the regions list: at least one called "Menu"
|
|
|
|
* locations placed inside those regions
|
2022-12-11 01:59:17 +00:00
|
|
|
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
2024-01-30 20:42:33 +00:00
|
|
|
* applying `self.multiworld.push_precollected` for world-defined start inventory
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
In addition, the following methods can be implemented and are called in this order during generation:
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
* `stage_assert_generate(cls, multiworld: MultiWorld)`
|
|
|
|
a class method called at the start of generation to check for the existence of prerequisite files, usually a ROM for
|
2023-02-19 22:16:56 +00:00
|
|
|
games which require one.
|
2023-11-15 16:07:42 +00:00
|
|
|
* `generate_early(self)`
|
2024-01-30 20:42:33 +00:00
|
|
|
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. This is the earliest step where the world should start
|
|
|
|
setting up for the current multiworld, as the multiworld itself is still setting up before this point.
|
2023-11-15 16:07:42 +00:00
|
|
|
* `create_regions(self)`
|
2024-01-30 20:42:33 +00:00
|
|
|
called to place player's regions and their locations into the MultiWorld's regions list.
|
|
|
|
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
2023-11-15 16:07:42 +00:00
|
|
|
* `create_items(self)`
|
2024-01-30 20:42:33 +00:00
|
|
|
called to place player's items into the MultiWorld's itempool. After this step all regions
|
|
|
|
and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward.
|
2023-11-15 16:07:42 +00:00
|
|
|
* `set_rules(self)`
|
2024-01-30 20:42:33 +00:00
|
|
|
called to set access and item rules on locations and entrances.
|
2023-11-15 16:07:42 +00:00
|
|
|
* `generate_basic(self)`
|
2024-01-30 20:42:33 +00:00
|
|
|
player-specific randomization that does not affect logic can be done here.
|
|
|
|
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
|
|
|
|
called to modify item placement before, during, and after the regular fill process; all finishing before
|
|
|
|
`generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there
|
|
|
|
are any items that need to be filled this way, but need to be in state while you fill other items, they can be
|
|
|
|
returned from `get_prefill_items`.
|
|
|
|
* `generate_output(self, output_directory: str)`
|
|
|
|
creates the output files if there is output to be generated. When this is called,
|
|
|
|
`self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the
|
|
|
|
item. `location.item.player` can be used to see if it's a local item.
|
2023-11-15 16:07:42 +00:00
|
|
|
* `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that
|
2021-10-07 17:41:29 +00:00
|
|
|
will be used by the server to host the MultiWorld.
|
2023-02-19 22:16:56 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
All instance methods can, optionally, have a class method defined which will be called after all instance methods are
|
|
|
|
finished running, by defining a method with `stage_` in front of the method name. These class methods will have the
|
|
|
|
args `(cls, multiworld: MultiWorld)`, followed by any other args that the relevant instance method has.
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
#### generate_early
|
|
|
|
|
|
|
|
```python
|
2022-04-28 16:03:44 +00:00
|
|
|
def generate_early(self) -> None:
|
2024-01-30 20:42:33 +00:00
|
|
|
# read player options to world instance
|
2023-10-10 20:30:20 +00:00
|
|
|
self.final_boss_hp = self.options.final_boss_hp.value
|
2021-10-07 17:41:29 +00:00
|
|
|
```
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
#### create_regions
|
|
|
|
|
|
|
|
```python
|
|
|
|
def create_regions(self) -> None:
|
|
|
|
# Add regions to the multiworld. "Menu" is the required starting point.
|
|
|
|
# Arguments to Region() are name, player, multiworld, and optionally hint_text
|
|
|
|
menu_region = Region("Menu", self.player, self.multiworld)
|
|
|
|
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
|
|
|
|
|
|
|
main_region = Region("Main Area", self.player, self.multiworld)
|
|
|
|
# add main area's locations to main area (all but final boss)
|
|
|
|
main_region.add_locations(main_region_locations, MyGameLocation)
|
|
|
|
# or
|
|
|
|
# main_region.locations = \
|
|
|
|
# [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region]
|
|
|
|
self.multiworld.regions.append(main_region)
|
|
|
|
|
|
|
|
boss_region = Region("Boss Room", self.player, self.multiworld)
|
|
|
|
# add event to Boss Room
|
|
|
|
boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region))
|
|
|
|
|
|
|
|
# if entrances are not randomized, they should be connected here, otherwise they can also be connected at a later stage
|
|
|
|
# create Entrances and connect the Regions
|
|
|
|
menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule
|
|
|
|
# or
|
|
|
|
main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)})
|
|
|
|
# connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse
|
|
|
|
|
|
|
|
# if setting location access rules from data is easier here, set_rules can possibly be omitted
|
|
|
|
```
|
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
#### create_item
|
|
|
|
|
|
|
|
```python
|
2024-01-30 20:42:33 +00:00
|
|
|
# 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
|
2023-11-15 16:07:42 +00:00
|
|
|
from .items import is_progression # this is just a dummy
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2023-11-15 16:07:42 +00:00
|
|
|
def create_item(self, item: str) -> MyGameItem:
|
2024-01-30 20:42:33 +00:00
|
|
|
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
|
|
|
|
classification = ItemClassification.progression if is_progression(item) else
|
|
|
|
ItemClassification.filler
|
|
|
|
|
|
|
|
|
|
|
|
return MyGameItem(item, classification, self.item_name_to_id[item],
|
|
|
|
self.player)
|
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
|
2023-11-15 16:07:42 +00:00
|
|
|
def create_event(self, event: str) -> MyGameItem:
|
2021-10-07 22:25:20 +00:00
|
|
|
# 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
|
2022-04-28 16:03:44 +00:00
|
|
|
def create_items(self) -> None:
|
2021-10-07 17:41:29 +00:00
|
|
|
# Add items to the Multiworld.
|
|
|
|
# If there are two of the same item, the item has to be twice in the pool.
|
2024-01-30 20:42:33 +00:00
|
|
|
# Which items are added to the pool may depend on player options, 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
|
2022-11-01 02:41:21 +00:00
|
|
|
exclude = [item for item in self.multiworld.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
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.itempool.append(self.create_item("nothing"))
|
2021-10-10 11:08:23 +00:00
|
|
|
else:
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.itempool.append(item)
|
2021-10-10 11:08:23 +00:00
|
|
|
|
|
|
|
# itempool and number of locations should match up.
|
|
|
|
# If this is not the case we want to fill the itempool with junk.
|
2024-01-30 20:42:33 +00:00
|
|
|
junk = 0 # calculate this based on player options
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.itempool += [self.create_item("nothing") for _ in range(junk)]
|
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
|
2023-11-23 23:35:37 +00:00
|
|
|
from worlds.generic.Rules import add_rule, set_rule, forbid_item, add_item_rule
|
2023-11-15 16:07:42 +00:00
|
|
|
from .items import get_item_type
|
2021-10-07 22:25:20 +00:00
|
|
|
|
2022-11-01 02:41:21 +00:00
|
|
|
|
2022-04-28 16:03:44 +00:00
|
|
|
def set_rules(self) -> None:
|
2021-10-07 17:41:29 +00:00
|
|
|
# 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
|
2022-11-01 02:41:21 +00:00
|
|
|
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Boss Key", self.player))
|
2024-01-30 20:42:33 +00:00
|
|
|
# location.access_rule = ... is likely to be a bit faster
|
2021-10-07 22:25:20 +00:00
|
|
|
# combine rules to require two items
|
2022-11-01 02:41:21 +00:00
|
|
|
add_rule(self.multiworld.get_location("Chest2", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Sword", self.player))
|
2022-11-01 02:41:21 +00:00
|
|
|
add_rule(self.multiworld.get_location("Chest2", self.player),
|
2021-10-07 22:25:20 +00:00
|
|
|
lambda state: state.has("Shield", self.player))
|
|
|
|
# or simply combine yourself
|
2022-11-01 02:41:21 +00:00
|
|
|
set_rule(self.multiworld.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
|
2022-11-01 02:41:21 +00:00
|
|
|
set_rule(self.multiworld.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
|
2022-11-01 02:41:21 +00:00
|
|
|
add_rule(self.multiworld.get_location("Chest3", self.player),
|
2021-10-09 09:06:41 +00:00
|
|
|
lambda state: state.has_group("weapons", self.player))
|
2023-11-23 23:35:37 +00:00
|
|
|
# state also has .count() for items, .has_any() and .has_all() for multiple
|
2021-10-09 09:06:41 +00:00
|
|
|
# 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
|
2022-11-01 02:41:21 +00:00
|
|
|
forbid_item(self.multiworld.get_location("Chest4", self.player), "Sword")
|
2021-10-07 22:25:20 +00:00
|
|
|
# disallow placing items with a specific property
|
2022-11-01 02:41:21 +00:00
|
|
|
add_item_rule(self.multiworld.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
|
2022-11-01 02:41:21 +00:00
|
|
|
add_item_rule(self.multiworld.get_location("Chest5", self.player),
|
2024-01-30 20:42:33 +00:00
|
|
|
lambda item: item.player != self.player or
|
2021-10-07 22:25:20 +00:00
|
|
|
item.my_type == "weapon")
|
|
|
|
# location.item_rule = ... is likely to be a bit faster
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
# place "Victory" at "Final Boss" and set collection as win condition
|
|
|
|
self.multiworld.get_location("Final Boss", self.player).place_locked_item(self.create_event("Victory"))
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
|
|
|
|
|
|
|
# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to
|
|
|
|
# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations
|
|
|
|
# are connected and placed as desired
|
|
|
|
# from Utils import visualize_regions
|
|
|
|
# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
|
|
|
|
```
|
2021-10-07 22:25:20 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
### Custom Logic Rules
|
|
|
|
|
|
|
|
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
|
|
|
Entrance should be
|
|
|
|
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
|
|
|
|
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
|
|
|
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
|
|
|
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
2021-10-07 22:25:20 +00:00
|
|
|
|
|
|
|
```python
|
2023-11-15 16:07:42 +00:00
|
|
|
# logic.py
|
2021-10-07 22:25:20 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
from BaseClasses import CollectionState
|
|
|
|
|
2021-10-07 17:41:29 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
def mygame_has_key(self, state: CollectionState, player: int) -> bool:
|
|
|
|
# More arguments above are free to choose, since you can expect this is only called in your world
|
|
|
|
# MultiWorld can be accessed through state.multiworld.
|
|
|
|
# Explicitly passing in MyGameWorld instance for easy options access is also a valid approach, but it's generally
|
|
|
|
# better to check options before rule assignment since the individual functions can be called thousands of times
|
|
|
|
return state.has("key", player) # or whatever
|
2021-10-07 22:25:20 +00:00
|
|
|
```
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
```python
|
|
|
|
# __init__.py
|
|
|
|
|
2022-09-17 22:00:54 +00:00
|
|
|
from worlds.generic.Rules import set_rule
|
2024-01-30 20:42:33 +00:00
|
|
|
from . import logic
|
|
|
|
|
2021-10-07 22:25:20 +00:00
|
|
|
|
|
|
|
class MyGameWorld(World):
|
|
|
|
# ...
|
2023-11-15 16:07:42 +00:00
|
|
|
def set_rules(self) -> None:
|
2023-07-09 16:04:24 +00:00
|
|
|
set_rule(self.multiworld.get_location("A Door", self.player),
|
2024-01-30 20:42:33 +00:00
|
|
|
lambda state: logic.mygame_has_key(state, self.player))
|
|
|
|
```
|
|
|
|
|
|
|
|
### Logic Mixin
|
|
|
|
|
|
|
|
While lambdas and events can do pretty much anything, more complex logic can be handled in logic mixins.
|
|
|
|
|
|
|
|
When importing a file that defines a class that inherits from `worlds.AutoWorld.LogicMixin`, the `CollectionState` class
|
|
|
|
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
|
|
|
|
world since the namespace is shared with all other logic mixins.
|
|
|
|
|
|
|
|
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
|
|
|
|
with the state.
|
|
|
|
Please do this with caution and only when necessary.
|
|
|
|
|
|
|
|
#### pre_fill
|
|
|
|
|
|
|
|
```python
|
|
|
|
def pre_fill(self) -> None:
|
|
|
|
# place item Herb into location Chest1 for some reason
|
|
|
|
item = self.create_item("Herb")
|
|
|
|
self.multiworld.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, such as manually placed and still in the itempool
|
2021-10-07 22:25:20 +00:00
|
|
|
```
|
|
|
|
|
2021-10-08 22:49:47 +00:00
|
|
|
### Generate Output
|
2021-10-07 17:41:29 +00:00
|
|
|
|
|
|
|
```python
|
2023-11-15 16:07:42 +00:00
|
|
|
from .mod import generate_mod
|
2021-10-07 22:25:20 +00:00
|
|
|
|
2022-11-01 02:41:21 +00:00
|
|
|
|
2023-11-15 16:07:42 +00:00
|
|
|
def generate_output(self, output_directory: str) -> None:
|
2024-01-30 20:42:33 +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.
|
2021-10-07 22:25:20 +00:00
|
|
|
# code below is a dummy
|
|
|
|
data = {
|
2022-11-01 02:41:21 +00:00
|
|
|
"seed": self.multiworld.seed_name, # to verify the server's multiworld
|
|
|
|
"slot": self.multiworld.player_name[self.player], # to connect to server
|
2021-10-07 22:25:20 +00:00
|
|
|
"items": {location.name: location.item.name
|
|
|
|
if location.item.player == self.player else "Remote"
|
2022-11-01 02:41:21 +00:00
|
|
|
for location in self.multiworld.get_filled_locations(self.player)},
|
2021-10-09 12:29:52 +00:00
|
|
|
# store start_inventory from player's .yaml
|
2022-12-11 13:14:27 +00:00
|
|
|
# make sure to mark as not remote_start_inventory when connecting if stored in rom/mod
|
2024-01-30 20:42:33 +00:00
|
|
|
"starter_items": [item.name for item in self.multiworld.precollected_items[self.player]],
|
2021-10-07 22:25:20 +00:00
|
|
|
}
|
2023-11-15 16:07:42 +00:00
|
|
|
|
|
|
|
# add needed option results to the dictionary
|
|
|
|
data.update(self.options.as_dict("final_boss_hp", "difficulty", "fix_xyz_glitch"))
|
2021-10-07 22:25:20 +00:00
|
|
|
# point to a ROM specified by the installation
|
2023-07-05 20:39:35 +00:00
|
|
|
src = self.settings.rom_file
|
2021-10-07 22:25:20 +00:00
|
|
|
# or point to worlds/mygame/data/mod_template
|
|
|
|
src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
|
|
|
# generate output path
|
2023-07-09 16:04:24 +00:00
|
|
|
mod_name = self.multiworld.get_out_file_name_base(self.player)
|
2021-10-07 22:25:20 +00:00
|
|
|
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
|
|
|
```
|
|
|
|
|
2023-10-10 20:30:20 +00:00
|
|
|
### Slot Data
|
|
|
|
|
|
|
|
If the game client needs to know information about the generated seed, a preferred method of transferring the data
|
2024-01-30 20:42:33 +00:00
|
|
|
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
|
2024-02-29 01:22:42 +00:00
|
|
|
a `dict` with `str` keys that can be serialized with json.
|
|
|
|
But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
|
|
|
|
once it has successfully [connected](network%20protocol.md#connected).
|
2024-01-30 20:42:33 +00:00
|
|
|
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
|
|
|
|
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
|
|
|
|
common usage of slot data is sending option results that the client needs to be aware of.
|
2023-10-10 20:30:20 +00:00
|
|
|
|
|
|
|
```python
|
2023-11-15 16:07:42 +00:00
|
|
|
def fill_slot_data(self) -> Dict[str, Any]:
|
2024-01-30 20:42:33 +00:00
|
|
|
# In order for our game client to handle the generated seed correctly we need to know what the user selected
|
|
|
|
# for their difficulty and final boss HP.
|
|
|
|
# A dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting.
|
|
|
|
# The options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the relevant
|
|
|
|
# option's value.
|
2023-10-10 20:30:20 +00:00
|
|
|
return self.options.as_dict("difficulty", "final_boss_hp")
|
|
|
|
```
|
|
|
|
|
2023-02-19 22:16:56 +00:00
|
|
|
### Documentation
|
|
|
|
|
|
|
|
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading
|
|
|
|
the `.md` files in your world's `/docs` directory.
|
|
|
|
|
|
|
|
#### Game Info
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2023-02-19 22:16:56 +00:00
|
|
|
The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional
|
|
|
|
information that may be useful to the player when learning your randomizer should also go here. The file name format
|
|
|
|
is `<language key>_<game name>.md`. While you can write these docs for multiple languages, currently only the english
|
|
|
|
version is displayed on the website.
|
|
|
|
|
|
|
|
#### Tutorials
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2023-02-19 22:16:56 +00:00
|
|
|
Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial`
|
2024-01-30 20:42:33 +00:00
|
|
|
defined in the `WebWorld`. The file name you use isn't particularly important, but it should be descriptive of what
|
|
|
|
the tutorial covers, and the name of the file must match the relative URL provided in the `Tutorial`. Currently,
|
2023-02-19 22:16:56 +00:00
|
|
|
the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where
|
|
|
|
`game/document/lang` is the provided URL.
|
|
|
|
|
|
|
|
### Tests
|
|
|
|
|
|
|
|
Each world is expected to include unit tests that cover its logic, to ensure no logic bug regressions occur. This can be
|
|
|
|
done by creating a `/test` package within your world package. The `__init__.py` within this folder is where the world's
|
|
|
|
TestBase should be defined. This can be inherited from the main TestBase, which will automatically set up a solo
|
|
|
|
multiworld for each test written using it. Within subsequent modules, classes should be defined which inherit the world
|
|
|
|
TestBase, and can then define options to test in the class body, and run tests in each test method.
|
|
|
|
|
|
|
|
Example `__init__.py`
|
2023-10-22 11:00:27 +00:00
|
|
|
|
2023-02-19 22:16:56 +00:00
|
|
|
```python
|
2023-12-01 09:26:27 +00:00
|
|
|
from test.bases import WorldTestBase
|
2023-02-19 22:16:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
class MyGameTestBase(WorldTestBase):
|
2024-01-30 20:42:33 +00:00
|
|
|
game = "My Game"
|
2023-02-19 22:16:56 +00:00
|
|
|
```
|
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
Next, using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
|
2023-02-19 22:16:56 +00:00
|
|
|
|
2023-12-01 09:26:27 +00:00
|
|
|
Example `test_chest_access.py`
|
2024-01-30 20:42:33 +00:00
|
|
|
|
2023-02-19 22:16:56 +00:00
|
|
|
```python
|
|
|
|
from . import MyGameTestBase
|
|
|
|
|
|
|
|
|
|
|
|
class TestChestAccess(MyGameTestBase):
|
2023-11-15 16:07:42 +00:00
|
|
|
def test_sword_chests(self) -> None:
|
2023-02-19 22:16:56 +00:00
|
|
|
"""Test locations that require a sword"""
|
|
|
|
locations = ["Chest1", "Chest2"]
|
|
|
|
items = [["Sword"]]
|
2024-01-30 20:42:33 +00:00
|
|
|
# this will test that each location can't be accessed without the "Sword", but can be accessed once obtained
|
2023-02-19 22:16:56 +00:00
|
|
|
self.assertAccessDependency(locations, items)
|
2023-07-09 16:04:24 +00:00
|
|
|
|
2023-11-15 16:07:42 +00:00
|
|
|
def test_any_weapon_chests(self) -> None:
|
2023-02-19 22:16:56 +00:00
|
|
|
"""Test locations that require any weapon"""
|
|
|
|
locations = [f"Chest{i}" for i in range(3, 6)]
|
|
|
|
items = [["Sword"], ["Axe"], ["Spear"]]
|
2024-01-30 20:42:33 +00:00
|
|
|
# this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them
|
2023-02-19 22:16:56 +00:00
|
|
|
self.assertAccessDependency(locations, items)
|
|
|
|
```
|
2023-12-01 09:26:27 +00:00
|
|
|
|
2024-01-30 20:42:33 +00:00
|
|
|
For more information on tests, check the [tests doc](tests.md).
|