Merge branch 'broken_shit' into 'master'

Broken shit

See merge request fainsil/booplaysgba!1
This commit is contained in:
Laurent Fainsin 2021-11-16 21:07:44 +00:00
commit 6940e6acb5
19 changed files with 688 additions and 235 deletions

View file

@ -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

6
.env Normal file
View file

@ -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

6
.vscode/launch.json vendored
View file

@ -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": [

View file

@ -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"
],
}

51
Dockerfile Normal file
View file

@ -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

View file

@ -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

41
docker-compose.yml Normal file
View file

@ -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

48
memo.full.sh Normal file
View file

@ -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

17
memo.sh Normal file
View file

@ -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

42
poetry.lock generated
View file

@ -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"},

View file

@ -4,9 +4,7 @@ version = "0.1.0"
description = "Émulateur collaboratif pour patienter dans le B00"
authors = ["Laureηt <laurentfainsin@protonmail.com>"]
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"

89
src/Controller.svelte Normal file
View file

@ -0,0 +1,89 @@
<script lang="ts">
const websocket: WebSocket = new WebSocket("ws://localhost:6789/");
const sendAction = (key: string) => () => {
websocket.send(JSON.stringify({ action: key }));
};
</script>
<table class="buttons">
<tr>
<td colspan="2" style="padding-bottom: 1rem;">
<button id="l" on:click={sendAction("l")}>L</button>
</td>
<td />
<td colspan="2" style="padding-bottom: 1rem; text-align: right;">
<button id="r" on:click={sendAction("r")}>R</button>
</td>
</tr>
<tr>
<td />
<td>
<button id="up" on:click={sendAction("up")}> </button>
</td>
<td />
<td>
<button id="select" on:click={sendAction("select")}> select </button>
</td>
<td>
<button id="start" on:click={sendAction("start")}> start </button>
</td>
</tr>
<tr>
<td>
<button id="left" on:click={sendAction("left")}> &lt; </button>
</td>
<td />
<td style="padding-right: 3rem;">
<button id="right" on:click={sendAction("right")}> > </button>
</td>
<td />
<td />
</tr>
<tr>
<td />
<td>
<button id="down" on:click={sendAction("down")}> </button>
</td>
<td />
<td>
<button id="a" on:click={sendAction("a")}>A</button>
</td>
<td>
<button id="b" on:click={sendAction("b")}>B</button>
</td>
</tr>
</table>
<style>
td {
text-align: center;
}
#a,
#b {
width: 2rem;
height: 2rem;
border-radius: 50%;
}
#left,
#right,
#up,
#down {
width: 2rem;
height: 2rem;
border-radius: 10%;
}
#start,
#select,
#l,
#r {
width: 3.5rem;
border-radius: 25%;
}
</style>

View file

@ -2,78 +2,78 @@
<html>
<head>
<title>Admin</title>
<title>Admin</title>
<style>
* {
padding: 0;
margin: 0;
}
<style>
* {
padding: 0;
margin: 0;
}
#dashboard {
display: none;
}
</style>
#dashboard {
display: none;
}
</style>
</head>
<body>
<div id="login">
<input type="password" id="password-text">
<input type="button" id="password-button" value="Login" onclick="sendCreds();">
</div>
<div id="login">
<input type="password" id="password-text">
<input type="button" id="password-button" value="Login" onclick="sendCreds();">
</div>
<div id="dashboard">
<ul id="stateList"></ul>
<button id="save">save</button>
</div>
<div id="dashboard">
<ul id="stateList"></ul>
<button id="save">save</button>
</div>
<script>
var websocket = new WebSocket("ws://127.0.0.1:6789/");
var passwordInput = document.querySelector('#password-text');
<script>
var websocket = new WebSocket("ws://127.0.0.1:6789/");
var passwordInput = document.querySelector('#password-text');
var divLogin = document.querySelector("#login");
var divDashboard = document.querySelector("#dashboard");
var stateList = document.querySelector("#stateList");
var saveButton = document.getElementById('save');
var divLogin = document.querySelector("#login");
var divDashboard = document.querySelector("#dashboard");
var stateList = document.querySelector("#stateList");
var saveButton = document.getElementById('save');
saveButton.onclick = function (event) {
websocket.send(JSON.stringify({ 'admin': 'save' }));
}
saveButton.onclick = function (event) {
websocket.send(JSON.stringify({ 'admin': 'save' }));
}
function receiveStates(ev) {
let msg = JSON.parse(ev.data);
let states = msg.state
for (var i = 0; i < states.length; i++) {
var state = states[i];
var li = document.createElement('li');
li.appendChild(document.createTextNode(state));
stateList.appendChild(li);
}
}
function receiveStates(ev) {
let msg = JSON.parse(ev.data);
let states = msg.state
for (var i = 0; i < states.length; i++) {
var state = states[i];
var li = document.createElement('li');
li.appendChild(document.createTextNode(state));
stateList.appendChild(li);
}
}
function authSuccess(ev) {
let msg = JSON.parse(ev.data);
if (msg.auth === "success") {
divLogin.style.display = "none";
divDashboard.style.display = "unset";
websocket.removeEventListener('message', authSuccess);
websocket.send(JSON.stringify({ "state": "get" }));
websocket.addEventListener('message', receiveStates);
}
};
function authSuccess(ev) {
let msg = JSON.parse(ev.data);
if (msg.auth === "success") {
divLogin.style.display = "none";
divDashboard.style.display = "unset";
websocket.removeEventListener('message', authSuccess);
websocket.send(JSON.stringify({ "state": "get" }));
websocket.addEventListener('message', receiveStates);
}
};
function sendCreds() {
var message = JSON.stringify({ "auth": passwordInput.value });
websocket.send(message)
websocket.addEventListener('message', authSuccess);
};
function sendCreds() {
var message = JSON.stringify({ "auth": passwordInput.value });
websocket.send(message)
websocket.addEventListener('message', authSuccess);
};
passwordInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") { sendCreds(); }
});
</script>
passwordInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") { sendCreds(); }
});
</script>
</body>
</html>

63
src/arrow.svg Normal file
View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="8.0677929mm"
height="9.2203341mm"
viewBox="0 0 8.0677928 9.2203341"
version="1.1"
id="svg5"
sodipodi:docname="arrow.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="13.108739"
inkscape:cx="6.5986514"
inkscape:cy="-6.7512214"
inkscape:window-width="1920"
inkscape:window-height="1054"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-93.682808,-119.4171)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="91.903885"
y="129.03831"
id="text49897"><tspan
sodipodi:role="line"
id="tspan49895"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:Inter;-inkscape-font-specification:'Inter, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583"
x="91.903885"
y="129.03831">&gt;</tspan></text>
<g
aria-label="&gt;"
id="text51577"
style="font-size:10.5833px;line-height:1.25;stroke-width:0.264583">
<path
d="m 101.7506,124.52837 -8.067792,4.10906 v -1.60354 l 6.188649,-2.98157 -0.05011,0.10022 v -0.25055 l 0.05011,0.10022 -6.188649,-2.98158 v -1.60353 l 8.067792,4.10906 z"
style="font-size:17.6389px;font-family:Inter;-inkscape-font-specification:'Inter, Normal'"
id="path51587" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -2,127 +2,127 @@
<html>
<head>
<title>Telecommande</title>
<title>Telecommande</title>
<style>
* {
padding: 0;
margin: 0;
}
<style>
* {
padding: 0;
margin: 0;
}
table {
margin: auto;
margin-top: 30vh;
}
table {
margin: auto;
margin-top: 30vh;
}
td {
text-align: center;
}
td {
text-align: center;
}
#a,
#b {
border-radius: 50%;
width: 2rem;
height: 2rem;
}
#a,
#b {
border-radius: 50%;
width: 2rem;
height: 2rem;
}
#left,
#right,
#up,
#down {
border-radius: 10%;
width: 2rem;
height: 2rem;
}
#left,
#right,
#up,
#down {
border-radius: 10%;
width: 2rem;
height: 2rem;
}
#start,
#select,
#l,
#r {
border-radius: 25%;
width: 3.5rem;
}
</style>
#start,
#select,
#l,
#r {
border-radius: 25%;
width: 3.5rem;
}
</style>
</head>
<body>
<table class="buttons">
<tr>
<td colspan="2" style="padding-bottom: 1rem;"><button id="l">L</button></td>
<td></td>
<td colspan="2" style="padding-bottom: 1rem; text-align: right;"><button id="r">R</button></td>
</tr>
<tr>
<td></td>
<td><button id="up"></button></td>
<td></td>
<td><button id="select">select</button></td>
<td><button id="start">start</button></td>
</tr>
<tr>
<td><button id="left">
< </button>
</td>
<td></td>
<td style="padding-right: 3rem;"><button id="right"> > </button></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td><button id="down"> </button></td>
<td></td>
<td><button id="a">A</button></td>
<td><button id="b">B</button></td>
</tr>
</table>
<table class="buttons">
<tr>
<td colspan="2" style="padding-bottom: 1rem;"><button id="l">L</button></td>
<td></td>
<td colspan="2" style="padding-bottom: 1rem; text-align: right;"><button id="r">R</button></td>
</tr>
<tr>
<td></td>
<td><button id="up"></button></td>
<td></td>
<td><button id="select">select</button></td>
<td><button id="start">start</button></td>
</tr>
<tr>
<td><button id="left">
< </button>
</td>
<td></td>
<td style="padding-right: 3rem;"><button id="right"> > </button></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td><button id="down"> </button></td>
<td></td>
<td><button id="a">A</button></td>
<td><button id="b">B</button></td>
</tr>
</table>
<script>
var websocket = new WebSocket("ws://127.0.0.1:6789/");
<script>
var websocket = new WebSocket("ws://localhost:6789/");
var a = document.getElementById('a'),
b = document.getElementById('b'),
select = document.getElementById('select'),
start = document.getElementById('start'),
right = document.getElementById('right'),
left = document.getElementById('left'),
up = document.getElementById('up'),
down = document.getElementById('down'),
r = document.getElementById('r'),
l = document.getElementById('l');
var a = document.getElementById('a'),
b = document.getElementById('b'),
select = document.getElementById('select'),
start = document.getElementById('start'),
right = document.getElementById('right'),
left = document.getElementById('left'),
up = document.getElementById('up'),
down = document.getElementById('down'),
r = document.getElementById('r'),
l = document.getElementById('l');
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' }));
}
</script>
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' }));
}
</script>
</body>
</html>

View file

@ -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()

View file

@ -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

View file

@ -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])

View file

@ -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)