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