Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
else:
logger.info("Progression balancing skipped.")

AutoWorld.call_all(multiworld, "finalize_multiworld")
AutoWorld.call_all(multiworld, "pre_output")

# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
multiworld.random.passthrough = False

Expand Down
7 changes: 7 additions & 0 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,13 @@ def __new__(cls, name, bases, attrs):
commands.update(base.commands)
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
command_name.startswith("_cmd_")})
for command_name, method in commands.items():
# wrap async def functions so they run on default asyncio loop
if inspect.iscoroutinefunction(method):
def _wrapper(self, *args, _method=method, **kwargs):
return async_start(_method(self, *args, **kwargs))
functools.update_wrapper(_wrapper, method)
commands[command_name] = _wrapper
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)


Expand Down
1 change: 1 addition & 0 deletions test/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ def fulfills_accessibility() -> bool:
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
Expand Down
1 change: 1 addition & 0 deletions test/general/test_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def test_postgen_datapackage(self):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,
Expand Down
3 changes: 3 additions & 0 deletions test/general/test_implemented.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def test_slot_data(self):
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
call_all(multiworld, "pre_output")
for key, data in multiworld.worlds[1].fill_slot_data().items():
self.assertIsInstance(key, str, "keys in slot data must be a string")
convert_to_base_types(data) # only put base data types into slot data
Expand Down Expand Up @@ -93,6 +95,7 @@ def test_explicit_indirect_conditions_spheres(self):
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")

# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
# is nondeterministic and may vary between runs with the same seed.
Expand Down
1 change: 1 addition & 0 deletions test/general/test_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def setup_link_multiworld(world: Type[World], link_replace: bool) -> None:
call_all(multiworld, "pre_fill")
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")

for game_name, world_type in AutoWorldRegister.world_types.items():
Expand Down
2 changes: 2 additions & 0 deletions test/multiworld/test_multiworlds.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def test_fills(self) -> None:
with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")


Expand All @@ -78,4 +79,5 @@ def test_two_player_single_game_fills(self) -> None:
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
17 changes: 17 additions & 0 deletions worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,23 @@ def post_fill(self) -> None:
This happens before progression balancing, so the items may not be in their final locations yet.
"""

def finalize_multiworld(self) -> None:
"""
Optional Method that is called after fill and progression balancing.
This is the last stage of generation where worlds may change logically relevant data,
such as item placements and connections. To not break assumptions,
only ever increase accessibility, never decrease it.
"""
pass

def pre_output(self):
"""
Optional method that is called before output generation.
Items and connections are not meant to be moved anymore,
anything that would affect logical spheres is forbidden at this point.
"""
pass

def generate_output(self, output_directory: str) -> None:
"""
This method gets called from a threadpool, do not use multiworld.random here.
Expand Down
Loading