mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
Core: Add settings API ("auto settings") for host.yaml (#1871)
* Add settings API ("auto settings") for host.yaml
* settings: no BOM when saving
* settings: fix saving / groups resetting themselves
* settings: fix AutoWorldRegister import
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
* Lufia2: settings: clean up imports
* settings: more consistent class naming
* Docs: update world api for settings api refactor
* settings: fix access from World instance
* settings: update migration timeline
* Docs: Apply suggestions from code review
Co-authored-by: Zach Parks <zach@alliware.com>
* Settings: correctly resolve .exe in UserPath and LocalPath
---------
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
This commit is contained in:
187
docs/settings api.md
Normal file
187
docs/settings api.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Archipelago Settings API
|
||||
|
||||
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
|
||||
host.yaml. For the player settings / player yamls see [options api.md](options api.md).
|
||||
|
||||
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
|
||||
as well as the predefined `host.yaml` in the repository.
|
||||
|
||||
For backwards compatibility with APWorlds, some interfaces are kept for now and will produce a warning when being used.
|
||||
|
||||
|
||||
## Config File
|
||||
|
||||
Settings use options.yaml (manual override), if that exists, or host.yaml (the default) otherwise.
|
||||
The files are searched for in the current working directory, if different from install directory, and in `user_path`,
|
||||
which either points to the installation directory, if writable, or to %home%/Archipelago otherwise.
|
||||
|
||||
**Examples:**
|
||||
* C:\Program Data\Archipelago\options.yaml
|
||||
* C:\Program Data\Archipelago\host.yaml
|
||||
* path\to\code\repository\host.yaml
|
||||
* ~/Archipelago/host.yaml
|
||||
|
||||
Using the settings API, AP can update the config file or create a new one with default values and comments,
|
||||
if it does not exist.
|
||||
|
||||
|
||||
## Global Settings
|
||||
|
||||
All non-world-specific settings are defined directly in settings.py.
|
||||
Each value needs to have a default. If the default should be `None`, define it as `typing.Optional` and assign `None`.
|
||||
|
||||
To access a "global" config value, with correct typing, use one of
|
||||
```python
|
||||
from settings import get_settings, GeneralOptions, FolderPath
|
||||
from typing import cast
|
||||
|
||||
x = get_settings().general_options.output_path
|
||||
y = cast(GeneralOptions, get_settings()["general_options"]).output_path
|
||||
z = cast(FolderPath, get_settings()["general_options"]["output_path"])
|
||||
```
|
||||
|
||||
|
||||
## World Settings
|
||||
|
||||
Worlds can define the top level key to use by defining `settings_key: ClassVar[str]` in their World class.
|
||||
It defaults to `{folder_name}_options` if undefined, i.e. `worlds/factorio/...` defaults to `factorio_options`.
|
||||
|
||||
Worlds define the layout of their config section using type annotation of the variable `settings` in the class.
|
||||
The type has to inherit from `settings.Group`. Each value in the config can have a comment by subclassing a built-in
|
||||
type. Some helper types are defined in `settings.py`, see [Types](#Types) for a list.```
|
||||
|
||||
Inside the class code, you can then simply use `self.settings.rom_file` to get the value.
|
||||
In case of paths they will automatically be read as absolute file paths. No need to use user_path or local_path.
|
||||
|
||||
```python
|
||||
import settings
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
class MyGameSettings(settings.Group):
|
||||
class RomFile(settings.SNESRomPath):
|
||||
"""Description that is put into host.yaml"""
|
||||
description = "My Game US v1.0 ROM File" # displayed in the file browser
|
||||
copy_to = "MyGame.sfc" # instead of storing the path, copy to AP dir
|
||||
md5s = ["..."]
|
||||
|
||||
rom_file: RomFile = RomFile("MyGame.sfc") # definition and default value
|
||||
|
||||
|
||||
class MyGameWorld(World):
|
||||
...
|
||||
settings: MyGameSettings
|
||||
...
|
||||
|
||||
def something(self):
|
||||
pass # use self.settings.rom_file here
|
||||
```
|
||||
|
||||
|
||||
## Types
|
||||
|
||||
When writing the host.yaml, the code will down cast the values to builtins.
|
||||
When reading the host.yaml, the code will upcast the values to what is defined in the type annotations.
|
||||
E.g. an IntEnum becomes int when saving and will construct the IntEnum when loading.
|
||||
|
||||
Types that can not be down cast to / up cast from a builtin can not be used except for Group, which will be converted
|
||||
to/from a dict.
|
||||
`bool` is a special case, see settings.py: ServerOptions.disable_item_cheat for an example.
|
||||
|
||||
Below are some predefined types that can be used if they match your requirements:
|
||||
|
||||
|
||||
### Group
|
||||
|
||||
A section / dict in the config file. Behaves similar to a dataclass.
|
||||
Type annotation and default assignment define how loading, saving and default values behave.
|
||||
It can be accessed using attributes or as a dict: `group["a"]` is equivalent to `group.a`.
|
||||
|
||||
In worlds, this should only be used for the top level to avoid issues when upgrading/migrating.
|
||||
|
||||
|
||||
### Bool
|
||||
|
||||
Since `bool` can not be subclassed, use the `settings.Bool` helper in a `typing.Union` to get a comment in host.yaml.
|
||||
|
||||
```python
|
||||
import settings
|
||||
import typing
|
||||
|
||||
class MySettings(settings.Group):
|
||||
class MyBool(settings.Bool):
|
||||
"""Doc string"""
|
||||
|
||||
my_value: typing.Union[MyBool, bool] = True
|
||||
```
|
||||
|
||||
### UserFilePath
|
||||
|
||||
Path to a single file. Automatically resolves as user_path:
|
||||
Source folder or AP install path on Windows. ~/Archipelago for the AppImage.
|
||||
Will open a file browser if the file is missing when in GUI mode.
|
||||
|
||||
#### class method validate(cls, path: str)
|
||||
|
||||
Override this and raise ValueError if validation fails.
|
||||
Checks the file against [md5s](#md5s) by default.
|
||||
|
||||
#### is_exe: bool
|
||||
|
||||
Resolves to an executable (varying file extension based on platform)
|
||||
|
||||
#### description: Optional\[str\]
|
||||
|
||||
Human-readable name to use in file browser
|
||||
|
||||
#### copy_to: Optional\[str\]
|
||||
|
||||
Instead of storing the path, copy the file.
|
||||
|
||||
#### md5s: List[Union[str, bytes]]
|
||||
|
||||
Provide md5 hashes as hex digests or raw bytes for automatic validation.
|
||||
|
||||
|
||||
### UserFolderPath
|
||||
|
||||
Same as [UserFilePath](#UserFilePath), but for a folder instead of a file.
|
||||
|
||||
|
||||
### LocalFilePath
|
||||
|
||||
Same as [UserFilePath](#UserFilePath), but resolves as local_path:
|
||||
path inside the AP dir or Appimage even if read-only.
|
||||
|
||||
|
||||
### LocalFolderPath
|
||||
|
||||
Same as [LocalFilePath](#LocalFilePath), but for a folder instead of a file.
|
||||
|
||||
|
||||
### OptionalUserFilePath, OptionalUserFolderPath, OptionalLocalFilePath, OptionalLocalFolderPath
|
||||
|
||||
Same as UserFilePath, UserFolderPath, LocalFilePath, LocalFolderPath but does not open a file browser if missing.
|
||||
|
||||
|
||||
### SNESRomPath
|
||||
|
||||
Specialized [UserFilePath](#UserFilePath) that ignores an optional 512 byte header when validating.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
### Circular Imports
|
||||
|
||||
Because the settings are defined on import, code that runs on import can not use settings since that would result in
|
||||
circular / partial imports. Instead, the code should fetch from settings on demand during generation.
|
||||
|
||||
"Global" settings are populated immediately, while worlds settings are lazy loaded, so if really necessary,
|
||||
"global" settings could be used in global scope of worlds.
|
||||
|
||||
|
||||
### APWorld Backwards Compatibility
|
||||
|
||||
APWorlds that want to be compatible with both stable and dev versions, have two options:
|
||||
1. use the old Utils.get_options() API until Archipelago 0.4.2 is out
|
||||
2. add some sort of compatibility code to your world that mimics the new API
|
||||
@@ -91,10 +91,13 @@ added to the `World` object for easy access.
|
||||
|
||||
### World Options
|
||||
|
||||
Any AP installation can provide settings for a world, for example a ROM file,
|
||||
accessible through `Utils.get_options()['<world>_options']['<option>']`.
|
||||
Any AP installation can provide settings for a world, for example a ROM file, accessible through `self.settings.option`
|
||||
or `cls.settings.option` (new API) or `Utils.get_options()["<world>_options"]["<option>"]` (deprecated).
|
||||
|
||||
Users can set those in their `host.yaml` file.
|
||||
Users can set those in their `host.yaml` file. Some options may automatically open a file browser if a file is missing.
|
||||
|
||||
Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md)
|
||||
for details.
|
||||
|
||||
### Locations
|
||||
|
||||
@@ -349,6 +352,8 @@ class MyGameWorld(World):
|
||||
```python
|
||||
# world/mygame/__init__.py
|
||||
|
||||
import settings
|
||||
import typing
|
||||
from .Options import mygame_options # 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
|
||||
@@ -356,16 +361,27 @@ from worlds.AutoWorld import World
|
||||
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
|
||||
from Utils import get_options, output_path
|
||||
|
||||
|
||||
class MyGameItem(Item): # or from Items import MyGameItem
|
||||
game = "My Game" # name of the game/world this item is from
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
rom_file: RomFile = RomFile("MyGame.sfc")
|
||||
|
||||
|
||||
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
|
||||
settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
|
||||
topology_present = True # show path to required location checks in spoiler
|
||||
|
||||
# ID of first item and location, could be hard-coded but code may be easier
|
||||
@@ -668,7 +684,7 @@ def generate_output(self, output_directory: str):
|
||||
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value
|
||||
}
|
||||
# point to a ROM specified by the installation
|
||||
src = Utils.get_options()["mygame_options"]["rom_file"]
|
||||
src = self.settings.rom_file
|
||||
# or point to worlds/mygame/data/mod_template
|
||||
src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
||||
# generate output path
|
||||
|
||||
Reference in New Issue
Block a user