WebHost: On-Server rolling
This commit is contained in:
parent
cfb8e2ce71
commit
22abd09087
8
Main.py
8
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],
|
||||
|
|
38
Mystery.py
38
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
Flask-Compress>=1.5.0
|
||||
Flask-Limiter>=1.3.1
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% for filename, resulttext in results.items() %}
|
||||
<span>{{ filename }}: {{ resulttext }}</span><br>
|
||||
{% for filename, resulttext in results.items() %}
|
||||
<span>{{ filename }}: {{ "Looks ok" if resulttext == True else resulttext }}</span><br>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% import "macros.html" as macros %}
|
||||
{% block head %}
|
||||
<title>Multiworld {{ room.id|suuid }}</title>
|
||||
<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
|
||||
later,
|
||||
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"] %}
|
||||
<form method=post>
|
||||
<div class="form-group">
|
||||
|
@ -26,15 +30,15 @@
|
|||
placeholder="Server Command. /help to list them, list gets appended to log.">
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
Log:
|
||||
|
||||
Log:
|
||||
<div id="logger"></div>
|
||||
<script>
|
||||
let xmlhttp = new XMLHttpRequest();
|
||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||
|
||||
xmlhttp.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
document.getElementById("logger").innerText = this.responseText;
|
||||
}
|
||||
};
|
||||
|
@ -47,5 +51,6 @@
|
|||
window.setTimeout(request_new, 1000);
|
||||
window.setInterval(request_new, 10000);
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -19,9 +19,15 @@
|
|||
</p>
|
||||
</div>
|
||||
<div id="landing-buttons">
|
||||
<a href="uploads">
|
||||
<a href="{{ url_for("generate") }}">
|
||||
<button>Start Playing</button>
|
||||
</a>
|
||||
<a href="{{ url_for("uploads") }}">
|
||||
<button>Upload Multiworld</button>
|
||||
</a>
|
||||
<a href="{{ url_for("mysterycheck") }}">
|
||||
<button>Test YAML Config</button>
|
||||
</a>
|
||||
</div>
|
||||
<div id="landing-body">
|
||||
<p>This is a <span data-tooltip="Allegedly.">randomizer</span> for The Legend of Zelda: A
|
||||
|
|
|
@ -6,3 +6,13 @@
|
|||
{{ caller() }}
|
||||
</ul>
|
||||
{%- 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 -%}
|
|
@ -21,25 +21,33 @@
|
|||
<td>Created: </td>
|
||||
<td id="creation-time" data-creation-time="{{ seed.creation_time }}"></td>
|
||||
</tr>
|
||||
{% if seed.spoiler %}
|
||||
<tr>
|
||||
<td>Spoiler: </td>
|
||||
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Players: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for team in seed.multidata["names"] %}
|
||||
<li>Team #{{ loop.index }} - {{ team | length }}
|
||||
<ul>
|
||||
{% for player in team %}
|
||||
<li>{{ player }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
<li>Team #{{ loop.index }} - {{ team | length }}
|
||||
<ul>
|
||||
{% for player in team %}
|
||||
<li>
|
||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=loop.index) }}">{{ player }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
{% call macros.list_rooms(rooms) %}
|
||||
<li>
|
||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||
|
|
Loading…
Reference in New Issue