258 lines
8.9 KiB
Python
258 lines
8.9 KiB
Python
import collections.abc
|
|
import json
|
|
import os
|
|
from textwrap import dedent
|
|
from typing import Dict, Union
|
|
|
|
import yaml
|
|
from flask import redirect, render_template, request, Response
|
|
|
|
import Options
|
|
from Utils import local_path
|
|
from worlds.AutoWorld import AutoWorldRegister
|
|
from . import app, cache
|
|
from .generate import get_meta
|
|
|
|
|
|
def create() -> None:
|
|
target_folder = local_path("WebHostLib", "static", "generated")
|
|
yaml_folder = os.path.join(target_folder, "configs")
|
|
|
|
Options.generate_yaml_templates(yaml_folder)
|
|
|
|
|
|
def get_world_theme(game_name: str) -> str:
|
|
if game_name in AutoWorldRegister.world_types:
|
|
return AutoWorldRegister.world_types[game_name].web.theme
|
|
return 'grass'
|
|
|
|
|
|
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
|
|
world = AutoWorldRegister.world_types[world_name]
|
|
if world.hidden or world.web.options_page is False:
|
|
return redirect("games")
|
|
visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui
|
|
|
|
start_collapsed = {"Game Options": False}
|
|
for group in world.web.option_groups:
|
|
start_collapsed[group.name] = group.start_collapsed
|
|
|
|
return render_template(
|
|
template,
|
|
world_name=world_name,
|
|
world=world,
|
|
option_groups=Options.get_option_groups(world, visibility_level=visibility_flag),
|
|
start_collapsed=start_collapsed,
|
|
issubclass=issubclass,
|
|
Options=Options,
|
|
theme=get_world_theme(world_name),
|
|
)
|
|
|
|
|
|
def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]:
|
|
from .generate import start_generation
|
|
return start_generation(options, get_meta({}))
|
|
|
|
|
|
def send_yaml(player_name: str, formatted_options: dict) -> Response:
|
|
response = Response(yaml.dump(formatted_options, sort_keys=False))
|
|
response.headers["Content-Type"] = "text/yaml"
|
|
response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml"
|
|
return response
|
|
|
|
|
|
@app.template_filter("dedent")
|
|
def filter_dedent(text: str) -> str:
|
|
return dedent(text).strip("\n ")
|
|
|
|
|
|
@app.template_test("ordered")
|
|
def test_ordered(obj):
|
|
return isinstance(obj, collections.abc.Sequence)
|
|
|
|
|
|
@app.route("/games/<string:game>/option-presets", methods=["GET"])
|
|
@cache.cached()
|
|
def option_presets(game: str) -> Response:
|
|
world = AutoWorldRegister.world_types[game]
|
|
|
|
presets = {}
|
|
for preset_name, preset in world.web.options_presets.items():
|
|
presets[preset_name] = {}
|
|
for preset_option_name, preset_option in preset.items():
|
|
if preset_option == "random":
|
|
presets[preset_name][preset_option_name] = preset_option
|
|
continue
|
|
|
|
option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option)
|
|
if isinstance(option, Options.NamedRange) and isinstance(preset_option, str):
|
|
assert preset_option in option.special_range_names, \
|
|
f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. " \
|
|
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
|
|
|
presets[preset_name][preset_option_name] = option.value
|
|
elif isinstance(option, Options.Range):
|
|
presets[preset_name][preset_option_name] = option.value
|
|
elif isinstance(preset_option, str):
|
|
# Ensure the option value is valid for Choice and Toggle options
|
|
assert option.name_lookup[option.value] == preset_option, \
|
|
f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. " \
|
|
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
|
# Use the name of the option
|
|
presets[preset_name][preset_option_name] = option.current_key
|
|
else:
|
|
# Use the name of the option
|
|
presets[preset_name][preset_option_name] = option.current_key
|
|
|
|
class SetEncoder(json.JSONEncoder):
|
|
def default(self, obj):
|
|
from collections.abc import Set
|
|
if isinstance(obj, Set):
|
|
return list(obj)
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
json_data = json.dumps(presets, cls=SetEncoder)
|
|
response = Response(json_data)
|
|
response.headers["Content-Type"] = "application/json"
|
|
return response
|
|
|
|
|
|
@app.route("/weighted-options")
|
|
def weighted_options_old():
|
|
return redirect("games", 301)
|
|
|
|
|
|
@app.route("/games/<string:game>/weighted-options")
|
|
@cache.cached()
|
|
def weighted_options(game: str):
|
|
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
|
|
|
|
|
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
|
def generate_weighted_yaml(game: str):
|
|
if request.method == "POST":
|
|
intent_generate = False
|
|
options = {}
|
|
|
|
for key, val in request.form.items():
|
|
if "||" not in key:
|
|
if len(str(val)) == 0:
|
|
continue
|
|
|
|
options[key] = val
|
|
else:
|
|
if int(val) == 0:
|
|
continue
|
|
|
|
[option, setting] = key.split("||")
|
|
options.setdefault(option, {})[setting] = int(val)
|
|
|
|
# Error checking
|
|
if "name" not in options:
|
|
return "Player name is required."
|
|
|
|
# Remove POST data irrelevant to YAML
|
|
if "intent-generate" in options:
|
|
intent_generate = True
|
|
del options["intent-generate"]
|
|
if "intent-export" in options:
|
|
del options["intent-export"]
|
|
|
|
# Properly format YAML output
|
|
player_name = options["name"]
|
|
del options["name"]
|
|
|
|
formatted_options = {
|
|
"name": player_name,
|
|
"game": game,
|
|
"description": f"Generated by https://archipelago.gg/ for {game}",
|
|
game: options,
|
|
}
|
|
|
|
if intent_generate:
|
|
return generate_game({player_name: formatted_options})
|
|
|
|
else:
|
|
return send_yaml(player_name, formatted_options)
|
|
|
|
|
|
# Player options pages
|
|
@app.route("/games/<string:game>/player-options")
|
|
@cache.cached()
|
|
def player_options(game: str):
|
|
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
|
|
|
|
|
# YAML generator for player-options
|
|
@app.route("/games/<string:game>/generate-yaml", methods=["POST"])
|
|
def generate_yaml(game: str):
|
|
if request.method == "POST":
|
|
options = {}
|
|
intent_generate = False
|
|
for key, val in request.form.items(multi=True):
|
|
if key in options:
|
|
if not isinstance(options[key], list):
|
|
options[key] = [options[key]]
|
|
options[key].append(val)
|
|
else:
|
|
options[key] = val
|
|
|
|
for key, val in options.copy().items():
|
|
key_parts = key.rsplit("||", 2)
|
|
# Detect and build ItemDict options from their name pattern
|
|
if key_parts[-1] == "qty":
|
|
if key_parts[0] not in options:
|
|
options[key_parts[0]] = {}
|
|
if val != "0":
|
|
options[key_parts[0]][key_parts[1]] = int(val)
|
|
del options[key]
|
|
|
|
# Detect keys which end with -custom, indicating a TextChoice with a possible custom value
|
|
elif key_parts[-1].endswith("-custom"):
|
|
if val:
|
|
options[key_parts[-1][:-7]] = val
|
|
|
|
del options[key]
|
|
|
|
# Detect random-* keys and set their options accordingly
|
|
for key, val in options.copy().items():
|
|
if key.startswith("random-"):
|
|
options[key.removeprefix("random-")] = "random"
|
|
del options[key]
|
|
|
|
# Error checking
|
|
if not options["name"]:
|
|
return "Player name is required."
|
|
|
|
# Remove POST data irrelevant to YAML
|
|
preset_name = 'default'
|
|
if "intent-generate" in options:
|
|
intent_generate = True
|
|
del options["intent-generate"]
|
|
if "intent-export" in options:
|
|
del options["intent-export"]
|
|
if "game-options-preset" in options:
|
|
preset_name = options["game-options-preset"]
|
|
del options["game-options-preset"]
|
|
|
|
# Properly format YAML output
|
|
player_name = options["name"]
|
|
del options["name"]
|
|
|
|
description = f"Generated by https://archipelago.gg/ for {game}"
|
|
if preset_name != 'default' and preset_name != 'custom':
|
|
description += f" using {preset_name} preset"
|
|
|
|
formatted_options = {
|
|
"name": player_name,
|
|
"game": game,
|
|
"description": description,
|
|
game: options,
|
|
}
|
|
|
|
if intent_generate:
|
|
return generate_game({player_name: formatted_options})
|
|
|
|
else:
|
|
return send_yaml(player_name, formatted_options)
|