WebHost: On-Server rolling

This commit is contained in:
Fabian Dill 2020-08-02 22:11:52 +02:00
parent cfb8e2ce71
commit 22abd09087
13 changed files with 330 additions and 87 deletions

View File

@ -216,11 +216,9 @@ def main(args, seed=None):
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '') 'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
outfilepname = f'_T{team + 1}' if world.teams > 1 else '' outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
if world.players > 1: outfilepname += f'_P{player}'
outfilepname += f'_P{player}' outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
if world.players > 1 or world.teams > 1: team] != 'Player%d' % player else ''
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], outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player],
world.difficulty_adjustments[player], world.difficulty_adjustments[player],
world.mode[player], world.goal[player], world.mode[player], world.goal[player],

View File

@ -103,27 +103,27 @@ def main(args=None, callback = ERmain):
# set up logger # set up logger
if args.loglevel: if args.loglevel:
erargs.loglevel = args.loglevel erargs.loglevel = args.loglevel
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.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 = ''
if args.log_output_path: 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 = logging.getLogger("stderr")
log.addHandler(logging.StreamHandler()) log.addHandler(logging.StreamHandler())
sys.stderr = LoggerWriter(log.error) sys.stderr = LoggerWriter(log.error)

View File

@ -42,6 +42,7 @@ app.config["PONY"] = {
'filename': os.path.abspath('db.db3'), 'filename': os.path.abspath('db.db3'),
'create_db': True 'create_db': True
} }
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "simple" app.config["CACHE_TYPE"] = "simple"
app.autoversion = True app.autoversion = True
av = Autoversion(app) av = Autoversion(app)
@ -133,5 +134,6 @@ def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'), return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon') 'favicon.ico', mimetype='image/vnd.microsoft.icon')
from WebHostLib.customserver import run_server_process 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

View File

@ -1,5 +1,5 @@
import zipfile import zipfile
from typing import *
from flask import request, flash, redirect, url_for, session, render_template from flask import request, flash, redirect, url_for, session, render_template
@ -11,9 +11,11 @@ banned_zip_contents = (".sfc",)
def allowed_file(filename): def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip")) return filename.endswith(('.txt', ".yaml", ".zip"))
from Mystery import roll_settings from Mystery import roll_settings
from Utils import parse_yaml from Utils import parse_yaml
@app.route('/mysterycheck', methods=['GET', 'POST']) @app.route('/mysterycheck', methods=['GET', 'POST'])
def mysterycheck(): def mysterycheck():
if request.method == 'POST': if request.method == 'POST':
@ -21,46 +23,56 @@ def mysterycheck():
if 'file' not in request.files: if 'file' not in request.files:
flash('No file part') flash('No file part')
else: else:
options = {}
file = request.files['file'] file = request.files['file']
# if user does not select file, browser also options = get_yaml_data(file)
# submit an empty part without filename if type(options) == str:
if file.filename == '': flash(options)
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)
else: 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") 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

150
WebHostLib/generate.py Normal file
View File

@ -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/<race>', 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/<int:patch_id>/<suuid:room_id>")
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/<suuid:seed_id>")
def download_spoiler(seed_id):
return Response(Seed.get(id=seed_id).spoiler[3:], mimetype="text/plain")
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
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

View File

@ -4,3 +4,4 @@ waitress>=1.4.4
flask-caching>=1.9.0 flask-caching>=1.9.0
Flask-Autoversion>=0.2.0 Flask-Autoversion>=0.2.0
Flask-Compress>=1.5.0 Flask-Compress>=1.5.0
Flask-Limiter>=1.3.1

View File

@ -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();
});
});

View File

@ -6,7 +6,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% for filename, resulttext in results.items() %} {% for filename, resulttext in results.items() %}
<span>{{ filename }}: {{ resulttext }}</span><br> <span>{{ filename }}: {{ "Looks ok" if resulttext == True else resulttext }}</span><br>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'layout.html' %}
{% block head %}
{{ super() }}
<title>Generate Game</title>
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("uploads.css") }}"/>
<script type="application/ecmascript" src="{{ static_autoversion("generate.js") }}"></script>
{% endblock %}
{% block body %}
<div id="uploads-wrapper">
<div id="uploads" class="main-content">
<h3>Upload YAML(s){% if race %} (Race Mode){% endif %}</h3>
<p>
This page accepts a yaml file containing generator options.
You can find a documented example at <a
href="https://raw.githubusercontent.com/Berserker66/MultiWorld-Utilities/master/easy.yaml">easy.yaml</a>.
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 <a href="{{ url_for("generate", race=True) }}">Race Mode</a> to create a game without
spoiler log.
{%- endif -%}
</p>
<p>
After generation is complete, you will have the option to download a patch file.
This patch file can be opened with the <a
href="https://github.com/Berserker66/MultiWorld-Utilities/releases">Client</a> to create a rom file.
In-Browser patching will come.
</p>
<div id="uploads-form-wrapper">
<form id="upload-form" method="post" enctype="multipart/form-data">
<input id="file-input" type="file" name="file">
</form>
<button id="upload-button">Upload</button>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% import "macros.html" as macros %}
{% block head %} {% block head %}
<title>Multiworld {{ room.id|suuid }}</title> <title>Multiworld {{ room.id|suuid }}</title>
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("host_room.css") }}"/> <link rel="stylesheet" type="text/css" href="{{ static_autoversion("host_room.css") }}"/>
@ -18,6 +18,10 @@
This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue
later, later,
you can simply refresh this page and the server will be started again.<br> you can simply refresh this page and the server will be started again.<br>
{% if room.last_port %}
You can connect to this room by using '/connect berserkermulti.world:{{ room.last_port }}'
in the <a href="https://github.com/Berserker66/MultiWorld-Utilities/releases">client</a>.<br>{% endif %}
{{ macros.list_patches_room(room.seed.patches, room) }}
{% if room.owner == session["_id"] %} {% if room.owner == session["_id"] %}
<form method=post> <form method=post>
<div class="form-group"> <div class="form-group">
@ -26,15 +30,15 @@
placeholder="Server Command. /help to list them, list gets appended to log."> placeholder="Server Command. /help to list them, list gets appended to log.">
</div> </div>
</form> </form>
{% endif %}
Log: Log:
<div id="logger"></div> <div id="logger"></div>
<script> <script>
let xmlhttp = new XMLHttpRequest(); let xmlhttp = new XMLHttpRequest();
let url = '{{ url_for('display_log', room = room.id) }}'; let url = '{{ url_for('display_log', room = room.id) }}';
xmlhttp.onreadystatechange = function () { xmlhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) { if (this.readyState === 4 && this.status === 200) {
document.getElementById("logger").innerText = this.responseText; document.getElementById("logger").innerText = this.responseText;
} }
}; };
@ -47,5 +51,6 @@
window.setTimeout(request_new, 1000); window.setTimeout(request_new, 1000);
window.setInterval(request_new, 10000); window.setInterval(request_new, 10000);
</script> </script>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -19,9 +19,15 @@
</p> </p>
</div> </div>
<div id="landing-buttons"> <div id="landing-buttons">
<a href="uploads"> <a href="{{ url_for("generate") }}">
<button>Start Playing</button> <button>Start Playing</button>
</a> </a>
<a href="{{ url_for("uploads") }}">
<button>Upload Multiworld</button>
</a>
<a href="{{ url_for("mysterycheck") }}">
<button>Test YAML Config</button>
</a>
</div> </div>
<div id="landing-body"> <div id="landing-body">
<p>This is a <span data-tooltip="Allegedly.">randomizer</span> for The Legend of Zelda: A <p>This is a <span data-tooltip="Allegedly.">randomizer</span> for The Legend of Zelda: A

View File

@ -6,3 +6,13 @@
{{ caller() }} {{ caller() }}
</ul> </ul>
{%- endmacro %} {%- endmacro %}
{% macro list_patches_room(patches, room) %}
{% if patches %}
<ul>
{% for patch in patches %}
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
Patch for player {{ patch.player }} - {{ room.seed.multidata["names"][0][patch.player-1] }}</a></li>
{% endfor %}
</ul>
{% endif %}
{%- endmacro -%}

View File

@ -21,25 +21,33 @@
<td>Created:&nbsp;</td> <td>Created:&nbsp;</td>
<td id="creation-time" data-creation-time="{{ seed.creation_time }}"></td> <td id="creation-time" data-creation-time="{{ seed.creation_time }}"></td>
</tr> </tr>
{% if seed.spoiler %}
<tr>
<td>Spoiler:&nbsp;</td>
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
</tr>
{% endif %}
<tr> <tr>
<td>Players:&nbsp;</td> <td>Players:&nbsp;</td>
<td> <td>
<ul> <ul>
{% for team in seed.multidata["names"] %} {% for team in seed.multidata["names"] %}
<li>Team #{{ loop.index }} - {{ team | length }} <li>Team #{{ loop.index }} - {{ team | length }}
<ul> <ul>
{% for player in team %} {% for player in team %}
<li>{{ player }}</li> <li>
{% endfor %} <a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=loop.index) }}">{{ player }}</a>
</ul> </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul> </li>
</td> {% endfor %}
</tr> </ul>
<tr> </td>
<td>Rooms:&nbsp;</td> </tr>
<td> <tr>
<td>Rooms:&nbsp;</td>
<td>
{% call macros.list_rooms(rooms) %} {% call macros.list_rooms(rooms) %}
<li> <li>
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a> <a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>