diff --git a/.editorconfig b/.editorconfig index cd45b0f..a7b7e60 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,5 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{yaml,yml,toml}] +[*.{yaml,yml,toml,html,svelte}] indent_size = 2 diff --git a/.env b/.env new file mode 100644 index 0000000..9096285 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +REDIS_HOST=redis +REDIS_PORT=6379 +WEBSOCKET_LISTEN=0.0.0.0 +WEBSOCKET_PORT=6789 +RTMP_HOST=rtmp +RTMP_PORT=1935 diff --git a/.vscode/launch.json b/.vscode/launch.json index 1e3dfdb..4b149a0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,14 +6,16 @@ "type": "python", "request": "launch", "program": "${workspaceFolder}/src/server.py", - "console": "integratedTerminal" + "console": "integratedTerminal", + "envFile": "" }, { "name": "Emulator", "type": "python", "request": "launch", "program": "${workspaceFolder}/src/emulator.py", - "console": "integratedTerminal" + "console": "integratedTerminal", + "envFile": "" } ], "compounds": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index adef844..cf1402f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,6 @@ } }, "python.analysis.extraPaths": [ - "./mgba/build/python/lib.linux-x86_64-3.9" + "./mgba/src/platform/python/build/lib.linux-x86_64-3.9/mgba" ], } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94e1540 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +FROM python:alpine + +# set /code as the work directory +WORKDIR /code + +RUN \ + # update alpine repositories + apk update \ + # build tools dependencies + && apk add build-base cmake git \ + # mgba dependencies + && apk add libffi-dev elfutils-dev libzip-tools minizip-dev libedit-dev sqlite-dev libepoxy-dev ffmpeg ffmpeg-dev libpng-dev jpeg-dev \ + # install poetry and cffi deps for mgba + && pip install poetry cffi + +# copy poetry config files +COPY ./pyproject.toml /code + +RUN \ + cd /code \ + # clone mgba + && git clone https://github.com/mgba-emu/mgba.git mgba \ + # create build directory + && mkdir mgba/build \ + # go to the build directory + && cd mgba/build \ + # configure the build + && cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. \ + # build mGBA + && make \ + # install mGBA + && make install + +RUN \ + cd /code/mgba/src/platform/python \ + # install mGBA bindings + && BINDIR=/code/mgba/build/include LIBDIR=/code/mgba/build/include python setup.py install + +RUN \ + # go to the workdir + cd /code/ \ + # # config poetry to not create a .venv + && poetry config virtualenvs.create false \ + # # upgrade pip + && poetry run pip install --upgrade pip \ + # install poetry + && poetry install --no-interaction --no-ansi --no-dev + +# copy the src files +COPY ./src /code/src +COPY ./roms/pokemon.gba /code/roms/pokemon.gba diff --git a/README.md b/README.md index e537aac..83c876e 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,15 @@ Build the mGBA python bindings: cd booplaysgba mkdir mgba/build cd mgba/build -cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF .. +cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF .. make ``` Install the dependencies : ```bash -poetry install +poetry run pip install --upgrade pip +BINDIR=`pwd`/mgba/build/ LIBDIR=`pwd`/mgba/build/ poetry install ``` ### Usage diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..689269b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3" +services: + server: + build: + context: . + entrypoint: poetry run python src/server.py + restart: unless-stopped + environment: + - REDIS_HOST=$REDIS_HOST + - REDIS_PORT=$REDIS_PORT + - WEBSOCKET_LISTEN=$WEBSOCKET_LISTEN + - WEBSOCKET_PORT=$WEBSOCKET_PORT + ports: + - $WEBSOCKET_PORT:$WEBSOCKET_PORT + depends_on: + - redis + emulator: + build: + context: . + entrypoint: poetry run python src/emulator.py + restart: unless-stopped + environment: + - REDIS_HOST=$REDIS_HOST + - REDIS_PORT=$REDIS_PORT + - RTMP_HOST=$RTMP_HOST + - RTMP_PORT=$RTMP_PORT + depends_on: + - rtmp + - redis + volumes: + - ./states/:/code/states/ + redis: + image: redis:alpine + restart: unless-stopped + ports: + - $REDIS_PORT:6379 + rtmp: + image: tiangolo/nginx-rtmp + restart: unless-stopped + ports: + - $RTMP_PORT:1935 diff --git a/memo.full.sh b/memo.full.sh new file mode 100644 index 0000000..eba225d --- /dev/null +++ b/memo.full.sh @@ -0,0 +1,48 @@ +0 apk update +1 apk add build-base cmake git +2 apk add libffi-dev elfutils-dev libzip-tools minizip-dev libedit-dev sqlite-dev libepoxy-dev ffmpeg-dev libpng-dev +3 pip install poetry cffi +4 mkdir /code +5 cd /code +6 git clone https://github.com/mgba-emu/mgba.git mgba +7 mkdir mgba/build +8 cd mgba/build +9 cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF .. +10 make +11 apk add vim +12 vim +13 vim pyproject.tom +14 vim pyproject.toml +15 cd /code +16 poetry config virtualenvs.create false +17 poetry run pip install --upgrade pip +18 vim pyproject.toml +19 poetry run pip install --upgrade pip +20 BINDIR=/code/mgba/build/ LIBDIR=/code/mgba/build/ poetry install --no-interaction --no-ansi --no-dev +21 which python +22 ls /usr/local +23 cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF -DCMAKE_INSTALL_PREFIX:PATH=/usr/local +24 cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. +25 cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF -DCMAKE_INSTALL_PREFIX:PATH=/usr/local/ .. +26 cd mgba/build +27 cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF -DCMAKE_INSTALL_PREFIX:PATH=/usr/local/ .. +28 make +29 cd /code/ +30 BINDIR=/code/mgba/build/ LIBDIR=/code/mgba/build/ poetry install +31 poetry install +32 cd mgba/src/platform/python/ +33 python setup.py install +34 BINDIR=/code/mgba/build/ LIBDIR=/code/mgba/build/ python setup.py install +35 BINDIR=/code/mgba/build/ LIBDIR=/code/mgba/build/ python setup.py installfind +36 find +37 find / | grep flags.h +38 BINDIR=/code/mgba/build/include/ LIBDIR=/code/mgba/build/include/ python setup.py installfind +39 BINDIR=/code/mgba/build/include/ LIBDIR=/code/mgba/build/include/ python setup.py install +40 cd ../../.. +41 cd build +42 make install +43 mgba +44 cd .. +45 cd src/platform/python/ +46 BINDIR=/code/mgba/build/include/ LIBDIR=/code/mgba/build/include/ python setup.py install +47 history diff --git a/memo.sh b/memo.sh new file mode 100644 index 0000000..880fed5 --- /dev/null +++ b/memo.sh @@ -0,0 +1,17 @@ +apk update +apk add build-base cmake git vim +apk add libffi-dev elfutils-dev libzip-tools minizip-dev libedit-dev sqlite-dev libepoxy-dev ffmpeg-dev libpng-dev jpeg-dev +pip install poetry cffi +mkdir /code +cd /code +vim pyproject.toml # copy le pyproject.tom manuel +git clone https://github.com/mgba-emu/mgba.git mgba +mkdir mgba/build +cd mgba/build +cmake -DBUILD_PYTHON=ON -DBUILD_QT=OFF -DBUILD_SDL=OFF -DUSE_DISCORD_RPC=OFF -DCMAKE_INSTALL_PREFIX:PATH=/usr/local/ .. +make +make install +cd /code/mgba/src/platform/python/ +BINDIR=/code/mgba/build/include/ LIBDIR=/code/mgba/build/include/ python setup.py install +cd /code/ +poetry install diff --git a/poetry.lock b/poetry.lock index 303c641..a14b351 100644 --- a/poetry.lock +++ b/poetry.lock @@ -176,7 +176,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\" [[package]] name = "identify" -version = "2.3.3" +version = "2.3.4" description = "File identification library for Python" category = "dev" optional = false @@ -207,6 +207,27 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mgba" +version = "0.10.0.7138+g59cb5c189" +description = "" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +cached-property = "*" +cffi = ">=1.6" + +[package.extras] +cinema = ["pytest"] +pil = ["Pillow (>=2.3)"] + +[package.source] +type = "directory" +url = "mgba/src/platform/python" + [[package]] name = "mypy" version = "0.910" @@ -250,7 +271,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pbr" -version = "5.6.0" +version = "5.7.0" description = "Python Build Reasonableness" category = "dev" optional = false @@ -302,7 +323,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -454,7 +475,7 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "23107f337b52d511f360f86746a69cbeec745e69d23beb1051a2fb9c5c303fd9" +content-hash = "06c8f160e3bf91f78480ff21a5e205147f1a23c38a405232ab8252e36cc7809f" [metadata.files] asyncio = [ @@ -568,8 +589,8 @@ gitpython = [ {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, ] identify = [ - {file = "identify-2.3.3-py2.py3-none-any.whl", hash = "sha256:ffab539d9121b386ffdea84628ff3eefda15f520f392ce11b393b0a909632cdf"}, - {file = "identify-2.3.3.tar.gz", hash = "sha256:b9ffbeb7ed87e96ce017c66b80ca04fda3adbceb5c74e54fc7d99281d27d0859"}, + {file = "identify-2.3.4-py2.py3-none-any.whl", hash = "sha256:4de55a93e0ba72bf917c840b3794eb1055a67272a1732351c557c88ec42011b1"}, + {file = "identify-2.3.4.tar.gz", hash = "sha256:595283a1c3a078ac5774ad4dc4d1bdd0c1602f60bcf11ae673b64cb2b1945762"}, ] isort = [ {file = "isort-5.10.0-py3-none-any.whl", hash = "sha256:1a18ccace2ed8910bd9458b74a3ecbafd7b2f581301b0ab65cfdd4338272d76f"}, @@ -579,6 +600,7 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mgba = [] mypy = [ {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, @@ -617,8 +639,8 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pbr = [ - {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, - {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, + {file = "pbr-5.7.0-py2.py3-none-any.whl", hash = "sha256:60002958e459b195e8dbe61bf22bcf344eedf1b4e03a321a5414feb15566100c"}, + {file = "pbr-5.7.0.tar.gz", hash = "sha256:4651ca1445e80f2781827305de3d76b3ce53195f2227762684eb08f17bc473b7"}, ] pillow = [ {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, @@ -676,8 +698,8 @@ pycodestyle = [ {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, diff --git a/pyproject.toml b/pyproject.toml index f5d31e5..af0607e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,7 @@ version = "0.1.0" description = "Émulateur collaboratif pour patienter dans le B00" authors = ["Laureηt "] license = "MIT" -packages = [ - { include = "mgba", from = "mgba/build/python/lib.linux-x86_64-3.9" } -] + [tool.poetry.dependencies] python = "^3.9" asyncio = "^3.4.3" @@ -15,6 +13,7 @@ cffi = "^1.15.0" cached-property = "^1.5.2" Pillow = "^8.4.0" redis = "^3.5.3" +mgba = { path="mgba/src/platform/python" } [tool.poetry.dev-dependencies] black = "^21.9b0" diff --git a/src/Controller.svelte b/src/Controller.svelte new file mode 100644 index 0000000..44f00dc --- /dev/null +++ b/src/Controller.svelte @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + +
+ + + + + + +
+ + + + + + + +
+ + diff --git a/src/admin.html b/src/admin.html index d18ef0b..9b02b40 100644 --- a/src/admin.html +++ b/src/admin.html @@ -2,78 +2,78 @@ - Admin + Admin - + #dashboard { + display: none; + } + -
- - -
+
+ + +
-
- - -
+
+ + +
- + passwordInput.addEventListener("keydown", function (e) { + if (e.key === "Enter") { sendCreds(); } + }); + diff --git a/src/arrow.svg b/src/arrow.svg new file mode 100644 index 0000000..f590efe --- /dev/null +++ b/src/arrow.svg @@ -0,0 +1,63 @@ + + + + + + + + > + + + + + diff --git a/src/client.html b/src/client.html index 3e06cf5..7e59e3d 100644 --- a/src/client.html +++ b/src/client.html @@ -2,127 +2,127 @@ - Telecommande + Telecommande - + #start, + #select, + #l, + #r { + border-radius: 25%; + width: 3.5rem; + } + - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- + a.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'a' })); + } + b.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'b' })); + } + select.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'select' })); + } + start.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'start' })); + } + right.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'right' })); + } + left.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'left' })); + } + up.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'up' })); + } + down.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'down' })); + } + r.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'r' })); + } + l.onclick = function (event) { + websocket.send(JSON.stringify({ action: 'l' })); + } + diff --git a/src/emulator.py b/src/emulator.py index b462ae7..807a271 100644 --- a/src/emulator.py +++ b/src/emulator.py @@ -1,4 +1,6 @@ +import asyncio import logging +import threading import time from subprocess import PIPE, Popen # nosec @@ -7,17 +9,36 @@ import mgba.image import mgba.log import redis -from settings import FPS, HEIGHT, KEYS, MGBA_KEYS, POLLING_RATE, REDIS_INIT, SPF, WIDTH +from settings import ( + EMULATOR_FPS, + EMULATOR_HEIGHT, + EMULATOR_POLLING_RATE, + EMULATOR_ROM_PATH, + EMULATOR_SPF, + EMULATOR_WIDTH, + FFMPEG_BITRATE, + FFMPEG_FPS, + FFMPEG_HEIGHT, + FFMPEG_WIDTH, + KEYS_ID, + KEYS_MGBA, + KEYS_RESET, + REDIS_HOST, + REDIS_PORT, + RTMP_STREAM_URI, +) +from utils import States -core = mgba.core.load_path("roms/pokemon.gba") -# core = mgba.core.load_path("roms/BtnTest.gba") -screen = mgba.image.Image(WIDTH, HEIGHT) +core = mgba.core.load_path(EMULATOR_ROM_PATH) +screen = mgba.image.Image(EMULATOR_WIDTH, EMULATOR_HEIGHT) core.set_video_buffer(screen) core.reset() logging.basicConfig(level=logging.DEBUG) mgba.log.silence() -r = redis.Redis(host="localhost", port=6379, db=0) +r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) + +states: States = States() def next_action(): @@ -26,9 +47,9 @@ def next_action(): Returns: int: key used by mgba """ - votes = list(map(int, r.mget(KEYS))) + votes: list[int] = list(map(int, r.mget(KEYS_ID))) if any(votes): - r.mset(REDIS_INIT) + r.mset(KEYS_RESET) return votes.index(max(votes)) else: return -1 @@ -43,45 +64,77 @@ stream = Popen( "-vcodec", "png", "-r", - f"{FPS}", + f"{EMULATOR_FPS}", "-s", - f"{WIDTH}x{HEIGHT}", + f"{EMULATOR_WIDTH}x{EMULATOR_HEIGHT}", "-i", "-", "-f", "flv", "-s", - f"{WIDTH}x{HEIGHT}", + f"{FFMPEG_WIDTH}x{FFMPEG_HEIGHT}", "-r", - "30", + f"{FFMPEG_FPS}", "-b:v", - "2M", + FFMPEG_BITRATE, "-fflags", "nobuffer", "-flags", "low_delay", "-strict", "experimental", - "rtmp://localhost:1935/live/test", + # "-loglevel", + # "quiet", + RTMP_STREAM_URI, ], stdin=PIPE, ) -while True: - last_frame_t = time.time() +def state_manager(loop): + print("ici") + ps = r.pubsub() + ps.subscribe("admin") + while True: + for message in ps.listen(): + logging.debug(message) + if message["type"] == "message": + data = message["data"].decode("utf-8") + if data == "save": + asyncio.ensure_future(states.save(core), loop=loop) + elif data.startswith("load:"): + asyncio.ensure_future(states.load(core, data.removeprefix("load:")), loop=loop) - if not (core.frame_counter % POLLING_RATE): - core.clear_keys(*MGBA_KEYS) - next_key = next_action() - if next_key != -1: - core.set_keys(next_key) - core.run_frame() +async def emulator(): + while True: + last_frame_t = time.time() - image = screen.to_pil().convert("RGB") - image.save(stream.stdin, "PNG") + if not (core.frame_counter % EMULATOR_POLLING_RATE): + core.clear_keys(*KEYS_MGBA) + next_key = next_action() + if next_key != -1: + core.set_keys(next_key) - sleep_t = last_frame_t - time.time() + SPF - if sleep_t > 0: - time.sleep(sleep_t) + core.run_frame() + + image = screen.to_pil().convert("RGB") + image.save(stream.stdin, "PNG") + + sleep_t = last_frame_t - time.time() + EMULATOR_SPF + if sleep_t > 0: + await asyncio.sleep(sleep_t) + + +async def main(loop): + thread = threading.Thread(target=state_manager, args=(loop,)) + thread.start() + + task_emulator = loop.create_task(emulator()) + await task_emulator + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main(loop)) + loop.close() diff --git a/src/server.py b/src/server.py index e9a1ac4..99f21a4 100644 --- a/src/server.py +++ b/src/server.py @@ -6,13 +6,22 @@ import time import redis import websockets -from settings import KEYS, PASSWORD_ADMIN, REDIS_INIT, USER_TIMEOUT +from settings import ( + KEYS_ID, + KEYS_RESET, + PASSWORD_ADMIN, + REDIS_HOST, + REDIS_PORT, + USER_TIMEOUT, + WEBSOCKET_LISTEN, + WEBSOCKET_PORT, +) from utils import User, Users logging.basicConfig(level=logging.DEBUG) -r = redis.Redis(host="localhost", port=6379, db=0) -r.mset(REDIS_INIT) +r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) +r.mset(KEYS_RESET) USERS: Users = Users() @@ -31,16 +40,27 @@ async def parse_message(user: User, message: dict[str, str]) -> None: logging.debug(f"admin authenticated: {user}") await user.send('{"auth":"success"}') + if "admin" in message: + if user == USERS.admin: + data = message["admin"] + if data == "save": + r.publish("admin", "save") + elif data == data.startswith("load:"): + r.publish("admin", data) + else: + logging.error(f"unsupported admin action: {data}") + else: + logging.error(f"user is not admin: {user}") + if "action" in message: data = message["action"] if user.last_message + USER_TIMEOUT > time.time(): logging.debug(f"dropping action: {data}") return None - elif data in KEYS: + elif data in KEYS_ID: r.incr(data) user.last_message = time.time() - user.has_voted = True else: logging.error(f"unsupported action: {data}") @@ -67,7 +87,7 @@ async def handler(websocket, path: str): async def main(): """Start the websocket server.""" - async with websockets.serve(handler, "localhost", 6789): + async with websockets.serve(handler, WEBSOCKET_LISTEN, WEBSOCKET_PORT): # nosec await asyncio.Future() # run forever diff --git a/src/settings.py b/src/settings.py index 66dd230..1ffd051 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,14 +1,36 @@ -WIDTH: int = 240 -HEIGHT: int = 160 +from os import getenv -URI: str = "ws://127.0.0.1:6789/" -PASSWORD_ADMIN: str = "password_admin" +WEBSOCKET_HOST: str = getenv("WEBSOCKET_HOST", "localhost") +WEBSOCKET_PORT: int = int(getenv("WEBSOCKET_PORT", 6789)) +WEBSOCKET_URI: str = f"ws://{WEBSOCKET_HOST}:{WEBSOCKET_PORT}/" +WEBSOCKET_LISTEN: str = getenv("WEBSOCKET_LISTEN", "localhost") -FPS: int = 60 -SPF: float = 1.0 / FPS -HZ: int = 10 -POLLING_RATE: int = FPS // HZ -USER_TIMEOUT: float = 0.5 +RTMP_HOST: str = getenv("RTMP_HOST", "localhost") +RTMP_PORT: int = int(getenv("RTMP_PORT", 1935)) +RTMP_URI: str = f"rtmp://{RTMP_HOST}:{RTMP_PORT}/" +RTMP_STREAM_PATH: str = getenv("RTMP_STREAM_PATH", "live") +RTMP_STREAM_KEY: str = getenv("RTMP_STREAM_KEY", "test") +RTMP_STREAM_URI: str = RTMP_URI + f"{RTMP_STREAM_PATH}/{RTMP_STREAM_KEY}" + +REDIS_HOST: str = getenv("REDIS_HOST", "localhost") +REDIS_PORT: int = int(getenv("REDIS_PORT", 6379)) + +EMULATOR_WIDTH: int = int(getenv("EMULATOR_WIDTH", 240)) +EMULATOR_HEIGHT: int = int(getenv("EMULATOR_HEIGHT", 160)) +EMULATOR_FPS: int = int(getenv("EMULATOR_FPS", 60)) +EMULATOR_SPF: float = 1.0 / EMULATOR_FPS +EMULATOR_INPUT_HZ: int = int(getenv("EMULATOR_INPUT_HZ", 10)) +EMULATOR_POLLING_RATE: int = EMULATOR_FPS // EMULATOR_INPUT_HZ +EMULATOR_ROM_PATH: str = getenv("EMULATOR_ROM_PATH", "roms/pokemon.gba") + +FFMPEG_WIDTH: int = int(getenv("FFMPEG_WIDTH", EMULATOR_WIDTH)) +FFMPEG_HEIGHT: int = int(getenv("FFMPEG_HEIGHT", EMULATOR_HEIGHT)) +FFMPEG_FPS: int = int(getenv("FFMPEG_FPS", 30)) +FFMPEG_BITRATE: str = getenv("FFMPEG_BIRATE", "2M") + +PASSWORD_ADMIN: str = getenv("PASSWORD_ADMIN", "password_admin") + +USER_TIMEOUT: float = float(getenv("USER_TIMEOUT", 0.5)) KEYMAP: dict[str, int] = { "a": 0, @@ -22,6 +44,6 @@ KEYMAP: dict[str, int] = { "r": 8, "l": 9, } -KEYS: tuple = tuple(KEYMAP.keys()) -MGBA_KEYS: tuple = tuple(KEYMAP.values()) -REDIS_INIT: dict = dict([(x, 0) for x in KEYS]) +KEYS_ID: tuple = tuple(KEYMAP.keys()) +KEYS_MGBA: tuple = tuple(KEYMAP.values()) +KEYS_RESET: dict = dict([(x, 0) for x in KEYS_ID]) diff --git a/src/utils.py b/src/utils.py index 6823811..04b99df 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,15 +1,17 @@ import logging +import os import time from dataclasses import dataclass from typing import Any, Optional +from mgba._pylib import ffi + class User: """Store infos related to a connected user.""" websocket: Any last_message: float - has_voted: bool def __init__(self, websocket: Any) -> None: """Construct a User object. @@ -19,7 +21,6 @@ class User: """ self.websocket = websocket self.last_message = time.time() - self.has_voted = False async def send(self, data: str): """Send data through the user's websocket. @@ -42,7 +43,6 @@ class User: class Users(set): """Store `User`s connected to the server.""" - emulator: Optional[User] = None admin: Optional[User] = None def register(self, user: User): @@ -63,7 +63,26 @@ class Users(set): self.remove(user) logging.debug(f"user unregistered: {self}") - def clear(self) -> None: - """Clear the `has_voted` of each user in the set.""" - for user in self: - user.has_voted = False + +class States(set): + """Save and load states from files.""" + + def __init__(self) -> None: + """Construct a `States` object.""" + files = os.listdir("states") + states = list(filter(lambda x: x.endswith(".state"), files)) + self.update(states) + + async def save(self, core): + state = core.save_raw_state() + with open(f"states/{time.strftime('%Y-%m-%dT%H:%M:%S')}.state", "wb") as state_file: + for byte in state: + state_file.write(byte.to_bytes(4, byteorder="big", signed=False)) + self.add(state) + + async def load(self, core, filename): + state = ffi.new("unsigned char[397312]") # pulled 397312 from my ass + with open(f"states/{filename}.state", "rb") as state_file: + for i in range(len(state)): + state[i] = int.from_bytes(state_file.read(4), byteorder="big", signed=False) + core.load_raw_state(state)