1
0
Fork 0

[v0.1.0] init

This commit is contained in:
sup39 2023-03-09 17:55:01 +09:00
commit 82f82eba3e
13 changed files with 342 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/dist
.venv/
*.egg-info/
__pycache__/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 sup39
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

23
README.md Normal file
View file

@ -0,0 +1,23 @@
# supDolphinWSServer
A WebSocket server for accessing memory of emulated games in Dolphin.
This tool only runs on Windows currently.
## Prerequisite
- [Dolphin beta/dev version](https://dolphin-emu.org/download/) (stable version is NOT available)
- Python >= 3.8
## Installation/Upgrade
```
pip install -U supDolphinWS-server
```
## Usage
```
python -m supDolphinWS.server
```
Use the following command to see the available options:
```
python -m supDolphinWS.server --help
```

3
pyproject.toml Normal file
View file

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"

27
setup.cfg Normal file
View file

@ -0,0 +1,27 @@
[metadata]
name = supDolphinWS-server
version = 0.1.0
author = sup39
author_email = sms@sup39.dev
description = A WebSocket server for accessing memory of emulated games in Dolphin
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/sup39/supDolphinWS-server
license = MIT
project_urls =
Bug Tracker = https://github.com/sup39/supDolphinWS-server/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: Microsoft :: Windows
[options]
include_package_data = True
packages = find:
python_requires = >=3.8
install_requires =
wsocket >= 2.1.0
psutil >= 5.9.4
[options.packages.find]
where = src

View file

View file

View file

@ -0,0 +1,17 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 sup39
import argparse
from .server import run, VERSION
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='python -m supDolphinWS.server', add_help=False)
parser.add_argument('-h', '--host', default='localhost', help='the IP address that the server will listen on')
parser.add_argument('-p', '--port', default=35353, type=int, help='the port number that the server will listen on')
parser.add_argument('--exclude-pids', default=[], nargs='*', help='the pid list to exclude when finding dolphin')
parser.add_argument('-?', '--help', action='help', default=argparse.SUPPRESS, help='show this help message and exit')
parser.add_argument('--version', action='store_true', help='show the version of the server')
args = parser.parse_args()
if args.version:
print(VERSION)
else:
run(args)

View file

@ -0,0 +1,72 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 sup39
VERSION = '0.1.0'
import struct
from .err import abort, ERR_DOLPHIN, ERR_BAD_REQ
from .dolphin import Dolphin
_d = Dolphin()
def get_dolphin():
if _d.m is None:
r = _d.hook()
if r is None: abort(ERR_DOLPHIN, 'Fail to find shared memory. Is the game running on Dolphin?')
return _d
def get_version(body):
return VERSION.encode()
def do_hook(body):
_d.unhook()
get_dolphin()
return b''
'''
u32 size
u32 base
s32[] addr segments
'''
def read_ram(body):
nbody = len(body)
if nbody < 4 or nbody % 4: abort(ERR_BAD_REQ, 'Bad payload')
## args
size, = struct.unpack('>I', body[:4])
addr = [
MEM1_START if nbody < 8 else struct.unpack('>I', body[4:8])[0],
*(p[0] for p in struct.iter_unpack('>i', body[8:])),
]
## access
d = get_dolphin()
return d.read_bytes(addr, size) or b''
'''
u32 size
u8[] content
u32 base
s32[] addr segments
'''
def write_ram(body):
nbody = len(body)
if nbody < 4: abort(ERR_BAD_REQ, 'Bad payload')
## size
size, = struct.unpack('>I', body[:4])
i = 4+size
if nbody < i: abort(ERR_BAD_REQ, 'Bad payload')
content = body[4:i]
## addr
if (nbody-i) % 4: abort(ERR_BAD_REQ, 'Bad payload')
addr = [
MEM1_START if nbody == i else struct.unpack('>I', body[i:i+4])[0],
*(p[0] for p in struct.iter_unpack('>i', body[i+4:])),
]
## access
d = get_dolphin()
a = d.write_bytes(addr, content)
return b'' if a is None else struct.pack('>I', a)
handlers = {
0x00: get_version,
0x01: do_hook,
0x02: read_ram,
0x03: write_ram,
}

View file

@ -0,0 +1,93 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 sup39
import os
import psutil
import struct
from multiprocessing.shared_memory import SharedMemory
MEM1_START = 0x80000000
MEM1_END = 0x81800000
MEM2_START = 0x90000000
MEM2_END = 0x93000000
MEM2_OFFSET = 0x4040000
dolphinProcNames = \
{'Dolphin.exe', 'DolphinQt2.exe', 'DolphinWx.exe'} if os.name == 'nt' \
else {'dolphin-emu', 'dolphin-emu-qt2', 'dolphin-emu-wx'}
def try_get_memory(pid):
try: return SharedMemory('dolphin-emu.'+str(pid))
except FileNotFoundError: return None
def find_memory(exclude_pid={}):
try: return next(
(p.pid, m)
for p in psutil.process_iter(['pid', 'name'])
if p.pid not in exclude_pid and p.name() in dolphinProcNames
for m in [try_get_memory(p.pid)]
if m is not None
)
except StopIteration: return None
class Dolphin():
def __init__(self):
self.pid = None
self.m = None
def hook(self, exclude_pid={}):
r = find_memory(exclude_pid)
if r is None: return
## success
self.pid, self.m = r
return r
def unhook(self):
self.pid = None
if self.m is not None:
self.m.close()
self.m = None
def _read_bytes(self, addr, size):
'''
addr: int
size: int
'''
if MEM1_START <= addr <= MEM1_END-size:
idx = addr-MEM1_START
elif MEM2_START <= addr <= MEM2_END-size and len(self.m) > MEM2_OFFSET:
idx = MEM2_OFFSET + addr-MEM2_START
else: return None
return self.m.buf[idx:idx+size].tobytes()
def _write_bytes(self, addr, payload):
'''
addr: int
payload: bytes
'''
size = len(payload)
if MEM1_START <= addr <= MEM1_END-size:
idx = addr-MEM1_START
elif MEM2_START <= addr <= MEM2_END-size and len(self.m) > MEM2_OFFSET:
idx = MEM2_OFFSET + addr-MEM2_START
else: return None
self.m.buf[idx:idx+size] = payload
return addr
def resolve_addr(self, addr):
'''
addr: int|int[]
'''
offs = [addr] if isinstance(addr, int) else addr
addr = 0
for off in offs[:-1]:
raw = self._read_bytes(addr+off, 4)
if raw is None: return None
addr, = struct.unpack('>I', raw)
return addr + (offs[-1] if len(offs) else 0)
def read_bytes(self, addr, size):
'''
addr: int|int[]
size: int
'''
return self._read_bytes(self.resolve_addr(addr), size)
def write_bytes(self, addr, payload):
'''
addr: int|int[]
payload: bytes
'''
return self._write_bytes(self.resolve_addr(addr), payload)

View file

@ -0,0 +1,14 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 sup39
ERR_UNKNOWN = 255
ERR_CMD_NOT_FOUND = 254
ERR_DOLPHIN = 253
ERR_BAD_REQ = 252
class ErrorResponse(Exception):
def __init__(self, code, payload):
self.code = code
self.payload = payload
def abort(code, payload=''):
raise ErrorResponse(code, payload)

View file

@ -0,0 +1,51 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 sup39
from wsocket import WSocketApp, run as _run
from .utils import tryParseInt, parseAddrPath
from .api import VERSION, handlers, _d
from .err import *
'''
u32 id
u8 cmdtype
'''
def on_message(msg, client):
if type(msg) != bytearray: return
if len(msg) < 5: return
ID, cmdtype, body = msg[:4], msg[4], msg[5:]
handler = handlers.get(cmdtype)
if handler is None:
return client.send(ID+struct.pack('>B', ERR_CMD_NOT_FOUND))
try:
res = handler(body)
code = 0
if res is None:
code = ERR_UNKNOWN
res = b''
except ErrorResponse as err:
code, res = err.code, err.payload.encode()
client.send(ID+code.to_bytes(1, 'big')+res)
class MyWSocketApp(WSocketApp):
def wsgi(self, environ, start_response):
wsock = environ.get("wsgi.websocket")
## plain HTTP
if not wsock:
start_response()
return '<h1>supDolphinWSServer</h1>'
## WebSocket
self.onconnect(wsock)
while True:
try:
message = wsock.receive()
if message != None:
self.onmessage(message, wsock)
except WebSocketError as e:
break
return []
def run(args):
app = WSocketApp()
app.onmessage += on_message
_d.hook()
_run(app, host=args.host, port=args.port)

View file

@ -0,0 +1,17 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 sup39
def tryParseInt(x, radix=10):
try:
return int(x, radix)
except ValueError:
return None
def parseAddrPath(s):
if s == '': return [MEM1_START]
try:
return [
int(ch, 16)
for ch in s.split('/')
]
except ValueError:
return None