desktop support
This commit is contained in:
		
							
								
								
									
										47
									
								
								app/desktop.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										47
									
								
								app/desktop.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
#!/usr/bin/env python
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import os
 | 
			
		||||
import pathlib
 | 
			
		||||
import sys
 | 
			
		||||
import threading
 | 
			
		||||
import webview
 | 
			
		||||
 | 
			
		||||
import flask
 | 
			
		||||
 | 
			
		||||
from pylocal import core, desktop, dev, tick
 | 
			
		||||
 | 
			
		||||
core.desktop_mode = True
 | 
			
		||||
sig_exit = threading.Event()
 | 
			
		||||
 | 
			
		||||
argp = argparse.ArgumentParser("seagull")
 | 
			
		||||
argp.add_argument("-d", "--debug", action="store_true")
 | 
			
		||||
argo = argp.parse_args()
 | 
			
		||||
 | 
			
		||||
@core.app.route("/")
 | 
			
		||||
def index():
 | 
			
		||||
    if not core.base_context_live:
 | 
			
		||||
        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"))
 | 
			
		||||
    return flask.render_template("main_page.j2", **core.base_context)
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    try:
 | 
			
		||||
        if sys.platform.startswith("win"):
 | 
			
		||||
            storage_dir = pathlib.Path(os.environ["APPDATA"]) / "seagull"
 | 
			
		||||
        elif sys.platform.startswith("darwin"): # macos
 | 
			
		||||
            storage_dir = pathlib.Path(os.environ["HOME"]) / "Library/Application Support/seagull"
 | 
			
		||||
        else:
 | 
			
		||||
            storage_dir = pathlib.Path(os.environ["HOME"]) / ".local/share/seagull"
 | 
			
		||||
        desktop.path_storagedir = pathlib.Path(storage_dir)
 | 
			
		||||
 | 
			
		||||
        if argo.debug:
 | 
			
		||||
            desktop.api.debug_mode = True
 | 
			
		||||
        storage_dir.mkdir(exist_ok=True, parents=True)
 | 
			
		||||
        webview.create_window("Seagull Game", core.app, js_api=desktop.api)
 | 
			
		||||
        webview.start(private_mode=False, storage_path=storage_dir.as_posix(), debug=True if argo.debug else False)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        core.log.info("Goodnight, moon ...")
 | 
			
		||||
        sig_exit.set()
 | 
			
		||||
        sys.exit(0)
 | 
			
		||||
							
								
								
									
										10
									
								
								app/index.wsgi
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										10
									
								
								app/index.wsgi
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -3,17 +3,22 @@
 | 
			
		||||
import gevent.monkey
 | 
			
		||||
gevent.monkey.patch_all()
 | 
			
		||||
 | 
			
		||||
import pathlib
 | 
			
		||||
import sys
 | 
			
		||||
import threading
 | 
			
		||||
 | 
			
		||||
import flask
 | 
			
		||||
from gevent.pywsgi import WSGIServer
 | 
			
		||||
 | 
			
		||||
from pylocal import core, tick
 | 
			
		||||
from pylocal import core, dev, tick
 | 
			
		||||
 | 
			
		||||
sig_exit = threading.Event()
 | 
			
		||||
 | 
			
		||||
@core.app.route("/")
 | 
			
		||||
def index():
 | 
			
		||||
    if not core.base_context_live:
 | 
			
		||||
        core.render_base_context()
 | 
			
		||||
        core.base_context["scripts"].append(core.app.url_for("static", filename="js/seagull-web.js"))
 | 
			
		||||
    return flask.render_template("main_page.j2", **core.base_context)
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
@@ -22,5 +27,6 @@ if __name__ == "__main__":
 | 
			
		||||
        http_server = WSGIServer(('', 80), core.app)
 | 
			
		||||
        http_server.serve_forever()
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        print("Goodnight, moon ...")
 | 
			
		||||
        core.log.info("Goodnight, moon ...")
 | 
			
		||||
        sig_exit.set()
 | 
			
		||||
        sys.exit(0)
 | 
			
		||||
@@ -1,10 +1,24 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import pathlib
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
import flask
 | 
			
		||||
import redis
 | 
			
		||||
 | 
			
		||||
app = flask.Flask("seagull-game", root_path="/app")
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
desktop_mode = False
 | 
			
		||||
 | 
			
		||||
app = flask.Flask("seagull-game", root_path=path_appdir)
 | 
			
		||||
orig_url_for = app.url_for
 | 
			
		||||
 | 
			
		||||
#REDIS_HOST="stub-implementation.example.net"
 | 
			
		||||
@@ -16,9 +30,8 @@ orig_url_for = app.url_for
 | 
			
		||||
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("/app/static/" + values["filename"]):
 | 
			
		||||
            sys.stderr.write("WARN:: requested {0} from local file, but it doesn't exist in this container. Redirecting to CDN...\n".format(values["filename"]))
 | 
			
		||||
            sys.stderr.flush()
 | 
			
		||||
        if not os.path.exists(path_appdir / "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)
 | 
			
		||||
@@ -51,5 +64,5 @@ def render_base_context():
 | 
			
		||||
    base_context_live = True
 | 
			
		||||
 | 
			
		||||
@app.route("/core/ping")
 | 
			
		||||
def aws_healthcheck_ping():
 | 
			
		||||
def healthcheck_ping():
 | 
			
		||||
    return flask.Response("OK", content_type="text/plain")
 | 
			
		||||
							
								
								
									
										32
									
								
								app/pylocal/desktop.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/pylocal/desktop.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import pathlib
 | 
			
		||||
 | 
			
		||||
from . import core
 | 
			
		||||
 | 
			
		||||
path_storagedir = pathlib.Path()
 | 
			
		||||
 | 
			
		||||
class JS_API:
 | 
			
		||||
    debug_mode = False
 | 
			
		||||
 | 
			
		||||
    def load_data(self, key):
 | 
			
		||||
        if not (path_storagedir / key).exists():
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        with open(path_storagedir / key) as fd_datafile:
 | 
			
		||||
            try:
 | 
			
		||||
                return fd_datafile.read()
 | 
			
		||||
            except Exception as exc:
 | 
			
		||||
                core.log.error(f"problem loading {key} (from {path_storagedir}): {exc}")
 | 
			
		||||
                return None
 | 
			
		||||
    
 | 
			
		||||
    def save_data(self, key, data):
 | 
			
		||||
        with open(path_storagedir / key, "w") as fd_datafile:
 | 
			
		||||
            try:
 | 
			
		||||
                fd_datafile.write(data)
 | 
			
		||||
            except Exception as exc:
 | 
			
		||||
                core.log.error(f"problem saving {key} (to {path_storagedir}): {exc}")
 | 
			
		||||
 | 
			
		||||
    def delete_data(self, key):
 | 
			
		||||
        if (path_storagedir / key).exists():
 | 
			
		||||
            (path_storagedir / key).unlink()
 | 
			
		||||
 | 
			
		||||
api = JS_API()
 | 
			
		||||
							
								
								
									
										7
									
								
								app/pylocal/dev.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/pylocal/dev.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import flask
 | 
			
		||||
 | 
			
		||||
from . import core
 | 
			
		||||
 | 
			
		||||
@core.app.route("/dev/get-toolbox")
 | 
			
		||||
def dev_toolbox():
 | 
			
		||||
    return flask.render_template("dev_toolbox.j2", ipaddr=flask.request.remote_addr, desktop=core.desktop_mode)
 | 
			
		||||
@@ -7,7 +7,11 @@ import flask
 | 
			
		||||
from . import core
 | 
			
		||||
 | 
			
		||||
def generate_flavor_text():
 | 
			
		||||
    proc_rant = subprocess.run(["rant", "/app/rant/flavor.rant"], capture_output=True)
 | 
			
		||||
    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)
 | 
			
		||||
    return proc_rant.stdout.decode()
 | 
			
		||||
 | 
			
		||||
class TickEvent(object):
 | 
			
		||||
@@ -36,11 +40,13 @@ def tick():
 | 
			
		||||
    result["event_type"] = random.choices(ticktypes, weights=tickweights)[0]
 | 
			
		||||
 | 
			
		||||
    match result["event_type"]:
 | 
			
		||||
        case 0:
 | 
			
		||||
            pass
 | 
			
		||||
        case 1: # FLAVOR
 | 
			
		||||
            result["log"] = generate_flavor_text()
 | 
			
		||||
        case 10: # ENCHUMAN
 | 
			
		||||
            result["items"] = [] # TODO: implement items
 | 
			
		||||
            result["items"] = {} # TODO: implement items
 | 
			
		||||
        case _:
 | 
			
		||||
            print("undefined tick: {0}".format(result["event_type"]))
 | 
			
		||||
            core.log.warning("undefined tick: {0}".format(result["event_type"]))
 | 
			
		||||
 | 
			
		||||
    return flask.Response(json.dumps(result), status=200, content_type="application/json")
 | 
			
		||||
@@ -18,9 +18,7 @@
 | 
			
		||||
        You have a polite conversation about birdly affairs. |
 | 
			
		||||
        It scoffs and flies away.
 | 
			
		||||
    }
 | 
			
		||||
    |
 | 
			
		||||
    You encounter a human and attempt to steal their `{
 | 
			
		||||
        [pick: <wordlist/nouns/food>] |
 | 
			
		||||
        [pick: <wordlist/nouns/fast_food>]
 | 
			
		||||
    }.
 | 
			
		||||
} |
 | 
			
		||||
{
 | 
			
		||||
    A nearby {`[pick: <wordlist/nouns/birds>]|colony of `[pick: <wordlist/nouns/birds>]s} seems to be harassing a human.
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								app/requirements-build-desktop.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/requirements-build-desktop.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pyinstaller>=6.14.2
 | 
			
		||||
							
								
								
									
										3
									
								
								app/requirements-desktop-linux.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/requirements-desktop-linux.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
pyqt6>=6.9.1
 | 
			
		||||
pyqtwebengine>=5.15.7
 | 
			
		||||
pywebview[qt]>=5.4
 | 
			
		||||
							
								
								
									
										1
									
								
								app/requirements-desktop.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/requirements-desktop.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pywebview>=5.4
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
Flask==2.2.2
 | 
			
		||||
gevent==22.10.2
 | 
			
		||||
Flask==3.1.1
 | 
			
		||||
gevent==25.5.1
 | 
			
		||||
hiredis>=1.0.0
 | 
			
		||||
redis==4.5.1
 | 
			
		||||
redis==6.2.0
 | 
			
		||||
							
								
								
									
										2
									
								
								app/templates/dev_toolbox.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/templates/dev_toolbox.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
{% if not desktop %}IP: {{ipaddr}}<br />{% endif %}
 | 
			
		||||
<button id="dev-reset" onClick="reset_game()">Reset Game</button>
 | 
			
		||||
@@ -12,8 +12,10 @@
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<noscript>
 | 
			
		||||
<div style="background:yellow;border:2px red;">
 | 
			
		||||
<h1>This doesn't work without JavaScript.</h1><br />
 | 
			
		||||
<h2>You're probably using a browser extension or privacy tool that disables it.</h2>
 | 
			
		||||
</div>
 | 
			
		||||
</noscript>
 | 
			
		||||
<div id="root">
 | 
			
		||||
 | 
			
		||||
@@ -22,12 +24,28 @@
 | 
			
		||||
    <div id="side-seagull-name"><span id="lbl-seagull-name">Nameless</span> <a href="javascript:change_seagull_name()">✏️</a></div>
 | 
			
		||||
    <div id="side-seagull-name-editor"><input type="text" id="edt-seagull-name"> <a href="javascript:confirm_seagull_name()">✅</a><a href="javascript:cancel_seagull_name()">❌</a></div>
 | 
			
		||||
    <div id="side-seagull-stats">
 | 
			
		||||
        <p id="side-seagull-lvl">Lv 1 LoadError</p>
 | 
			
		||||
        <p id="side-seagull-lvl">Lv <span id="lbl-seagull-lvl">1</span> <span id="lbl-seagull-class">LoadError</span></p>
 | 
			
		||||
        <p id="side-seagull-xp">XP: <span id="lbl-seagull-xp-current">0</span>/<span id="lbl-seagull-xp-next">0</span></p>
 | 
			
		||||
        <p id="side-seagull-misc">
 | 
			
		||||
            Colony: <span id="lbl-seagull-colony">1337</span><br />
 | 
			
		||||
            Shinies: <span id="lbl-seagull-shinies">420</span><br />
 | 
			
		||||
            Food: <span id="lbl-seagull-food">69</span>
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <hr />
 | 
			
		||||
    <div id="side-action-bar">
 | 
			
		||||
        <p>Human encounters: <select id="menu-enc-human">
 | 
			
		||||
            <option value="pause">Stop and ask</option>
 | 
			
		||||
            <option value="steal-food">Steal food</option>
 | 
			
		||||
            <option value="steal-shinies">Steal shiny things</option>
 | 
			
		||||
        </select></p>
 | 
			
		||||
        <p>Seagull encounters: <select id="menu-enc-seagull">
 | 
			
		||||
            <option value="pause">Stop and ask</option>
 | 
			
		||||
            <option value="recruit">Attempt recruiting</option>
 | 
			
		||||
            <option value="steal-food">Steal food</option>
 | 
			
		||||
            <option value="steal-shinies">Steal shiny things</option>
 | 
			
		||||
        </select></p>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div id="main-content">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user