mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-05 01:38:14 -07:00
Merge branch 'ArchipelagoMW:main' into main
This commit is contained in:
@@ -87,6 +87,9 @@
|
||||
# Inscryption
|
||||
/worlds/inscryption/ @DrBibop @Glowbuzz
|
||||
|
||||
# Jak and Daxter: The Precursor Legacy
|
||||
/worlds/jakanddaxter/ @massimilianodelliubaldini
|
||||
|
||||
# Kirby's Dream Land 3
|
||||
/worlds/kdl3/ @Silvris
|
||||
|
||||
@@ -157,6 +160,9 @@
|
||||
# Saving Princess
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# shapez
|
||||
/worlds/shapez/ @BlastSlimey
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
@@ -175,6 +181,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 +193,6 @@
|
||||
# Secret of Evermore
|
||||
/worlds/soe/ @black-sliver
|
||||
|
||||
# Slay the Spire
|
||||
/worlds/spire/ @KonoTyran
|
||||
|
||||
# Stardew Valley
|
||||
/worlds/stardew_valley/ @agilbert1412
|
||||
|
||||
@@ -232,14 +238,10 @@
|
||||
# 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)
|
||||
@@ -248,15 +250,6 @@
|
||||
# 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 ##
|
||||
###################
|
||||
|
||||
@@ -8,7 +8,11 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
||||
|
||||
### 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 +22,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 +47,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,24 +57,39 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -85,3 +109,34 @@ Common situations where this can happen include:
|
||||
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
|
||||
|
||||
@@ -117,8 +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]
|
||||
|
||||
@@ -231,11 +231,11 @@ Sent to clients after a client requested this message be sent to them, more info
|
||||
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.
|
||||
@@ -551,14 +551,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.
|
||||
|
||||
@@ -333,7 +333,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
|
||||
@@ -352,8 +352,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -119,8 +124,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
|
||||
|
||||
@@ -561,7 +561,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)
|
||||
|
||||
|
||||
@@ -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