Merge branch 'master' into website-redesign

This commit is contained in:
Chris Wilson
2020-11-23 17:49:53 -05:00
23 changed files with 673 additions and 149 deletions

View File

@@ -28,7 +28,7 @@ def mysterycheck():
if type(options) == str:
flash(options)
else:
results, _ = roll_yamls(options)
results, _ = roll_options(options)
return render_template("checkresult.html", results=results)
return render_template("check.html")
@@ -60,12 +60,15 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
return options
def roll_yamls(options: Dict[str, Union[str, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
results = {}
rolled_results = {}
for filename, text in options.items():
try:
yaml_data = parse_yaml(text)
if type(text) is dict:
yaml_data = text
else:
yaml_data = parse_yaml(text)
except Exception as e:
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
else:

View File

@@ -37,9 +37,10 @@ def download_raw_patch(seed_id, player_id):
return "Patch not found"
else:
import io
pname = patch.seed.multidata["names"][0][patch.player - 1]
if patch.seed.multidata:
pname = patch.seed.multidata["names"][0][patch.player - 1]
else:
pname = "unknown"
patch_data = update_patch_data(patch.data, server="")
patch_data = io.BytesIO(patch_data)

View File

@@ -13,7 +13,7 @@ import pickle
from .models import *
from WebHostLib import app
from .check import get_yaml_data, roll_yamls
from .check import get_yaml_data, roll_options
@app.route('/generate', methods=['GET', 'POST'])
@@ -29,7 +29,7 @@ def generate(race=False):
if type(options) == str:
flash(options)
else:
results, gen_options = roll_yamls(options)
results, gen_options = roll_options(options)
if any(type(result) == str for result in results.values()):
return render_template("checkresult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
@@ -52,6 +52,55 @@ def generate(race=False):
return render_template("generate.html", race=race)
@app.route('/api/generate', methods=['POST'])
def generate_api():
try:
options = {}
race = False
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
return {"text": options}, 400
if "race" in request.form:
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
json_data = request.get_json()
if json_data:
if 'weights' in json_data:
# example: options = {"player1weights" : {<weightsdata>}}
options = json_data["weights"]
if "race" in json_data:
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
if not options:
return {"text": "No options found. Expected file attachment or json weights."
}, 400
if len(options) > app.config["MAX_ROLL"]:
return {"text": "Max size of multiworld exceeded",
"detail": app.config["MAX_ROLL"]}, 409
results, gen_options = roll_options(options)
if any(type(result) == str for result in results.values()):
return {"text": str(results),
"detail": results}, 400
else:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
owner=session["_id"])
commit()
return {"text": f"Generation of seed {gen.id} started successfully.",
"detail": gen.id,
"encoded": app.url_map.converters["suuid"].to_url(None, gen.id),
"wait_api_url": url_for("wait_seed_api", seed=gen.id),
"url": url_for("wait_seed", seed=gen.id)}, 201
except Exception as e:
return {"text": "Uncaught Exception:" + str(e)}, 500
def gen_game(gen_options, race=False, owner=None, sid=None):
try:
target = tempfile.TemporaryDirectory()
@@ -92,7 +141,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
del (erargs.progression_balancing)
ERmain(erargs, seed)
return upload_to_db(target.name, owner, sid)
return upload_to_db(target.name, owner, sid, race)
except BaseException:
if sid:
with db_session:
@@ -117,9 +166,25 @@ def wait_seed(seed: UUID):
return render_template("waitSeed.html", seed_id=seed_id)
def upload_to_db(folder, owner, sid):
@app.route('/api/status/<suuid:seed>')
def wait_seed_api(seed: UUID):
seed_id = seed
seed = Seed.get(id=seed_id)
if seed:
return {"text": "Generation done"}, 201
generation = Generation.get(id=seed_id)
if not generation:
return {"text": "Generation not found"}, 404
elif generation.state == STATE_ERROR:
return {"text": "Generation failed"}, 500
return {"text": "Generation running"}, 202
def upload_to_db(folder, owner, sid, race:bool):
patches = set()
spoiler = ""
multidata = None
for file in os.listdir(folder):
file = os.path.join(folder, file)
@@ -129,20 +194,26 @@ def upload_to_db(folder, owner, sid):
elif file.endswith(".txt"):
spoiler = open(file, "rt").read()
elif file.endswith("multidata"):
try:
multidata = json.loads(zlib.decompress(open(file, "rb").read()))
except Exception as e:
flash(e)
if multidata:
with db_session:
if sid:
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid)
else:
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner)
for patch in patches:
patch.seed = seed
if sid:
gen = Generation.get(id=sid)
if gen is not None:
gen.delete()
return seed.id
multidata = file
if not race or len(patches) > 1:
try:
multidata = json.loads(zlib.decompress(open(multidata, "rb").read()))
except Exception as e:
flash(e)
raise e
else:
multidata = {}
with db_session:
if sid:
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid)
else:
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner)
for patch in patches:
patch.seed = seed
if sid:
gen = Generation.get(id=sid)
if gen is not None:
gen.delete()
return seed.id

View File

@@ -1206,6 +1206,110 @@
}
}
},
"countdown_start_time": {
"keyString": "countdown_start_time",
"friendlyName": "Countdown Starting Time",
"description": "The amount of time, in minutes, to start with in Timed Countdown and Timed OHKO modes.",
"inputType": "range",
"subOptions": {
"0": {
"keyString": "countdown_start_time.0",
"friendlyName": 0,
"description": "Start with no time on the timer. In Timed OHKO mode, start in OHKO mode.",
"defaultValue": 0
},
"10": {
"keyString": "countdown_start_time.10",
"friendlyName": 10,
"description": "Start with 10 minutes on the timer.",
"defaultValue": 50
},
"20": {
"keyString": "countdown_start_time.20",
"friendlyName": 20,
"description": "Start with 20 minutes on the timer.",
"defaultValue": 0
},
"30": {
"keyString": "countdown_start_time.30",
"friendlyName": 30,
"description": "Start with 30 minutes on the timer.",
"defaultValue": 0
},
"60": {
"keyString": "countdown_start_time.60",
"friendlyName": 60,
"description": "Start with an hour on the timer.",
"defaultValue": 0
}
}
},
"red_clock_time": {
"keyString": "red_clock_time",
"friendlyName": "Red Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a red clock.",
"inputType": "range",
"subOptions": {
"-2": {
"keyString": "red_clock_time.-2",
"friendlyName": -2,
"description": "Subtract 2 minutes from the timer upon picking up a red clock.",
"defaultValue": 0
},
"1": {
"keyString": "red_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a red clock.",
"defaultValue": 50
}
}
},
"blue_clock_time": {
"keyString": "blue_clock_time",
"friendlyName": "Blue Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a blue clock.",
"inputType": "range",
"subOptions": {
"1": {
"keyString": "blue_clock_time.1",
"friendlyName": 1,
"description": "Add a minute to the timer upon picking up a blue clock.",
"defaultValue": 0
},
"2": {
"keyString": "blue_clock_time.2",
"friendlyName": 2,
"description": "Add 2 minutes to the timer upon picking up a blue clock.",
"defaultValue": 50
}
}
},
"green_clock_time": {
"keyString": "green_clock_time",
"friendlyName": "Green Clock Time",
"description": "The amount of time, in minutes, to add to or subtract from the timer upon picking up a green clock.",
"inputType": "range",
"subOptions": {
"4": {
"keyString": "green_clock_time.4",
"friendlyName": 4,
"description": "Add 4 minutes to the timer upon picking up a green clock.",
"defaultValue": 50
},
"10": {
"keyString": "green_clock_time.10",
"friendlyName": 10,
"description": "Add 10 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
},
"15": {
"keyString": "green_clock_time.15",
"friendlyName": 15,
"description": "Add 15 minutes to the timer upon picking up a green clock.",
"defaultValue": 0
}
}
},
"glitch_boots": {
"keyString": "glitch_boots",
"friendlyName": "Glitch Boots",
@@ -1572,26 +1676,26 @@
},
"hud_palettes": {
"keyString": "rom.hud_palettes",
"friendlyName": "Underworld Palettes",
"description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.",
"friendlyName": "HUD Palettes",
"description": "Randomize the colors of the HUD (user interface), within reason.",
"inputType": "range",
"subOptions": {
"default": {
"keyString": "rom.hud_palettes.default",
"friendlyName": "Vanilla",
"description": "Overworld colors will remain unchanged.",
"description": "HUD colors will remain unchanged.",
"defaultValue": 50
},
"random": {
"keyString": "rom.hud_palettes.random",
"friendlyName": "Random",
"description": "Shuffles the colors of the overworld palette.",
"description": "Shuffles the colors of the HUD palette.",
"defaultValue": 0
},
"blackout": {
"keyString": "rom.hud_palettes.blackout",
"friendlyName": "Blackout",
"description": "Never use this. Makes all overworld palette colors black.",
"description": "Never use this. Makes all HUD palette colors black.",
"defaultValue": 0
},
"grayscale": {
@@ -1634,26 +1738,26 @@
},
"shield_palettes": {
"keyString": "rom.shield_palettes",
"friendlyName": "Underworld Palettes",
"description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.",
"friendlyName": "Shield Palettes",
"description": "Randomize the colors of the shield, within reason.",
"inputType": "range",
"subOptions": {
"default": {
"keyString": "rom.shield_palettes.default",
"friendlyName": "Vanilla",
"description": "Overworld colors will remain unchanged.",
"description": "Shield colors will remain unchanged.",
"defaultValue": 50
},
"random": {
"keyString": "rom.shield_palettes.random",
"friendlyName": "Random",
"description": "Shuffles the colors of the overworld palette.",
"description": "Shuffles the colors of the shield palette.",
"defaultValue": 0
},
"blackout": {
"keyString": "rom.shield_palettes.blackout",
"friendlyName": "Blackout",
"description": "Never use this. Makes all overworld palette colors black.",
"description": "Never use this. Makes all shield palette colors black.",
"defaultValue": 0
},
"grayscale": {
@@ -1696,26 +1800,26 @@
},
"sword_palettes": {
"keyString": "rom.sword_palettes",
"friendlyName": "Underworld Palettes",
"description": "Randomize the colors of the underworld (caves, dungeons, etc.), within reason.",
"friendlyName": "Sword Palettes",
"description": "Randomize the colors of the sword, within reason.",
"inputType": "range",
"subOptions": {
"default": {
"keyString": "rom.sword_palettes.default",
"friendlyName": "Vanilla",
"description": "Overworld colors will remain unchanged.",
"description": "Sword colors will remain unchanged.",
"defaultValue": 50
},
"random": {
"keyString": "rom.sword_palettes.random",
"friendlyName": "Random",
"description": "Shuffles the colors of the overworld palette.",
"description": "Shuffles the colors of the sword palette.",
"defaultValue": 0
},
"blackout": {
"keyString": "rom.sword_palettes.blackout",
"friendlyName": "Blackout",
"description": "Never use this. Makes all overworld palette colors black.",
"description": "Never use this. Makes all sword palette colors black.",
"defaultValue": 0
},
"grayscale": {

View File

@@ -232,6 +232,22 @@ timer:
ohko: 0 # Timer always at zero. Permanent OHKO.
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
10: 50
20: 0
30: 0
60: 0
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
-2: 50
1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 0
15: 0
# Can be uncommented to use it
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
@@ -391,13 +407,3 @@ rom:
dizzy: 0
sick: 0
puke: 0
uw_palettes: # Change the colors of shields
default: 50 # No changes
random: 0 # Shuffle the colors
blackout: 0 # Never use this
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0

View File

@@ -19,10 +19,10 @@
You can also upload a .zip with multiple YAMLs.
A proper menu is in the works.
{% if race -%}
Race Mode means the spoiler log will be unavailable.
Race Mode means the spoiler log will be unavailable, roms will be encrypted and single-player has no multidata.
{%- else -%}
You can go to <a href="{{ url_for("generate", race=True) }}">Race Mode</a> to create a game without
spoiler log.
spoiler log and with encryption.
{%- endif -%}
</p>
<p>

View File

@@ -122,7 +122,7 @@
<td>{{ player_names[(team, loop.index)]|e }}</td>
{%- for area in ordered_areas -%}
{%- set checks_done = checks[area] -%}
{%- set checks_total = checks_in_area[area] -%}
{%- set checks_total = checks_in_area[player][area] -%}
{%- if checks_done == checks_total -%}
<td class="item-acquired center-column">
{{ checks_done }}/{{ checks_total }}</td>

View File

@@ -11,6 +11,11 @@
<div id="view-seed-wrapper">
<div class="main-content">
<h3>Seed Info</h3>
{% if not seed.multidata and not seed.spoiler %}
<h4>
Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms cannot be created.
</h4>
{% endif %}
<table>
<tbody>
<tr>
@@ -27,6 +32,7 @@
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
</tr>
{% endif %}
{% if seed.multidata %}
<tr>
<td>Players:&nbsp;</td>
<td>
@@ -55,6 +61,23 @@
{% endcall %}
</td>
</tr>
{% else %}
<tr>
<td>Patches:&nbsp;</td>
<td>
<ul>
{% for patch in seed.patches %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player) }}">Player {{ patch.player }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>

View File

@@ -180,6 +180,25 @@ default_locations = {
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157},
'Total': set()}
key_only_locations = {
'Light World': set(),
'Dark World': set(),
'Desert Palace': {0x140031, 0x14002b, 0x140061, 0x140028},
'Eastern Palace': {0x14005b, 0x140049},
'Hyrule Castle': {0x140037, 0x140034, 0x14000d, 0x14003d},
'Agahnims Tower': {0x140061, 0x140052},
'Tower of Hera': set(),
'Swamp Palace': {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a},
'Thieves Town': {0x14005e, 0x14004f},
'Skull Woods': {0x14002e, 0x14001c},
'Ice Palace': {0x140004, 0x140022, 0x140025, 0x140046},
'Misery Mire': {0x140055, 0x14004c, 0x140064},
'Turtle Rock': {0x140058, 0x140007},
'Palace of Darkness': set(),
'Ganons Tower': {0x140040, 0x140043, 0x14003a, 0x14001f},
'Total': set()
}
key_locations = {"Desert Palace", "Eastern Palace", "Hyrule Castle", "Agahnims Tower", "Tower of Hera", "Swamp Palace",
"Thieves Town", "Skull Woods", "Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness",
"Ganons Tower"}
@@ -191,6 +210,10 @@ for area, locations in default_locations.items():
for location in locations:
location_to_area[location] = area
for area, locations in key_only_locations.items():
for location in locations:
location_to_area[location] = area
checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
checks_in_area["Total"] = 216
@@ -235,6 +258,14 @@ def render_timedelta(delta: datetime.timedelta):
_multidata_cache = {}
def get_location_table(checks_table: dict) -> dict:
loc_to_area = {}
for area, locations in checks_table.items():
if area == "Total":
continue
for location in locations:
loc_to_area[location] = area
return loc_to_area
def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None)
@@ -244,11 +275,30 @@ def get_static_room_data(room: Room):
# in > 100 players this can take a bit of time and is the main reason for the cache
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
names = multidata["names"]
seed_checks_in_area = checks_in_area.copy()
use_door_tracker = False
if "tags" in multidata:
use_door_tracker = "DR" in multidata["tags"]
result = locations, names, use_door_tracker
if use_door_tracker:
for area, checks in key_only_locations.items():
seed_checks_in_area[area] += len(checks)
seed_checks_in_area["Total"] = 249
if "checks_in_area" not in multidata:
player_checks_in_area = {playernumber: (seed_checks_in_area if use_door_tracker and
(0x140031, playernumber) in locations else checks_in_area)
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: location_to_area
for playernumber in range(1, len(names[0]) + 1)}
else:
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][f'{playernumber}'][areaname])
if areaname != "Total" else multidata["checks_in_area"][f'{playernumber}']["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][f'{playernumber}'])
for playernumber in range(1, len(names[0]) + 1)}
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area
_multidata_cache[room.seed.id] = result
return result
@@ -259,7 +309,7 @@ def get_tracker(tracker: UUID):
room = Room.get(tracker=tracker)
if not room:
abort(404)
locations, names, use_door_tracker = get_static_room_data(room)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)}
@@ -280,9 +330,12 @@ def get_tracker(tracker: UUID):
for item_id in precollected:
attribute_item(inventory, team, player, item_id)
for location in locations_checked:
if (location, player) not in locations or location not in player_location_to_area[player]:
continue
item, recipient = locations[location, player]
attribute_item(inventory, team, recipient, item)
checks_done[team][player][location_to_area[location]] += 1
checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1
for (team, player), game_state in room.multisave.get("client_game_state", []):
@@ -311,7 +364,7 @@ def get_tracker(tracker: UUID):
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
checks_in_area=checks_in_area, activity_timers=activity_timers,
checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
key_locations=key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
video=video, big_key_locations=key_locations if use_door_tracker else big_key_locations,
hints=hints, long_player_names = long_player_names)