mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-16 13:53:29 -07:00
Merge branch 'main' into custom-port-range
This commit is contained in:
@@ -15,15 +15,16 @@
|
||||
# A Link to the Past
|
||||
/worlds/alttp/ @Berserker66
|
||||
|
||||
# APQuest
|
||||
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
||||
/worlds/apquest/ @NewSoupVi
|
||||
|
||||
# Sudoku (APSudoku)
|
||||
/worlds/apsudoku/ @EmilyV99
|
||||
|
||||
# Aquaria
|
||||
/worlds/aquaria/ @tioui
|
||||
|
||||
# ArchipIDLE
|
||||
/worlds/archipidle/ @LegendaryLinux
|
||||
|
||||
# Blasphemous
|
||||
/worlds/blasphemous/ @TRPG0
|
||||
|
||||
@@ -42,15 +43,18 @@
|
||||
# Celeste 64
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
# Celeste (Open World)
|
||||
/worlds/celeste_open_world/ @PoryGone
|
||||
|
||||
# ChecksFinder
|
||||
/worlds/checksfinder/ @SunCatMC
|
||||
|
||||
# Choo-Choo Charles
|
||||
/worlds/cccharles/ @Yaranorgoth
|
||||
|
||||
# Civilization VI
|
||||
/worlds/civ6/ @hesto2
|
||||
|
||||
# Clique
|
||||
/worlds/clique/ @ThePhar
|
||||
|
||||
# Dark Souls III
|
||||
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||
|
||||
@@ -66,12 +70,18 @@
|
||||
# DOOM II
|
||||
/worlds/doom_ii/ @Daivuk @KScl
|
||||
|
||||
# EarthBound
|
||||
/worlds/earthbound/ @PinkSwitch
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
# Faxanadu
|
||||
/worlds/faxanadu/ @Daivuk
|
||||
|
||||
# Final Fantasy (1)
|
||||
/worlds/ff1/ @Rosalie-A
|
||||
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
@@ -87,6 +97,9 @@
|
||||
# Inscryption
|
||||
/worlds/inscryption/ @DrBibop @Glowbuzz
|
||||
|
||||
# Jak and Daxter: The Precursor Legacy
|
||||
/worlds/jakanddaxter/ @massimilianodelliubaldini
|
||||
|
||||
# Kirby's Dream Land 3
|
||||
/worlds/kdl3/ @Silvris
|
||||
|
||||
@@ -118,9 +131,6 @@
|
||||
# The Messenger
|
||||
/worlds/messenger/ @alwaysintreble
|
||||
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
@@ -139,6 +149,9 @@
|
||||
# Overcooked! 2
|
||||
/worlds/overcooked2/ @toasterparty
|
||||
|
||||
# Paint
|
||||
/worlds/paint/ @MarioManTAW
|
||||
|
||||
# Pokemon Emerald
|
||||
/worlds/pokemon_emerald/ @Zunawe
|
||||
|
||||
@@ -148,15 +161,15 @@
|
||||
# Raft
|
||||
/worlds/raft/ @SunnyBat
|
||||
|
||||
# Rogue Legacy
|
||||
/worlds/rogue_legacy/ @ThePhar
|
||||
|
||||
# Risk of Rain 2
|
||||
/worlds/ror2/ @kindasneaki
|
||||
|
||||
# Saving Princess
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# shapez
|
||||
/worlds/shapez/ @BlastSlimey
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
@@ -166,8 +179,12 @@
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
# Satisfactory
|
||||
/worlds/satisfactory/ @Jarno458 @budak7273
|
||||
|
||||
# Starcraft 2
|
||||
/worlds/sc2/ @Ziktofel
|
||||
# Note: @Ziktofel acts as a mentor
|
||||
/worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles
|
||||
|
||||
# Super Metroid
|
||||
/worlds/sm/ @lordlou
|
||||
@@ -175,6 +192,9 @@
|
||||
# Super Mario 64
|
||||
/worlds/sm64ex/ @N00byKing
|
||||
|
||||
# Super Mario Land 2: 6 Golden Coins
|
||||
/worlds/marioland2/ @Alchav
|
||||
|
||||
# Super Mario World
|
||||
/worlds/smw/ @PoryGone
|
||||
|
||||
@@ -184,9 +204,6 @@
|
||||
# Secret of Evermore
|
||||
/worlds/soe/ @black-sliver
|
||||
|
||||
# Slay the Spire
|
||||
/worlds/spire/ @KonoTyran
|
||||
|
||||
# Stardew Valley
|
||||
/worlds/stardew_valley/ @agilbert1412
|
||||
|
||||
@@ -200,7 +217,7 @@
|
||||
/worlds/timespinner/ @Jarno458
|
||||
|
||||
# The Legend of Zelda (1)
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
/worlds/tloz/ @Rosalie-A
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer @ScipioWright
|
||||
@@ -214,6 +231,9 @@
|
||||
# Wargroove
|
||||
/worlds/wargroove/ @FlySniper
|
||||
|
||||
# The Wind Waker
|
||||
/worlds/tww/ @tanjo3
|
||||
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
@@ -229,31 +249,15 @@
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
# Zork Grand Inquisitor
|
||||
/worlds/zork_grand_inquisitor/ @nbrochu
|
||||
|
||||
|
||||
## Active Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks
|
||||
# compatibility, these worlds may be moved to `worlds_disabled`. If you are interested in stepping up as maintainer for
|
||||
# compatibility, these worlds may be deleted. If you are interested in stepping up as maintainer for
|
||||
# any of these worlds, please review `/docs/world maintainer.md` documentation.
|
||||
|
||||
# Final Fantasy (1)
|
||||
# /worlds/ff1/
|
||||
|
||||
# Ocarina of Time
|
||||
# /worlds/oot/
|
||||
|
||||
## Disabled Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||
# interested in stepping up as maintainer for any of these worlds, please review `/docs/world maintainer.md`
|
||||
# documentation.
|
||||
|
||||
# Ori and the Blind Forest
|
||||
# /worlds_disabled/oribf/
|
||||
|
||||
###################
|
||||
## Documentation ##
|
||||
###################
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Adding Games
|
||||
|
||||
Like all contributions to Archipelago, New Game implementations should follow the [Contributing](/docs/contributing.md)
|
||||
guide.
|
||||
|
||||
Adding a new game to Archipelago has two major parts:
|
||||
|
||||
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
||||
@@ -13,30 +16,70 @@ it will not be detailed here.
|
||||
|
||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
|
||||
to behave as expected are:
|
||||
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document. Additional help with specific game
|
||||
engines and rom formats can be found in the #ap-modding-help channel in the [Discord](https://archipelago.gg/discord).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
In order for the game client to behave as expected, it must be able to perform these functions:
|
||||
|
||||
* Handle both secure and unsecure websocket connections
|
||||
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
|
||||
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
|
||||
demand
|
||||
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
|
||||
normally expect from features such as starting inventory, item link replacement, or item cheating
|
||||
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
|
||||
a player or location attributed to them
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Be able to change the port for saved connection info
|
||||
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
|
||||
order.
|
||||
* Receive items that were sent to the player while they were not connected to the server
|
||||
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
|
||||
strictly required
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Send a status update packet alerting the server that the player has completed their goal
|
||||
|
||||
Libraries for most modern languages and the spec for various packets can be found in the
|
||||
[network protocol](/docs/network%20protocol.md) API reference document.
|
||||
Regarding items and locations, the game client must be able to handle these tasks:
|
||||
|
||||
#### Location Handling
|
||||
|
||||
Send a network packet to the server when it detects a location has been "checked" by the player in-game.
|
||||
|
||||
* If actions were taken in game that would usually trigger a location check, and those actions can only ever be taken
|
||||
once, but the client was not connected when they happened: The client must send those location checks on connection
|
||||
so that they are not permanently lost, e.g. by reading flags in the game state or save file.
|
||||
|
||||
#### Item Handling
|
||||
|
||||
Receive and parse network packets from the server when the player receives an item.
|
||||
|
||||
* It must reward items to the player on demand, as items can come from other players at any time.
|
||||
* It must be able to reward copies of an item, up to and beyond the number the game normally expects. This may happen
|
||||
due to features such as starting inventory, item link replacement, admin commands, or item cheating. **Any** of
|
||||
your items can be received **any** number of times.
|
||||
* Admins and players may use server commands to create items without a player or location attributed to them. The
|
||||
client must be able to handle these items.
|
||||
* It must keep an index for items received in order to resync. The ItemsReceived Packets are a single list with a
|
||||
guaranteed order.
|
||||
* It must be able to receive items that were sent to the player while they were not connected to the server.
|
||||
|
||||
### Encouraged Features
|
||||
|
||||
These are "nice to have" features for a client, but they are not strictly required. It is encouraged to add them
|
||||
if possible.
|
||||
|
||||
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
|
||||
other clients. The icon size is 48x48 pixels, but smaller or larger images will scale to that size.
|
||||
|
||||
### Launcher Integration
|
||||
|
||||
If you have a python client or want to utilize the integration features of the Archipelago Launcher (ex. Slot links in
|
||||
webhost) you can define a Component to be a part of the Launcher. `LauncherComponents.components` can be appended to
|
||||
with additional Components in order to automatically add them to the Launcher. Most Components only need a
|
||||
`display_name` and `func`, but `supports_uri` and `game_name` can be defined to support launching by webhost links,
|
||||
`icon` and `description` can be used to customize display in the Launcher UI, and `file_identifier` can be used to
|
||||
launch by file.
|
||||
|
||||
Additionally, if you use `func` you have access to LauncherComponent.launch or launch_subprocess to run your
|
||||
function as a subprocesses that can be utilized side by side other clients.
|
||||
```py
|
||||
def my_func(*args: str):
|
||||
from .client import run_client
|
||||
LauncherComponent.launch(run_client, name="My Client", args=args)
|
||||
```
|
||||
|
||||
|
||||
## World
|
||||
|
||||
@@ -44,35 +87,94 @@ The world is your game integration for the Archipelago generator, webhost, and m
|
||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
|
||||
following requirements:
|
||||
repository and creating a new world package in `/worlds/`.
|
||||
|
||||
* A folder within `/worlds/` that contains an `__init__.py`
|
||||
* A `World` subclass where you create your world and define all of its rules
|
||||
* A unique game name
|
||||
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
|
||||
definition
|
||||
* The game_info doc must follow the format `{language_code}_{game_name}.md`
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
A bare minimum world implementation must satisfy the following requirements:
|
||||
|
||||
* It has a folder with the name of your game (or an abbreviation) under `/worlds/`
|
||||
* The `/worlds/{game}` folder contains an `__init__.py`
|
||||
* Any subfolders within `/worlds/{game}` that contain `*.py` files also contain an `__init__.py` for frozen build
|
||||
packaging
|
||||
* The game folder has at least one game_info doc named with follow the format `{language_code}_{game_name}.md`
|
||||
* The game folder has at least one setup doc
|
||||
* There must be a `World` subclass in your game folder (typically in `/worlds/{game}/__init__.py`) where you create
|
||||
your world and define all of its rules and features
|
||||
|
||||
Within the `World` subclass you should also have:
|
||||
|
||||
* A [unique game name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L260)
|
||||
* An [instance](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295) of a `WebWorld`
|
||||
subclass for webhost documentation and behaviors
|
||||
* In your `WebWorld`, if you wrote a game_info doc in more than one language, override the list of
|
||||
[game info languages](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L210) with the
|
||||
ones you include.
|
||||
* In your `WebWorld`, override the list of
|
||||
[tutorials](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L213) with each tutorial
|
||||
or setup doc you included in the game folder.
|
||||
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* Create an item when `create_item` is called both by your code and externally
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* A `Region` for your player with the name "Menu" to start from
|
||||
* Create a non-zero number of locations and add them to your regions
|
||||
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
|
||||
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* An implementation of `create_item` that can create an item when called by either your code or by another process
|
||||
within Archipelago
|
||||
* At least one `Region` for your player to start from (i.e. the Origin Region)
|
||||
* The default name of this region is "Menu" but you may configure a different name with
|
||||
[origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
|
||||
* A non-zero number of locations, added to your regions
|
||||
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
|
||||
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
|
||||
* A set
|
||||
[completion condition](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#L77) (aka "goal") for
|
||||
the player.
|
||||
* Use your player as the index (`multiworld.completion_condition[player]`) for your world's completion goal.
|
||||
|
||||
Notable caveats:
|
||||
* The "Menu" region will always be considered the "start" for the player
|
||||
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
### Encouraged Features
|
||||
|
||||
These are "nice to have" features for a world, but they are not strictly required. It is encouraged to add them
|
||||
if possible.
|
||||
|
||||
* An implementation of
|
||||
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||
* By default, this function chooses any item name from `item_name_to_id`, which may include items you consider
|
||||
"non-repeatable".
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* This should be accompanied by a type hint for `options` with the same class name
|
||||
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||
* A list of [option groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L226)
|
||||
for better organization on the webhost
|
||||
* A dictionary of [options presets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L223)
|
||||
for player convenience
|
||||
* A dictionary of [item name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L273)
|
||||
for player convenience
|
||||
* A dictionary of
|
||||
[location name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L276)
|
||||
for player convenience
|
||||
* Other games may also benefit from your name group dictionaries for hints, features, etc.
|
||||
|
||||
### Discouraged or Prohibited Behavior
|
||||
|
||||
These are behaviors or implementations that are known to cause various issues. Some of these points have notable
|
||||
workarounds or preferred methods which should be used instead:
|
||||
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World.
|
||||
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
|
||||
multiworld itempool.
|
||||
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
|
||||
* It is discouraged to use PyYAML (i.e. `yaml.load`) directly due to security concerns.
|
||||
* When possible, use `Utils.parse_yaml` instead, as this defaults to the safe loader and the faster C parser.
|
||||
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
|
||||
do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||
* Instead, use `append`, `extend`, or `+=`.
|
||||
|
||||
### Notable Caveats
|
||||
|
||||
* The Origin Region will always be considered the "start" for the player
|
||||
* The Origin Region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
start of the game from anywhere
|
||||
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
|
||||
`append`, `extend`, or `+=`. **Do not use `=`**
|
||||
* Regions are simply containers for locations that share similar access rules. They do not have to map to
|
||||
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
|
||||
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
|
||||
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md).
|
||||
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
@@ -1,35 +1,110 @@
|
||||
# apworld Specification
|
||||
# APWorld Specification
|
||||
|
||||
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
|
||||
Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
|
||||
These are called "APWorlds".
|
||||
They are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
|
||||
See [world api.md](world%20api.md) for details.
|
||||
APWorlds can either be a folder, or they can be packaged as an .apworld file.
|
||||
|
||||
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
|
||||
file into the worlds folder.
|
||||
## .apworld File Format
|
||||
|
||||
**Warning:** apworlds have to be all lower case, otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
|
||||
The `.apworld` file format provides a way to package and ship an APWorld that is not part of the main distribution
|
||||
by placing a `*.apworld` file into the worlds folder.
|
||||
|
||||
|
||||
## File Format
|
||||
|
||||
apworld files are zip archives, all lower case, with the file ending `.apworld`.
|
||||
`.apworld` files are zip archives, all lower case, with the file ending `.apworld`.
|
||||
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
|
||||
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
|
||||
|
||||
**Warning:** `.apworld` files have to be all lower case,
|
||||
otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
|
||||
|
||||
## Metadata
|
||||
|
||||
No metadata is specified yet.
|
||||
Metadata about the APWorld is defined in an `archipelago.json` file.
|
||||
|
||||
If the APWorld is a folder, the only required field is "game":
|
||||
```json
|
||||
{
|
||||
"game": "Game Name"
|
||||
}
|
||||
```
|
||||
|
||||
## Extra Data
|
||||
There are also the following optional fields:
|
||||
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
|
||||
Archipelago version respectively to filter those files from being loaded.
|
||||
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
|
||||
An APWorld without a world_version is always treated as older than one with a version
|
||||
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
|
||||
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
|
||||
package managers. Should always be a list of strings.
|
||||
|
||||
The zip can contain arbitrary files in addition what was specified above.
|
||||
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
||||
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
||||
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
|
||||
["Build APWorlds" launcher component](#build-apworlds-launcher-component),
|
||||
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||
|
||||
### "Build APWorlds" Launcher Component
|
||||
|
||||
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
|
||||
and add `archipelago.json` manifest files to them.
|
||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||
`version` and `compatible_version`.
|
||||
The component can also be called from the command line to allow for specifying a certain list of worlds to build.
|
||||
For example, running `Launcher.py "Build APWorlds" -- "Game Name"` will build only the game called `Game Name`.
|
||||
|
||||
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
|
||||
So, a world folder with an `archipelago.json` that looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"game": "Game Name",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "2.1.4",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
```
|
||||
|
||||
will be packaged into an `.apworld` with a manifest file inside of it that looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "2.1.4",
|
||||
"authors": ["NewSoupVi"],
|
||||
"version": 7,
|
||||
"compatible_version": 7,
|
||||
"game": "Game Name"
|
||||
}
|
||||
```
|
||||
|
||||
This is the recommended workflow for packaging your world to an `.apworld`.
|
||||
|
||||
### .apignore Exclusions
|
||||
|
||||
By default, any additional files inside of the world folder will be packaged into the resulting `.apworld` archive and
|
||||
can then be read by the world. However, if there are any other files that aren't needed in the resulting `.apworld`, you
|
||||
can automatically prevent the build component from including them by specifying them in a file called `.apignore` inside
|
||||
the root of the world folder.
|
||||
|
||||
The `.apignore` file selects files in the same way as the `.gitignore` format with patterns separated by line describing
|
||||
which files to ignore. For example, an `.apignore` like this:
|
||||
|
||||
```gitignore
|
||||
*.iso
|
||||
scripts/
|
||||
!scripts/needed.py
|
||||
```
|
||||
|
||||
would ignore any `.iso` files and anything in the scripts folder except for `scripts/needed.py`.
|
||||
|
||||
Some exclusions are made by default for all worlds such as `__pycache__` folders. These are listed in the
|
||||
`GLOBAL.apignore` file inside of the `data` directory.
|
||||
|
||||
## Caveats
|
||||
|
||||
Imports from other files inside the apworld have to use relative imports. e.g. `from .options import MyGameOptions`
|
||||
Imports from other files inside the APWorld have to use relative imports. e.g. `from .options import MyGameOptions`
|
||||
|
||||
Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or
|
||||
`from worlds.AutoWorld import World`
|
||||
|
||||
@@ -6,9 +6,56 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
||||
|
||||
---
|
||||
|
||||
### I've never added a game to Archipelago before. Should I start with the APWorld or the game client?
|
||||
|
||||
Strictly speaking, this is a false dichotomy: we do *not* recommend doing 100% of client work before the APWorld,
|
||||
or 100% of APWorld work before the client. It's important to iterate on both parts and test them together.
|
||||
However, the early iterations tend to be very similar for most games,
|
||||
so the typical recommendation for first-time AP developers is:
|
||||
|
||||
- Start with a proof-of-concept for [the game client](adding%20games.md#client)
|
||||
- Figure out how to interface with the game. Whether that means "modding" the game, or patching a ROM file,
|
||||
or developing a separate client program that edits the game's memory, or some other technique.
|
||||
- Figure out how to give items and detect locations in the actual game. Not every item and location,
|
||||
just one of each major type (e.g. opening a chest vs completing a sidequest) to prove all the items and locations
|
||||
you want can actually be implemented.
|
||||
- Figure out how to make a websocket connection to an AP server, possibly using a client library (see [Network Protocol](<network%20protocol.md>).
|
||||
To make absolutely sure this part works, you may want to test the connection by generating a multiworld
|
||||
with a different game, then making your client temporarily pretend to be that other game.
|
||||
- Next, make a "trivial" APWorld, i.e. an APWorld that always generates the same items and locations
|
||||
- If you've never done this before, likely the fastest approach is to copy-paste [APQuest](<../worlds/apquest>), and read the many
|
||||
comments in there until you understand how to edit the items and locations.
|
||||
- Then you can do your first "end-to-end test": generate a multiworld using your APWorld, [run a local server](<running%20from%20source.md>)
|
||||
to host it, connect to that local server from your game client, actually check a location in the game,
|
||||
and finally make sure the client successfully sent that location check to the AP server
|
||||
as well as received an item from it.
|
||||
|
||||
That's about where general recommendations end. What you should do next will depend entirely on your game
|
||||
(e.g. implement more items, write down logic rules, add client features, prototype a tracker, etc).
|
||||
If you're not sure, then this would be a good time to re-read [Adding Games](<adding%20games.md>), and [World API](<world%20api.md>).
|
||||
|
||||
There are a few assumptions in this recommendation worth stating explicitly, namely:
|
||||
|
||||
- If something you want to do is infeasible, you want to find out that it's infeasible as soon as possible, before
|
||||
you write a bunch of code assuming it could be done. That's why we recommend starting with the game client.
|
||||
- Getting an APWorld to generate whatever items/locations you want is always feasible, since items/locations are
|
||||
little more than id numbers and name strings during generation.
|
||||
- You generally want to get to an "end-to-end playable" prototype quickly. On top of all the technical challenges these
|
||||
docs describe, it's also important to check that a randomizer is *fun to play*, and figure out what features would be
|
||||
essential for a public release.
|
||||
- A first-time world developer may or may not be deeply familiar with Archipelago, but they're almost certainly familiar
|
||||
with the game they want to randomize. So judging whether your game client is working correctly might be significantly
|
||||
easier than judging if your APWorld is working.
|
||||
|
||||
---
|
||||
|
||||
### My game has a restrictive start that leads to fill errors
|
||||
|
||||
Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
|
||||
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||
than one item to get a player to sphere 2.
|
||||
|
||||
One way to fix this is to hint to the Generator that an item needs to be in sphere one with local_early_items.
|
||||
Here, `1` represents the number of "Sword" items the Generator will attempt to place in sphere one.
|
||||
```py
|
||||
early_item_name = "Sword"
|
||||
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
||||
@@ -18,15 +65,19 @@ Some alternative ways to try to fix this problem are:
|
||||
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
||||
* Pre-place items yourself, such as during `create_items`
|
||||
* Put items into the player's starting inventory using `push_precollected`
|
||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
|
||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a
|
||||
restrictive start
|
||||
|
||||
---
|
||||
|
||||
### I have multiple settings that change the item/location pool counts and need to balance them out
|
||||
### I have multiple options that change the item/location pool counts and need to make sure I am not submitting more/fewer items than locations
|
||||
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be
|
||||
unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your
|
||||
itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations)
|
||||
to your list of items to submit
|
||||
|
||||
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
||||
```py
|
||||
@@ -39,7 +90,8 @@ for _ in range(total_locations - len(item_pool)):
|
||||
self.multiworld.itempool += item_pool
|
||||
```
|
||||
|
||||
A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||
A faster alternative to the `for` loop would be to use a
|
||||
[list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||
```py
|
||||
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
||||
```
|
||||
@@ -48,21 +100,141 @@ item_pool += [self.create_filler() for _ in range(total_locations - len(item_poo
|
||||
|
||||
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
|
||||
|
||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and **when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is quite complicated.
|
||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and
|
||||
**when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is
|
||||
quite complicated.
|
||||
|
||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph. It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to the queue until there is nothing more to check.
|
||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph.
|
||||
It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to
|
||||
the queue until there is nothing more to check.
|
||||
|
||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region access, then the following may happen:
|
||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been reached yet during the graph search.
|
||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region
|
||||
access, then the following may happen:
|
||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been
|
||||
reached yet during the graph search.
|
||||
2. Then, the region in its access_rule is determined to be reachable.
|
||||
|
||||
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
|
||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new regions are reached.
|
||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new
|
||||
regions are reached.
|
||||
|
||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep if a specific region is reached during it.
|
||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness), using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they call `region.can_reach` on their respective parent/source region.
|
||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep
|
||||
if a specific region is reached during it.
|
||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness),
|
||||
using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they
|
||||
call `region.can_reach` on their respective parent/source region.
|
||||
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition,
|
||||
and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682)
|
||||
being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of
|
||||
checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should
|
||||
be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are
|
||||
much faster.
|
||||
|
||||
---
|
||||
|
||||
### I uploaded the generated output of my world to the webhost and webhost is erroring on corrupted multidata
|
||||
|
||||
The error `Could not load multidata. File may be corrupted or incompatible.` occurs when uploading a locally generated
|
||||
file where there is an issue with the multidata contained within it. It may come with a description like
|
||||
`(No module named 'worlds.myworld')` or `(global 'worlds.myworld.names.ItemNames' is forbidden)`
|
||||
|
||||
Pickling is a way to compress python objects such that they can be decompressed and be used to rebuild the
|
||||
python objects. This means that if one of your custom class instances ends up in the multidata, the server would not
|
||||
be able to load that custom class to decompress the data, which can fail either because the custom class is unknown
|
||||
(because it cannot load your world module) or the class it's attempting to import to decompress is deemed unsafe.
|
||||
|
||||
Common situations where this can happen include:
|
||||
* Using Option instances directly in slot_data. Ex: using `options.option_name` instead of `options.option_name.value`.
|
||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||
|
||||
---
|
||||
|
||||
### Some locations are technically possible to check with few or no items, but they'd be very tedious or frustrating. How do worlds deal with this?
|
||||
|
||||
Sometimes the game can be modded to skip these locations or make them less tedious. But when this issue is due to a fundamental aspect of the game, then the general answer is "soft logic" (and its subtypes like "combat logic", "money logic", etc.). For example: you can logically require that a player have several helpful items before fighting the final boss, even if a skilled player technically needs no items to beat it. Randomizer logic should describe what's *fun* rather than what's technically possible.
|
||||
|
||||
Concrete examples of soft logic include:
|
||||
- Defeating a boss might logically require health upgrades, damage upgrades, certain weapons, etc. that aren't strictly necessary.
|
||||
- Entering a high-level area might logically require access to enough other parts of the game that checking other locations should naturally get the player to the soft-required level.
|
||||
- Buying expensive shop items might logically require access to a place where you can quickly farm money, or logically require access to enough parts of the game that checking other locations should naturally generate enough money without grinding.
|
||||
|
||||
Remember that all items referenced by logic (however hard or soft) must be `progression`. Since you typically don't want to turn a ton of `filler` items into `progression` just for this, it's common to e.g. write money logic using only the rare "$100" item, so the dozens of "$1" and "$10" items in your world can remain `filler`.
|
||||
|
||||
---
|
||||
|
||||
### What if my game has "missable" or "one-time-only" locations or region connections?
|
||||
|
||||
Archipelago logic assumes that once a region or location becomes reachable, it stays reachable forever, no matter what
|
||||
the player does in-game. Slightly more formally: Receiving an AP item must never cause a region connection or location
|
||||
to "go out of logic" (become unreachable when it was previously reachable), and receiving AP items is the only kind of
|
||||
state change that AP logic acknowledges. No other actions or events can change reachability.
|
||||
|
||||
So when the game itself does not follow this assumption, the options are:
|
||||
- Modify the game to make that location/connection repeatable
|
||||
- If there are both missable and repeatable ways to check the location/traverse the connection, then write logic for
|
||||
only the repeatable ways
|
||||
- Don't generate the missable location/connection at all
|
||||
- For connections, any logical regions will still need to be reachable through other, *repeatable* connections
|
||||
- For locations, this may require game changes to remove the vanilla item if it affects logic
|
||||
- Decide that resetting the save file is part of the game's logic, and warn players about that
|
||||
|
||||
---
|
||||
|
||||
### What are "local" vs "remote" items, and what are the pros and cons of each?
|
||||
|
||||
First off, these terms can be misleading. Since the whole point of a multi-game multiworld randomizer is that some items
|
||||
are going to be placed in other slots (unless there's only one slot), the choice isn't really "local vs remote";
|
||||
it's "mixed local/remote vs all remote". You have to get "remote items" working to be an AP implementation at all, and
|
||||
it's often simpler to handle every item/location the same way, so you generally shouldn't worry about "local items"
|
||||
until you've finished more critical features.
|
||||
|
||||
Next, "local" and "remote" items confusingly refer to multiple concepts, so it's important to clearly separate them:
|
||||
|
||||
- Whether an item happens to get placed in the same slot it originates from, or a different slot. I'll call these
|
||||
"locally placed" and "remotely placed" items.
|
||||
- Whether an AP client implements location checking for locally placed items by skipping the usual AP server roundtrip
|
||||
(i.e. sending [LocationChecks](<network%20protocol.md#locationchecks>)
|
||||
then receiving [ReceivedItems](<network%20protocol.md#receiveditems>)
|
||||
) and directly giving the item to the player, or by doing the AP server roundtrip regardless. I'll call these
|
||||
"locally implemented" items and "remotely implemented" items.
|
||||
- Locally implementing items requires the AP client to know what the locally placed items were without asking an AP
|
||||
server (or else you'd effectively be doing remote items with extra steps). Typically, it gets that information from
|
||||
a patch file, which is one reason why games that already need a patch file are more likely to choose local items.
|
||||
- If items are remotely implemented, the AP client can use [location scouts](<network%20protocol.md#LocationScouts>)
|
||||
to learn what items are placed on what locations. Features that require this information are sometimes mistakenly
|
||||
assumed to require locally implemented items, but location scouts work just as well as patch file data.
|
||||
- [The `items_handling` bitflags in the Connect packet](<network%20protocol.md#items_handling-flags>).
|
||||
AP clients with remotely implemented items will typically set all three flags, including "from your own world".
|
||||
Clients with locally implemented items might set only the "from other worlds" flag.
|
||||
- Whether a local items client sets the "starting inventory" flag likely depends on other details. For example, if a ROM
|
||||
is being patched, and starting inventory can be added to that patch, then it makes sense to leave the flag unset.
|
||||
|
||||
When people talk about "local vs remote items" as a choice that world devs have to make, they mean deciding whether
|
||||
your client will locally or remotely implement the items which happen to be locally placed (or make both
|
||||
implementations, or let the player choose an implementation).
|
||||
|
||||
Theoretically, the biggest benefit of "local items" is that it allows a solo (single slot) multiworld to be played
|
||||
entirely offline, with no AP server, from start to finish. This is similar to a "standalone"/non-AP randomizer,
|
||||
except that you still get AP's player options, generation, etc. for free.
|
||||
For some games, there are also technical constraints that make certain items easier to implement locally,
|
||||
or less glitchy when implemented locally, as long as you're okay with never allowing these items to be placed remotely
|
||||
(or offering the player even more options).
|
||||
|
||||
The main downside (besides more implementation work) is that "local items" can't support "same slot co-op".
|
||||
That's when two players on two different machines connect to the same slot and play together.
|
||||
This only works if both players receive all the items for that slot, including ones found by the other player,
|
||||
which requires those items to be implemented remotely so the AP server can send them to all of that slot's clients.
|
||||
|
||||
So to recap:
|
||||
|
||||
- (All) remote items is often the simplest choice, since you have to implement remote items anyway.
|
||||
- Remote items enable same slot co-op.
|
||||
- Local items enable solo offline play.
|
||||
- If you want to support both solo offline play and same slot co-op,
|
||||
you might need to expose local vs remote items as an option to the player.
|
||||
|
||||
@@ -16,11 +16,11 @@ game contributions:
|
||||
* **Do not introduce unit test failures/regressions.**
|
||||
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
||||
your changes. Currently, the oldest supported version
|
||||
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
|
||||
is [Python 3.11](https://www.python.org/downloads/release/python-31113/).
|
||||
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
||||
pushing.
|
||||
You can turn them on here:
|
||||

|
||||

|
||||
|
||||
* **When reviewing PRs, please leave a message about what was done.**
|
||||
We don't have full test coverage, so manual testing can help.
|
||||
|
||||
92
docs/deploy using containers.md
Normal file
92
docs/deploy using containers.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Deploy Using Containers
|
||||
|
||||
If you just want to play and there is a compiled version available on the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases), use that version.
|
||||
To build the full Archipelago software stack, refer to [Running From Source](running%20from%20source.md).
|
||||
Follow these steps to build and deploy a containerized instance of the web host software, optionally integrating [Gunicorn](https://gunicorn.org/) WSGI HTTP Server running behind the [nginx](https://nginx.org/) reverse proxy.
|
||||
|
||||
|
||||
## Building the Container Image
|
||||
|
||||
What you'll need:
|
||||
* A container runtime engine such as:
|
||||
* [Docker](https://www.docker.com/) (Version 23.0 or later)
|
||||
* [Podman](https://podman.io/) (version 4.0 or later)
|
||||
* For running with rootless podman, you need to ensure all ports used are usable rootless, by default ports less than 1024 are root only. See [the official tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md) for details.
|
||||
* The Docker Buildx plugin (for Docker), as the Dockerfile uses `$TARGETARCH` for architecture detection. Follow [Docker's guide](https://docs.docker.com/build/buildx/install/). Verify with `docker buildx version`.
|
||||
|
||||
Starting from the root repository directory, the standalone Archipelago image can be built and run with the command:
|
||||
`docker build -t archipelago .`
|
||||
Or:
|
||||
`podman build -t archipelago .`
|
||||
|
||||
It is recommended to tag the image using `-t` to more easily identify the image and run it.
|
||||
|
||||
|
||||
## Running the Container
|
||||
|
||||
Running the container can be performed using:
|
||||
`docker run --network host archipelago`
|
||||
Or:
|
||||
`podman run --network host archipelago`
|
||||
|
||||
The Archipelago web host requires access to multiple ports in order to host game servers simultaneously. To simplify configuration for this purpose, specify `--network host`.
|
||||
|
||||
Given the default configuration, the website will be accessible at the hostname/IP address (localhost if run locally) of the machine being deployed to, at port 80. It can be configured by creating a YAML file and mapping a volume to the container when running initially:
|
||||
`docker run archipelago --network host -v /path/to/config.yaml:/app/config.yaml`
|
||||
See `docs/webhost configuration sample.yaml` for example.
|
||||
|
||||
|
||||
## Using Docker Compose
|
||||
|
||||
An example [docker compose](../deploy/docker-compose.yml) file can be found in [deploy](../deploy), along with example configuration files used by the services it orchestrates. Using these files as-is will spin up two separate archipelago containers with special modifications to their runtime arguments, in addition to deploying an `nginx` reverse proxy container.
|
||||
|
||||
To deploy in this manner, from the ["deploy"](../deploy) directory, run:
|
||||
`docker compose up -d`
|
||||
|
||||
### Services
|
||||
|
||||
The `docker-compose.yaml` file defines three services:
|
||||
* multiworld:
|
||||
* Executes the main `WebHost` process, using the [example config](../deploy/example_config.yaml), and overriding with a secondary [selflaunch example config](../deploy/example_selflaunch.yaml). This is because we do not want to launch the website through this service.
|
||||
* web:
|
||||
* Executes `gunicorn` using its [example config](../deploy/example_gunicorn.conf.py), which will bind it to the `WebHost` application, in effect launching it.
|
||||
* We mount the main [config](../deploy/example_config.yaml) without an override to specify that we are launching the website through this service.
|
||||
* No ports are exposed through to the host.
|
||||
* nginx:
|
||||
* Serves as a reverse proxy with `web` as its upstream.
|
||||
* Directs all HTTP traffic from port 80 to the upstream service.
|
||||
* Exposed to the host on port 8080. This is where we can reach the website.
|
||||
|
||||
### Configuration
|
||||
|
||||
As these are examples, they can be copied and modified. For instance setting the value of `HOST_ADDRESS` in [example config](../deploy/example_config.yaml) to host machines local IP address, will expose the service to its local area network.
|
||||
|
||||
The configuration files may be modified to handle for machine-specific optimizations, such as:
|
||||
* Web pages responding too slowly
|
||||
* Edit [the gunicorn config](../deploy/example_gunicorn.conf.py) to increase thread and/or worker count.
|
||||
* Game generation stalls
|
||||
* Increase the generator count in [selflaunch config](../deploy/example_selflaunch.yaml)
|
||||
* Gameplay lags
|
||||
* Increase the hoster count in [selflaunch config](../deploy/example_selflaunch.yaml)
|
||||
|
||||
Changes made to `docker-compose.yaml` can be applied by running `docker compose up -d`, while those made to other files are applied by running `docker compose restart`.
|
||||
|
||||
|
||||
## Windows
|
||||
|
||||
It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
error if it is required.
|
||||
Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory:
|
||||
`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`.
|
||||
Enemizer is not currently available for `aarch64`.
|
||||
|
||||
|
||||
## Optional: Git
|
||||
|
||||
Building the image requires a local copy of the ArchipelagoMW source code.
|
||||
Refer to [Running From Source](running%20from%20source.md#optional-git).
|
||||
@@ -352,14 +352,14 @@ direction_matching_group_lookup = {
|
||||
|
||||
Terrain matching or dungeon shuffle:
|
||||
```python
|
||||
def randomize_within_same_group(group: int) -> List[int]:
|
||||
def randomize_within_same_group(group: int) -> list[int]:
|
||||
return [group]
|
||||
identity_group_lookup = bake_target_group_lookup(world, randomize_within_same_group)
|
||||
```
|
||||
|
||||
Directional + area shuffle:
|
||||
```python
|
||||
def get_target_groups(group: int) -> List[int]:
|
||||
def get_target_groups(group: int) -> list[int]:
|
||||
# example group: LEFT | CAVE
|
||||
# example result: [RIGHT | CAVE, DOOR | CAVE]
|
||||
direction = group & Groups.DIRECTION_MASK
|
||||
|
||||
@@ -117,14 +117,6 @@ flowchart LR
|
||||
%% Java Based Games
|
||||
subgraph Java
|
||||
JM[Mod with Archipelago.MultiClient.Java]
|
||||
STS[Slay the Spire]
|
||||
JM <-- Mod the Spire --> STS
|
||||
subgraph Minecraft
|
||||
MCS[Minecraft Forge Server]
|
||||
JMC[Any Java Minecraft Clients]
|
||||
MCS <-- TCP --> JMC
|
||||
end
|
||||
JM <-- Forge Mod Loader --> MCS
|
||||
end
|
||||
AS <-- WebSockets --> JM
|
||||
|
||||
@@ -133,10 +125,8 @@ flowchart LR
|
||||
NM[Mod with Archipelago.MultiClient.Net]
|
||||
subgraph FNA/XNA
|
||||
TS[Timespinner]
|
||||
RL[Rogue Legacy]
|
||||
end
|
||||
NM <-- TsRandomizer --> TS
|
||||
NM <-- RogueLegacyRandomizer --> RL
|
||||
subgraph Unity
|
||||
ROR[Risk of Rain 2]
|
||||
SN[Subnautica]
|
||||
@@ -185,4 +175,4 @@ flowchart LR
|
||||
FMOD <--> FMAPI
|
||||
end
|
||||
CC <-- Integrated --> FC
|
||||
```
|
||||
```
|
||||
|
||||
@@ -79,7 +79,7 @@ Sent to clients when they connect to an Archipelago server.
|
||||
| generator_version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which generated the multiworld. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| password | bool | Denoted whether a password is required to join this room. |
|
||||
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
|
||||
| permissions | dict\[str, [Permission](#Permission)\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
|
||||
| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. |
|
||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. |
|
||||
| games | list\[str\] | List of games present in this multiworld. |
|
||||
@@ -225,17 +225,17 @@ Sent to clients after a client requested this message be sent to them, more info
|
||||
| games | list\[str\] | Optional. Game names this message is targeting |
|
||||
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
|
||||
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
|
||||
| data | dict | The data in the [Bounce](#Bounce) package copied |
|
||||
| data | dict | Optional. The data in the [Bounce](#Bounce) package copied |
|
||||
|
||||
### InvalidPacket
|
||||
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
|
||||
| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
|
||||
| text | str | A descriptive message of the problem at hand. |
|
||||
| Name | Type | Notes |
|
||||
| ---- |-------------| ----- |
|
||||
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
|
||||
| original_cmd | str \| None | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
|
||||
| text | str | A descriptive message of the problem at hand. |
|
||||
|
||||
##### PacketProblemType
|
||||
`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future.
|
||||
@@ -276,6 +276,7 @@ These packets are sent purely from client to server. They are not accepted by cl
|
||||
* [Sync](#Sync)
|
||||
* [LocationChecks](#LocationChecks)
|
||||
* [LocationScouts](#LocationScouts)
|
||||
* [CreateHints](#CreateHints)
|
||||
* [UpdateHint](#UpdateHint)
|
||||
* [StatusUpdate](#StatusUpdate)
|
||||
* [Say](#Say)
|
||||
@@ -294,7 +295,7 @@ Sent by the client to initiate a connection to an Archipelago game session.
|
||||
| password | str | If the game session requires a password, it should be passed here. |
|
||||
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
|
||||
| name | str | The player name for this client. |
|
||||
| uuid | str | Unique identifier for player client. |
|
||||
| uuid | str | Unique identifier for player. Cached in the user cache \Archipelago\Cache\common.json |
|
||||
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
|
||||
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
|
||||
@@ -339,7 +340,8 @@ Sent to the server to retrieve the items that are on a specified list of locatio
|
||||
Fully remote clients without a patch file may use this to "place" items onto their in-game locations, most commonly to display their names or item classifications before/upon pickup.
|
||||
|
||||
LocationScouts can also be used to inform the server of locations the client has seen, but not checked. This creates a hint as if the player had run `!hint_location` on a location, but without deducting hint points.
|
||||
This is useful in cases where an item appears in the game world, such as 'ledge items' in _A Link to the Past_. To do this, set the `create_as_hint` parameter to a non-zero value.
|
||||
This is useful in cases where an item appears in the game world, such as 'ledge items' in _A Link to the Past_. To do this, set the `create_as_hint` parameter to a non-zero value.
|
||||
Note that LocationScouts with a non-zero `create_as_hint` value will _always_ create a **persistent** hint (listed in the Hints tab of concerning players' TextClients), even if the location was already found. If this is not desired behavior, you need to prevent sending LocationScouts with `create_as_hint` for already found locations in your client-side code.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
@@ -347,6 +349,21 @@ This is useful in cases where an item appears in the game world, such as 'ledge
|
||||
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
||||
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
|
||||
|
||||
### CreateHints
|
||||
|
||||
Sent to the server to create hints for a specified list of locations.
|
||||
Hints that already exist will be silently skipped and their status will not be updated.
|
||||
|
||||
When creating hints for another slot's locations, the packet will fail if any of those locations don't contain items for the requesting slot.
|
||||
When creating hints for your own slot's locations, non-existing locations will silently be skipped.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| locations | list\[int\] | The ids of the locations to create hints for. |
|
||||
| player | int | The ID of the player whose locations are being hinted for. Defaults to the requesting slot. |
|
||||
| status | [HintStatus](#HintStatus) | If included, sets the status of the hint to this status. Defaults to `HINT_UNSPECIFIED`. Cannot set `HINT_FOUND`. |
|
||||
|
||||
### UpdateHint
|
||||
Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails.
|
||||
|
||||
@@ -408,7 +425,7 @@ the server will forward the message to all those targets to which any one requir
|
||||
| games | list\[str\] | Optional. Game names that should receive this message |
|
||||
| slots | list\[int\] | Optional. Player IDs that should receive this message |
|
||||
| tags | list\[str\] | Optional. Client tags that should receive this message |
|
||||
| data | dict | Any data you want to send |
|
||||
| data | dict | Optional. Any data you want to send |
|
||||
|
||||
### Get
|
||||
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
|
||||
@@ -470,7 +487,7 @@ The following operations can be applied to a datastorage key
|
||||
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
||||
| remove | List only: removes the first instance of `value` found in the list. |
|
||||
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
|
||||
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. |
|
||||
| update | List or Dict: Adds the elements of `value` to the container if they weren't already present. In the case of a Dict, already present keys will have their corresponding values updated. |
|
||||
|
||||
### SetNotify
|
||||
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
||||
@@ -551,14 +568,14 @@ In JSON this may look like:
|
||||
Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet.
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Optional
|
||||
from typing import TypedDict
|
||||
class JSONMessagePart(TypedDict):
|
||||
type: Optional[str]
|
||||
text: Optional[str]
|
||||
color: Optional[str] # only available if type is a color
|
||||
flags: Optional[int] # only available if type is an item_id or item_name
|
||||
player: Optional[int] # only available if type is either item or location
|
||||
hint_status: Optional[HintStatus] # only available if type is hint_status
|
||||
type: str | None
|
||||
text: str | None
|
||||
color: str | None # only available if type is a color
|
||||
flags: int | None # only available if type is an item_id or item_name
|
||||
player: int | None # only available if type is either item or location
|
||||
hint_status: HintStatus | None # only available if type is hint_status
|
||||
```
|
||||
|
||||
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
|
||||
@@ -630,6 +647,16 @@ class Version(NamedTuple):
|
||||
build: int
|
||||
```
|
||||
|
||||
If constructing version information as a dict for a custom client rather than as a NamedTuple built into the CommonClient, you must add the `class` key to allow Archipelago to compare version support.
|
||||
```
|
||||
"version": {
|
||||
"class": "Version",
|
||||
"build": X,
|
||||
"major": Y,
|
||||
"minor": Z
|
||||
}
|
||||
```
|
||||
|
||||
### SlotType
|
||||
An enum representing the nature of a slot.
|
||||
|
||||
@@ -645,13 +672,14 @@ class SlotType(enum.IntFlag):
|
||||
An object representing static information about a slot.
|
||||
|
||||
```python
|
||||
import typing
|
||||
from collections.abc import Sequence
|
||||
from typing import NamedTuple
|
||||
from NetUtils import SlotType
|
||||
class NetworkSlot(typing.NamedTuple):
|
||||
class NetworkSlot(NamedTuple):
|
||||
name: str
|
||||
game: str
|
||||
type: SlotType
|
||||
group_members: typing.List[int] = [] # only populated if type == group
|
||||
group_members: Sequence[int] = [] # only populated if type == group
|
||||
```
|
||||
|
||||
### Permission
|
||||
@@ -669,8 +697,8 @@ class Permission(enum.IntEnum):
|
||||
### Hint
|
||||
An object representing a Hint.
|
||||
```python
|
||||
import typing
|
||||
class Hint(typing.NamedTuple):
|
||||
from typing import NamedTuple
|
||||
class Hint(NamedTuple):
|
||||
receiving_player: int
|
||||
finding_player: int
|
||||
location: int
|
||||
@@ -756,8 +784,8 @@ Tags are represented as a list of strings, the common client tags follow:
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
|
||||
@@ -269,7 +269,8 @@ placed on them.
|
||||
|
||||
### PriorityLocations
|
||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
||||
the pool.
|
||||
the pool. Progression items without a deprioritized flag will be used first when filling priority_locations. Progression items with
|
||||
a deprioritized flag will be used next.
|
||||
|
||||
### ItemLinks
|
||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||
@@ -333,7 +334,7 @@ within the world.
|
||||
### TextChoice
|
||||
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
|
||||
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
|
||||
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
|
||||
class or within world, if necessary. Value for this class is `str | int` so if you need the value at a specified
|
||||
point, `self.options.my_option.current_key` will always return a string.
|
||||
|
||||
### PlandoBosses
|
||||
@@ -344,7 +345,7 @@ names, and `def can_place_boss`, which passes a boss and location, allowing you
|
||||
your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is
|
||||
also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False
|
||||
by default, and will reject duplicate boss names from the user. For an example of using this class, refer to
|
||||
`worlds.alttp.options.py`
|
||||
`worlds/alttp/Options.py`
|
||||
|
||||
### OptionDict
|
||||
This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the
|
||||
@@ -352,8 +353,15 @@ template. If you set a [Schema](https://pypi.org/project/schema/) on the class w
|
||||
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
|
||||
format.
|
||||
|
||||
### OptionCounter
|
||||
This is a special case of OptionDict where the dictionary values can only be integers.
|
||||
It returns a [collections.Counter](https://docs.python.org/3/library/collections.html#collections.Counter).
|
||||
This means that if you access a key that isn't present, its value will be 0.
|
||||
The upside of using an OptionCounter (instead of an OptionDict with integer values) is that an OptionCounter can be
|
||||
displayed on the Options page on WebHost.
|
||||
|
||||
### ItemDict
|
||||
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world.
|
||||
An OptionCounter that will verify that every key in the dictionary is a valid name for an item for your world.
|
||||
|
||||
### OptionList
|
||||
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You
|
||||
|
||||
482
docs/rule builder.md
Normal file
482
docs/rule builder.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Rule Builder
|
||||
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
|
||||
|
||||
- Rule classes that avoid all the common pitfalls
|
||||
- Logic optimization
|
||||
- Automatic result caching (opt-in)
|
||||
- Serialization/deserialization
|
||||
- Human-readable logic explanations for players
|
||||
|
||||
## Overview
|
||||
|
||||
The rule builder consists of 3 main parts:
|
||||
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
|
||||
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
|
||||
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
|
||||
## Usage
|
||||
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
|
||||
```python
|
||||
# In your world's create_regions method
|
||||
location = MyWorldLocation(...)
|
||||
self.set_rule(location, Has("A Big Gun"))
|
||||
```
|
||||
|
||||
The rule builder comes with a number of rules by default:
|
||||
|
||||
- `True_`: Always returns true
|
||||
- `False_`: Always returns false
|
||||
- `And`: Checks that all child rules are true (also provided by `&` operator)
|
||||
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
|
||||
- `Has`: Checks that the player has the given item with the given count (default 1)
|
||||
- `HasAll`: Checks that the player has all given items
|
||||
- `HasAny`: Checks that the player has at least one of the given items
|
||||
- `HasAllCounts`: Checks that the player has all of the counts for the given items
|
||||
- `HasAnyCount`: Checks that the player has any of the counts for the given items
|
||||
- `HasFromList`: Checks that the player has some number of given items
|
||||
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
|
||||
- `HasGroup`: Checks that the player has some number of items from a given item group
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
|
||||
- `CanReachLocation`: Checks that the player can logically reach the given location
|
||||
- `CanReachRegion`: Checks that the player can logically reach the given region
|
||||
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
|
||||
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
|
||||
```python
|
||||
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
|
||||
```
|
||||
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
|
||||
|
||||
### Assigning rules
|
||||
|
||||
When assigning the rule you must use the `set_rule` helper to correctly resolve and register the rule.
|
||||
|
||||
```python
|
||||
self.set_rule(location_or_entrance, rule)
|
||||
```
|
||||
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
|
||||
```python
|
||||
self.create_entrance(from_region, to_region, rule)
|
||||
```
|
||||
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
|
||||
You can also set a rule for your world's completion condition:
|
||||
|
||||
```python
|
||||
self.set_completion_rule(rule)
|
||||
```
|
||||
|
||||
### Restricting options
|
||||
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
|
||||
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
|
||||
|
||||
- `eq`: `==`
|
||||
- `ne`: `!=`
|
||||
- `gt`: `>`
|
||||
- `lt`: `<`
|
||||
- `ge`: `>=`
|
||||
- `le`: `<=`
|
||||
- `contains`: `in`
|
||||
|
||||
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
|
||||
|
||||
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
Has("Red switch", options=[OptionFilter(SwitchRando, 1)])
|
||||
| CanReachLocation("Red switch", options=[OptionFilter(SwitchRando, 0)])
|
||||
)
|
||||
```
|
||||
|
||||
To add an extra logic requirement on the easiest difficulty which is ignored for other difficulties:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
# ...the rest of the logic
|
||||
& Has("QoL item", options=[OptionFilter(Difficulty, Difficulty.option_easy)], filtered_resolution=True)
|
||||
)
|
||||
```
|
||||
|
||||
If you would like to provide option filters when reusing or composing rules, you can use the `Filtered` helper rule:
|
||||
|
||||
```python
|
||||
common_rule = Has("A") | HasAny("B", "C")
|
||||
...
|
||||
rule = (
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
|
||||
)
|
||||
```
|
||||
|
||||
You can also use the & and | operators to apply options to rules:
|
||||
|
||||
```python
|
||||
common_rule = Has("A")
|
||||
easy_filter = [OptionFilter(Difficulty, Difficulty.option_easy)]
|
||||
common_rule_only_on_easy = common_rule & easy_filter
|
||||
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||
```
|
||||
|
||||
## Enabling caching
|
||||
|
||||
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
game = "My Game"
|
||||
```
|
||||
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
|
||||
### Item name mapping
|
||||
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
|
||||
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
item_mapping = {
|
||||
"Currency x10": "Currency",
|
||||
"Currency x50": "Currency",
|
||||
"Currency x100": "Currency",
|
||||
"Currency x500": "Currency",
|
||||
}
|
||||
```
|
||||
|
||||
## Defining custom rules
|
||||
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
|
||||
|
||||
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class CanGoal(Rule["MyWorld"], game="My Game"):
|
||||
@override
|
||||
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||
# caching_enabled only needs to be passed in when your world inherits from CachedRuleBuilderWorld
|
||||
return self.Resolved(world.required_mcguffins, player=world.player, caching_enabled=True)
|
||||
|
||||
class Resolved(Rule.Resolved):
|
||||
goal: int
|
||||
|
||||
@override
|
||||
def _evaluate(self, state: CollectionState) -> bool:
|
||||
return state.has("McGuffin", self.player, count=self.goal)
|
||||
|
||||
@override
|
||||
def item_dependencies(self) -> dict[str, set[int]]:
|
||||
# this function is only required if you have caching enabled
|
||||
return {"McGuffin": {id(self)}}
|
||||
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
# this method can be overridden to display custom explanations
|
||||
return [
|
||||
{"type": "text", "text": "Goal with "},
|
||||
{"type": "color", "color": "green" if state and self(state) else "salmon", "text": str(self.goal)},
|
||||
{"type": "text", "text": " McGuffins"},
|
||||
]
|
||||
```
|
||||
|
||||
Your custom rule can also resolve to builtin rules instead of needing to define your own:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
|
||||
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||
if world.some_precalculated_bool:
|
||||
return Has("Item 1").resolve(world)
|
||||
if world.options.some_option:
|
||||
return CanReachRegion("Region 1").resolve(world)
|
||||
return False_().resolve(world)
|
||||
```
|
||||
|
||||
### Item dependencies
|
||||
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
item_name: str
|
||||
|
||||
@override
|
||||
def item_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.item_name: {id(self)}}
|
||||
```
|
||||
|
||||
All of the default `Has*` rules define this function already.
|
||||
|
||||
### Region dependencies
|
||||
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
region_name: str
|
||||
|
||||
@override
|
||||
def region_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.region_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules define this function already.
|
||||
|
||||
### Location dependencies
|
||||
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
location_name: str
|
||||
|
||||
@override
|
||||
def location_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.location_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachLocation` rule defines this function already.
|
||||
|
||||
### Entrance dependencies
|
||||
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
entrance_name: str
|
||||
|
||||
@override
|
||||
def entrance_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.entrance_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachEntrance` rule defines this function already.
|
||||
|
||||
### Rule explanations
|
||||
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
|
||||
|
||||
```python
|
||||
class MyRule(Rule, game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
has_item = state and state.has("growth spurt", self.player)
|
||||
color = "yellow"
|
||||
start = "You must be "
|
||||
if has_item:
|
||||
start = "You are "
|
||||
color = "green"
|
||||
elif state is not None:
|
||||
start = "You are not "
|
||||
color = "salmon"
|
||||
return [
|
||||
{"type": "text", "text": start},
|
||||
{"type": "color", "color": color, "text": "THIS"},
|
||||
{"type": "text", "text": " tall to beat the game"},
|
||||
]
|
||||
|
||||
@override
|
||||
def explain_str(self, state: CollectionState | None = None) -> str:
|
||||
if state is None:
|
||||
return str(self)
|
||||
if state.has("growth spurt", self.player):
|
||||
return "You ARE this tall and can beat the game"
|
||||
return "You are not THIS tall and cannot beat the game"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return "You must be THIS tall to beat the game"
|
||||
```
|
||||
|
||||
### Cache control
|
||||
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
|
||||
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
|
||||
|
||||
### Caveats
|
||||
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
|
||||
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
|
||||
|
||||
## Serialization
|
||||
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
|
||||
|
||||
```python
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"args": {
|
||||
"item_name": "Some item",
|
||||
"count": 1,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
|
||||
|
||||
```python
|
||||
{
|
||||
"rule": "And",
|
||||
"options": [],
|
||||
"children": [
|
||||
..., # each serialized rule
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A full example is as follows:
|
||||
|
||||
```python
|
||||
rule = And(
|
||||
Has("a", options=[OptionFilter(ToggleOption, 0)]),
|
||||
Or(Has("b", count=2), CanReachRegion("c"), options=[OptionFilter(ToggleOption, 1)]),
|
||||
)
|
||||
assert rule.to_dict() == {
|
||||
"rule": "And",
|
||||
"options": [],
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [
|
||||
{
|
||||
"option": "worlds.my_world.options.ToggleOption",
|
||||
"value": 0,
|
||||
"operator": "eq",
|
||||
},
|
||||
],
|
||||
"args": {
|
||||
"item_name": "a",
|
||||
"count": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "Or",
|
||||
"options": [
|
||||
{
|
||||
"option": "worlds.my_world.options.ToggleOption",
|
||||
"value": 1,
|
||||
"operator": "eq",
|
||||
},
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"args": {
|
||||
"item_name": "b",
|
||||
"count": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "CanReachRegion",
|
||||
"options": [],
|
||||
"args": {
|
||||
"region_name": "c",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Custom serialization
|
||||
|
||||
To define a different format for your custom rules, override the `to_dict` function:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
items = ("one", "two")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
# Return whatever format works best for you
|
||||
return {
|
||||
"logic": "basic",
|
||||
"items": self.items,
|
||||
}
|
||||
```
|
||||
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any], world_cls: type[World]) -> Self:
|
||||
items = data.get("items", ())
|
||||
return cls(*items)
|
||||
```
|
||||
|
||||
## APIs
|
||||
|
||||
This section is provided for reference, refer to the above sections for examples.
|
||||
|
||||
### World API
|
||||
|
||||
These are properties and helpers that are available to you in your world.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
|
||||
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
|
||||
|
||||
#### CachedRuleBuilderWorld Properties
|
||||
|
||||
The following property is only available when inheriting from `CachedRuleBuilderWorld`
|
||||
|
||||
- `item_mapping: dict[str, str]`: A mapping of actual item name to logical item name
|
||||
|
||||
### Rule API
|
||||
|
||||
These are properties and helpers that you can use or override for custom rules.
|
||||
|
||||
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
|
||||
- `__str__()`: Basic string representation of a rule, useful for debugging
|
||||
|
||||
#### Resolved rule API
|
||||
|
||||
- `player: int`: The slot this rule is resolved for
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
|
||||
@@ -7,10 +7,9 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.10.11 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [Python 3.11.9 or newer but less than 3.14](https://www.python.org/downloads/), not the Windows Store version
|
||||
* On Windows, please consider only using the latest supported version in production environments since security
|
||||
updates for older versions are not easily available.
|
||||
* Python 3.12.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
* possibly optional, read operating system specific sections
|
||||
@@ -53,6 +52,32 @@ Recommended steps
|
||||
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
|
||||
|
||||
|
||||
## Linux
|
||||
|
||||
If your Linux distribution ships a compatible Python version (see [General](#general)) and pip, you can use that,
|
||||
otherwise you may need to install Python from a 3rd party. Refer to documentation of your Linux distribution.
|
||||
|
||||
Installing a C compiler is usually optional. The package is typically named `gcc`, sometimes another package with the
|
||||
base build tools may be required, i.e. `build-essential` (Debian/Ubuntu) or `base-devel` (Arch).
|
||||
|
||||
After getting the source code, it is strongly recommended to create a
|
||||
[venv](https://docs.python.org/3/tutorial/venv.html) (Virtual Environment)
|
||||
by hand or using an IDE, such as PyCharm, because Archipelago requires specific versions of Python packages.
|
||||
|
||||
Run `python ModuleUpdate.py` in the project root to install packages, run `python Launcher.py` to run the Launcher.
|
||||
|
||||
### Building
|
||||
|
||||
Builds contain (almost) all dependencies to run Archipelago on any Linux distribution that is as new or newer than the
|
||||
one it was built on. Beware that currently only the oldest Ubuntu LTS available in GitHub actions is supported for that.
|
||||
This means the easiest way to generate a build is by running the `Build` action from GitHub actions instead of building
|
||||
locally. If you still want to, e.g. for local testing, you can by running
|
||||
|
||||
`python setup.py build_exe` to generate a binary distribution of Archipelago in `build/`. Or to generate an AppImage
|
||||
first generate the binary distribution and then run `python setup.py bdist_appimage` to populate `dist/`. You need to
|
||||
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
|
||||
@@ -28,7 +28,7 @@ if it does not exist.
|
||||
## Global Settings
|
||||
|
||||
All non-world-specific settings are defined directly in settings.py.
|
||||
Each value needs to have a default. If the default should be `None`, define it as `typing.Optional` and assign `None`.
|
||||
Each value needs to have a default. If the default should be `None`, annotate it using `T | None = None`.
|
||||
|
||||
To access a "global" config value, with correct typing, use one of
|
||||
```python
|
||||
@@ -102,17 +102,16 @@ In worlds, this should only be used for the top level to avoid issues when upgra
|
||||
|
||||
### Bool
|
||||
|
||||
Since `bool` can not be subclassed, use the `settings.Bool` helper in a `typing.Union` to get a comment in host.yaml.
|
||||
Since `bool` can not be subclassed, use the `settings.Bool` helper in a union to get a comment in host.yaml.
|
||||
|
||||
```python
|
||||
import settings
|
||||
import typing
|
||||
|
||||
class MySettings(settings.Group):
|
||||
class MyBool(settings.Bool):
|
||||
"""Doc string"""
|
||||
|
||||
my_value: typing.Union[MyBool, bool] = True
|
||||
my_value: MyBool | bool = True
|
||||
```
|
||||
|
||||
### UserFilePath
|
||||
@@ -134,15 +133,15 @@ Checks the file against [md5s](#md5s) by default.
|
||||
|
||||
Resolves to an executable (varying file extension based on platform)
|
||||
|
||||
#### description: Optional\[str\]
|
||||
#### description: str | None
|
||||
|
||||
Human-readable name to use in file browser
|
||||
|
||||
#### copy_to: Optional\[str\]
|
||||
#### copy_to: str | None
|
||||
|
||||
Instead of storing the path, copy the file.
|
||||
|
||||
#### md5s: List[Union[str, bytes]]
|
||||
#### md5s: list[str | bytes]
|
||||
|
||||
Provide md5 hashes as hex digests or raw bytes for automatic validation.
|
||||
|
||||
@@ -182,10 +181,3 @@ circular / partial imports. Instead, the code should fetch from settings on dema
|
||||
|
||||
"Global" settings are populated immediately, while worlds settings are lazy loaded, so if really necessary,
|
||||
"global" settings could be used in global scope of worlds.
|
||||
|
||||
|
||||
### APWorld Backwards Compatibility
|
||||
|
||||
APWorlds that want to be compatible with both stable and dev versions, have two options:
|
||||
1. use the old Utils.get_options() API until Archipelago 0.4.2 is out
|
||||
2. add some sort of compatibility code to your world that mimics the new API
|
||||
|
||||
18
docs/shared_cache.md
Normal file
18
docs/shared_cache.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Shared Cache
|
||||
|
||||
Archipelago maintains a shared folder of information that can be persisted for a machine and reused across Libraries.
|
||||
It can be found at the User Cache Directory for appname `Archipelago` in the `Cache` subfolder
|
||||
(ex. `%LOCALAPPDATA%/Archipelago/Cache`).
|
||||
|
||||
## Common Cache
|
||||
|
||||
The Common Cache `common.json` can be used to store any generic data that is expected to be shared across programs
|
||||
for the same User.
|
||||
|
||||
* `uuid`: A UUID identifier used to identify clients as from the same user/machine, to be sent in the Connect packet
|
||||
|
||||
## Data Package Cache
|
||||
|
||||
The `datapackage` folder in the shared cache folder is used to store datapackages by game and checksum to be reused
|
||||
in order to save network traffic. The expected structure is `datapackage/Game Name/checksum_value.json` with the
|
||||
contents of each json file being the no-whitespace datapackage contents.
|
||||
@@ -15,8 +15,10 @@
|
||||
* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation,
|
||||
use single quotes inside them: `f"Like {dct['key']}"`
|
||||
* Use type annotations where possible for function signatures and class members.
|
||||
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
|
||||
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
|
||||
* Use type annotations where appropriate for local variables (e.g. `var: list[int] = []`, or when the
|
||||
type is hard or impossible to deduce). Clear annotations help developers look up and validate API calls.
|
||||
* Prefer new style type annotations for new code (e.g. `var: dict[str, str | int]` over
|
||||
`var: Dict[str, Union[str, int]]`).
|
||||
* If a line ends with an open bracket/brace/parentheses, the matching closing bracket should be at the
|
||||
beginning of a line at the same indentation as the beginning of the line with the open bracket.
|
||||
```python
|
||||
@@ -29,6 +31,10 @@
|
||||
* New classes, attributes, and methods in core code should have docstrings that follow
|
||||
[reST style](https://peps.python.org/pep-0287/).
|
||||
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
|
||||
* [Match statements](https://docs.python.org/3/tutorial/controlflow.html#tut-match)
|
||||
may be used instead of `if`-`elif` if they result in nicer code, or they actually use pattern matching.
|
||||
Beware of the performance: they are not `goto`s, but `if`-`elif` under the hood, and you may have less control. When
|
||||
in doubt, just don't use it.
|
||||
|
||||
## Markdown
|
||||
|
||||
@@ -41,18 +47,30 @@
|
||||
|
||||
## HTML
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* Indent with 4 spaces for new code.
|
||||
* kebab-case for ids and classes.
|
||||
* Avoid using on* attributes (onclick, etc.).
|
||||
|
||||
## CSS
|
||||
## CSS / SCSS
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* Indent with 4 spaces for new code.
|
||||
* `{` on the same line as the selector.
|
||||
* No space between selector and `{`.
|
||||
* Space between selector and `{`.
|
||||
|
||||
## JS
|
||||
|
||||
* Indent with 2 spaces.
|
||||
* Indent `case` inside `switch ` with 2 spaces.
|
||||
* Use single quotes.
|
||||
* Indent with 4 spaces.
|
||||
* Indent `case` inside `switch ` with 4 spaces.
|
||||
* Prefer double quotation marks (`"`).
|
||||
* Semicolons are required after every statement.
|
||||
* Use [IIFEs](https://developer.mozilla.org/docs/Glossary/IIFE) to avoid polluting global scope.
|
||||
* Prefer to use [defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer)
|
||||
in script tags, which retains order of execution but does not block.
|
||||
* Avoid `<script async ...` in most cases, see [async and defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#async_and_defer).
|
||||
* Use addEventListener.
|
||||
|
||||
## KV
|
||||
|
||||
* Style should be defined in `.kv` as much as possible, only Python when unavailable.
|
||||
* Should follow [our Python style](#python-code) where appropriate (quotation marks, indentation).
|
||||
* When escaping a line break, add a space between code and backslash.
|
||||
|
||||
@@ -11,8 +11,13 @@ found in the [general test directory](/test/general).
|
||||
## Defining World Tests
|
||||
|
||||
In order to run tests from your world, you will need to create a `test` package within your world package. This can be
|
||||
done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base
|
||||
for your world tests can be created in this file that you can then import into other modules.
|
||||
done by creating a `test` directory inside your world with an (empty) `__init__.py` inside it. By convention, a base
|
||||
for your world tests can be created in `bases.py` or any file that does not start with `test`, that you can then import
|
||||
into other modules. All tests should be defined in files named `test_*.py` (all lower case) and be member functions
|
||||
(named `test_*`) of classes (named `Test*` or `*Test`) that inherit from `unittest.TestCase` or a test base.
|
||||
|
||||
Defining anything inside `test/__init__.py` is deprecated. Defining TestBase there was previously the norm; however,
|
||||
it complicates test discovery because some worlds also put actual tests into `__init__.py`.
|
||||
|
||||
### WorldTestBase
|
||||
|
||||
@@ -21,7 +26,7 @@ interactions in the world interact as expected, you will want to use the [WorldT
|
||||
comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying
|
||||
options combinations.
|
||||
|
||||
Example `/worlds/<my_game>/test/__init__.py`:
|
||||
Example `/worlds/<my_game>/test/bases.py`:
|
||||
|
||||
```python
|
||||
from test.bases import WorldTestBase
|
||||
@@ -49,7 +54,7 @@ with `test_`.
|
||||
Example `/worlds/<my_game>/test/test_chest_access.py`:
|
||||
|
||||
```python
|
||||
from . import MyGameTestBase
|
||||
from .bases import MyGameTestBase
|
||||
|
||||
|
||||
class TestChestAccess(MyGameTestBase):
|
||||
@@ -77,10 +82,10 @@ overridden. For more information on what methods are available to your class, ch
|
||||
|
||||
#### Alternatives to WorldTestBase
|
||||
|
||||
Unit tests can also be created using [TestBase](/test/bases.py#L16) or
|
||||
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
|
||||
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
|
||||
testing portions of your code that can be tested without relying on a multiworld to be created first.
|
||||
Unit tests can also be created using
|
||||
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) directly. These may be useful
|
||||
for generating a multiworld under very specific constraints without using the generic world setup, or for testing
|
||||
portions of your code that can be tested without relying on a multiworld to be created first.
|
||||
|
||||
#### Parametrization
|
||||
|
||||
@@ -97,8 +102,7 @@ for multiple inputs) the base test. Some important things to consider when attem
|
||||
|
||||
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
|
||||
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
|
||||
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly
|
||||
or setting `WorldTestBase.run_default_tests` to False.
|
||||
extra CPU time. Consider using `unittest.TestCase` directly or setting `WorldTestBase.run_default_tests` to False.
|
||||
|
||||
#### Performance Considerations
|
||||
|
||||
@@ -119,8 +123,12 @@ variable to keep all the benefits of the test framework while not running the ma
|
||||
#### Using Pycharm
|
||||
|
||||
In PyCharm, running all tests can be done by right-clicking the root test directory and selecting Run 'Archipelago Unittests'.
|
||||
Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this, edit the run configuration,
|
||||
and set the working directory to the Archipelago directory which contains all the project files.
|
||||
If you have never previously run ModuleUpdate.py, then you will need to do this once before the tests will run.
|
||||
You can run ModuleUpdate.py by right-clicking ModuleUpdate.py and selecting `Run 'ModuleUpdate'`.
|
||||
After running ModuleUpdate.py you may still get a `ModuleNotFoundError: No module named 'flask'` for the webhost tests.
|
||||
If this happens, run WebHost.py by right-clicking it and selecting `Run 'WebHost'`. Make sure to press enter when prompted.
|
||||
Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this,
|
||||
edit the run configuration, and set the working directory to the Archipelago directory which contains all the project files.
|
||||
|
||||
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
|
||||
Your working directory should be the directory of your world in the worlds directory and the script should be the
|
||||
|
||||
620
docs/webhost api.md
Normal file
620
docs/webhost api.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# API Guide
|
||||
|
||||
Archipelago has a rudimentary API that can be queried by endpoints. The API is a work-in-progress and should be improved over time.
|
||||
|
||||
The following API requests are formatted as: `https://<Archipelago URL>/api/<endpoint>`
|
||||
|
||||
The returned data will be formatted in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
||||
|
||||
Current endpoints:
|
||||
- Datapackage API
|
||||
- [`/datapackage`](#datapackage)
|
||||
- [`/datapackage/<string:checksum>`](#datapackagestringchecksum)
|
||||
- [`/datapackage_checksum`](#datapackagechecksum)
|
||||
- Generation API
|
||||
- [`/generate`](#generate)
|
||||
- [`/status/<suuid:seed>`](#status)
|
||||
- Room API
|
||||
- [`/room_status/<suuid:room_id>`](#roomstatus)
|
||||
- Tracker API
|
||||
- [`/tracker/<suuid:tracker>`](#tracker)
|
||||
- [`/static_tracker/<suuid:tracker>`](#statictracker)
|
||||
- [`/slot_data_tracker/<suuid:tracker>`](#slotdatatracker)
|
||||
- User API
|
||||
- [`/get_rooms`](#getrooms)
|
||||
- [`/get_seeds`](#getseeds)
|
||||
|
||||
## API Data Caching
|
||||
To reduce the strain on an Archipelago WebHost, many API endpoints will cache their data and only poll new data in timed intervals. Each endpoint has their own caching time related to the type of data being served. More dynamic data is refreshed more frequently, while static data is cached for longer.
|
||||
Each API endpoint will have their "Cache timer" listed under their definition (if any).
|
||||
API calls to these endpoints should not be faster than the listed timer. This will result in wasted processing for your client and (more importantly) the Archipelago WebHost, as the data will not be refreshed by the WebHost until the internal timer has elapsed.
|
||||
|
||||
|
||||
## Datapackage Endpoints
|
||||
These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations.
|
||||
|
||||
### `/datapackage`
|
||||
<a name="datapackage"></a>
|
||||
|
||||
Fetches the current datapackage from the WebHost.
|
||||
**Cache timer: None**
|
||||
|
||||
You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago.
|
||||
Each game will have:
|
||||
- A checksum `checksum`
|
||||
- A dict of item groups `item_name_groups`
|
||||
- Item name to AP ID dict `item_name_to_id`
|
||||
- A dict of location groups `location_name_groups`
|
||||
- Location name to AP ID dict `location_name_to_id`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"games": {
|
||||
...
|
||||
"Clique": {
|
||||
"checksum": "0271f7a80b44ba72187f92815c2bc8669cb464c7",
|
||||
"item_name_groups": {
|
||||
"Everything": [
|
||||
"A Cool Filler Item (No Satisfaction Guaranteed)",
|
||||
"Button Activation",
|
||||
"Feeling of Satisfaction"
|
||||
]
|
||||
},
|
||||
"item_name_to_id": {
|
||||
"A Cool Filler Item (No Satisfaction Guaranteed)": 69696967,
|
||||
"Button Activation": 69696968,
|
||||
"Feeling of Satisfaction": 69696969
|
||||
},
|
||||
"location_name_groups": {
|
||||
"Everywhere": [
|
||||
"The Big Red Button",
|
||||
"The Item on the Desk"
|
||||
]
|
||||
},
|
||||
"location_name_to_id": {
|
||||
"The Big Red Button": 69696969,
|
||||
"The Item on the Desk": 69696968
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `/datapackage/<string:checksum>`
|
||||
<a name="datapackagestringchecksum"></a>
|
||||
|
||||
Fetches a single datapackage by checksum.
|
||||
**Cache timer: None**
|
||||
|
||||
Returns a dict of the game's data with:
|
||||
- A checksum `checksum`
|
||||
- A dict of item groups `item_name_groups`
|
||||
- Item name to AP ID dict `item_name_to_id`
|
||||
- A dict of location groups `location_name_groups`
|
||||
- Location name to AP ID dict `location_name_to_id`
|
||||
|
||||
Its format will be identical to the whole-datapackage endpoint (`/datapackage`), except you'll only be returned the single game's data in a dict.
|
||||
|
||||
### `/datapackage_checksum`
|
||||
<a name="datapackagechecksum"></a>
|
||||
|
||||
Fetches the checksums of the current static datapackages on the WebHost.
|
||||
**Cache timer: None**
|
||||
|
||||
You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games.
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
...
|
||||
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
||||
"Factorio":"a699194a9589db3ebc0d821915864b422c782f44",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Generation Endpoint
|
||||
These endpoints are used internally for the WebHost to generate games and validate their generation. They are also used by external applications to generate games automatically.
|
||||
|
||||
### `/generate`
|
||||
<a name="generate"></a>
|
||||
Submits a game to the WebHost for generation.
|
||||
**This endpoint only accepts a POST HTTP request.**
|
||||
**Cache timer: None**
|
||||
|
||||
There are two ways to submit data for generation: With a file and with JSON.
|
||||
|
||||
#### With a file:
|
||||
Have your ZIP of yaml(s) or a single yaml, and submit a POST request to the `/generate` endpoint.
|
||||
If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response))
|
||||
|
||||
Example using the python requests library:
|
||||
```python
|
||||
file = {'file': open('Games.zip', 'rb')}
|
||||
req = requests.post("https://archipelago.gg/api/generate", files=file)
|
||||
```
|
||||
|
||||
#### With JSON:
|
||||
Compile your weights/yaml data into a dict. Then insert that into a dict with the key `"weights"`.
|
||||
Finally, submit a POST request to the `/generate` endpoint.
|
||||
If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response))
|
||||
|
||||
Example using the python requests library:
|
||||
```python
|
||||
data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},}
|
||||
weights={"weights": data}
|
||||
req = requests.post("https://archipelago.gg/api/generate", json=weights)
|
||||
```
|
||||
|
||||
#### Generation Response:
|
||||
##### Successful Generation:
|
||||
Upon successful generation, you'll be sent a JSON dict response detailing the generation:
|
||||
- The UUID of the generation `detail`
|
||||
- The SUUID of the generation `encoded`
|
||||
- The response text `text`
|
||||
- The page that will resolve to the seed/room generation page once generation has completed `url`
|
||||
- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status))
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"detail": "19878f16-5a58-4b76-aab7-d6bf38be9463",
|
||||
"encoded": "GYePFlpYS3aqt9a_OL6UYw",
|
||||
"text": "Generation of seed 19878f16-5a58-4b76-aab7-d6bf38be9463 started successfully.",
|
||||
"url": "http://archipelago.gg/wait/GYePFlpYS3aqt9a_OL6UYw",
|
||||
"wait_api_url": "http://archipelago.gg/api/status/GYePFlpYS3aqt9a_OL6UYw"
|
||||
}
|
||||
```
|
||||
|
||||
##### Failed Generation:
|
||||
|
||||
Upon failed generation, you'll be returned a single key-value pair. The key will always be `text`
|
||||
The value will give you a hint as to what may have gone wrong.
|
||||
- Options without tags, and a 400 status code
|
||||
- Options in a string, and a 400 status code
|
||||
- Invalid file/weight string, `No options found. Expected file attachment or json weights.` with a 400 status code
|
||||
- Too many slots for the server to process, `Max size of multiworld exceeded` with a 409 status code
|
||||
|
||||
If the generation detects a issue in generation, you'll be sent a dict with two key-value pairs (`text` and `detail`) and a 400 status code. The values will be:
|
||||
- Summary of issue in `text`
|
||||
- Detailed issue in `detail`
|
||||
|
||||
In the event of an unhandled server exception, you'll be provided a dict with a single key `text`:
|
||||
- Exception, `Uncaught Exception: <error>` with a 500 status code
|
||||
|
||||
### `/status/<suuid:seed>`
|
||||
<a name="status"></a>
|
||||
Retrieves the status of the seed's generation.
|
||||
**Cache timer: None**
|
||||
|
||||
This endpoint will return a dict with a single key-value pair. The key will always be `text`
|
||||
The value will tell you the status of the generation:
|
||||
- Generation was completed: `Generation done` with a 201 status code
|
||||
- Generation request was not found: `Generation not found` with a 404 status code
|
||||
- Generation of the seed failed: `Generation failed` with a 500 status code
|
||||
- Generation is in progress still: `Generation running` with a 202 status code
|
||||
|
||||
## Room Endpoints
|
||||
Endpoints to fetch information of the active WebHost room with the supplied room_ID.
|
||||
|
||||
### `/room_status/<suuid:room_id>`
|
||||
<a name="roomstatus"></a>
|
||||
**Cache timer: None**
|
||||
|
||||
Will provide a dict of room data with the following keys:
|
||||
- Tracker SUUID (`tracker`)
|
||||
- A list of players (`players`)
|
||||
- Each item containing a list with the Slot name and Game
|
||||
- Last known hosted port (`last_port`)
|
||||
- Last activity timestamp (`last_activity`)
|
||||
- The room timeout counter (`timeout`)
|
||||
- A list of downloads for files required for gameplay (`downloads`)
|
||||
- Each item is a dict containing the download URL and slot (`slot`, `download`)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"downloads": [
|
||||
{
|
||||
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/1",
|
||||
"slot": 1
|
||||
},
|
||||
{
|
||||
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/2",
|
||||
"slot": 2
|
||||
},
|
||||
{
|
||||
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/3",
|
||||
"slot": 3
|
||||
},
|
||||
{
|
||||
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/4",
|
||||
"slot": 4
|
||||
},
|
||||
{
|
||||
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/5",
|
||||
"slot": 5
|
||||
}
|
||||
],
|
||||
"last_activity": "Fri, 18 Apr 2025 20:35:45 GMT",
|
||||
"last_port": 52122,
|
||||
"players": [
|
||||
[
|
||||
"Slot_Name_1",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_2",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_3",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_4",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_5",
|
||||
"Ocarina of Time"
|
||||
]
|
||||
],
|
||||
"timeout": 7200,
|
||||
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||
}
|
||||
```
|
||||
|
||||
## Tracker Endpoints
|
||||
Endpoints to fetch information regarding players of an active WebHost room with the supplied tracker_ID. The tracker ID
|
||||
can either be viewed while on a room tracker page, or from the [room's endpoint](#room-endpoints).
|
||||
|
||||
### `/tracker/<suuid:tracker>`
|
||||
<a name=tracker></a>
|
||||
**Cache timer: 60 seconds**
|
||||
|
||||
Will provide a dict of tracker data with the following keys:
|
||||
|
||||
- A list of players current alias data (`aliases`)
|
||||
- Each item containing a dict with, their alias `alias`, their player number `player`, and their team `team`
|
||||
- `alias` will return `null` if there is no alias set
|
||||
- A list of items each player has received as a [NetworkItem](network%20protocol.md#networkitem) (`player_items_received`)
|
||||
- Each item containing a dict with, a list of NetworkItems `items`, their player number `player`, their team `team`
|
||||
- A list of checks done by each player as a list of the location id's (`player_checks_done`)
|
||||
- Each item containing a dict with, a list of checked location id's `locations`, their player number `player`, and their team `team`
|
||||
- A list of the total number of checks done by all players (`total_checks_done`)
|
||||
- Each item will contain a dict with, the total checks done `checks_done`, and the team `team`
|
||||
- A list of [Hints](network%20protocol.md#hint) data that players have used or received (`hints`)
|
||||
- Each item containing a dict containing, a list of hint data `hints`, the player number `player`, and their team `team`
|
||||
- A list containing the last activity time for each player, formatted in RFC 1123 format (`activity_timers`)
|
||||
- Each item containing, last activity time `time`, their player number `player`, and their team `team`
|
||||
- A list containing the last connection time for each player, formatted in RFC 1123 format (`connection_timers`)
|
||||
- Each item containing, the time of their last connection `time`, their player number `player`, and their team `team`
|
||||
- A list of the current [ClientStatus](network%20protocol.md#clientstatus) of each player (`player_status`)
|
||||
- Each item will contain, their status `status`, their player number `player`, and their team `team`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"alias": "Incompetence"
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"alias": "Slot_Name_2"
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 3,
|
||||
"alias": null
|
||||
},
|
||||
],
|
||||
"player_items_received": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"items": [
|
||||
[1, 1, 1, 0],
|
||||
[2, 2, 2, 1]
|
||||
]
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"items": [
|
||||
[1, 1, 1, 2],
|
||||
[2, 2, 2, 0]
|
||||
]
|
||||
}
|
||||
],
|
||||
"player_checks_done": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"locations": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"locations": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_checks_done": [
|
||||
{
|
||||
"team": 0,
|
||||
"checks_done": 4
|
||||
}
|
||||
],
|
||||
"hints": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"hints": [
|
||||
[1, 2, 4, 6, 0, "", 4, 0]
|
||||
]
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"hints": []
|
||||
}
|
||||
],
|
||||
"activity_timers": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"time": "Fri, 18 Apr 2025 20:35:45 GMT"
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"time": "Fri, 18 Apr 2025 20:42:46 GMT"
|
||||
}
|
||||
],
|
||||
"connection_timers": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"time": "Fri, 18 Apr 2025 20:38:25 GMT"
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"time": "Fri, 18 Apr 2025 21:03:00 GMT"
|
||||
}
|
||||
],
|
||||
"player_status": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"status": 0
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"status": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `/static_tracker/<suuid:tracker>`
|
||||
<a name=statictracker></a>
|
||||
**Cache timer: 300 seconds**
|
||||
|
||||
Will provide a dict of static tracker data with the following keys:
|
||||
|
||||
- A list of item_link groups and their member players (`groups`)
|
||||
- Each item containing a dict with, the slot registering the group `slot`, the item_link name `name`, and a list of members `members`
|
||||
- A dict of datapackage hashes for each game (`datapackage`)
|
||||
- Each item is a named dict of the game's name.
|
||||
- Each game contains two keys, the datapackage's checksum hash `checksum`, and the version `version`
|
||||
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
||||
- A list of number of checks found vs. total checks available per player (`player_locations_total`)
|
||||
- Each list item contains a dict with three keys, the total locations for that slot `total_locations`, their player number `player`, and their team `team`
|
||||
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
||||
- The game each player is playing (`player_game`)
|
||||
- Provided as a list of objects with `team`, `player`, and `game`.
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"slot": 5,
|
||||
"name": "testGroup",
|
||||
"members": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"slot": 6,
|
||||
"name": "myCoolLink",
|
||||
"members": [
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"datapackage": {
|
||||
"Archipelago": {
|
||||
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb"
|
||||
},
|
||||
"The Messenger": {
|
||||
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b"
|
||||
}
|
||||
},
|
||||
"player_locations_total": [
|
||||
{
|
||||
"player": 1,
|
||||
"team" : 0,
|
||||
"total_locations": 10
|
||||
},
|
||||
{
|
||||
"player": 2,
|
||||
"team" : 0,
|
||||
"total_locations": 20
|
||||
}
|
||||
],
|
||||
"player_game": [
|
||||
{
|
||||
"team": 0,
|
||||
"player": 1,
|
||||
"game": "Archipelago"
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"game": "The Messenger"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `/slot_data_tracker/<suuid:tracker>`
|
||||
<a name=slotdatatracker></a>
|
||||
Will provide a list of each player's slot_data.
|
||||
**Cache timer: 300 seconds**
|
||||
|
||||
Each list item will contain a dict with the player's data:
|
||||
- player slot number `player`
|
||||
- A named dict `slot_data` containing any set slot data for that player
|
||||
|
||||
Example:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"player": 1,
|
||||
"slot_data": {
|
||||
"example_option": 1,
|
||||
"other_option": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"player": 2,
|
||||
"slot_data": {
|
||||
"example_option": 1,
|
||||
"other_option": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## User Endpoints
|
||||
User endpoints can get room and seed details from the current session tokens (cookies)
|
||||
|
||||
### `/get_rooms`
|
||||
<a name="getrooms"></a>
|
||||
Retreives a list of all rooms currently owned by the session token.
|
||||
**Cache timer: None**
|
||||
|
||||
Each list item will contain a dict with the room's details:
|
||||
- Room SUUID (`room_id`)
|
||||
- Seed SUUID (`seed_id`)
|
||||
- Creation timestamp (`creation_time`)
|
||||
- Last activity timestamp (`last_activity`)
|
||||
- Last known AP port (`last_port`)
|
||||
- Room timeout counter in seconds (`timeout`)
|
||||
- Room tracker SUUID (`tracker`)
|
||||
|
||||
Example:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 19:46:53 GMT",
|
||||
"last_activity": "Fri, 18 Apr 2025 21:16:02 GMT",
|
||||
"last_port": 52122,
|
||||
"room_id": "0D30FgQaRcWivFsw9o8qzw",
|
||||
"seed_id": "TFjiarBgTsCj5-Jbe8u33A",
|
||||
"timeout": 7200,
|
||||
"tracker": "52BycvJhRe6knrYH8v4bag"
|
||||
},
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 20:36:42 GMT",
|
||||
"last_activity": "Fri, 18 Apr 2025 20:36:46 GMT",
|
||||
"last_port": 56884,
|
||||
"room_id": "LMCFchESSNyuqcY3GxkhwA",
|
||||
"seed_id": "CENtJMXCTGmkIYCzjB5Csg",
|
||||
"timeout": 7200,
|
||||
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `/get_seeds`
|
||||
<a name="getseeds"></a>
|
||||
Retreives a list of all seeds currently owned by the session token.
|
||||
**Cache timer: None**
|
||||
|
||||
Each item in the list will contain a dict with the seed's details:
|
||||
- Seed SUUID (`seed_id`)
|
||||
- Creation timestamp (`creation_time`)
|
||||
- A list of player slots (`players`)
|
||||
- Each item in the list will contain a list of the slot name and game
|
||||
|
||||
Example:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 19:46:52 GMT",
|
||||
"players": [
|
||||
[
|
||||
"Slot_Name_1",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_2",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_3",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_4",
|
||||
"Ocarina of Time"
|
||||
],
|
||||
[
|
||||
"Slot_Name_5",
|
||||
"Ocarina of Time"
|
||||
]
|
||||
],
|
||||
"seed_id": "CENtJMXCTGmkIYCzjB5Csg"
|
||||
},
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 20:36:39 GMT",
|
||||
"players": [
|
||||
[
|
||||
"Slot_Name_1",
|
||||
"Clique"
|
||||
],
|
||||
[
|
||||
"Slot_Name_2",
|
||||
"Clique"
|
||||
],
|
||||
[
|
||||
"Slot_Name_3",
|
||||
"Clique"
|
||||
],
|
||||
[
|
||||
"Slot_Name_4",
|
||||
"Archipelago"
|
||||
]
|
||||
],
|
||||
"seed_id": "TFjiarBgTsCj5-Jbe8u33A"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -76,8 +76,8 @@ webhost:
|
||||
* `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'.
|
||||
|
||||
* `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
|
||||
* `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.
|
||||
|
||||
@@ -225,7 +225,10 @@ and has a classification. The name needs to be unique within each game and must
|
||||
letter or symbol). The ID needs to be unique across all locations within the game.
|
||||
Locations and items can share IDs, and locations can share IDs with other games' locations.
|
||||
|
||||
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
|
||||
World-specific IDs **must** be in the range 1 to 2<sup>53</sup>-1 (the largest integer that is "[safe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER#description)"
|
||||
to store in a 64-bit float, and thus all popular programming languages can handle). IDs ≤ 0 are global and reserved.
|
||||
It's **recommended** to keep your IDs in the range 1 to 2<sup>31</sup>-1,
|
||||
so only 32-bit integers are needed to hold your IDs.
|
||||
|
||||
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
||||
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||
@@ -257,31 +260,14 @@ another flag like "progression", it means "an especially useful progression item
|
||||
combined with `progression`; see below)
|
||||
* `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that
|
||||
will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres
|
||||
|
||||
### Events
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import ItemClassification
|
||||
from .subclasses import MyGameLocation, MyGameItem
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
victory_loc = MyGameLocation(self.player, "Victory", None)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
```
|
||||
* `deprioritized`: denotes that an item should not be placed on priority locations
|
||||
(to be combined with `progression`; see below)
|
||||
* `progression_deprioritized`: the combination of `progression` and `deprioritized`, i.e. a progression item that
|
||||
should not be placed on priority locations, despite being progression;
|
||||
like skip_balancing, this is commonly used for currency or tokens.
|
||||
* `progression_deprioritized_skip_balancing`: the combination of `progression`, `deprioritized` and `skip_balancing`.
|
||||
Since there is overlap between the kind of items that want `skip_balancing` and `deprioritized`,
|
||||
this combined classification exists for convenience
|
||||
|
||||
### Regions
|
||||
|
||||
@@ -291,7 +277,7 @@ like entrance randomization in logic.
|
||||
|
||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L310-L311)),
|
||||
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
|
||||
|
||||
### Entrances
|
||||
@@ -339,6 +325,63 @@ avoiding the need for indirect conditions at the expense of performance.
|
||||
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.
|
||||
|
||||
### Events (or "generation-only items/locations")
|
||||
|
||||
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
|
||||
Event locations can never be checked by the player, and event items cannot be received during play.
|
||||
|
||||
Events are used to represent in-game actions (that aren't regular Archipelago locations) when either:
|
||||
|
||||
* We want to show in the spoiler log when the player is expected to perform the in-game action.
|
||||
* It's the cleanest way to represent how that in-game action impacts logic.
|
||||
|
||||
Typical examples include completing the goal, defeating a boss, or flipping a switch that affects multiple areas.
|
||||
|
||||
To be precise: the term "event" on its own refers to the special combination of an "event item" placed on an "event
|
||||
location". Event items and locations are created the same way as normal items and locations, except that they have an
|
||||
`id` of `None`, and an event item must be placed on an event location
|
||||
(and vice versa). Finally, although events are often described as "fake" items and locations, it's important to
|
||||
understand that they are perfectly real during generation.
|
||||
|
||||
The most common way to create an event is to create the event item and the event location, then immediately call
|
||||
`Location.place_locked_item()`:
|
||||
|
||||
```python
|
||||
victory_loc = MyGameLocation(self.player, "Defeat the Final Boss", None, final_boss_arena_region)
|
||||
victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
set_rule(victory_loc, lambda state: state.has("Boss Defeating Sword", self.player))
|
||||
```
|
||||
|
||||
Requiring an event to finish the game will make the spoiler log display an additional
|
||||
`Defeat the Final Boss: Victory` line when the player is expected to finish, rather than only showing their last
|
||||
relevant item. But events aren't just about the spoiler log; a more substantial example of using events to structure
|
||||
your logic might be:
|
||||
|
||||
```python
|
||||
water_loc = MyGameLocation(self.player, "Water Level Switch", None, pump_station_region)
|
||||
water_loc.place_locked_item(MyGameItem("Lowered Water Level", ItemClassification.progression, None, self.player))
|
||||
pump_station_region.locations.append(water_loc)
|
||||
set_rule(water_loc, lambda state: state.has("Double Jump", self.player)) # the switch is really high up
|
||||
...
|
||||
basement_loc = MyGameLocation(self.player, "Flooded House - Basement Chest", None, flooded_house_region)
|
||||
flooded_house_region.locations += [upstairs_loc, ground_floor_loc, basement_loc]
|
||||
...
|
||||
set_rule(basement_loc, lambda state: state.has("Lowered Water Level", self.player))
|
||||
```
|
||||
|
||||
This creates a "Lowered Water Level" event and a regular location whose access rule depends on that
|
||||
event being reachable. If you made several more locations the same way, this would ensure all of those locations can
|
||||
only become reachable when the event location is reachable (i.e. when the water level can be lowered), without
|
||||
copy-pasting the event location's access rule and then repeatedly re-evaluating it. Also, the spoiler log will show
|
||||
`Water Level Switch: Lowered Water Level` when the player is expected to do this.
|
||||
|
||||
To be clear, this example could also be modeled with a second Region (perhaps "Un-Flooded House"). Or you could modify
|
||||
the game so flipping that switch checks a regular AP location in addition to lowering the water level.
|
||||
Events are never required, but it may be cleaner to use an event if e.g. flipping that switch affects the logic in
|
||||
dozens of half-flooded areas that would all otherwise need additional Regions, and you don't want it to be a regular
|
||||
location. It depends on the game.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Your World
|
||||
@@ -483,13 +526,14 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
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.
|
||||
You cannot modify `local_items`, or `non_local_items` after this step.
|
||||
* `create_regions(self)`
|
||||
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.
|
||||
* `create_items(self)`
|
||||
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
|
||||
after this step. Locations cannot be moved to different regions after this step.
|
||||
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions after
|
||||
this step. Locations cannot be moved to different regions after this step. This includes event items and locations.
|
||||
* `set_rules(self)`
|
||||
called to set access and item rules on locations and entrances.
|
||||
* `connect_entrances(self)`
|
||||
@@ -501,12 +545,12 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
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`.
|
||||
returned from `get_pre_fill_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.
|
||||
* `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that
|
||||
* `fill_slot_data(self)` and `modify_multidata(self, multidata: MultiData)` can be used to modify the data that
|
||||
will be used by the server to host the MultiWorld.
|
||||
|
||||
All instance methods can, optionally, have a class method defined which will be called after all instance methods are
|
||||
@@ -561,7 +605,7 @@ from .items import is_progression # this is just a dummy
|
||||
|
||||
|
||||
def create_item(self, item: str) -> MyGameItem:
|
||||
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
|
||||
# this is called when AP wants to create an item by name (for plando, start inventory, item links) 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)
|
||||
|
||||
@@ -579,17 +623,10 @@ def create_items(self) -> None:
|
||||
# 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 options, e.g. custom win condition like triforce hunt.
|
||||
# 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.
|
||||
|
||||
# List of items to exclude, as a copy since it will be destroyed below
|
||||
exclude = [item for item in self.multiworld.precollected_items[self.player]]
|
||||
# If you want to do that, use start_inventory_from_pool
|
||||
|
||||
for item in map(self.create_item, mygame_items):
|
||||
if item in exclude:
|
||||
exclude.remove(item) # this is destructive. create unique list above
|
||||
self.multiworld.itempool.append(self.create_item("nothing"))
|
||||
else:
|
||||
self.multiworld.itempool.append(item)
|
||||
self.multiworld.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.
|
||||
@@ -606,8 +643,8 @@ from .items import get_item_type
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# For some worlds this step can be omitted if either a Logic mixin
|
||||
# (see below) is used, it's easier to apply the rules from data during
|
||||
# location generation or everything is in generate_basic
|
||||
# (see below) is used or it's easier to apply the rules from data during
|
||||
# location generation
|
||||
|
||||
# set a simple rule for an region
|
||||
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
||||
@@ -719,7 +756,7 @@ from BaseClasses import CollectionState, MultiWorld
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
class MyGameState(LogicMixin):
|
||||
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
|
||||
mygame_defeatable_enemies: dict[int, set[str]] # per player
|
||||
|
||||
def init_mixin(self, multiworld: MultiWorld) -> None:
|
||||
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
|
||||
@@ -733,6 +770,7 @@ class MyGameState(LogicMixin):
|
||||
new_state.mygame_defeatable_enemies = {
|
||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
||||
}
|
||||
return new_state
|
||||
```
|
||||
|
||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
||||
@@ -848,11 +886,11 @@ item/location pairs is unnecessary since the AP server already retains and freel
|
||||
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
|
||||
|
||||
```python
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
def fill_slot_data(self) -> dict[str, Any]:
|
||||
# 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
|
||||
# The options dataclass has a method to return a `dict[str, Any]` of each option name provided and the relevant
|
||||
# option's value.
|
||||
return self.options.as_dict("difficulty", "final_boss_hp")
|
||||
```
|
||||
|
||||
@@ -65,5 +65,5 @@ date, voting members and final result in the commit message.
|
||||
|
||||
## Handling of Unmaintained Worlds
|
||||
|
||||
As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be
|
||||
moved from `worlds/` to `worlds_disabled/`.
|
||||
As long as worlds are known to work for the most part, they can stay included. Once the world becomes broken, it shall
|
||||
be deleted.
|
||||
|
||||
Reference in New Issue
Block a user