|
|
@@ -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 <i>interesting</i> 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 <i>EVEN MORE interesting</i> 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) |