From 699b13cdd26561d29acc67907f2e947716df2518 Mon Sep 17 00:00:00 2001 From: fenceFoil Date: Mon, 31 May 2021 02:57:54 -0400 Subject: [PATCH] Add simulator loop example --- .gitignore | 3 +- src/envVars.bat | 2 + src/requirements.txt | 4 +- src/simulatorSampleApp.bat | 3 + src/simulatorSampleApp.py | 170 +++++++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/envVars.bat create mode 100644 src/simulatorSampleApp.bat create mode 100644 src/simulatorSampleApp.py diff --git a/.gitignore b/.gitignore index 01d7f95..932725b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ -venv \ No newline at end of file +venv +logs \ No newline at end of file diff --git a/src/envVars.bat b/src/envVars.bat new file mode 100644 index 0000000..bba75de --- /dev/null +++ b/src/envVars.bat @@ -0,0 +1,2 @@ +set GOTIFY_ADDRESS=http://billkarnavas.com:8080 +set GOTIFY_APIKEY=AXoe0zpZN6iO7wv \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index eb9d006..4c66504 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,4 +6,6 @@ fastapi aiofiles python-multipart uvicorn[standard] -jinja2 \ No newline at end of file +jinja2 +readerwriterlock +requests \ No newline at end of file diff --git a/src/simulatorSampleApp.bat b/src/simulatorSampleApp.bat new file mode 100644 index 0000000..342e72f --- /dev/null +++ b/src/simulatorSampleApp.bat @@ -0,0 +1,3 @@ +call ../venv/scripts/Activate.bat +call envVars.bat +uvicorn simulatorSampleApp:app --reload --no-access-log --log-level warning --port 5777 --host 0.0.0.0 \ No newline at end of file diff --git a/src/simulatorSampleApp.py b/src/simulatorSampleApp.py new file mode 100644 index 0000000..d6843fa --- /dev/null +++ b/src/simulatorSampleApp.py @@ -0,0 +1,170 @@ +#standard +import asyncio +import html +import traceback +from datetime import datetime +import logging +from logging import debug, info, warning, error, critical, exception +import os +import threading +import time +import uuid +#custom +import dataset +from readerwriterlock import rwlock +import requests +from fastapi import FastAPI, WebSocket, Form, File, UploadFile, Response, Request, WebSocketDisconnect, websockets +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +import starlette.status as status +from starlette.responses import RedirectResponse + +# ---- LOGGER SETUP ---- + +LOGS_PATH = '../logs/' +def setupLogger(level = logging.INFO): + + startTime = datetime.now().strftime("%Y-%m-%d %H-%M-%S") + + logFormat = "[%(levelname)s] %(asctime)s: %(message)s" + logFormatter = logging.Formatter(logFormat) + + # Log to a file and to the error stream + if not os.path.exists(LOGS_PATH): + os.makedirs(LOGS_PATH) + logging.basicConfig(filename='{}log {}.txt'.format(LOGS_PATH, startTime), + level=level, format=logFormat) + streamHandler = logging.StreamHandler() + streamHandler.setLevel(level) + streamHandler.setFormatter(logFormatter) + logging.getLogger().addHandler(streamHandler) + + info("New instance started") + +# ---- END LOGGER SETUP --- + +def sendGotifyNotification(title, text, priority): + def doSendGotifyNotification(title, text, priority): + try: + response = requests.post(os.environ['GOTIFY_ADDRESS']+'/message', params={ + "token": os.environ['GOTIFY_APIKEY']}, json={'message': text, 'title': title, 'priority': priority}) + if response.status_code != 200: + error("sendGotifyNotification(): server error {}, description: {}".format( + response.status_code, response.text)) + except requests.exceptions.ConnectionError: + error("sendGotifyNotification(): connection error. not retrying.") + t = threading.Thread(target=doSendGotifyNotification, + args=(title, text, priority)) + t.start() + +def make_readable_sim_timestamp(ms): + days = int(ms // (24*60*60*1000)) + hours = int(ms // (60*60*1000)) + hours -= days*24 + mins = int(ms // (60*1000)) + mins -= hours*60 + secs = int(ms // 1000) + secs -= mins*60 + msecs = int(ms % 1000) + return f"{days}d{hours}h{mins}m{secs}.{msecs}s" + + +app = FastAPI() +app.mount('/static', StaticFiles(directory="./static"), name="static") +templates = Jinja2Templates(directory="templates") +@app.get('/', response_class=HTMLResponse) +async def getRoot(request: Request): + return RedirectResponse('/static/index.html', status_code=status.HTTP_302_FOUND) + +db = dataset.connect('sqlite:///../state.db') + +# Not accurate, but roughly in hz. +# Does not affect speed of simulation, only how often it is sampled. +simRefreshRate = 50 + +# ---- Create the data used to interface between websockets and the simulator ---- +# Keep a list of incoming events from outside world +incomingEventQueue = asyncio.Queue(maxsize=10000) +# Keep a list of events generated by the sim +outgoingEventQueue = asyncio.Queue(maxsize=10000) + +# Store connections and their subscription information +conns_locks = rwlock.RWLockFair() +conns = {} + +# Create a world +world_locks = rwlock.RWLockFair() +simWorld = None + +simShutdownSignal = False +simWasShutdownSignal = False +# ---- End interface data setup ---- + +async def simulator_runner(): + global simShutdownSignal, simWasShutdownSignal + runnerStartTimeMS = round((time.monotonic_ns()/1_000_000)) + while not simShutdownSignal: + # Pad cycle + await asyncio.sleep(1.0/simRefreshRate) + + # Simulate + print('hi') + + with world_locks.gen_wlock(): + # Accept new events + # Run simulation tick + pass + simWasShutdownSignal = True + +@app.on_event("startup") +async def setupSimulator(): + setupLogger(logging.DEBUG) + asyncio.create_task(simulator_runner()) + + +@app.websocket('/websocket') +async def runGameSession(websocket: WebSocket): + + async def sendDisplay(dest, rawHTML): + await websocket.send_json({ + 'type': dest, + 'rawHTML': rawHTML + }) + + async def sendNarration(rawHTML, update=False): + await sendDisplay('narrative' if not update else 'narrativeUpdate', rawHTML) + + async def sendPlayerEcho(rawHTML): + await sendDisplay('playerEcho', rawHTML) + + await websocket.accept() + myID = uuid.uuid4().hex + + # Register connection with empty data + with conns_locks.gen_wlock(): + conns[myID] = { + } + + try: + await sendDisplay('header', 'Testing Header Text') + + while True: + await sendNarration('Something interesting happened.') + startedWaitingAtMS = round((time.monotonic_ns()/1_000_000)) + # TODO bit of a confused mess but lots to adapt in this loop + while round((time.monotonic_ns()/1_000_000)) < startedWaitingAtMS+5000: + try: + resp = await asyncio.wait_for(websocket.receive_json(), timeout=1/simRefreshRate) + await sendPlayerEcho('> ' + html.escape(resp['playerInput'])) + break + except asyncio.exceptions.TimeoutError: + # timeout + await sendNarration('Something EVEN MORE interesting happened.', update=True) + except Exception as e: + traceback.print_exc() + + # Remove websocket from connections if still there + debug ('Removing a connection from list') + with conns_locks.gen_wlock(): + conns.pop(myID, None) \ No newline at end of file