diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f32ca2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.egg-info/ +/dist/ +/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..630c651 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Change Log +## \[v0.1.0] pack into one PyPI package (2022/06/25) +Complete all basic functions, including: +- find dolphin, init shared memory +- read/write raw bytes +- read/write struct +- read/write single value (uint32/16/8, int32/16/8, float) + +## Base +This library is based on [Yoshi2's dolphin-memory-lib](https://github.com/RenolY2/dolphin-memory-lib) +``` +Copyright (c) 2022 Yoshi2, NerduMiner +``` +and [aldelaro5's Dolphin-memory-engine](https://github.com/aldelaro5/Dolphin-memory-engine) +``` +Copyright (c) 2017 aldelaro5 +``` diff --git a/LICENSE.txt b/LICENSE similarity index 93% rename from LICENSE.txt rename to LICENSE index 07645a0..9623032 100644 --- a/LICENSE.txt +++ b/LICENSE @@ -1,21 +1,23 @@ -MIT License - -Copyright (c) 2022 Yoshi2, NerduMiner - -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. +MIT License + +Copyright (c) 2022 sup39 +Copyright (c) 2022 Yoshi2, NerduMiner +Copyright (c) 2017 aldelaro5 + +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. diff --git a/README.md b/README.md index e2cfd74..c3a5115 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# dolphin-memory-lib -A Python library for reading and writing the memory of an emulated game in Dolphin. +# dolphin-memory-lib +A Python library for reading and writing the memory of an emulated game in Dolphin. + +## Usage +```python +from dolphin.memorylib import Dolphin +if dolphin.hook() is None: + print('No game is running') + +addr = 0x80000000 +dolphin.write_uint32(addr, 39) +result = dolphin.read_uint32(0x80000000) +# assert result == 39 +``` diff --git a/ctypes.txt b/ctypes.txt deleted file mode 100644 index 53eb12d..0000000 --- a/ctypes.txt +++ /dev/null @@ -1,120 +0,0 @@ -__name__ -__doc__ -__package__ -__loader__ -__spec__ -__path__ -__file__ -__cached__ -__builtins__ -_os -_sys -_types -__version__ -Union -Structure -Array -_Pointer -_CFuncPtr -_ctypes_version -RTLD_LOCAL -RTLD_GLOBAL -ArgumentError -_calcsize -FormatError -DEFAULT_MODE -_FUNCFLAG_CDECL -_FUNCFLAG_PYTHONAPI -_FUNCFLAG_USE_ERRNO -_FUNCFLAG_USE_LASTERROR -create_string_buffer -c_buffer -_c_functype_cache -CFUNCTYPE -_dlopen -_FUNCFLAG_STDCALL -_win_functype_cache -WINFUNCTYPE -sizeof -byref -addressof -alignment -resize -get_errno -set_errno -_SimpleCData -_check_size -py_object -c_short -c_ushort -c_long -c_ulong -c_int -c_uint -c_float -c_double -c_longdouble -c_longlong -c_ulonglong -c_ubyte -c_byte -c_char -c_char_p -c_void_p -c_voidp -c_bool -POINTER -pointer -_pointer_type_cache -c_wchar_p -c_wchar -_reset_cache -create_unicode_buffer -SetPointerType -ARRAY -CDLL -PyDLL -WinDLL -_check_HRESULT -HRESULT -OleDLL -LibraryLoader -cdll -pydll -pythonapi -windll -oledll -GetLastError -get_last_error -set_last_error -WinError -c_size_t -c_ssize_t -_memmove_addr -_memset_addr -_string_at_addr -_cast_addr -memmove -memset -PYFUNCTYPE -_cast -cast -_string_at -string_at -_wstring_at_addr -_wstring_at -wstring_at -DllGetClassObject -DllCanUnloadNow -_endian -BigEndianStructure -LittleEndianStructure -c_int8 -c_uint8 -c_int16 -c_int32 -c_int64 -c_uint16 -c_uint32 -c_uint64 -wintypes diff --git a/memorylib.py b/memorylib.py deleted file mode 100644 index 5b1875b..0000000 --- a/memorylib.py +++ /dev/null @@ -1,228 +0,0 @@ -import ctypes -import struct -from struct import pack, unpack -from ctypes import wintypes, sizeof, addressof, POINTER, pointer -from ctypes.wintypes import DWORD, ULONG, LONG, WORD -from multiprocessing import shared_memory - -# Various Windows structs/enums needed for operation -NULL = 0 - -TH32CS_SNAPHEAPLIST = 0x00000001 -TH32CS_SNAPPROCESS = 0x00000002 -TH32CS_SNAPTHREAD = 0x00000004 -TH32CS_SNAPMODULE = 0x00000008 -TH32CS_SNAPALL = TH32CS_SNAPHEAPLIST | TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD | TH32CS_SNAPMODULE -assert TH32CS_SNAPALL == 0xF - - -PROCESS_QUERY_INFORMATION = 0x0400 -PROCESS_VM_OPERATION = 0x0008 -PROCESS_VM_READ = 0x0010 -PROCESS_VM_WRITE = 0x0020 - -MEM_MAPPED = 0x40000 - -ULONG_PTR = ctypes.c_ulonglong - -class PROCESSENTRY32(ctypes.Structure): - _fields_ = [ ( 'dwSize' , DWORD ) , - ( 'cntUsage' , DWORD) , - ( 'th32ProcessID' , DWORD) , - ( 'th32DefaultHeapID' , ctypes.POINTER(ULONG)) , - ( 'th32ModuleID' , DWORD) , - ( 'cntThreads' , DWORD) , - ( 'th32ParentProcessID' , DWORD) , - ( 'pcPriClassBase' , LONG) , - ( 'dwFlags' , DWORD) , - ( 'szExeFile' , ctypes.c_char * 260 ) ] - - -class MEMORY_BASIC_INFORMATION(ctypes.Structure): - _fields_ = [ ( 'BaseAddress' , ctypes.c_void_p), - ( 'AllocationBase' , ctypes.c_void_p), - ( 'AllocationProtect' , DWORD), - ( 'PartitionID' , WORD), - ( 'RegionSize' , ctypes.c_size_t), - ( 'State' , DWORD), - ( 'Protect' , DWORD), - ( 'Type' , DWORD)] - - -class PSAPI_WORKING_SET_EX_BLOCK(ctypes.Structure): - _fields_ = [ ( 'Flags', ULONG_PTR), - ( 'Valid', ULONG_PTR), - ( 'ShareCount', ULONG_PTR), - ( 'Win32Protection', ULONG_PTR), - ( 'Shared', ULONG_PTR), - ( 'Node', ULONG_PTR), - ( 'Locked', ULONG_PTR), - ( 'LargePage', ULONG_PTR), - ( 'Reserved', ULONG_PTR), - ( 'Bad', ULONG_PTR), - ( 'ReservedUlong', ULONG_PTR)] - - -#class PSAPI_WORKING_SET_EX_INFORMATION(ctypes.Structure): -# _fields_ = [ ( 'VirtualAddress' , ctypes.c_void_p), -# ( 'VirtualAttributes' , PSAPI_WORKING_SET_EX_BLOCK)] - -class PSAPI_WORKING_SET_EX_INFORMATION(ctypes.Structure): - _fields_ = [ ( 'VirtualAddress' , ctypes.c_void_p), - #( 'Flags', ULONG_PTR), - ( 'Valid', ULONG_PTR, 1)] - #( 'ShareCount', ULONG_PTR), - #( 'Win32Protection', ULONG_PTR), - #( 'Shared', ULONG_PTR), - #( 'Node', ULONG_PTR), - #( 'Locked', ULONG_PTR), - #( 'LargePage', ULONG_PTR), - #( 'Reserved', ULONG_PTR), - #( 'Bad', ULONG_PTR), - #( 'ReservedUlong', ULONG_PTR)] - - #def print_values(self): - # for i,v in self._fields_: - # print(i, getattr(self, i)) - - -# The find_dolphin function is based on WindowsDolphinProcess::findPID() from -# aldelaro5's Dolphin memory engine -# https://github.com/aldelaro5/Dolphin-memory-engine - -""" -MIT License - -Copyright (c) 2017 aldelaro5 - -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.""" - -class Dolphin(object): - def __init__(self): - self.pid = -1 - self.memory = None - - def reset(self): - self.pid = -1 - self.memory = None - - def find_dolphin(self, skip_pids=[]): - entry = PROCESSENTRY32() - - entry.dwSize = sizeof(PROCESSENTRY32) - snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL) - print(addressof(entry), hex(addressof(entry))) - a = ULONG(addressof(entry)) - - self.pid = -1 - - if ctypes.windll.kernel32.Process32First(snapshot, pointer(entry)): - if entry.th32ProcessID not in skip_pids and entry.szExeFile in (b"Dolphin.exe", b"DolphinQt2.exe", b"DolphinWx.exe"): - self.pid = entry.th32ProcessID - else: - while ctypes.windll.kernel32.Process32Next(snapshot, pointer(entry)): - if entry.th32ProcessID in skip_pids: - continue - if entry.szExeFile in (b"Dolphin.exe", b"DolphinQt2.exe", b"DolphinWx.exe"): - self.pid = entry.th32ProcessID - - - ctypes.windll.kernel32.CloseHandle(snapshot) - - if self.pid == -1: - return False - - return True - - def init_shared_memory(self): - try: - self.memory = shared_memory.SharedMemory('dolphin-emu.'+str(self.pid)) - return True - except FileNotFoundError: - return False - - def read_ram(self, offset, size): - return self.memory.buf[offset:offset+size] - - def write_ram(self, offset, data): - self.memory.buf[offset:offset+len(data)] = data - - def read_uint32(self, addr): - assert addr >= 0x80000000 - value = self.read_ram(addr-0x80000000, 4) - - return unpack(">I", value)[0] - - def write_uint32(self, addr, val): - assert addr >= 0x80000000 - return self.write_ram(addr - 0x80000000, pack(">I", val)) - - def read_float(self, addr): - assert addr >= 0x80000000 - value = self.read_ram(addr - 0x80000000, 4) - - return unpack(">f", value)[0] - - def write_float(self, addr, val): - assert addr >= 0x80000000 - return self.write_ram(addr - 0x80000000, pack(">f", val)) - - -"""with open("ctypes.txt", "w") as f: - for a in ctypes.__dict__: - f.write(str(a)) - f.write("\n")""" - -if __name__ == "__main__": - dolphin = Dolphin() - import multiprocessing - - if dolphin.find_dolphin(): - - print("Found Dolphin!") - else: - print("Didn't find Dolphin") - - print(dolphin.pid) - - dolphin.init_shared_memory() - if dolphin.init_shared_memory(): - print("We found MEM1 and/or MEM2!") - else: - print("We didn't find it...") - - import random - randint = random.randint - from timeit import default_timer - - start = default_timer() - - print("Testing Shared Memory Method") - start = default_timer() - count = 500000 - for i in range(count): - value = randint(0, 2**32-1) - dolphin.write_uint32(0x80000000, value) - - result = dolphin.read_uint32(0x80000000) - assert result == value - diff = default_timer()-start - print(count/diff, "per sec") - print("time: ", diff) - \ No newline at end of file diff --git a/memtest_lin.py b/memtest_lin.py deleted file mode 100644 index da375b0..0000000 --- a/memtest_lin.py +++ /dev/null @@ -1,187 +0,0 @@ -import ctypes -import struct -import os -import sys -from subprocess import check_output -from ctypes import sizeof, addressof, POINTER, pointer - -# Various Linux structs needed for operation - -class iovec(ctypes.Structure): - _fields_ = [("iov_base",ctypes.c_void_p),("iov_len",ctypes.c_size_t)] - -libc = ctypes.cdll.LoadLibrary("libc.so.6") -vm = libc.process_vm_readv -vm.argtypes = [ctypes.c_int, POINTER(iovec), ctypes.c_ulong, POINTER(iovec), ctypes.c_ulong, ctypes.c_ulong] -vmwrite = libc.process_vm_writev -vmwrite.argtypes = [ctypes.c_int, POINTER(iovec), ctypes.c_ulong, POINTER(iovec), ctypes.c_ulong, ctypes.c_ulong] - - -# The following code is a port of aldelaro5's Dolphin memory access methods -# for Linux into Python+ctypes. -# https://github.com/aldelaro5/Dolphin-memory-engine - -""" -MIT License - -Copyright (c) 2017 aldelaro5 - -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.""" - -class Dolphin(object): - def __init__(self): - self.pid = -1 - self.handle = -1 - - self.address_start = 0 - self.mem1_start = 0 - self.mem2_start = 0 - self.mem2_exists = False - - def find_dolphin(self): - try: - if check_output(["pidof", "dolphin-emu"]) != '\n': - self.pid = int(check_output(["pidof", "dolphin-emu"])) - if check_output(["pidof", "dolphin-emu-qt2"]) != '\n': - self.pid = int(check_output(["pidof", "dolphin-emu-qt2"])) - if check_output(["pidof", "dolphin-emu-wx"]) != '\n': - self.pid = int(check_output(["pidof", "dolphin-emu-wx"])) - except Exception: #subprocess.CalledProcessError - # Do nothing because self.pid cant be modified until a successful run of pidof - pass - - if self.pid == -1: - return False - - return True - - def get_emu_info(self): - MEM1_found = False - try: - maps_file = open("/proc/{}/maps".format(self.pid), 'r') - except IOError: - print("Cant open maps for process {}".format(self.pid)) - heap_info = None - for line in maps_file: - foundDevShmDolphin = False - if '/dev/shm/dolphinmem' in line: - heap_info = line.split() - if '/dev/shm/dolphin-emu' in line: - heap_info = line.split() - if heap_info is None: - continue - else: - offset = 0 - offset_str = "0x" + str(heap_info[2]) - offset = int(offset_str, 16) - if offset != 0 and offset != 0x2000000: - continue - first_address = 0 - second_address = 0 - index_dash = heap_info[0].find('-') - - first_address_str = "0x" + str(heap_info[0][: index_dash]) - second_address_str = "0x" + str(heap_info[0][(index_dash + 1):]) - - first_address = int(first_address_str, 16) - second_address = int(second_address_str, 16) - - if (second_address - first_address) == 0x4000000 and offset == 0x2000000: - self.mem2_start = first_address - self.mem2_exists = True - if (second_address - first_address) == 0x2000000 and offset == 0x0: - self.address_start = first_address - - if self.address_start == 0: - return False - return True - - def read_ram(self, offset, size): - buffer_ = (ctypes.c_char*size)() - nread = ctypes.c_size_t - local = (iovec*1)() - remote = (iovec*1)() - local[0].iov_base = ctypes.addressof(buffer_) - local[0].iov_len = size - remote[0].iov_base = ctypes.c_void_p(self.address_start + offset) - remote[0].iov_len = size - nread = vm(self.pid, local, 1, remote, 1, 0) - if nread != size: - return False, buffer_ - return True, buffer_ - - def write_ram(self, offset, data): - buffer_ = (ctypes.c_char*len(data))(*data) - nwrote = ctypes.c_size_t - local = (iovec*1)() - remote = (iovec*1)() - local[0].iov_base = ctypes.addressof(buffer_) - local[0].iov_len = len(data) - remote[0].iov_base = ctypes.c_void_p(self.address_start + offset) - remote[0].iov_len = len(data) - nwrote = vmwrite(self.pid, local, 1, remote, 1, 0) - if nwrote != len(data): - return False - return True - - def read_uint32(self, addr): - assert addr >= 0x80000000 - success, value = self.read_ram(addr-0x80000000, 4) - - if success: - return struct.unpack(">I", value)[0] - else: - return None - - def read_float(self, addr): - assert addr >= 0x80000000 - success, value = self.read_ram(addr - 0x80000000, 4) - - if success: - return struct.unpack(">f", value)[0] - else: - return None - - def write_float(self, addr, val): - assert addr >= 0x80000000 - return self.write_ram(addr - 0x80000000, struct.pack(">f", val)) - - -if __name__ == "__main__": - dolphin = Dolphin() - - if dolphin.find_dolphin(): - - print("Found Dolphin! ") - else: - print("Didn't find Dolphin") - - print(dolphin.pid) - - if dolphin.get_emu_info(): - print("We found MEM1 and/or MEM2!", dolphin.address_start, dolphin.mem2_start) - else: - print("We didn't find it...") - print(dolphin.write_ram(0, b"GMS")) - success, result = dolphin.read_ram(0, 8) - print(result[0:8]) - - print(dolphin.write_ram(0, b"AWA")) - success, result = dolphin.read_ram(0, 8) - print(result[0:8]) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c0df35a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42"] +build-backend = "setuptools.build_meta" diff --git a/run.bat b/run.bat deleted file mode 100644 index 8d494e5..0000000 --- a/run.bat +++ /dev/null @@ -1,2 +0,0 @@ -python memorylib.py -pause \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100755 index ca650bc..0000000 --- a/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -echo "dolphin-memory-lib requires sudo permission to read and write to the emulator process memory." -sudo python memtest_lin.py -read -n1 -r diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7829559 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,25 @@ +[metadata] +name = sup-dolphin-memory-lib +version = 0.1.0 +author = sup39 +author_email = sms@sup39.dev +description = A Python library for reading and writing the memory of an emulated game in Dolphin +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/sup39/dolphin-memory-lib +project_urls = + Bug Tracker = https://github.com/sup39/dolphin-memory-lib/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.8 +install_requires = + +[options.packages.find] +where = src diff --git a/src/dolphin/__init__.py b/src/dolphin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dolphin/memorylib.py b/src/dolphin/memorylib.py new file mode 100644 index 0000000..5d194fa --- /dev/null +++ b/src/dolphin/memorylib.py @@ -0,0 +1,255 @@ +# SPDX-License-Identifier: MIT +''' + Copyright (c) 2022 sup39 + +This file is based on Yoshi2's dolphin-memory-lib +https://github.com/RenolY2/dolphin-memory-lib + Copyright (c) 2022 Yoshi2, NerduMiner + +The find_dolphin function is based on WindowsDolphinProcess::findPID() from +aldelaro5's Dolphin memory engine +https://github.com/aldelaro5/Dolphin-memory-engine + Copyright (c) 2017 aldelaro5 +''' + +import os +from struct import pack, unpack, calcsize +from multiprocessing.shared_memory import SharedMemory + +if os.name == 'nt': + # windows + from ctypes import Structure, POINTER, sizeof, byref, windll + from ctypes.wintypes import DWORD, ULONG, LONG, CHAR, MAX_PATH + kernel32 = windll.kernel32 + NULL = 0 + ## https://docs.microsoft.com/ja-jp/windows/win32/api/tlhelp32/ns-tlhelp32-processentry32 + class PROCESSENTRY32(Structure): + _fields_ = [ + ('dwSize', DWORD), + ('cntUsage', DWORD), + ('th32ProcessID', DWORD), + ('th32DefaultHeapID', POINTER(ULONG)), + ('th32ModuleID', DWORD), + ('cntThreads', DWORD), + ('th32ParentProcessID', DWORD), + ('pcPriClassBase', LONG), + ('dwFlags', DWORD), + ('szExeFile', CHAR*MAX_PATH), + ] + ## https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot + TH32CS_SNAPPROCESS = 2 + ## find pids of dolphin + def find_dolphin(): + # prepare entry struct + entry = PROCESSENTRY32() + entry.dwSize = sizeof(PROCESSENTRY32) + # prepare snapshot + snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL) + # find pids + pids = [] + if kernel32.Process32First(snapshot, byref(entry)): + while True: + if entry.szExeFile in (b'Dolphin.exe', b'DolphinQt2.exe', b'DolphinWx.exe'): + pids.append(entry.th32ProcessID) + if not kernel32.Process32Next(snapshot, byref(entry)): break + kernel32.CloseHandle(snapshot); + # done + return pids +else: + # UNIX + import psutil + def find_dolphin(): + return [ + proc.pid + for proc in psutil.process_iter() + if proc.name() in ('dolphin-emu', 'dolphin-emu-qt2', 'dolphin-emu-wx') + ] + +''' +@typedef {(int|str) | [(int|str), ...int[]]} Addr + -- address or symbol name with arbitrary offsets + -- e.g. 0x8040A378, 'gpMarioOriginal', + -- (0x8040A2A8, 0x54), ('gpMap', 0x10, 0x04) +''' + +class Dolphin(): + def __init__(self): + self.pid = None + self.memory = None + def reset(self): + self.pid = None + self.memory = None + def hook(self, pids=None): + ''' + @params pids {None|int|Iterable} + -- pid or pid array of dolphin + @returns {int|None} + -- pid of hooked dolphin + ''' + self.memory = None + # init pids + if pids is None: # auto-detect + pids = Dolphin.find_dolphin() + elif type(pids) is int: # pid -> [pid] + pids = [pids] + ## no process found + if len(pids) == 0: return None + # init memory + for pid in pids: + memory = Dolphin.init_shared_memory(pid) + if memory is not None: + self.pid = pid + self.memory = memory + return pid + ## no memory found + return None + + # virtual methods + def get_symb_addr(self, name): + ''' + @params {str} name + -- name of the symbol + @returns {int|never} + -- addr of the symbol + ''' + raise NotImplemented + + # private methods + def _get_slice(self, addr, size): + ''' + @params {int} addr + -- memory address + @params {int} size + -- size of memory slice + @returns {slice|never} + -- slice object for self.memory at the address + ''' + idx = addr - 0x8000_0000 + assert 0 <= idx < 0x0180_0000 + return slice(idx, idx+size) + def _read_bytes(self, addr, size): + ''' + @params {int} addr + -- memory address + @params {int} size + -- size to read + @returns {bytes|never} + -- bytes at the address + ''' + return self.memory.buf[self._get_slice(addr, size)].tobytes() + def _write_bytes(self, addr, data): + ''' + @params {int} addr + -- memory address + @params {bytes} data + -- bytes to write + ''' + self.memory.buf[self._get_slice(addr, len(data))] = data + + # public methods + def try_resolve_addr(self, addr): + ''' + @params {Addr} addr + -- address or symbol name with arbitrary offsets + @returns {int|None} + -- (resolved address) or (None if NullPointerException occurred) + ''' + try: addr, *offsets = addr + except TypeError: offsets = [] + # resolve base + if type(addr) == str: + addr = self.get_symb_addr(addr) + # offset + for off in offsets: + # dereference + addr = unpack('>I', self._read_bytes(addr, 4))[0] + # check nullptr + if addr == 0: return None + # add offset + addr += off + return addr + def read_bytes(self, addr, size): + ''' + @params {Addr} addr + ## See `addr` of `try_resolve_addr()` + @params {int} size + -- size to read + @returns {bytes|None} + -- (bytes at addr) or (None if NullPointerException occurred) + ''' + addr = self.try_resolve_addr(addr) + if addr is None: return None + return self._read_bytes(addr, size) + def write_bytes(self, addr, data): + ''' + @params {Addr} addr + ## See `addr` of `try_resolve_addr()` + @params {bytes} data + -- bytes to write + @returns {int|None} + -- (written address) or (None if NullPointerException occurred) + ''' + addr = self.try_resolve_addr(addr) + if addr is None: return None + self._write_bytes(addr, data) + return addr + def read_struct(self, addr, fmt): + ''' + @params {Addr} addr + ## See `addr` of `try_resolve_addr()` + @params {int} size + -- size to read + @returns {bytes|None} + ## See `addr` of `read_bytes()` + ''' + data = self.read_bytes(addr, calcsize(fmt)) + return None if data is None else unpack(fmt, data) + def write_struct(self, addr, fmt, *args): + ''' + @params {Addr} addr + ## See `addr` of `try_resolve_addr()` + @params {str} fmt + -- format string for struct.pack + @params {...} *args + -- args for struct.pack(fmt, *args) + @returns {int|None} + ## See `addr` of `write_bytes()` + ''' + return self.write_bytes(addr, pack(fmt, *args)) + + ## read single value from memory + ''' + @params {Addr} addr + ## See `addr` of `try_resolve_addr()` + @returns {bytes|None} + ## See `addr` of `read_bytes()` + ''' + def read_uint32(self, addr): return self.read_struct(addr, '>I')[0] + def read_uint16(self, addr): return self.read_struct(addr, '>H')[0] + def read_uint8(self, addr): return self.read_struct(addr, '>B')[0] + def read_int32(self, addr): return self.read_struct(addr, '>i')[0] + def read_int16(self, addr): return self.read_struct(addr, '>h')[0] + def read_int8(self, addr): return self.read_struct(addr, '>b')[0] + def read_float(self, addr): return self.read_struct(addr, '>f')[0] + ## write single value to memory + ''' + @params {Addr} addr + ## See `addr` of `try_resolve_addr()` + @params {...} val + -- value to write + @returns {int|None} + ## See `addr` of `write_bytes()` + ''' + def write_uint32(self, addr, val): return self.write_struct(addr, '>I', val) + def write_uint16(self, addr, val): return self.write_struct(addr, '>H', val) + def write_uint8(self, addr, val): return self.write_struct(addr, '>B', val) + def write_int32(self, addr, val): return self.write_struct(addr, '>i', val) + def write_int16(self, addr, val): return self.write_struct(addr, '>h', val) + def write_int8(self, addr, val): return self.write_struct(addr, '>b', val) + def write_float(self, addr, val): return self.write_struct(addr, '>f', val) + + # static methods + def init_shared_memory(pid): + try: return SharedMemory('dolphin-emu.'+str(pid)) + except FileNotFoundError: return None + find_dolphin = find_dolphin diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_memorylib.py b/tests/test_memorylib.py new file mode 100644 index 0000000..5464806 --- /dev/null +++ b/tests/test_memorylib.py @@ -0,0 +1,27 @@ +import unittest +from dolphin.memorylib import Dolphin + +class TestMemorylib(unittest.TestCase): + def test_rw_uint32(self): + dolphin = Dolphin() + import multiprocessing + self.assertIsNotNone(dolphin.hook(), msg='No game is running') + + from random import randint + from timeit import default_timer + start = default_timer() + + print("Testing Shared Memory Method") + start = default_timer() + count = 500000 + for i in range(count): + value = randint(0, 2**32-1) + dolphin.write_uint32(0x80000000, value) + result = dolphin.read_uint32(0x80000000) + self.assertEqual(result, value) + diff = default_timer()-start + print(count/diff, "per sec") + print("time: ", diff) + +if __name__ == '__main__': + unittest.main()