state sync, many changes:
* separated css/js/rule files to pak file (glorified zip) to reduce full rebuilds * implemented build cache * some frontend UI spiffing up
This commit is contained in:
@@ -8,8 +8,9 @@ import threading
|
||||
import webview
|
||||
|
||||
import flask
|
||||
import fs.tree
|
||||
|
||||
from pylocal import core, actions, desktop, dev, items, tick
|
||||
from pylocal import core, actions, desktop, dev, gamedata, items, tick
|
||||
|
||||
core.desktop_mode = True
|
||||
sig_exit = threading.Event()
|
||||
@@ -24,6 +25,7 @@ def index():
|
||||
core.render_base_context()
|
||||
core.base_context["scripts"].insert(0, core.app.url_for("static", filename="js/desktop-structuredclone.js"))
|
||||
core.base_context["scripts"].insert(1, core.app.url_for("static", filename="js/seagull-desktop.js"))
|
||||
gamedata.vfs.copy_out("templates/main_page.j2", dest=core.path_appdir.as_posix())
|
||||
return flask.render_template("main_page.j2", **core.base_context)
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -37,6 +39,9 @@ if __name__ == "__main__":
|
||||
storage_dir = pathlib.Path(os.environ["HOME"]) / ".local/share/seagull"
|
||||
desktop.path_storagedir = storage_dir
|
||||
|
||||
gamedata.vfs.load_data_source("basepak")
|
||||
gamedata.vfs.load_data_source("seagull.pak", proto="zip")
|
||||
|
||||
if argo.debug:
|
||||
desktop.api.debug_mode = True
|
||||
storage_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
@@ -10,7 +10,7 @@ import threading
|
||||
import flask
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
from pylocal import core, actions, dev, items, tick
|
||||
from pylocal import core, actions, dev, gamedata, items, tick
|
||||
|
||||
sig_exit = threading.Event()
|
||||
|
||||
|
@@ -18,7 +18,9 @@ log.critical(path_appdir)
|
||||
|
||||
desktop_mode = False
|
||||
|
||||
app = flask.Flask("seagull-game", root_path=path_appdir)
|
||||
from . import gamedata
|
||||
|
||||
app = flask.Flask("seagull-game", root_path=path_appdir, template_folder="templates", static_folder="static")
|
||||
orig_url_for = app.url_for
|
||||
|
||||
xml_namespaces = {
|
||||
@@ -34,12 +36,15 @@ xml_namespaces = {
|
||||
|
||||
def url_for_override(endpoint, *posargs, _anchor=None, _method=None, _scheme=None, _external=None, self=app, **values):
|
||||
if endpoint == "static":
|
||||
# bandaid for #1
|
||||
if not os.path.exists(path_appdir / "static" / values["filename"]):
|
||||
if not gamedata.vfs.exists(f"static/{values["filename"]}"):
|
||||
log.warning("requested {0} from local file, but it doesn't exist in this container. Redirecting to CDN...\n".format(values["filename"]))
|
||||
return "https://cdn.otl-hga.net/seagull/" + values["filename"]
|
||||
|
||||
return orig_url_for(endpoint, *posargs, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values)
|
||||
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)
|
||||
|
||||
app.url_for = url_for_override
|
||||
|
||||
@@ -48,7 +53,8 @@ base_context_live = False
|
||||
|
||||
@app.route("/dialog/<dialog>")
|
||||
def render_dialog(dialog):
|
||||
if os.path.exists(path_appdir / f"templates/{dialog}.j2"):
|
||||
if gamedata.vfs.exists(f"templates/{dialog}.j2"):
|
||||
gamedata.vfs.copy_out(f"templates/{dialog}.j2", dest=path_appdir.as_posix())
|
||||
return flask.render_template(f"{dialog}.j2")
|
||||
else:
|
||||
return "", 404
|
||||
|
@@ -5,8 +5,10 @@ from . import core
|
||||
path_storagedir = pathlib.Path()
|
||||
|
||||
class JS_API:
|
||||
def __init__(self):
|
||||
self.debug_mode = False
|
||||
debug_mode = False
|
||||
|
||||
def __init__(self, debug_mode=False):
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
def load_data(self, key):
|
||||
if not (path_storagedir / key).exists():
|
||||
|
79
app/pylocal/gamedata.py
Normal file
79
app/pylocal/gamedata.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import typing
|
||||
import tempfile
|
||||
|
||||
import fs
|
||||
import fs.base
|
||||
import fs.copy
|
||||
import fs.osfs
|
||||
|
||||
from . import core
|
||||
|
||||
class GameVFSHandler(object):
|
||||
vfs = None
|
||||
log = logging.getLogger().getChild("vfs")
|
||||
proto_handlers = {}
|
||||
|
||||
def _osfs_handle(self, path):
|
||||
if self.osfs_cwd.exists(path):
|
||||
return self.osfs_cwd.opendir(path)
|
||||
elif self.osfs_appdir.exists(path):
|
||||
return self.osfs_appdir.opendir(path)
|
||||
else:
|
||||
raise FileNotFoundError
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.vfs = fs.open_fs("mem://")
|
||||
self.proto_handlers["osfs"] = self._osfs_handle
|
||||
self.osfs_cwd = fs.osfs.OSFS(os.getcwd())
|
||||
self.osfs_appdir = fs.osfs.OSFS(core.path_appdir.as_posix())
|
||||
self.pth_temp = pathlib.Path(tempfile.mkdtemp())
|
||||
self.osfs_temp = fs.osfs.OSFS(self.pth_temp.as_posix())
|
||||
|
||||
def __del__(self):
|
||||
if self.pth_temp and self.pth_temp.exists():
|
||||
shutil.rmtree(self.pth_temp)
|
||||
|
||||
def __getattr__(self, name: str) -> typing.Any:
|
||||
try:
|
||||
return getattr(self.vfs, name)
|
||||
except:
|
||||
raise
|
||||
|
||||
def load_data_source(self, source: pathlib.Path | fs.base.FS | typing.Text, proto="osfs"):
|
||||
print(f"data source: {source} ({proto})")
|
||||
assert self.vfs is not None
|
||||
if isinstance(source, pathlib.Path):
|
||||
assert isinstance(source, pathlib.Path) # for linter
|
||||
self.log.info(f"loading vfs source: {source.as_posix()}")
|
||||
if proto in self.proto_handlers:
|
||||
fs_source = self.proto_handlers[proto](source.as_posix())
|
||||
fs.copy.copy_fs(fs_source, self.vfs)
|
||||
else:
|
||||
fs_source = fs.open_fs(f"{proto}:/{source.as_posix()}")
|
||||
fs.copy.copy_fs(fs_source, self.vfs)
|
||||
else:
|
||||
if proto in self.proto_handlers:
|
||||
fs_source = self.proto_handlers[proto](source)
|
||||
else:
|
||||
fs_source = fs.open_fs(f"{proto}://{source}")
|
||||
self.log.info(f"loading vfs source: {fs_source} (pyfilesystem2 handler)")
|
||||
fs.copy.copy_fs(fs_source, self.vfs)
|
||||
|
||||
def copy_out(self, filepath, dest=None):
|
||||
if not dest:
|
||||
self.osfs_temp.makedirs(pathlib.Path(filepath).parent.as_posix(), recreate=True)
|
||||
fs.copy.copy_file(self.vfs, filepath, self.osfs_temp, filepath)
|
||||
return self.pth_temp / filepath
|
||||
else:
|
||||
pth_dest = pathlib.Path(dest)
|
||||
pth_file = pathlib.Path(filepath)
|
||||
osfs_dest = fs.osfs.OSFS(dest)
|
||||
osfs_dest.makedirs(pth_file.parent.as_posix(), recreate=True)
|
||||
fs.copy.copy_file(self.vfs, filepath, dest, filepath)
|
||||
return (pth_dest / filepath).as_posix()
|
||||
|
||||
vfs = GameVFSHandler()
|
@@ -4,17 +4,18 @@ import subprocess
|
||||
|
||||
import lxml.etree as xmltree
|
||||
|
||||
from . import core
|
||||
from . import core, gamedata
|
||||
|
||||
valid_resources = [
|
||||
"food", "shinies", "psi" # early game
|
||||
]
|
||||
|
||||
rant_env = os.environ.copy()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "rant").as_posix()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "basepak/rant").as_posix()
|
||||
|
||||
fd_item_schema = xmltree.parse(core.path_appdir / "rules/schemas/items.xsd")
|
||||
item_schema = xmltree.XMLSchema(fd_item_schema)
|
||||
pth_item_schema = core.path_appdir / "basepak/rules/schemas/items.xsd"
|
||||
doc_item_schema = xmltree.parse(pth_item_schema.as_posix())
|
||||
item_schema = xmltree.XMLSchema(doc_item_schema)
|
||||
item_schema_parser = xmltree.XMLParser(schema=item_schema)
|
||||
|
||||
def generate_item_description(resource, target):
|
||||
@@ -22,7 +23,8 @@ def generate_item_description(resource, target):
|
||||
rant_path = core.path_appdir / "opt/rant/bin/rant"
|
||||
else:
|
||||
rant_path = "rant" # rely on OS PATH
|
||||
proc_rant = subprocess.run([rant_path, (core.path_appdir / f"rant/{resource}/{target}.rant").as_posix()], env=rant_env, capture_output=True)
|
||||
pth_rantfile = gamedata.vfs.copy_out(f"rant/{resource}/{target}.rant")
|
||||
proc_rant = subprocess.run([rant_path, (core.path_appdir / pth_rantfile).as_posix()], env=rant_env, capture_output=True)
|
||||
if proc_rant.stderr:
|
||||
core.log.warning("rant is throwing up:\n" + proc_rant.stderr.decode())
|
||||
return proc_rant.stdout.decode().strip()
|
||||
@@ -30,7 +32,7 @@ def generate_item_description(resource, target):
|
||||
def generate_item_list(resource, target, min, max, storybeat=0):
|
||||
count = random.randint(min, max)
|
||||
result = []
|
||||
rulefile = xmltree.parse(core.path_appdir / f"rules/items/{target}.xml", item_schema_parser)
|
||||
rulefile = xmltree.parse(gamedata.vfs.open(f"/rules/items/{target}.xml"), item_schema_parser)
|
||||
ruleset = rulefile.getroot()
|
||||
resource_rules = []
|
||||
for res_rule in ruleset.iter(f"{{seagull:rules/items}}{resource.title()}"):
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
import subprocess
|
||||
@@ -6,12 +7,15 @@ import flask
|
||||
|
||||
from . import core, items, jsonizer
|
||||
|
||||
rant_env = os.environ.copy()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "basepak/rant").as_posix()
|
||||
|
||||
def generate_flavor_text():
|
||||
if core.desktop_mode:
|
||||
rant_path = core.path_appdir / "opt/rant/bin/rant"
|
||||
else:
|
||||
rant_path = "rant" # rely on OS PATH
|
||||
proc_rant = subprocess.run([rant_path, (core.path_appdir / "rant/flavor.rant").as_posix()], capture_output=True)
|
||||
proc_rant = subprocess.run([rant_path, (core.path_appdir / "basepak/rant/flavor.rant").as_posix()], env=rant_env, capture_output=True)
|
||||
return proc_rant.stdout.decode()
|
||||
|
||||
class TickEvent(object):
|
||||
@@ -61,4 +65,12 @@ def tick():
|
||||
case _:
|
||||
core.log.warning("undefined tick: {0}".format(result["event_type"]))
|
||||
|
||||
return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json")
|
||||
return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json")
|
||||
|
||||
@core.app.route("/tick/colony", methods=["POST"])
|
||||
def tick_colony() -> flask.Response:
|
||||
req = flask.request.json
|
||||
if not req:
|
||||
return flask.make_response("Bad request", 400)
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
Flask==3.1.1
|
||||
gevent==25.5.1
|
||||
lxml>=6.0.0
|
||||
Flask>=3.1.1
|
||||
gevent>=25.5.1
|
||||
lxml>=6.0.0
|
||||
fs>=2.4.16
|
@@ -49,7 +49,14 @@
|
||||
</div>
|
||||
|
||||
<div id="main-content">
|
||||
<div id="main-day-stats">It has been <span id="main-day-counter">a cosmically unknowable number of</span> days.</div>
|
||||
<div id="main-header">
|
||||
<div id="main-day-stats">It has been <span id="main-day-counter">a cosmically unknowable number of</span> days.</div>
|
||||
<div id="main-button-bar">
|
||||
<button id="button-charsheet" class="main-bar">📊</button>
|
||||
<button id="button-settings" class="main-bar">⚙️</button>
|
||||
<button id="button-about" class="main-bar">ℹ️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-log"></div>
|
||||
</div>
|
||||
|
||||
|
Reference in New Issue
Block a user