diff --git a/Main.py b/Main.py
index 5558abef..b4e1b936 100644
--- a/Main.py
+++ b/Main.py
@@ -216,11 +216,9 @@ def main(args, seed=None):
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
- if world.players > 1:
- outfilepname += f'_P{player}'
- if world.players > 1 or world.teams > 1:
- outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
- team] != 'Player%d' % player else ''
+ outfilepname += f'_P{player}'
+ outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
+ team] != 'Player%d' % player else ''
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player],
world.difficulty_adjustments[player],
world.mode[player], world.goal[player],
diff --git a/Mystery.py b/Mystery.py
index c93b287b..d975a5e5 100644
--- a/Mystery.py
+++ b/Mystery.py
@@ -103,27 +103,27 @@ def main(args=None, callback = ERmain):
# set up logger
if args.loglevel:
erargs.loglevel = args.loglevel
- loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.loglevel]
-
- import sys
- class LoggerWriter(object):
- def __init__(self, writer):
- self._writer = writer
- self._msg = ''
-
- def write(self, message):
- self._msg = self._msg + message
- while '\n' in self._msg:
- pos = self._msg.find('\n')
- self._writer(self._msg[:pos])
- self._msg = self._msg[pos + 1:]
-
- def flush(self):
- if self._msg != '':
- self._writer(self._msg)
- self._msg = ''
+ loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
+ erargs.loglevel]
if args.log_output_path:
+ import sys
+ class LoggerWriter(object):
+ def __init__(self, writer):
+ self._writer = writer
+ self._msg = ''
+
+ def write(self, message):
+ self._msg = self._msg + message
+ while '\n' in self._msg:
+ pos = self._msg.find('\n')
+ self._writer(self._msg[:pos])
+ self._msg = self._msg[pos + 1:]
+
+ def flush(self):
+ if self._msg != '':
+ self._writer(self._msg)
+ self._msg = ''
log = logging.getLogger("stderr")
log.addHandler(logging.StreamHandler())
sys.stderr = LoggerWriter(log.error)
diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py
index 8b6a8d9c..2c0bb9c5 100644
--- a/WebHostLib/__init__.py
+++ b/WebHostLib/__init__.py
@@ -42,6 +42,7 @@ app.config["PONY"] = {
'filename': os.path.abspath('db.db3'),
'create_db': True
}
+app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "simple"
app.autoversion = True
av = Autoversion(app)
@@ -133,5 +134,6 @@ def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
+
from WebHostLib.customserver import run_server_process
-from . import tracker, upload, landing, check # to trigger app routing picking up on it
+from . import tracker, upload, landing, check, generate # to trigger app routing picking up on it
diff --git a/WebHostLib/check.py b/WebHostLib/check.py
index 557cbc97..7b2e275a 100644
--- a/WebHostLib/check.py
+++ b/WebHostLib/check.py
@@ -1,5 +1,5 @@
import zipfile
-
+from typing import *
from flask import request, flash, redirect, url_for, session, render_template
@@ -11,9 +11,11 @@ banned_zip_contents = (".sfc",)
def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
+
from Mystery import roll_settings
from Utils import parse_yaml
+
@app.route('/mysterycheck', methods=['GET', 'POST'])
def mysterycheck():
if request.method == 'POST':
@@ -21,46 +23,56 @@ def mysterycheck():
if 'file' not in request.files:
flash('No file part')
else:
- options = {}
file = request.files['file']
- # if user does not select file, browser also
- # submit an empty part without filename
- if file.filename == '':
- flash('No selected file')
- elif file and allowed_file(file.filename):
- if file.filename.endswith(".zip"):
-
- with zipfile.ZipFile(file, 'r') as zfile:
- infolist = zfile.infolist()
-
- for file in infolist:
- if file.filename.endswith(banned_zip_contents):
- return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
- elif file.filename.endswith(".yaml"):
- options[file.filename] = zfile.open(file, "r").read()
- elif file.filename.endswith(".txt"):
- options[file.filename] = zfile.open(file, "r").read()
- else:
- options = {file.filename: file.read()}
- if not options:
- flash("Did not find a .yaml file to process.")
- else:
- results = {}
- for filename, text in options.items():
- try:
- yaml_data = parse_yaml(text)
- except Exception as e:
- results[filename] = f"Failed to parse YAML data in {filename}: {e}"
- else:
- try:
- roll_settings(yaml_data)
- except Exception as e:
- results[filename] = f"Failed to generate mystery in {filename}: {e}"
- else:
- results[filename] = "Looks fine"
- return render_template("checkresult.html", results=results)
-
+ options = get_yaml_data(file)
+ if type(options) == str:
+ flash(options)
else:
- flash("Not recognized file format. Awaiting a .yaml file.")
+ results, _ = roll_yamls(options)
+ return render_template("checkresult.html", results=results)
return render_template("check.html")
+
+
+def get_yaml_data(file) -> Union[Dict[str, str], str]:
+ options = {}
+ # if user does not select file, browser also
+ # submit an empty part without filename
+ if file.filename == '':
+ return 'No selected file'
+ elif file and allowed_file(file.filename):
+ if file.filename.endswith(".zip"):
+
+ with zipfile.ZipFile(file, 'r') as zfile:
+ infolist = zfile.infolist()
+
+ for file in infolist:
+ if file.filename.endswith(banned_zip_contents):
+ return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
+ elif file.filename.endswith(".yaml"):
+ options[file.filename] = zfile.open(file, "r").read()
+ elif file.filename.endswith(".txt"):
+ options[file.filename] = zfile.open(file, "r").read()
+ else:
+ options = {file.filename: file.read()}
+ if not options:
+ return "Did not find a .yaml file to process."
+ return options
+
+
+def roll_yamls(options: Dict[str, Union[str, 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)
+ except Exception as e:
+ results[filename] = f"Failed to parse YAML data in {filename}: {e}"
+ else:
+ try:
+ rolled_results[filename] = roll_settings(yaml_data)
+ except Exception as e:
+ results[filename] = f"Failed to generate mystery in {filename}: {e}"
+ else:
+ results[filename] = True
+ return results, rolled_results
diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py
new file mode 100644
index 00000000..6c9ec30f
--- /dev/null
+++ b/WebHostLib/generate.py
@@ -0,0 +1,150 @@
+import os
+import tempfile
+import random
+import zlib
+import json
+
+from flask import request, flash, redirect, url_for, session, render_template, send_file, Response
+
+from EntranceRandomizer import parse_arguments
+from Main import main as ERmain
+from Main import get_seed, seeddigits
+from Patch import update_patch_data
+
+from .models import *
+from WebHostLib import app
+from .check import get_yaml_data, roll_yamls
+
+
+@app.route('/generate', methods=['GET', 'POST'])
+@app.route('/generate/', methods=['GET', 'POST'])
+def generate(race=False):
+ if request.method == 'POST':
+ # check if the post request has the file part
+ if 'file' not in request.files:
+ flash('No file part')
+ else:
+ file = request.files['file']
+ options = get_yaml_data(file)
+ if type(options) == str:
+ flash(options)
+ else:
+ results, gen_options = roll_yamls(options)
+ if any(result == str for result in results.values()):
+ return render_template("checkresult.html", results=results)
+ elif len(gen_options) > app.config["MAX_ROLL"]:
+ flash(f"Sorry, generating of multiworld is limited to {app.config['MAX_ROLL']} players for now. "
+ f"If you have a larger group, please generate it yourself and upload it.")
+ else:
+ seed_id = gen(gen_options, race=race)
+ return redirect(url_for("view_seed", seed=seed_id))
+ return render_template("generate.html", race=race)
+
+
+@app.route("/dl_patch//")
+def download_patch(patch_id, room_id):
+ patch = Patch.get(id=patch_id)
+ if not patch:
+ return "Patch not found"
+ else:
+ import io
+
+ room = Room.get(id=room_id)
+ last_port = room.last_port
+ pname = room.seed.multidata["names"][0][patch.player - 1]
+
+ patch_data = update_patch_data(patch.data, server="berserkermulti.world:" + str(last_port))
+ patch_data = io.BytesIO(patch_data)
+
+ fname = f"P{patch.player}_{pname}_{app.jinja_env.filters['suuid'](room_id)}.bmbp"
+ return send_file(patch_data, as_attachment=True, attachment_filename=fname)
+
+
+@app.route("/dl_spoiler/")
+def download_spoiler(seed_id):
+ return Response(Seed.get(id=seed_id).spoiler[3:], mimetype="text/plain")
+
+
+@app.route("/dl_raw_patch//")
+def download_raw_patch(seed_id, player_id):
+ patch = select(patch for patch in Patch if patch.player == player_id and patch.seed.id == seed_id).first()
+
+ if not patch:
+ return "Patch not found"
+ else:
+ import io
+
+ pname = patch.seed.multidata["names"][0][patch.player - 1]
+
+ patch_data = update_patch_data(patch.data, server="")
+ patch_data = io.BytesIO(patch_data)
+
+ fname = f"P{patch.player}_{pname}_{app.jinja_env.filters['suuid'](seed_id)}.bmbp"
+ return send_file(patch_data, as_attachment=True, attachment_filename=fname)
+
+
+def gen(gen_options, race=False):
+ target = tempfile.TemporaryDirectory()
+ with target:
+ playercount = len(gen_options)
+ seed = get_seed()
+ random.seed(seed)
+
+ if race:
+ random.seed() # reset to time-based random source
+
+ seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
+
+ erargs = parse_arguments(['--multi', str(playercount)])
+ erargs.seed = seed
+ erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
+ erargs.create_spoiler = not race
+ erargs.race = race
+ erargs.skip_playthrough = race
+ erargs.outputname = seedname
+ erargs.outputpath = target.name
+ erargs.teams = 1
+ erargs.progression_balancing = {}
+ erargs.create_diff = True
+
+ for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
+ for k, v in vars(settings).items():
+ if v is not None:
+ getattr(erargs, k)[player] = v
+
+ if not erargs.name[player]:
+ erargs.name[player] = os.path.split(playerfile)[-1].split(".")[0]
+
+ erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
+ del (erargs.name)
+
+ erargs.skip_progression_balancing = {player: not balanced for player, balanced in
+ erargs.progression_balancing.items()}
+ del (erargs.progression_balancing)
+ ERmain(erargs, seed)
+ return upload_to_db(target.name)
+
+
+def upload_to_db(folder):
+ patches = set()
+ spoiler = ""
+ multidata = None
+ for file in os.listdir(folder):
+ file = os.path.join(folder, file)
+ if file.endswith(".bmbp"):
+ player = int(file.split("P")[-1].split(".")[0].split("_")[0])
+ patches.add(Patch(data=open(file, "rb").read(), player=player))
+ 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:
+ commit() # commit patches
+ seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
+ commit() # create seed
+ for patch in patches:
+ patch.seed = seed
+ return seed.id
diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt
index d82c5314..63b0e1d3 100644
--- a/WebHostLib/requirements.txt
+++ b/WebHostLib/requirements.txt
@@ -3,4 +3,5 @@ pony>=0.7.13
waitress>=1.4.4
flask-caching>=1.9.0
Flask-Autoversion>=0.2.0
-Flask-Compress>=1.5.0
\ No newline at end of file
+Flask-Compress>=1.5.0
+Flask-Limiter>=1.3.1
\ No newline at end of file
diff --git a/WebHostLib/static/generate.js b/WebHostLib/static/generate.js
new file mode 100644
index 00000000..189d46af
--- /dev/null
+++ b/WebHostLib/static/generate.js
@@ -0,0 +1,9 @@
+window.addEventListener('load', () => {
+ document.getElementById('upload-button').addEventListener('click', () => {
+ document.getElementById('file-input').click();
+ });
+
+ document.getElementById('file-input').addEventListener('change', () => {
+ document.getElementById('upload-form').submit();
+ });
+});
diff --git a/WebHostLib/templates/checkresult.html b/WebHostLib/templates/checkresult.html
index 8411696e..d32aa8a0 100644
--- a/WebHostLib/templates/checkresult.html
+++ b/WebHostLib/templates/checkresult.html
@@ -6,7 +6,7 @@
{% endblock %}
{% block body %}
- {% for filename, resulttext in results.items() %}
- {{ filename }}: {{ resulttext }}
+ {% for filename, resulttext in results.items() %}
+ {{ filename }}: {{ "Looks ok" if resulttext == True else resulttext }}
{% endfor %}
{% endblock %}
diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html
new file mode 100644
index 00000000..a2532f5f
--- /dev/null
+++ b/WebHostLib/templates/generate.html
@@ -0,0 +1,42 @@
+{% extends 'layout.html' %}
+
+{% block head %}
+ {{ super() }}
+ Generate Game
+
+
+{% endblock %}
+
+{% block body %}
+
+
+
Upload YAML(s){% if race %} (Race Mode){% endif %}
+
+ This page accepts a yaml file containing generator options.
+ You can find a documented example at easy.yaml.
+ This file can be saved as .yaml, edited to your liking and then supplied to the generator.
+ 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.
+ {%- else -%}
+ You can go to Race Mode to create a game without
+ spoiler log.
+ {%- endif -%}
+
+
+ After generation is complete, you will have the option to download a patch file.
+ This patch file can be opened with the Client to create a rom file.
+ In-Browser patching will come.
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/WebHostLib/templates/host_room.html b/WebHostLib/templates/host_room.html
index 3150ce11..ef80b455 100644
--- a/WebHostLib/templates/host_room.html
+++ b/WebHostLib/templates/host_room.html
@@ -1,5 +1,5 @@
{% extends 'layout.html' %}
-
+{% import "macros.html" as macros %}
{% block head %}
Multiworld {{ room.id|suuid }}
@@ -18,6 +18,10 @@
This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue
later,
you can simply refresh this page and the server will be started again.
+ {% if room.last_port %}
+ You can connect to this room by using '/connect berserkermulti.world:{{ room.last_port }}'
+ in the client. {% endif %}
+ {{ macros.list_patches_room(room.seed.patches, room) }}
{% if room.owner == session["_id"] %}
- {% endif %}
- Log:
+
+ Log:
+ {% endif %}
{% endblock %}
diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html
index 42a6fabd..fa714ad9 100644
--- a/WebHostLib/templates/landing.html
+++ b/WebHostLib/templates/landing.html
@@ -19,9 +19,15 @@
This is a randomizer for The Legend of Zelda: A
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html
index 425428fc..d7c9edec 100644
--- a/WebHostLib/templates/macros.html
+++ b/WebHostLib/templates/macros.html
@@ -6,3 +6,13 @@
{{ caller() }}
{%- endmacro %}
+{% macro list_patches_room(patches, room) %}
+ {% if patches %}
+