mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 18:43:23 -07:00
Compare commits
9 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d362fa7997 | ||
|
|
5b7cd09470 | ||
|
|
eeea2bc85d | ||
|
|
2cbe302a49 | ||
|
|
6609d9f138 | ||
|
|
b60d0b8f41 | ||
|
|
6f1a8a30e7 | ||
|
|
ceec51b9e1 | ||
|
|
d3312287a8 |
@@ -692,17 +692,25 @@ class CollectionState():
|
|||||||
|
|
||||||
def update_reachable_regions(self, player: int):
|
def update_reachable_regions(self, player: int):
|
||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
|
world: AutoWorld.World = self.multiworld.worlds[player]
|
||||||
reachable_regions = self.reachable_regions[player]
|
reachable_regions = self.reachable_regions[player]
|
||||||
blocked_connections = self.blocked_connections[player]
|
|
||||||
queue = deque(self.blocked_connections[player])
|
queue = deque(self.blocked_connections[player])
|
||||||
start = self.multiworld.get_region("Menu", player)
|
start: Region = world.get_region(world.origin_region_name)
|
||||||
|
|
||||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||||
if start not in reachable_regions:
|
if start not in reachable_regions:
|
||||||
reachable_regions.add(start)
|
reachable_regions.add(start)
|
||||||
blocked_connections.update(start.exits)
|
self.blocked_connections[player].update(start.exits)
|
||||||
queue.extend(start.exits)
|
queue.extend(start.exits)
|
||||||
|
|
||||||
|
if world.explicit_indirect_conditions:
|
||||||
|
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
|
||||||
|
else:
|
||||||
|
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||||
|
|
||||||
|
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
||||||
|
reachable_regions = self.reachable_regions[player]
|
||||||
|
blocked_connections = self.blocked_connections[player]
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
while queue:
|
while queue:
|
||||||
connection = queue.popleft()
|
connection = queue.popleft()
|
||||||
@@ -722,6 +730,29 @@ class CollectionState():
|
|||||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||||
queue.append(new_entrance)
|
queue.append(new_entrance)
|
||||||
|
|
||||||
|
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
||||||
|
reachable_regions = self.reachable_regions[player]
|
||||||
|
blocked_connections = self.blocked_connections[player]
|
||||||
|
new_connection: bool = True
|
||||||
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
|
while new_connection:
|
||||||
|
new_connection = False
|
||||||
|
while queue:
|
||||||
|
connection = queue.popleft()
|
||||||
|
new_region = connection.connected_region
|
||||||
|
if new_region in reachable_regions:
|
||||||
|
blocked_connections.remove(connection)
|
||||||
|
elif connection.can_reach(self):
|
||||||
|
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||||
|
reachable_regions.add(new_region)
|
||||||
|
blocked_connections.remove(connection)
|
||||||
|
blocked_connections.update(new_region.exits)
|
||||||
|
queue.extend(new_region.exits)
|
||||||
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||||
|
new_connection = True
|
||||||
|
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||||
|
queue.extend(blocked_connections)
|
||||||
|
|
||||||
def copy(self) -> CollectionState:
|
def copy(self) -> CollectionState:
|
||||||
ret = CollectionState(self.multiworld)
|
ret = CollectionState(self.multiworld)
|
||||||
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ like entrance randomization in logic.
|
|||||||
|
|
||||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||||
|
|
||||||
There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to
|
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#L295-L296)),
|
||||||
return to the "Menu" region by resetting the game ("Save and quit").
|
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
|
### Entrances
|
||||||
|
|
||||||
@@ -303,6 +303,34 @@ generation (entrance randomization).
|
|||||||
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
||||||
(items that have been collected).
|
(items that have been collected).
|
||||||
|
|
||||||
|
The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
|
||||||
|
- `def rule(state: CollectionState) -> bool:`
|
||||||
|
- `lambda state: ... boolean expression ...`
|
||||||
|
|
||||||
|
An access rule can be assigned through `set_rule(location, rule)`.
|
||||||
|
|
||||||
|
Access rules usually check for one of two things.
|
||||||
|
- Items that have been collected (e.g. `state.has("Sword", player)`)
|
||||||
|
- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
|
||||||
|
|
||||||
|
Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
|
||||||
|
|
||||||
|
#### An important note on Entrance access rules:
|
||||||
|
When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
|
||||||
|
|
||||||
|
For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
|
||||||
|
This is fine when checking for items using `state.has`, because items do not change during a region sweep.
|
||||||
|
However, `state.can_reach` checks for the very same thing we are updating: Regions.
|
||||||
|
This can lead to non-deterministic behavior and, in the worst case, even generation failures.
|
||||||
|
Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
|
||||||
|
|
||||||
|
**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
|
||||||
|
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||||
|
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||||
|
|
||||||
|
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
|
||||||
|
avoiding the need for indirect conditions at the expense of performance.
|
||||||
|
|
||||||
### Item Rules
|
### Item Rules
|
||||||
|
|
||||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||||
@@ -438,7 +466,7 @@ The world has to provide the following things for generation:
|
|||||||
|
|
||||||
* the properties mentioned above
|
* the properties mentioned above
|
||||||
* additions to the item pool
|
* additions to the item pool
|
||||||
* additions to the regions list: at least one called "Menu"
|
* additions to the regions list: at least one named after the world class's origin_region_name ("Menu" by default)
|
||||||
* locations placed inside those regions
|
* locations placed inside those regions
|
||||||
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||||
* applying `self.multiworld.push_precollected` for world-defined start inventory
|
* applying `self.multiworld.push_precollected` for world-defined start inventory
|
||||||
@@ -491,7 +519,7 @@ def generate_early(self) -> None:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
# Add regions to the multiworld. "Menu" is the required starting point.
|
# Add regions to the multiworld. One of them must use the origin_region_name as its name ("Menu" by default).
|
||||||
# Arguments to Region() are name, player, multiworld, and optionally hint_text
|
# Arguments to Region() are name, player, multiworld, and optionally hint_text
|
||||||
menu_region = Region("Menu", self.player, self.multiworld)
|
menu_region = Region("Menu", self.player, self.multiworld)
|
||||||
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
||||||
@@ -630,7 +658,7 @@ def set_rules(self) -> None:
|
|||||||
|
|
||||||
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
||||||
Entrance should be
|
Entrance should be
|
||||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
|
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
|
||||||
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
||||||
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
||||||
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
||||||
|
|||||||
@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
web: ClassVar[WebWorld] = WebWorld()
|
web: ClassVar[WebWorld] = WebWorld()
|
||||||
"""see WebWorld for options"""
|
"""see WebWorld for options"""
|
||||||
|
|
||||||
|
origin_region_name: str = "Menu"
|
||||||
|
"""Name of the Region from which accessibility is tested."""
|
||||||
|
|
||||||
|
explicit_indirect_conditions: bool = True
|
||||||
|
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
|
||||||
|
If False, everything is rechecked at every step, which is slower computationally,
|
||||||
|
but may be desirable in complex/dynamic worlds."""
|
||||||
|
|
||||||
multiworld: "MultiWorld"
|
multiworld: "MultiWorld"
|
||||||
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
||||||
player: int
|
player: int
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class Factorio(World):
|
|||||||
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
||||||
tech_mix: int = 0
|
tech_mix: int = 0
|
||||||
skip_silo: bool = False
|
skip_silo: bool = False
|
||||||
|
origin_region_name = "Nauvis"
|
||||||
science_locations: typing.List[FactorioScienceLocation]
|
science_locations: typing.List[FactorioScienceLocation]
|
||||||
|
|
||||||
settings: typing.ClassVar[FactorioSettings]
|
settings: typing.ClassVar[FactorioSettings]
|
||||||
@@ -125,9 +126,6 @@ class Factorio(World):
|
|||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
random = self.multiworld.random
|
random = self.multiworld.random
|
||||||
menu = Region("Menu", player, self.multiworld)
|
|
||||||
crash = Entrance(player, "Crash Land", menu)
|
|
||||||
menu.exits.append(crash)
|
|
||||||
nauvis = Region("Nauvis", player, self.multiworld)
|
nauvis = Region("Nauvis", player, self.multiworld)
|
||||||
|
|
||||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||||
@@ -184,8 +182,7 @@ class Factorio(World):
|
|||||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||||
location.place_locked_item(event)
|
location.place_locked_item(event)
|
||||||
|
|
||||||
crash.connect(nauvis)
|
self.multiworld.regions.append(nauvis)
|
||||||
self.multiworld.regions += [menu, nauvis]
|
|
||||||
|
|
||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
player = self.player
|
player = self.player
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SubnauticaWorld(World):
|
|||||||
options_dataclass = options.SubnauticaOptions
|
options_dataclass = options.SubnauticaOptions
|
||||||
options: options.SubnauticaOptions
|
options: options.SubnauticaOptions
|
||||||
required_client_version = (0, 5, 0)
|
required_client_version = (0, 5, 0)
|
||||||
|
origin_region_name = "Planet 4546B"
|
||||||
creatures_to_scan: List[str]
|
creatures_to_scan: List[str]
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
@@ -66,13 +66,9 @@ class SubnauticaWorld(World):
|
|||||||
creature_pool, self.options.creature_scans.value)
|
creature_pool, self.options.creature_scans.value)
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
# Create Regions
|
# Create Region
|
||||||
menu_region = Region("Menu", self.player, self.multiworld)
|
|
||||||
planet_region = Region("Planet 4546B", self.player, self.multiworld)
|
planet_region = Region("Planet 4546B", self.player, self.multiworld)
|
||||||
|
|
||||||
# Link regions together
|
|
||||||
menu_region.connect(planet_region, "Lifepod 5")
|
|
||||||
|
|
||||||
# Create regular locations
|
# Create regular locations
|
||||||
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
|
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
|
||||||
(creature + creatures.suffix for creature in self.creatures_to_scan))
|
(creature + creatures.suffix for creature in self.creatures_to_scan))
|
||||||
@@ -93,11 +89,8 @@ class SubnauticaWorld(World):
|
|||||||
# make the goal event the victory "item"
|
# make the goal event the victory "item"
|
||||||
location.item.name = "Victory"
|
location.item.name = "Victory"
|
||||||
|
|
||||||
# Register regions to multiworld
|
# Register region to multiworld
|
||||||
self.multiworld.regions += [
|
self.multiworld.regions.append(planet_region)
|
||||||
menu_region,
|
|
||||||
planet_region
|
|
||||||
]
|
|
||||||
|
|
||||||
# refer to rules.py
|
# refer to rules.py
|
||||||
set_rules = set_rules
|
set_rules = set_rules
|
||||||
|
|||||||
Reference in New Issue
Block a user