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:
Aaron Wagener
2023-10-10 15:30:20 -05:00
committed by GitHub
parent a7b4914bb7
commit 7193182294
69 changed files with 1587 additions and 1603 deletions

View File

@@ -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