WebHost: job pool based world generation
This commit is contained in:
parent
52cf99c5c8
commit
39f85aa291
|
@ -24,6 +24,7 @@ app.jinja_env.filters['any'] = any
|
||||||
app.jinja_env.filters['all'] = all
|
app.jinja_env.filters['all'] = all
|
||||||
|
|
||||||
app.config["SELFHOST"] = True
|
app.config["SELFHOST"] = True
|
||||||
|
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||||
app.config["SELFLAUNCH"] = True
|
app.config["SELFLAUNCH"] = True
|
||||||
app.config["DEBUG"] = False
|
app.config["DEBUG"] = False
|
||||||
app.config["PORT"] = 80
|
app.config["PORT"] = 80
|
||||||
|
|
|
@ -4,8 +4,9 @@ import multiprocessing
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
|
import time
|
||||||
|
|
||||||
from pony.orm import db_session, select
|
from pony.orm import db_session, select, commit
|
||||||
|
|
||||||
|
|
||||||
class CommonLocker():
|
class CommonLocker():
|
||||||
|
@ -23,7 +24,6 @@ class AlreadyRunningException(Exception):
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
class Locker(CommonLocker):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
|
@ -42,7 +42,6 @@ if sys.platform == 'win32':
|
||||||
else: # unix
|
else: # unix
|
||||||
import fcntl
|
import fcntl
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
class Locker(CommonLocker):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
try:
|
try:
|
||||||
|
@ -66,24 +65,70 @@ def launch_room(room: Room, config: dict):
|
||||||
multiworld.start()
|
multiworld.start()
|
||||||
|
|
||||||
|
|
||||||
def autohost(config: dict):
|
def handle_generation_success(seed_id):
|
||||||
import time
|
logging.info(f"Generation finished for seed {seed_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_generation_failure(result: BaseException):
|
||||||
|
try: # hacky way to get the full RemoteTraceback
|
||||||
|
raise result
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||||
|
@db_session
|
||||||
|
def handle_fail(result: BaseException):
|
||||||
|
generation.state = STATE_ERROR
|
||||||
|
handle_generation_failure(result)
|
||||||
|
|
||||||
|
logging.info(f"Generating {generation.id} for {len(generation.options)} players")
|
||||||
|
|
||||||
|
pool.apply_async(gen_game, (generation.options,),
|
||||||
|
{"race": generation.meta["race"], "sid": generation.id, "owner": generation.owner},
|
||||||
|
handle_generation_success, handle_generation_failure)
|
||||||
|
generation.state = STATE_STARTED
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(pony_config: dict):
|
||||||
|
db.bind(**pony_config)
|
||||||
|
db.generate_mapping()
|
||||||
|
|
||||||
|
|
||||||
|
def autohost(config: dict):
|
||||||
def keep_running():
|
def keep_running():
|
||||||
try:
|
try:
|
||||||
with Locker("autohost"):
|
with Locker("autohost"):
|
||||||
logging.info("Starting autohost service")
|
|
||||||
# db.bind(**config["PONY"])
|
|
||||||
# db.generate_mapping(check_tables=False)
|
|
||||||
while 1:
|
|
||||||
time.sleep(3)
|
|
||||||
with db_session:
|
|
||||||
rooms = select(
|
|
||||||
room for room in Room if
|
|
||||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
|
||||||
for room in rooms:
|
|
||||||
launch_room(room, config)
|
|
||||||
|
|
||||||
|
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
||||||
|
initargs=(config["PONY"],)) as generator_pool:
|
||||||
|
with db_session:
|
||||||
|
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
|
||||||
|
|
||||||
|
if to_start:
|
||||||
|
logging.info("Resuming generation")
|
||||||
|
for generation in to_start:
|
||||||
|
sid = Seed.get(id=generation.id)
|
||||||
|
if sid:
|
||||||
|
generation.delete()
|
||||||
|
else:
|
||||||
|
launch_generator(generator_pool, generation)
|
||||||
|
|
||||||
|
commit()
|
||||||
|
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
time.sleep(0.50)
|
||||||
|
with db_session:
|
||||||
|
rooms = select(
|
||||||
|
room for room in Room if
|
||||||
|
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
||||||
|
for room in rooms:
|
||||||
|
launch_room(room, config)
|
||||||
|
to_start = select(
|
||||||
|
generation for generation in Generation if generation.state == STATE_QUEUED)
|
||||||
|
for generation in to_start:
|
||||||
|
launch_generator(generator_pool, generation)
|
||||||
except AlreadyRunningException:
|
except AlreadyRunningException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -117,5 +162,6 @@ class MultiworldInstance():
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
|
|
||||||
from .models import Room
|
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
||||||
from .customserver import run_server_process
|
from .customserver import run_server_process
|
||||||
|
from .generate import gen_game
|
||||||
|
|
|
@ -3,6 +3,7 @@ import tempfile
|
||||||
import random
|
import random
|
||||||
import zlib
|
import zlib
|
||||||
import json
|
import json
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
|
|
||||||
|
@ -32,57 +33,78 @@ def generate(race=False):
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return render_template("checkresult.html", results=results)
|
return render_template("checkresult.html", results=results)
|
||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||||
flash(f"Sorry, generating of multiworld is limited to {app.config['MAX_ROLL']} players for now. "
|
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
|
||||||
f"If you have a larger group, please generate it yourself and upload it.")
|
f"If you have a larger group, please generate it yourself and upload it.")
|
||||||
else:
|
else:
|
||||||
seed_id = gen(gen_options, race=race)
|
|
||||||
return redirect(url_for("view_seed", seed=seed_id))
|
gen = Generation(options={name: vars(options) for name, options in gen_options.items()},
|
||||||
|
# convert to json compatible
|
||||||
|
meta={"race": race, "owner": session["_id"].int}, state=STATE_QUEUED,
|
||||||
|
owner=session["_id"])
|
||||||
|
commit()
|
||||||
|
|
||||||
|
return redirect(url_for("wait_seed", seed=gen.id))
|
||||||
return render_template("generate.html", race=race)
|
return render_template("generate.html", race=race)
|
||||||
|
|
||||||
|
|
||||||
def gen(gen_options, race=False):
|
def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||||
target = tempfile.TemporaryDirectory()
|
target = tempfile.TemporaryDirectory()
|
||||||
with target:
|
playercount = len(gen_options)
|
||||||
playercount = len(gen_options)
|
seed = get_seed()
|
||||||
seed = get_seed()
|
random.seed(seed)
|
||||||
random.seed(seed)
|
|
||||||
|
|
||||||
if race:
|
if race:
|
||||||
random.seed() # reset to time-based random source
|
random.seed() # reset to time-based random source
|
||||||
|
|
||||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||||
|
|
||||||
erargs = parse_arguments(['--multi', str(playercount)])
|
erargs = parse_arguments(['--multi', str(playercount)])
|
||||||
erargs.seed = seed
|
erargs.seed = seed
|
||||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
|
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
|
||||||
erargs.create_spoiler = not race
|
erargs.create_spoiler = not race
|
||||||
erargs.race = race
|
erargs.race = race
|
||||||
erargs.skip_playthrough = race
|
erargs.skip_playthrough = race
|
||||||
erargs.outputname = seedname
|
erargs.outputname = seedname
|
||||||
erargs.outputpath = target.name
|
erargs.outputpath = target.name
|
||||||
erargs.teams = 1
|
erargs.teams = 1
|
||||||
erargs.progression_balancing = {}
|
erargs.progression_balancing = {}
|
||||||
erargs.create_diff = True
|
erargs.create_diff = True
|
||||||
|
|
||||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||||
for k, v in vars(settings).items():
|
for k, v in settings.items():
|
||||||
if v is not None:
|
if v is not None:
|
||||||
getattr(erargs, k)[player] = v
|
getattr(erargs, k)[player] = v
|
||||||
|
|
||||||
if not erargs.name[player]:
|
if not erargs.name[player]:
|
||||||
erargs.name[player] = os.path.split(playerfile)[-1].split(".")[0]
|
erargs.name[player] = os.path.split(playerfile)[-1].split(".")[0]
|
||||||
|
|
||||||
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
|
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
|
||||||
del (erargs.name)
|
del (erargs.name)
|
||||||
|
|
||||||
erargs.skip_progression_balancing = {player: not balanced for player, balanced in
|
erargs.skip_progression_balancing = {player: not balanced for player, balanced in
|
||||||
erargs.progression_balancing.items()}
|
erargs.progression_balancing.items()}
|
||||||
del (erargs.progression_balancing)
|
del (erargs.progression_balancing)
|
||||||
ERmain(erargs, seed)
|
ERmain(erargs, seed)
|
||||||
return upload_to_db(target.name)
|
|
||||||
|
return upload_to_db(target.name, owner, sid)
|
||||||
|
|
||||||
|
|
||||||
def upload_to_db(folder):
|
@app.route('/wait/<suuid:seed>')
|
||||||
|
def wait_seed(seed: UUID):
|
||||||
|
seed_id = seed
|
||||||
|
seed = Seed.get(id=seed_id)
|
||||||
|
if seed:
|
||||||
|
return redirect(url_for("view_seed", seed=seed_id))
|
||||||
|
generation = Generation.get(id=seed_id)
|
||||||
|
|
||||||
|
if not generation:
|
||||||
|
return "Generation not found."
|
||||||
|
elif generation.state == STATE_ERROR:
|
||||||
|
return "Generation failed, please retry."
|
||||||
|
return render_template("wait_seed.html", seed_id=seed_id)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_db(folder, owner, sid):
|
||||||
patches = set()
|
patches = set()
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
multidata = None
|
multidata = None
|
||||||
|
@ -99,9 +121,9 @@ def upload_to_db(folder):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(e)
|
flash(e)
|
||||||
if multidata:
|
if multidata:
|
||||||
commit() # commit patches
|
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, id=sid)
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
|
||||||
commit() # create seed
|
|
||||||
for patch in patches:
|
for patch in patches:
|
||||||
patch.seed = seed
|
patch.seed = seed
|
||||||
|
Generation.get(id=sid).delete()
|
||||||
|
commit()
|
||||||
return seed.id
|
return seed.id
|
||||||
|
|
|
@ -4,6 +4,10 @@ from pony.orm import *
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|
||||||
|
STATE_QUEUED = 0
|
||||||
|
STATE_STARTED = 1
|
||||||
|
STATE_ERROR = -1
|
||||||
|
|
||||||
|
|
||||||
class Patch(db.Entity):
|
class Patch(db.Entity):
|
||||||
id = PrimaryKey(int, auto=True)
|
id = PrimaryKey(int, auto=True)
|
||||||
|
@ -40,3 +44,11 @@ class Command(db.Entity):
|
||||||
id = PrimaryKey(int, auto=True)
|
id = PrimaryKey(int, auto=True)
|
||||||
room = Required(Room)
|
room = Required(Room)
|
||||||
commandtext = Required(str)
|
commandtext = Required(str)
|
||||||
|
|
||||||
|
|
||||||
|
class Generation(db.Entity):
|
||||||
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
|
owner = Required(UUID)
|
||||||
|
options = Required(Json, lazy=True)
|
||||||
|
meta = Required(Json)
|
||||||
|
state = Required(int, default=0, index=True)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% import "macros.html" as macros %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<title>Multiworld Seed {{ seed_id|suuid }} (generating...)</title>
|
||||||
|
<meta http-equiv="refresh" content="1">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("styles/view_seed.css") }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div id="wait-seed-wrapper">
|
||||||
|
<div class="main-content">
|
||||||
|
Waiting for game to generate, this page auto-refreshes to check.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue