From b917bae8cc8e26dbf15b0a67b301a22171d62237 Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Sat, 5 Nov 2022 13:08:00 -0400 Subject: [PATCH 01/10] partially complete major rewrite --- bizhook/memory.py | 242 +++++++++------------------------------------- 1 file changed, 47 insertions(+), 195 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index b19825b..dfa93a7 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -2,11 +2,35 @@ from socket import socket as Socket from socket import create_connection -from typing import Union - from .exceptions import InvalidRequest, InvalidResponse - +FORM = { + "INPUT": 0, + "READ": 1, + "WRITE": 2, + "CLIENT": 3 +} +''' +Planned: +emulator: + load rom + reset? +''' + +DELIMITER = '/' + +# BUTTON_TRANSLATE = { +# "P1 A": 0, +# "P1 B": 1, +# "P1 Down": 2, +# "P1 Left": 3, +# "P1 Right": 4, +# "P1 Select": 5, +# "P1 Start": 6, +# "P1 Up": 7, +# "Power": 8, +# "Reset": 9 +# } class Memory: """Client for reading from and writing to Bizhawk memory""" @@ -29,47 +53,13 @@ def __init__(self, self.port = port - def _request(self, query: Union[bytes, str]): - """Send a request to the Bizhawk Lua memory hook - - - Pattern: DOMN/ADDR/TSLE/VLUE - - DOMN - Memory domain - ADDR - Address - TSLE--.-------------------. - Type: Signage: - | * [b]yte * [u]nsigned - | * [i]nteger * [s]igned - | * [f]loat - | - .-------------------. - Size: Endianness: - * [1] byte * [l]ittle endian - * [2] bytes * [b]ig endian - * [3] bytes - * [4] bytes - - VLUE - Integer or float, ex. 12 or -1.2 - - - # EXAMPLES - - VRAM/11423051/iu4b/23 - Write (23) to (11423051) in (VRAM) - an [i]nteger, [u]nsigned, - [4] bytes long and [b]ig endian - - WRAM/21512962/fs2l/ - Read from (21512962) in (WRAM) - a [f]loat, [s]igned, - [2] bytes long and [l]ittle endian - """ + def _request(self, query): # Socket requires a byte string to send if type(query) is not bytes: query = query.encode() + print(query.decode('ascii')) with create_connection((self.address, self.port)) as socket: # Send request and expect response @@ -123,168 +113,30 @@ def _receive(self, socket: Socket, n: int=128): return b''.join(buffer) - def _format_tsle(self, type_: type, signed: bool, size: int, endianness: str): - """Format the type, signage, size, and endianness for a request""" - - if type_ not in (bytes, int, float): - raise ValueError('Type must be bytes, int or float') + def build_query(self, form, address=0x00): + ''' + [input] + 0 / button - if size not in (1, 2, 3, 4): - raise ValueError('Size must be 1, 2, 3 or 4 bytes') + [read bytes] + 1 / domain / address - if endianness not in ('little', 'big'): - raise ValueError('Endianness must be little or big') + ''' + if form == FORM['INPUT']: + pass - return ''.join([ - type_.__name__[0], - 'us'[signed], - str(size), - endianness[0] - ]) - - def _format_query(self, address: int, type_: type=bytes, signed: bool=False, - size: int=1, endianness: str='big', value: Union[int, float]=None): - """Format all request parameters into a valid query for a request""" + elif form == FORM['READ']: + # form / domain / address + query = str(form) + DELIMITER + str(self.domain) + DELIMITER + str(address) + return query - tsle = self._format_tsle(type_, signed, size, endianness) - return f'{self.domain}/{address}/{tsle}/{"" if value is None else value}' + else: + raise("Invalid argument type") + return def read_byte(self, address: int): """Read byte from memory""" - return self._request(self._format_query( - address=address, - type_=bytes, - value=None - )) - - def write_byte(self, address: int, value: int): - """Write byte from memory""" - return self._request(self._format_query( - address=address, - type_=bytes, - value=value - )) - - def read_int(self, address: int, signed: bool=None, size: int=None, endianness: str=None): - """Read integer from memory""" - return self._request(self._format_query( - address=address, - type_=int, - signed=signed if signed is not None else self.default_signed, - size=size if size is not None else self.default_size, - endianness=endianness if endianness is not None else self.default_endianness, - value=None - )) - - def write_int(self, address: int, value: int, signed: bool=None, size: int=None, endianness: str=None): - """Write integer from memory""" - return self._request(self._format_query( - address=address, - type_=int, - signed=signed if signed is not None else self.default_signed, - size=size if size is not None else self.default_size, - endianness=endianness if endianness is not None else self.default_endianness, - value=value - )) - - def read_float(self, address: int, endianness: str=None): - """Read float from memory""" - return self._request(self._format_query( - address=address, - type_=float, - endianness=endianness if endianness is not None else self.default_endianness, - value=None - )) - - def write_float(self, address: int, value: float, endianness: str=None): - """Write float from memory""" - return self._request(self._format_query( - address=address, - type_=float, - endianness=endianness if endianness is not None else self.default_endianness, - value=value - )) - - - # |--------------------------------- - # | Non-argumentative alternatives - - def read_u8(self, address: int): - return self.read_int(address, signed=False, size=1) - - def read_u16_be(self, address: int): - return self.read_int(address, signed=False, size=2, endianness='big') - - def read_u24_be(self, address: int): - return self.read_int(address, signed=False, size=3, endianness='big') - - def read_u32_be(self, address: int): - return self.read_int(address, signed=False, size=4, endianness='big') - - def read_u16_le(self, address: int): - return self.read_int(address, signed=False, size=2, endianness='little') - - def read_u24_le(self, address: int): - return self.read_int(address, signed=False, size=3, endianness='little') - - def read_u32_le(self, address: int): - return self.read_int(address, signed=False, size=4, endianness='little') - - def read_s16_be(self, address: int): - return self.read_int(address, signed=True, size=2, endianness='big') - - def read_s24_be(self, address: int): - return self.read_int(address, signed=True, size=3, endianness='big') - - def read_s32_be(self, address: int): - return self.read_int(address, signed=True, size=4, endianness='big') - - def read_s16_le(self, address: int): - return self.read_int(address, signed=True, size=2, endianness='little') - - def read_s24_le(self, address: int): - return self.read_int(address, signed=True, size=3, endianness='little') - - def read_s32_le(self, address: int): - return self.read_int(address, signed=True, size=4, endianness='little') - - def write_u8(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=1) - - def write_u16_be(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=2, endianness='big') - - def write_u24_be(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=3, endianness='big') - - def write_u32_be(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=4, endianness='big') - - def write_u16_le(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=2, endianness='little') - - def write_u24_le(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=3, endianness='little') - - def write_u32_le(self, address: int, value: int): - return self.write_int(address, value, signed=False, size=4, endianness='little') - - def write_s16_be(self, address: int, value: int): - return self.write_int(address, value, signed=True, size=2, endianness='big') - - def write_s24_be(self, address: int, value: int): - return self.write_int(address, value, signed=True, size=3, endianness='big') - - def write_s32_be(self, address: int, value: int): - return self.write_int(address, value, signed=True, size=4, endianness='big') - - def write_s16_le(self, address: int, value: int): - return self.write_int(address, value, signed=True, size=2, endianness='little') - - def write_s24_le(self, address: int, value: int): - return self.write_int(address, value, signed=True, size=3, endianness='little') - - def write_s32_le(self, address: int, value: int): - return self.write_int(address, value, signed=True, size=4, endianness='little') \ No newline at end of file + q = self.build_query(address) + return self._request(q) From 255c6c46e0125c11ffe8469c5dc2b8a7b73049d6 Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 13:43:50 -0500 Subject: [PATCH 02/10] ignore autogenerated python files --- .gitignore | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28f4b7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +### MANUAL ### + + +### TEMPLATES ### +## Ignoreable Python Files (via https://github.com/github/gitignore/blob/main/Python.gitignore) +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ From 27b45bed34eb6308eaf2e3f18a75c452987653d3 Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 13:44:08 -0500 Subject: [PATCH 03/10] rename form to query_type --- bizhook/memory.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index dfa93a7..ad6ed0c 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -4,7 +4,7 @@ from .exceptions import InvalidRequest, InvalidResponse -FORM = { +QUERY_TYPE = { "INPUT": 0, "READ": 1, "WRITE": 2, @@ -113,7 +113,7 @@ def _receive(self, socket: Socket, n: int=128): return b''.join(buffer) - def build_query(self, form, address=0x00): + def build_query(self, query_type, address=0x00): ''' [input] 0 / button @@ -123,12 +123,12 @@ def build_query(self, form, address=0x00): ''' - if form == FORM['INPUT']: + if query_type == QUERY_TYPE['INPUT']: pass - elif form == FORM['READ']: - # form / domain / address - query = str(form) + DELIMITER + str(self.domain) + DELIMITER + str(address) + elif query_type == QUERY_TYPE['READ']: + # query_type / domain / address + query = str(query_type) + DELIMITER + str(self.domain) + DELIMITER + str(address) + DELIMITER return query else: From c65a6c68b8685da22a45f1960e7c2956f6d3bada Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 13:48:19 -0500 Subject: [PATCH 04/10] input query format setup --- bizhook/memory.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index ad6ed0c..161aba3 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -113,17 +113,18 @@ def _receive(self, socket: Socket, n: int=128): return b''.join(buffer) - def build_query(self, query_type, address=0x00): + def build_query(self, query_type, address=0x00, button_name: str=None, button_state: bool=None): ''' [input] - 0 / button + 0 / button_name / button_state / [read bytes] - 1 / domain / address + 1 / domain / address / ''' if query_type == QUERY_TYPE['INPUT']: + query = str(query_type) + DELIMITER + button_name + DELIMITER + str(button_state) + DELIMITER pass elif query_type == QUERY_TYPE['READ']: From b66e43790e63c360b8acb88cc5b35499f90b35cb Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 13:49:44 -0500 Subject: [PATCH 05/10] query types defined --- bizhook/memory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index 161aba3..b7ff18f 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -113,8 +113,10 @@ def _receive(self, socket: Socket, n: int=128): return b''.join(buffer) - def build_query(self, query_type, address=0x00, button_name: str=None, button_state: bool=None): + def build_query(self, query_type: int, address: int=0x00, button_name: str=None, button_state: bool=None): ''' + QUERY FORMATS: + [input] 0 / button_name / button_state / @@ -124,11 +126,12 @@ def build_query(self, query_type, address=0x00, button_name: str=None, button_st ''' if query_type == QUERY_TYPE['INPUT']: + # 0 / button_name / button_state / query = str(query_type) + DELIMITER + button_name + DELIMITER + str(button_state) + DELIMITER pass elif query_type == QUERY_TYPE['READ']: - # query_type / domain / address + # 1 / domain / address / query = str(query_type) + DELIMITER + str(self.domain) + DELIMITER + str(address) + DELIMITER return query From a320ced8f3b36d0a2e4e02ec0e0d6b896d25f973 Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 15:05:36 -0500 Subject: [PATCH 06/10] now check if arguments are valid --- bizhook/memory.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index b7ff18f..2afe939 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -12,7 +12,7 @@ } ''' Planned: -emulator: +client: load rom reset? ''' @@ -127,13 +127,20 @@ def build_query(self, query_type: int, address: int=0x00, button_name: str=None, if query_type == QUERY_TYPE['INPUT']: # 0 / button_name / button_state / - query = str(query_type) + DELIMITER + button_name + DELIMITER + str(button_state) + DELIMITER - pass + try: + query = str(query_type) + DELIMITER + button_name + DELIMITER + str(button_state) + DELIMITER + return query + except TypeError as e: + raise(f"Arguments missing from query...\n{e}") + elif query_type == QUERY_TYPE['READ']: # 1 / domain / address / - query = str(query_type) + DELIMITER + str(self.domain) + DELIMITER + str(address) + DELIMITER - return query + try: + query = str(query_type) + DELIMITER + str(self.domain) + DELIMITER + str(address) + DELIMITER + return query + except TypeError as e: + raise(f"Arguments missing from query...\n{e}") else: raise("Invalid argument type") From 7a007a7fbba485b618e7b86d4d96015816a06ccd Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 15:10:39 -0500 Subject: [PATCH 07/10] fixed missing query type in read_byte --- bizhook/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index 2afe939..e1941af 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -149,5 +149,5 @@ def build_query(self, query_type: int, address: int=0x00, button_name: str=None, def read_byte(self, address: int): """Read byte from memory""" - q = self.build_query(address) + q = self.build_query(QUERY_TYPE["READ"], address) return self._request(q) From d07d4bf1314e203849948118409a9fffdebde359 Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 18:52:05 -0500 Subject: [PATCH 08/10] first implimentation of new API --- bizhook/memory.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index e1941af..da03a43 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -1,6 +1,7 @@ from socket import AF_INET, SOCK_STREAM, SHUT_RDWR from socket import socket as Socket from socket import create_connection +import time from .exceptions import InvalidRequest, InvalidResponse @@ -54,14 +55,16 @@ def __init__(self, def _request(self, query): + # print("_request") # Socket requires a byte string to send if type(query) is not bytes: query = query.encode() - print(query.decode('ascii')) + print("_request: query:", query.decode('ascii')) with create_connection((self.address, self.port)) as socket: + # print("_request: sending") # Send request and expect response socket.sendall(query) response = self._receive(socket) @@ -71,6 +74,7 @@ def _request(self, query): # Extract response code and message code, _, message = response.decode('UTF-8').partition('_') code = int(code) + print(f"_request: code={code} message={message}") except ValueError: raise InvalidResponse('Response could not be divided into code and message') @@ -102,11 +106,11 @@ def _receive(self, socket: Socket, n: int=128): # and concatenate them at the end buffer = [] - while True: data = socket.recv(n) if not data: + print("_receive: no data") break buffer.append(data) @@ -124,7 +128,6 @@ def build_query(self, query_type: int, address: int=0x00, button_name: str=None, 1 / domain / address / ''' - if query_type == QUERY_TYPE['INPUT']: # 0 / button_name / button_state / try: From c511360b19a6dd82d6685f7169e8e33f3d1595c0 Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Mon, 7 Nov 2022 20:51:14 -0500 Subject: [PATCH 09/10] removed some unnecessary printing --- bizhook/memory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index da03a43..7268036 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -61,7 +61,7 @@ def _request(self, query): if type(query) is not bytes: query = query.encode() - print("_request: query:", query.decode('ascii')) + # print("_request: query:", query.decode('ascii')) with create_connection((self.address, self.port)) as socket: # print("_request: sending") @@ -74,7 +74,7 @@ def _request(self, query): # Extract response code and message code, _, message = response.decode('UTF-8').partition('_') code = int(code) - print(f"_request: code={code} message={message}") + # print(f"_request: code={code} message={message}") except ValueError: raise InvalidResponse('Response could not be divided into code and message') @@ -110,7 +110,7 @@ def _receive(self, socket: Socket, n: int=128): data = socket.recv(n) if not data: - print("_receive: no data") + # print("_receive: no data") break buffer.append(data) From 2fb955409ee082870e017eac174bb1e9558ebaec Mon Sep 17 00:00:00 2001 From: huttsMichael Date: Wed, 9 Nov 2022 22:08:16 -0500 Subject: [PATCH 10/10] basic input-sending complete --- bizhook/memory.py | 49 ++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/bizhook/memory.py b/bizhook/memory.py index 7268036..2370847 100644 --- a/bizhook/memory.py +++ b/bizhook/memory.py @@ -1,37 +1,33 @@ from socket import AF_INET, SOCK_STREAM, SHUT_RDWR from socket import socket as Socket from socket import create_connection -import time from .exceptions import InvalidRequest, InvalidResponse -QUERY_TYPE = { - "INPUT": 0, - "READ": 1, - "WRITE": 2, - "CLIENT": 3 -} ''' Planned: client: load rom reset? ''' +QUERY_TYPE = { + "INPUT": 0, + "READ": 1, + "WRITE": 2, + "CLIENT": 3 +} + +RESPONSE_CODES = { + "INPUT": 0, # Successfully passed input + "BYTE": 1, # Successfully read byte + "INTEGER": 2, # Successfully read integer + "FLOAT": 3, # Successfully read float + "ERROR": 4 # Generic error +} + DELIMITER = '/' -# BUTTON_TRANSLATE = { -# "P1 A": 0, -# "P1 B": 1, -# "P1 Down": 2, -# "P1 Left": 3, -# "P1 Right": 4, -# "P1 Select": 5, -# "P1 Start": 6, -# "P1 Up": 7, -# "Power": 8, -# "Reset": 9 -# } class Memory: """Client for reading from and writing to Bizhawk memory""" @@ -81,19 +77,19 @@ def _request(self, query): # Successfully wrote to memory - if code == 0: + if code == RESPONSE_CODES["INPUT"]: return True # Successfully read byte - if code == 1: + if code == RESPONSE_CODES["BYTE"]: return response[response.index(b'_'):] # Successfully read integer - if code == 2: + if code == RESPONSE_CODES["INTEGER"]: return int(message) # Successfully read float - if code == 3: + if code == RESPONSE_CODES["FLOAT"]: return float(message) @@ -152,5 +148,10 @@ def build_query(self, query_type: int, address: int=0x00, button_name: str=None, def read_byte(self, address: int): """Read byte from memory""" - q = self.build_query(QUERY_TYPE["READ"], address) + q = self.build_query(QUERY_TYPE["READ"], address=address) + return self._request(q) + + def send_input(self, key_name: str, key_state: bool): + """Pass input to emulator""" + q = self.build_query(QUERY_TYPE["INPUT"], button_name=key_name, button_state=key_state) return self._request(q)