From 21c7f3cd92cc1c0384cacf758af472a98a183239 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:22:40 -0600 Subject: [PATCH] Launcher: generate templates for option presets (#5062) --- Options.py | 48 ++++++++++++++++--------- data/options.yaml | 30 +++++++++------- test/options/test_generate_templates.py | 40 +++++++++++++-------- 3 files changed, 74 insertions(+), 44 deletions(-) diff --git a/Options.py b/Options.py index c9518df6c6..8c1c8b15c3 100644 --- a/Options.py +++ b/Options.py @@ -1747,8 +1747,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge from Utils import local_path, __version__ full_path: str + preset_folder = os.path.join(target_folder, "Presets") os.makedirs(target_folder, exist_ok=True) + os.makedirs(preset_folder, exist_ok=True) # clean out old for file in os.listdir(target_folder): @@ -1756,11 +1758,16 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge if os.path.isfile(full_path) and full_path.endswith(".yaml"): os.unlink(full_path) - def dictify_range(option: Range): - data = {option.default: 50} + for file in os.listdir(preset_folder): + full_path = os.path.join(preset_folder, file) + if os.path.isfile(full_path) and full_path.endswith(".yaml"): + os.unlink(full_path) + + def dictify_range(option: Range, option_val: int | str): + data = {option_val: 50} for sub_option in ["random", "random-low", "random-high", f"random-range-{option.range_start}-{option.range_end}"]: - if sub_option != option.default: + if sub_option != option_val: data[sub_option] = 0 notes = { "random-low": "random value weighted towards lower values", @@ -1773,6 +1780,8 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge if number in data: data[name] = data[number] del data[number] + elif name in data: + pass else: data[name] = 0 @@ -1788,20 +1797,27 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: + presets = world.web.options_presets.copy() + presets.update({"": {}}) + option_groups = get_option_groups(world) - - res = template.render( - option_groups=option_groups, - __version__=__version__, - game=game_name, - world_version=world.world_version.as_simple_string(), - yaml_dump=yaml_dump_scalar, - dictify_range=dictify_range, - cleandoc=cleandoc, - ) - - with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f: - f.write(res) + for name, preset in presets.items(): + res = template.render( + option_groups=option_groups, + __version__=__version__, + game=game_name, + world_version=world.world_version.as_simple_string(), + yaml_dump=yaml_dump_scalar, + dictify_range=dictify_range, + cleandoc=cleandoc, + preset_name=name, + preset=preset, + ) + preset_name = f" - {name}" if name else "" + with open(os.path.join(preset_folder if name else target_folder, + get_file_safe_name(game_name + preset_name) + ".yaml"), + "w", encoding="utf-8-sig") as f: + f.write(res) def dump_player_options(multiworld: MultiWorld) -> None: diff --git a/data/options.yaml b/data/options.yaml index 3278a3c5c1..2cc6ffa36d 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -28,7 +28,7 @@ name: Player{number} # Used to describe your yaml. Useful if you have multiple files. -description: {{ yaml_dump("Default %s Template" % game) }} +description: {{ yaml_dump("%s Preset for %s" % (preset_name, game)) if preset_name else yaml_dump("Default %s Template" % game) }} game: {{ yaml_dump(game) }} requires: @@ -38,11 +38,11 @@ requires: {{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected. {%- endif %} -{%- macro range_option(option) %} +{%- macro range_option(option, option_val) %} # You can define additional values between the minimum and maximum values. # Minimum value is {{ option.range_start }} # Maximum value is {{ option.range_end }} - {%- set data, notes = dictify_range(option) %} + {%- set data, notes = dictify_range(option, option_val) %} {%- for entry, default in data.items() %} {{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %} {%- endfor -%} @@ -56,6 +56,10 @@ requires: {%- for option_key, option in group_options.items() %} {{ option_key }}: + {%- set option_val = option.default %} + {%- if option_key in preset %} + {%- set option_val = preset[option_key] %} + {%- endif -%} {%- if option.__doc__ %} # {{ cleandoc(option.__doc__) | trim @@ -69,25 +73,25 @@ requires: {%- endif -%} {%- if option.range_start is defined and option.range_start is number %} - {{- range_option(option) -}} + {{- range_option(option, option_val) -}} {%- elif option.options -%} {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} - {{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} + {{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option_val or sub_option_name == option_val %}50{% else %}0{% endif %} {%- endfor -%} - - {%- if option.name_lookup[option.default] not in option.options %} - {{ yaml_dump(option.default) }}: 50 + + {%- if option.name_lookup[option_val] not in option.options and option_val not in option.options %} + {{ yaml_dump(option_val) }}: 50 {%- endif -%} - {%- elif option.default is string %} - {{ yaml_dump(option.default) }}: 50 + {%- elif option_val is string %} + {{ yaml_dump(option_val) }}: 50 - {%- elif option.default is iterable and option.default is not mapping %} - {{ option.default | list }} + {%- elif option_val is iterable and option_val is not mapping %} + {{ option_val | list }} {%- else %} - {{ yaml_dump(option.default) | indent(4, first=false) }} + {{ yaml_dump(option_val) | indent(4, first=false) }} {%- endif -%} {{ "\n" }} {%- endfor %} diff --git a/test/options/test_generate_templates.py b/test/options/test_generate_templates.py index cab97c54b1..1691f3c2cd 100644 --- a/test/options/test_generate_templates.py +++ b/test/options/test_generate_templates.py @@ -25,31 +25,41 @@ class TestGenerateYamlTemplates(unittest.TestCase): if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types: del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"] + def test_name_with_colon(self) -> None: from Options import generate_yaml_templates from worlds.AutoWorld import AutoWorldRegister - from worlds.AutoWorld import World + from worlds.AutoWorld import World, WebWorld + + class WebWorldWithColon(WebWorld): + options_presets = { + "Generic": { + "progression_balancing": "disabled", + "accessibility": "minimal", + } + } class WorldWithColon(World): game = "World: with colon" item_name_to_id = {} location_name_to_id = {} + web = WebWorldWithColon() AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon} with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir: generate_yaml_templates(temp_dir) path: Path - for path in Path(temp_dir).iterdir(): - self.assertTrue(path.is_file()) - self.assertTrue(path.suffix == ".yaml") - with path.open(encoding="utf-8") as f: - try: - data = parse_yaml(f) - except: - f.seek(0) - print(f"Error in {path.name}:\n{f.read()}") - raise - self.assertIn("game", data) - self.assertIn(":", data["game"]) - self.assertIn(data["game"], data) - self.assertIsInstance(data[data["game"]], dict) + for path in Path(temp_dir).rglob("*"): + if path.is_file(): + self.assertTrue(path.suffix == ".yaml") + with path.open(encoding="utf-8") as f: + try: + data = parse_yaml(f) + except: + f.seek(0) + print(f"Error in {path.name}:\n{f.read()}") + raise + self.assertIn("game", data) + self.assertIn(":", data["game"]) + self.assertIn(data["game"], data) + self.assertIsInstance(data[data["game"]], dict)