[v0.1.0] pack into one PyPI package
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)
This commit is contained in:
parent
0b7bfc2e81
commit
2527142f07
15 changed files with 368 additions and 564 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.egg-info/
|
||||
/dist/
|
||||
/build/
|
17
CHANGELOG.md
Normal file
17
CHANGELOG.md
Normal file
|
@ -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
|
||||
```
|
|
@ -1,6 +1,8 @@
|
|||
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
|
12
README.md
12
README.md
|
@ -1,2 +1,14 @@
|
|||
# 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
|
||||
```
|
||||
|
|
120
ctypes.txt
120
ctypes.txt
|
@ -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
|
228
memorylib.py
228
memorylib.py
|
@ -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)
|
||||
|
187
memtest_lin.py
187
memtest_lin.py
|
@ -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])
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
2
run.bat
2
run.bat
|
@ -1,2 +0,0 @@
|
|||
python memorylib.py
|
||||
pause
|
4
run.sh
4
run.sh
|
@ -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
|
25
setup.cfg
Normal file
25
setup.cfg
Normal file
|
@ -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
|
0
src/dolphin/__init__.py
Normal file
0
src/dolphin/__init__.py
Normal file
255
src/dolphin/memorylib.py
Normal file
255
src/dolphin/memorylib.py
Normal file
|
@ -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<int>}
|
||||
-- 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
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
27
tests/test_memorylib.py
Normal file
27
tests/test_memorylib.py
Normal file
|
@ -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()
|
Loading…
Reference in a new issue