Core: move option results to the World class instead of MultiWorld (#993)
🤞 * map option objects to a `World.options` dict * convert RoR2 to options dict system for testing * add temp behavior for lttp with notes * copy/paste bad * convert `set_default_common_options` to a namespace property * reorganize test call order * have fill_restrictive use the new options system * update world api * update soe tests * fix world api * core: auto initialize a dataclass on the World class with the option results * core: auto initialize a dataclass on the World class with the option results: small tying improvement * add `as_dict` method to the options dataclass * fix namespace issues with tests * have current option updates use `.value` instead of changing the option * update ror2 to use the new options system again * revert the junk pool dict since it's cased differently * fix begin_with_loop typo * write new and old options to spoiler * change factorio option behavior back * fix comparisons * move common and per_game_common options to new system * core: automatically create missing options_dataclass from legacy option_definitions * remove spoiler special casing and add back the Factorio option changing but in new system * give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly * reimplement `inspect.get_annotations` * move option info generation for webhost to new system * need to include Common and PerGame common since __annotations__ doesn't include super * use get_type_hints for the options dictionary * typing.get_type_hints returns the bases too. * forgot to sweep through generate * sweep through all the tests * swap to a metaclass property * move remaining usages from get_type_hints to metaclass property * move remaining usages from __annotations__ to metaclass property * move remaining usages from legacy dictionaries to metaclass property * remove legacy dictionaries * cache the metaclass property * clarify inheritance in world api * move the messenger to new options system * add an assert for my dumb * update the doc * rename o to options * missed a spot * update new messenger options * comment spacing Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * fix tests * fix missing import * make the documentation definition more accurate * use options system for loc creation * type cast MessengerWorld * fix typo and use quotes for cast * LTTP: set random seed in tests * ArchipIdle: remove change here as it's default on AutoWorld * Stardew: Need to set state because `set_default_common_options` used to * The Messenger: update shop rando and helpers to new system; optimize imports * Add a kwarg to `as_dict` to do the casing for you * RoR2: use new kwarg for less code * RoR2: revert some accidental reverts * The Messenger: remove an unnecessary variable * remove TypeVar that isn't used * CommonOptions not abstract * Docs: fix mistake in options api.md Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> * create options for item link worlds * revert accidental doc removals * Item Links: set default options on group * change Zillion to new options dataclass * remove unused parameter to function * use TypeGuard for Literal narrowing * move dlc quest to new api * move overcooked 2 to new api * fixed some missed code in oc2 * - Tried to be compliant with 993 (WIP?) * - I think it all works now * - Removed last trace of me touching core * typo * It now passes all tests! * Improve options, fix all issues I hope * - Fixed init options * dlcquest: fix bad imports * missed a file * - Reduce code duplication * add as_dict documentation * - Use .items(), get option name more directly, fix slot data content * - Remove generic options from the slot data * improve slot data documentation * remove `CommonOptions.get_value` (#21) * better slot data description Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: Doug Hoskisson <beauxq@yahoo.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
This commit is contained in:
@@ -28,19 +28,23 @@ Choice, and defining `alias_true = option_full`.
|
||||
and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||
|
||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
|
||||
create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
|
||||
options:
|
||||
create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
|
||||
|
||||
```python
|
||||
# Options.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Toggle, PerGameCommonOptions
|
||||
|
||||
|
||||
class StartingSword(Toggle):
|
||||
"""Adds a sword to your starting inventory."""
|
||||
display_name = "Start With Sword"
|
||||
|
||||
|
||||
example_options = {
|
||||
"starting_sword": StartingSword
|
||||
}
|
||||
@dataclass
|
||||
class ExampleGameOptions(PerGameCommonOptions):
|
||||
starting_sword: StartingSword
|
||||
```
|
||||
|
||||
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
|
||||
@@ -48,27 +52,30 @@ to our world's `__init__.py`:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
from .Options import options
|
||||
from .Options import ExampleGameOptions
|
||||
|
||||
|
||||
class ExampleWorld(World):
|
||||
option_definitions = options
|
||||
# this gives the generator all the definitions for our options
|
||||
options_dataclass = ExampleGameOptions
|
||||
# this gives us typing hints for all the options we defined
|
||||
options: ExampleGameOptions
|
||||
```
|
||||
|
||||
### Option Checking
|
||||
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
||||
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
||||
`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to
|
||||
`self.options.my_option_name`. This is an instance of the option class, which supports direct comparison methods to
|
||||
relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is
|
||||
the option class's `value` attribute. For our example above we can do a simple check:
|
||||
```python
|
||||
if self.multiworld.starting_sword[self.player]:
|
||||
if self.options.starting_sword:
|
||||
do_some_things()
|
||||
```
|
||||
|
||||
or if I need a boolean object, such as in my slot_data I can access it as:
|
||||
```python
|
||||
start_with_sword = bool(self.multiworld.starting_sword[self.player].value)
|
||||
start_with_sword = bool(self.options.starting_sword.value)
|
||||
```
|
||||
|
||||
## Generic Option Classes
|
||||
@@ -120,7 +127,7 @@ Like Toggle, but 1 (true) is the default value.
|
||||
A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do
|
||||
comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
|
||||
```python
|
||||
if self.multiworld.sword_availability[self.player] == "early_sword":
|
||||
if self.options.sword_availability == "early_sword":
|
||||
do_early_sword_things()
|
||||
```
|
||||
|
||||
@@ -128,7 +135,7 @@ or:
|
||||
```python
|
||||
from .Options import SwordAvailability
|
||||
|
||||
if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword:
|
||||
if self.options.sword_availability == SwordAvailability.option_early_sword:
|
||||
do_early_sword_things()
|
||||
```
|
||||
|
||||
@@ -160,7 +167,7 @@ within the world.
|
||||
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
|
||||
point, `self.multiworld.my_option[self.player].current_key` will always return a string.
|
||||
point, `self.options.my_option.current_key` will always return a string.
|
||||
|
||||
### PlandoBosses
|
||||
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports
|
||||
|
||||
@@ -86,9 +86,11 @@ inside a `World` object.
|
||||
### Player Options
|
||||
|
||||
Players provide customized settings for their World in the form of yamls.
|
||||
Those are accessible through `self.multiworld.<option_name>[self.player]`. A dict
|
||||
of valid options has to be provided in `self.option_definitions`. Options are automatically
|
||||
added to the `World` object for easy access.
|
||||
A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`.
|
||||
(It must be a subclass of `PerGameCommonOptions`.)
|
||||
Option results are automatically added to the `World` object for easy access.
|
||||
Those are accessible through `self.options.<option_name>`, and you can get a dictionary of the option values via
|
||||
`self.options.as_dict(<option_names>)`, passing the desired options as strings.
|
||||
|
||||
### World Settings
|
||||
|
||||
@@ -221,11 +223,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 mygame_options` from your `__init__.py` will load
|
||||
`worlds/<world_name>/Options.py` and make its `mygame_options` 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.mygame_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
|
||||
@@ -273,8 +275,9 @@ Each option has its own class, inherits from a base option type, has a docstring
|
||||
to describe it and a `display_name` property for display on the website and in
|
||||
spoiler logs.
|
||||
|
||||
The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is
|
||||
assigned to the world under `self.option_definitions`.
|
||||
The actual name as used in the yaml is defined via the field names of a `dataclass` that is
|
||||
assigned to the world under `self.options_dataclass`. By convention, the strings
|
||||
that define your option names should be in `snake_case`.
|
||||
|
||||
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
|
||||
For more see `Options.py` in AP's base directory.
|
||||
@@ -309,8 +312,8 @@ default = 0
|
||||
```python
|
||||
# Options.py
|
||||
|
||||
from Options import Toggle, Range, Choice, Option
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from Options import Toggle, Range, Choice, PerGameCommonOptions
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Sets overall game difficulty."""
|
||||
@@ -333,23 +336,27 @@ class FixXYZGlitch(Toggle):
|
||||
"""Fixes ABC when you do XYZ"""
|
||||
display_name = "Fix XYZ Glitch"
|
||||
|
||||
# By convention we call the options dict variable `<world>_options`.
|
||||
mygame_options: typing.Dict[str, AssembleOptions] = {
|
||||
"difficulty": Difficulty,
|
||||
"final_boss_hp": FinalBossHP,
|
||||
"fix_xyz_glitch": FixXYZGlitch,
|
||||
}
|
||||
# By convention, we call the options dataclass `<world>Options`.
|
||||
# It has to be derived from 'PerGameCommonOptions'.
|
||||
@dataclass
|
||||
class MyGameOptions(PerGameCommonOptions):
|
||||
difficulty: Difficulty
|
||||
final_boss_hp: FinalBossHP
|
||||
fix_xyz_glitch: FixXYZGlitch
|
||||
```
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from worlds.AutoWorld import World
|
||||
from .Options import mygame_options # import the options dict
|
||||
from .Options import MyGameOptions # import the options dataclass
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
#...
|
||||
option_definitions = mygame_options # assign the options dict to the world
|
||||
#...
|
||||
# ...
|
||||
options_dataclass = MyGameOptions # assign the options dataclass to the world
|
||||
options: MyGameOptions # typing for option results
|
||||
# ...
|
||||
```
|
||||
|
||||
### A World Class Skeleton
|
||||
@@ -359,13 +366,14 @@ class MyGameWorld(World):
|
||||
|
||||
import settings
|
||||
import typing
|
||||
from .Options import mygame_options # the options we defined earlier
|
||||
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
|
||||
|
||||
|
||||
|
||||
class MyGameItem(Item): # or from Items import MyGameItem
|
||||
game = "My Game" # name of the game/world this item is from
|
||||
|
||||
@@ -374,6 +382,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
|
||||
game = "My Game" # name of the game/world this location is in
|
||||
|
||||
|
||||
|
||||
class MyGameSettings(settings.Group):
|
||||
class RomFile(settings.SNESRomPath):
|
||||
"""Insert help text for host.yaml here."""
|
||||
@@ -384,7 +393,8 @@ class MyGameSettings(settings.Group):
|
||||
class MyGameWorld(World):
|
||||
"""Insert description of the world/game here."""
|
||||
game = "My Game" # name of the game/world
|
||||
option_definitions = mygame_options # options the player can set
|
||||
options_dataclass = MyGameOptions # options the player can set
|
||||
options: MyGameOptions # typing hints for option results
|
||||
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
|
||||
topology_present = True # show path to required location checks in spoiler
|
||||
|
||||
@@ -460,7 +470,7 @@ In addition, the following methods can be implemented and are called in this ord
|
||||
```python
|
||||
def generate_early(self) -> None:
|
||||
# read player settings to world instance
|
||||
self.final_boss_hp = self.multiworld.final_boss_hp[self.player].value
|
||||
self.final_boss_hp = self.options.final_boss_hp.value
|
||||
```
|
||||
|
||||
#### create_item
|
||||
@@ -687,9 +697,9 @@ def generate_output(self, output_directory: str):
|
||||
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.multiworld.difficulty[self.player].current_key,
|
||||
"difficulty": self.options.difficulty.current_key,
|
||||
# store option value True or False for fixing a glitch
|
||||
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value,
|
||||
"fix_xyz_glitch": self.options.fix_xyz_glitch.value,
|
||||
}
|
||||
# point to a ROM specified by the installation
|
||||
src = self.settings.rom_file
|
||||
@@ -702,6 +712,26 @@ def generate_output(self, output_directory: str):
|
||||
generate_mod(src, out_file, data)
|
||||
```
|
||||
|
||||
### Slot Data
|
||||
|
||||
If the game client needs to know information about the generated seed, a preferred method of transferring the data
|
||||
is through the slot data. This can be filled from the `fill_slot_data` method of your world by returning a `Dict[str, Any]`,
|
||||
but should be limited to data that is absolutely necessary to not waste resources. Slot data is sent to your client once
|
||||
it has successfully [connected](network%20protocol.md#connected).
|
||||
If you need to know information about locations in your world, instead
|
||||
of propagating the slot data, it is preferable to use [LocationScouts](network%20protocol.md#locationscouts) since that
|
||||
data already exists on the server. The most common usage of slot data is to send option results that the client needs
|
||||
to be aware of.
|
||||
|
||||
```python
|
||||
def fill_slot_data(self):
|
||||
# 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 option's value
|
||||
return self.options.as_dict("difficulty", "final_boss_hp")
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading
|
||||
|
||||
Reference in New Issue
Block a user