2025-10-05 11:11:54 -07:00
|
|
|
import glob
|
2025-07-29 12:50:35 -07:00
|
|
|
import logging
|
2023-02-24 12:39:18 -08:00
|
|
|
import os
|
2025-07-29 12:50:35 -07:00
|
|
|
import pathlib
|
2023-02-24 12:39:18 -08:00
|
|
|
import sys
|
2023-01-28 21:08:47 -08:00
|
|
|
|
|
|
|
import flask
|
2025-09-29 20:31:42 -07:00
|
|
|
from flask_cors import CORS
|
2025-10-05 11:11:54 -07:00
|
|
|
import lxml.etree as xmltree
|
2023-01-28 21:08:47 -08:00
|
|
|
|
2025-07-29 12:50:35 -07:00
|
|
|
log = logging.getLogger()
|
|
|
|
pipe_stderr = logging.StreamHandler(sys.stderr)
|
|
|
|
pipe_stderr.setLevel(logging.DEBUG)
|
|
|
|
log.addHandler(pipe_stderr)
|
|
|
|
|
|
|
|
if getattr(sys, "frozen", False):
|
|
|
|
path_appdir = pathlib.Path(sys._MEIPASS)
|
|
|
|
else:
|
|
|
|
path_appdir = pathlib.Path.cwd()
|
|
|
|
log.critical(path_appdir)
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
## \internal
|
|
|
|
# \brief Signals whether we are a desktop application (as opposed to a Docker container).
|
2025-07-29 12:50:35 -07:00
|
|
|
desktop_mode = False
|
|
|
|
|
2025-09-02 16:44:28 -07:00
|
|
|
from . import gamedata
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
## \internal
|
|
|
|
# \brief The Flask instance. See <a href="https://flask.palletsprojects.com/en/stable/api/">Flask documentation</a>.
|
2025-09-02 16:44:28 -07:00
|
|
|
app = flask.Flask("seagull-game", root_path=path_appdir, template_folder="templates", static_folder="static")
|
2025-09-29 20:31:42 -07:00
|
|
|
CORS(app)
|
2023-02-24 12:39:18 -08:00
|
|
|
orig_url_for = app.url_for
|
|
|
|
|
2025-08-07 14:10:20 -07:00
|
|
|
xml_namespaces = {
|
2025-10-05 11:11:54 -07:00
|
|
|
"rule": "seagull:rules",
|
2025-08-22 13:01:58 -07:00
|
|
|
"items": "seagull:rules/items",
|
|
|
|
"upgrades": "seagull:rules/upgrades"
|
2025-08-07 14:10:20 -07:00
|
|
|
}
|
|
|
|
|
2023-02-28 14:21:03 -08:00
|
|
|
#REDIS_HOST="stub-implementation.example.net"
|
|
|
|
#REDIS_PORT=6379
|
|
|
|
#REDIS_USER="seagull"
|
|
|
|
#REDIS_PASS="i am not a real password"
|
|
|
|
#state_cache = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, username=REDIS_USER, password=REDIS_PASS)
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
# ham5 standing by...
|
|
|
|
|
|
|
|
## \internal
|
|
|
|
# \brief Returns CDN URLs for files not present within the container itself, or copies VFS data out so Flask etc can use it.
|
2023-02-24 12:39:18 -08:00
|
|
|
def url_for_override(endpoint, *posargs, _anchor=None, _method=None, _scheme=None, _external=None, self=app, **values):
|
|
|
|
if endpoint == "static":
|
2025-09-02 16:44:28 -07:00
|
|
|
if not gamedata.vfs.exists(f"static/{values["filename"]}"):
|
2025-07-29 12:50:35 -07:00
|
|
|
log.warning("requested {0} from local file, but it doesn't exist in this container. Redirecting to CDN...\n".format(values["filename"]))
|
2023-02-24 12:39:18 -08:00
|
|
|
return "https://cdn.otl-hga.net/seagull/" + values["filename"]
|
2025-09-02 16:44:28 -07:00
|
|
|
else:
|
|
|
|
gamedata.vfs.copy_out(f"static/{values["filename"]}", dest=path_appdir.as_posix())
|
|
|
|
return orig_url_for(endpoint, *posargs, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values)
|
|
|
|
else:
|
|
|
|
print(endpoint)
|
|
|
|
return orig_url_for(endpoint, *posargs, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values)
|
2023-02-24 12:39:18 -08:00
|
|
|
|
|
|
|
app.url_for = url_for_override
|
2023-01-28 21:08:47 -08:00
|
|
|
|
2025-10-05 11:11:54 -07:00
|
|
|
schemas = []
|
|
|
|
pth_common_rule_schema = path_appdir / "basepak/rules/schemas/rules.xsd"
|
|
|
|
doc_common_rule_schema = xmltree.parse(pth_common_rule_schema.as_posix())
|
|
|
|
common_rule_schema = xmltree.XMLSchema(doc_common_rule_schema)
|
|
|
|
|
|
|
|
## \brief Validates all XML files in gamedata.
|
|
|
|
# \internal
|
|
|
|
#
|
|
|
|
# This is typically run on program startup.
|
|
|
|
def validate_xml_files():
|
|
|
|
for schema in schemas:
|
|
|
|
for file in gamedata.vfs.vfs.glob(schema[1]):
|
|
|
|
pth_xml = file.path[1:]
|
|
|
|
fd_xml = gamedata.vfs.open(pth_xml)
|
|
|
|
doc = xmltree.parse(fd_xml)
|
|
|
|
|
|
|
|
if not schema[0].validate(doc):
|
|
|
|
log.error(f"Bogus XML document: {pth_xml}")
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
## \internal
|
|
|
|
# \brief Base Flask rendering context. Generated with render_base_context().
|
2023-01-28 21:08:47 -08:00
|
|
|
base_context = {}
|
|
|
|
base_context_live = False
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
## \brief Renders a dialog template and sends it to the client.
|
|
|
|
# \param dialog The dialog to render.
|
|
|
|
# \api{GET} /dialog/`<dialog>`
|
2025-08-22 13:01:58 -07:00
|
|
|
@app.route("/dialog/<dialog>")
|
|
|
|
def render_dialog(dialog):
|
2025-09-02 16:44:28 -07:00
|
|
|
if gamedata.vfs.exists(f"templates/{dialog}.j2"):
|
|
|
|
gamedata.vfs.copy_out(f"templates/{dialog}.j2", dest=path_appdir.as_posix())
|
2025-09-29 20:31:42 -07:00
|
|
|
if gamedata.vfs.exists(f"static/js/dlg-{dialog}.js"):
|
|
|
|
gamedata.vfs.copy_out(f"static/js/dlg-{dialog}.js", dest=path_appdir.as_posix())
|
2025-08-22 13:01:58 -07:00
|
|
|
return flask.render_template(f"{dialog}.j2")
|
|
|
|
else:
|
|
|
|
return "", 404
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
## \brief Prepares the base rendering context for Flask to serve our content.
|
2023-01-28 21:08:47 -08:00
|
|
|
def render_base_context():
|
|
|
|
global base_context
|
|
|
|
global base_context_live
|
|
|
|
|
2023-02-13 13:26:50 -08:00
|
|
|
base_context["svchost"] = flask.request.host
|
2023-01-28 21:08:47 -08:00
|
|
|
|
|
|
|
domain_components = flask.request.host.split(".")
|
|
|
|
base_domain = ".".join(domain_components[-2:])
|
|
|
|
|
2025-10-05 11:11:54 -07:00
|
|
|
gamedata.vfs.copy_dir("static", dest=path_appdir.as_posix())
|
2025-09-29 20:31:42 -07:00
|
|
|
|
2023-01-28 21:08:47 -08:00
|
|
|
# all this wind up for...
|
|
|
|
if base_domain == "otl-hga.net": # production, use assets from S3
|
|
|
|
base_context["styles"] = ["https://cdn.otl-hga.net/seagull/css/seagull.css"]
|
2025-09-29 20:31:42 -07:00
|
|
|
base_context["scripts"] = ["https://cdn.otl-hga.net/seagull/js/seagull.js", "https://cdn.otl-hga.net/seagull/js/konami.js"]
|
2023-01-28 21:08:47 -08:00
|
|
|
base_context["seagull_pic"] = "https://cdn.otl-hga.net/seagull/image/seagull.jpg"
|
|
|
|
else: # dev, serve files from here
|
2023-02-27 17:12:27 -08:00
|
|
|
#print(base_domain)
|
2023-02-24 12:39:18 -08:00
|
|
|
base_context["styles"] = [app.url_for("static", filename="css/seagull.css")]
|
2025-09-29 20:31:42 -07:00
|
|
|
base_context["scripts"] = [(app.url_for("static", filename="js/konami.js"), True), (app.url_for("static", filename="js/seagull.js"), True)]
|
2023-02-24 12:39:18 -08:00
|
|
|
base_context["seagull_pic"] = app.url_for("static", filename="image/seagull.jpg")
|
2023-01-28 21:08:47 -08:00
|
|
|
|
2023-02-13 12:57:30 -08:00
|
|
|
base_context_live = True
|
|
|
|
|
2025-09-29 20:31:42 -07:00
|
|
|
## \brief Returns OK. Useful for health checks.
|
|
|
|
# \api{GET} /core/ping
|
2023-02-13 12:57:30 -08:00
|
|
|
@app.route("/core/ping")
|
2025-07-29 12:50:35 -07:00
|
|
|
def healthcheck_ping():
|
2025-09-29 20:31:42 -07:00
|
|
|
return flask.Response("OK", content_type="text/plain")
|
|
|
|
|
|
|
|
## \brief Informs the game we're about to request a file from JavaScript.
|
|
|
|
# \internal
|
|
|
|
# \api{POST} /core/ready_file
|
|
|
|
# \apidata Plaintext path to the intended file.
|
|
|
|
@app.route("/core/ready_file", methods=["POST"])
|
|
|
|
def ready_file():
|
|
|
|
gamedata.vfs.copy_out(flask.request.data)
|
2023-02-13 13:26:50 -08:00
|
|
|
return flask.Response("OK", content_type="text/plain")
|