diff --git a/docs/apworld specification.md b/docs/apworld specification.md index 98cd25a7..ed2e8b1c 100644 --- a/docs/apworld specification.md +++ b/docs/apworld specification.md @@ -29,6 +29,7 @@ The zip can contain arbitrary files in addition what was specified above. ## Caveats -Imports from other files inside the apworld have to use relative imports. +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. Options.py and worlds/AutoWorld.py. +Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or +`from worlds.AutoWorld import World` diff --git a/docs/options api.md b/docs/options api.md index 2c868338..622d0a7e 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -31,7 +31,7 @@ As an example, suppose we want an option that lets the user start their game wit create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass: ```python -# Options.py +# options.py from dataclasses import dataclass from Options import Toggle, PerGameCommonOptions diff --git a/docs/world api.md b/docs/world api.md index 9b7573dc..67a44c06 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -286,11 +286,11 @@ See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requireme AP will only import the `__init__.py`. Depending on code size it makes sense to use multiple files and use relative imports to access them. -e.g. `from .Options import MyGameOptions` from your `__init__.py` will load -`world/[world_name]/Options.py` and make its `MyGameOptions` accessible. +e.g. `from .options import MyGameOptions` from your `__init__.py` will load +`world/[world_name]/options.py` and make its `MyGameOptions` accessible. -When imported names pile up it may be easier to use `from . import Options` -and access the variable as `Options.MyGameOptions`. +When imported names pile up it may be easier to use `from . import options` +and access the variable as `options.MyGameOptions`. Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is required for zipped worlds to @@ -311,7 +311,7 @@ class MyGameItem(Item): game: str = "My Game" ``` By convention this class definition will either be placed in your `__init__.py` -or your `Items.py`. For a more elaborate example see `worlds/oot/Items.py`. +or your `items.py`. For a more elaborate example see `worlds/oot/Items.py`. ### Your location type @@ -323,15 +323,15 @@ class MyGameLocation(Location): game: str = "My Game" # override constructor to automatically mark event locations as such - def __init__(self, player: int, name = "", code = None, parent = None): + def __init__(self, player: int, name = "", code = None, parent = None) -> None: super(MyGameLocation, self).__init__(player, name, code, parent) self.event = code is None ``` -in your `__init__.py` or your `Locations.py`. +in your `__init__.py` or your `locations.py`. ### Options -By convention options are defined in `Options.py` and will be used when parsing +By convention options are defined in `options.py` and will be used when parsing the players' yaml files. Each option has its own class, inherits from a base option type, has a docstring @@ -347,7 +347,7 @@ For more see `Options.py` in AP's base directory. #### Toggle, DefaultOnToggle -Those don't need any additional properties defined. After parsing the option, +These don't need any additional properties defined. After parsing the option, its `value` will either be True or False. #### Range @@ -373,7 +373,7 @@ default = 0 #### Sample ```python -# Options.py +# options.py from dataclasses import dataclass from Options import Toggle, Range, Choice, PerGameCommonOptions @@ -412,7 +412,7 @@ class MyGameOptions(PerGameCommonOptions): # __init__.py from worlds.AutoWorld import World -from .Options import MyGameOptions # import the options dataclass +from .options import MyGameOptions # import the options dataclass class MyGameWorld(World): @@ -429,9 +429,9 @@ class MyGameWorld(World): import settings import typing -from .Options import MyGameOptions # the options we defined earlier -from .Items import mygame_items # data used below to add items to the World -from .Locations import mygame_locations # same as above +from .options import MyGameOptions # the options we defined earlier +from .items import mygame_items # data used below to add items to the World +from .locations import mygame_locations # same as above from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification @@ -490,7 +490,7 @@ The world has to provide the following things for generation * additions to the regions list: at least one called "Menu" * locations placed inside those regions * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand -* applying `self.multiworld.push_precollected` for start inventory +* applying `self.multiworld.push_precollected` for world defined start inventory * `required_client_version: Tuple[int, int, int]` Optional client version as tuple of 3 ints to make sure the client is compatible to this world (e.g. implements all required features) when connecting. @@ -500,31 +500,32 @@ In addition, the following methods can be implemented and are called in this ord * `stage_assert_generate(cls, multiworld)` is a class method called at the start of generation to check the existence of prerequisite files, usually a ROM for games which require one. -* `def generate_early(self)` - 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. -* `def create_regions(self)` +* `generate_early(self)` + 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 any steps before this, the multiworld itself is still getting set up +* `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. -* `def create_items(self)` +* `create_items(self)` called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterwards. -* `def set_rules(self)` +* `set_rules(self)` called to set access and item rules on locations and entrances. Locations have to be defined before this, or rule application can miss them. -* `def generate_basic(self)` +* `generate_basic(self)` called after the previous steps. Some placement and player specific randomizations can be done here. -* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement +* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` are called to modify item placement before, during and after the regular fill process, before `generate_output`. If items need to be placed during pre_fill, these items can be determined and created using `get_prefill_items` -* `def generate_output(self, output_directory: str)` that creates the output +* `generate_output(self, output_directory: str)` that 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` and `modify_multidata` can be used to modify the data that +* `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that will be used by the server to host the MultiWorld. @@ -541,9 +542,9 @@ def generate_early(self) -> None: ```python # we need a way to know if an item provides progress in the game ("key item") # this can be part of the items definition, or depend on recipe randomization -from .Items import is_progression # this is just a dummy +from .items import is_progression # this is just a dummy -def create_item(self, item: str): +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. classification = ItemClassification.progression if is_progression(item) else \ @@ -551,7 +552,7 @@ def create_item(self, item: str): return MyGameItem(item, classification, self.item_name_to_id[item], self.player) -def create_event(self, event: str): +def create_event(self, event: str) -> MyGameItem: # while we are at it, we can also add a helper to create events return MyGameItem(event, True, None, self.player) ``` @@ -644,7 +645,7 @@ def generate_basic(self) -> None: ```python from worlds.generic.Rules import add_rule, set_rule, forbid_item -from Items import get_item_type +from .items import get_item_type def set_rules(self) -> None: @@ -713,12 +714,12 @@ Please do this with caution and only when necessary. #### Sample ```python -# Logic.py +# logic.py from worlds.AutoWorld import LogicMixin class MyGameLogic(LogicMixin): - def mygame_has_key(self, player: int): + def mygame_has_key(self, player: int) -> bool: # Arguments above are free to choose # MultiWorld can be accessed through self.multiworld, explicitly passing in # MyGameWorld instance for easy options access is also a valid approach @@ -728,11 +729,11 @@ class MyGameLogic(LogicMixin): # __init__.py from worlds.generic.Rules import set_rule -import .Logic # apply the mixin by importing its file +import .logic # apply the mixin by importing its file class MyGameWorld(World): # ... - def set_rules(self): + def set_rules(self) -> None: set_rule(self.multiworld.get_location("A Door", self.player), lambda state: state.mygame_has_key(self.player)) ``` @@ -740,10 +741,10 @@ class MyGameWorld(World): ### Generate Output ```python -from .Mod import generate_mod +from .mod import generate_mod -def generate_output(self, output_directory: str): +def generate_output(self, output_directory: str) -> None: # How to generate the mod or ROM highly depends on the game # if the mod is written in Lua, Jinja can be used to fill a template # if the mod reads a json file, `json.dump()` can be used to generate that @@ -758,12 +759,10 @@ def generate_output(self, output_directory: str): # make sure to mark as not remote_start_inventory when connecting if stored in rom/mod "starter_items": [item.name for item in self.multiworld.precollected_items[self.player]], - "final_boss_hp": self.final_boss_hp, - # store option name "easy", "normal" or "hard" for difficuly - "difficulty": self.options.difficulty.current_key, - # store option value True or False for fixing a glitch - "fix_xyz_glitch": self.options.fix_xyz_glitch.value, } + + # add needed option results to the dictionary + data.update(self.options.as_dict("final_boss_hp", "difficulty", "fix_xyz_glitch")) # point to a ROM specified by the installation src = self.settings.rom_file # or point to worlds/mygame/data/mod_template @@ -787,7 +786,7 @@ data already exists on the server. The most common usage of slot data is to send to be aware of. ```python -def fill_slot_data(self): +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 @@ -839,14 +838,14 @@ from . import MyGameTestBase class TestChestAccess(MyGameTestBase): - def test_sword_chests(self): + def test_sword_chests(self) -> None: """Test locations that require a sword""" locations = ["Chest1", "Chest2"] items = [["Sword"]] # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained. self.assertAccessDependency(locations, items) - def test_any_weapon_chests(self): + def test_any_weapon_chests(self) -> None: """Test locations that require any weapon""" locations = [f"Chest{i}" for i in range(3, 6)] items = [["Sword"], ["Axe"], ["Spear"]]