Skip to content

Commit

Permalink
feat: Add tests (#220)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmctune authored Apr 30, 2024
1 parent e5fbed6 commit ee88a9f
Show file tree
Hide file tree
Showing 18 changed files with 301 additions and 137 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

- name: Build package
run: |
rsync -av --exclude="imgs/" --exclude="launcher/" app/* dqxclarity/;
rsync -av --exclude="imgs/" --exclude="launcher/" --exclude="tests/" app/* dqxclarity/;
cp version.update requirements.txt user_settings.ini dqxclarity.exe dqxclarity/;
cp clarity_dialog.db dqxclarity/misc_files;
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:

- name: Build package
run: |
rsync -av --exclude="imgs/" --exclude="launcher/" app/* dqxclarity/;
rsync -av --exclude="imgs/" --exclude="launcher/" --exclude="tests/" app/* dqxclarity/;
cp version.update requirements.txt user_settings.ini dqxclarity.exe dqxclarity/;
cp clarity_dialog.db dqxclarity/misc_files;
Expand Down
37 changes: 0 additions & 37 deletions app/common/lib.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,8 @@
from locale import getencoding
from loguru import logger as log
from pathlib import Path

import json
import logging
import os
import shutil


def read_json_file(file):
"""Reads JSON file and returns content."""
with open(file, encoding="utf-8") as json_data:
return json.loads(json_data.read())


def write_file(path, filename, attr, data):
"""Writes a string to a file."""
with open(f"{path}/{filename}", attr, encoding="utf-8") as open_file:
open_file.write(data)


def delete_folder(folder):
"""Deletes a folder and all subfolders."""
try:
shutil.rmtree(folder, ignore_errors=True)
except Exception:
pass


def delete_file(file):
"""Deletes a file."""
try:
Path(file).unlink()
except Exception:
pass


def setup_logging():
Expand Down Expand Up @@ -73,9 +42,3 @@ def get_project_root(add_file=None):
if add_file:
abs_path = "/".join([abs_path, add_file])
return abs_path


def encode_to_utf8(string: str):
"""Encodes a string of the current machine's encoding to utf-8."""
current_locale = getencoding()
return string.encode(current_locale).decode(current_locale).encode()
47 changes: 27 additions & 20 deletions app/common/memory.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
from common.errors import AddressOutOfRange, MemoryReadError, MemoryWriteError
from loguru import logger as log
from pymem.pattern import pattern_scan_all, pattern_scan_module

import pymem
import pymem.exception
import pymem.process
import struct
import sys
import traceback


class MemWriter:
def __init__(self, process_name: str = "DQXGame.exe"):
self.proc = self.attach(process_name)

def __init__(self):
self.proc = self.attach()


def attach(self):
proc = pymem.Pymem("DQXGame.exe")
def attach(self, process_name: str = "DQXGame.exe"):
proc = pymem.Pymem(process_name)
# obscure issue seen on Windows 11 getting an OverflowError
# https://github.com/srounet/Pymem/issues/19
proc.process_handle &= 0xFFFFFFFF
Expand Down Expand Up @@ -76,21 +74,18 @@ def write_string(self, address: int, text: str):
return self.proc.write_string(address, text + "\x00")


def pattern_scan(self, pattern: bytes, return_multiple=False, use_regex=False, module=None):
def pattern_scan(self, pattern: bytes, return_multiple=False, use_regex=False, module=None, all_protections: bool = False):
"""Scan for a byte pattern."""
if module is not None:
return pattern_scan_module(
handle=self.proc.process_handle,
return self.proc.pattern_scan_module(
pattern=pattern,
return_multiple=return_multiple,
module=pymem.process.module_from_name(self.proc.process_handle, module),
use_regex=use_regex
module=module
)
else:
return pattern_scan_all(
handle=self.proc.process_handle,
return self.proc.pattern_scan_all(
pattern=pattern,
all_protections=False,
all_protections=all_protections,
return_multiple=return_multiple,
use_regex=use_regex
)
Expand All @@ -110,11 +105,8 @@ def get_ptr_address(self, base: int, offsets: list):
return addr + offsets[-1]


def get_base_address(self, name="DQXGame.exe") -> int:
"""Returns the base address of a module.
Defaults to DQXGame.exe.
"""
def get_base_address(self, name: str ="DQXGame.exe") -> int:
"""Returns the base address of a module."""
return pymem.process.module_from_name(self.proc.process_handle, name).lpBaseOfDll


Expand Down Expand Up @@ -158,6 +150,21 @@ def get_hook_bytecode(self, hook_address: int):
return b"\xE9" + self.pack_to_int(hook_address)


def inject_python(self):
"""Injects the Python interpreter into the process."""
try:
self.proc.inject_python_interpreter()
if self.proc._python_injected:
if self.proc.py_run_simple_string:
return self.proc.py_run_simple_string

log.exception(f"Python dll failed to inject. Details:\n{self.proc.__dict__}")
return False
except Exception:
log.exception(f"Python dll failed to inject. Error: \n{str(traceback.print_exc())}\nDetails:\n{self.proc.__dict__}")
return False


def close(self):
"""Closes the process."""
return self.proc.close_process()
27 changes: 13 additions & 14 deletions app/common/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import configparser
import deepl
import json
import langdetect
import os
import pykakasi
Expand Down Expand Up @@ -435,14 +434,19 @@ def sanitize_and_translate(self, text: str, wrap_width: int, max_lines=None, add
return pristine_str


def load_user_config():
def load_user_config(filepath: str = None):
"""Returns a user's config settings. If the config doesn't exist, a default
config is generated. If the user's config is missing values, we back up the
old config and generate a new default one for them.
:param filepath: Path to the user_settings.ini file. Don't include
the filename or trailing forward slash.
:returns: Dict of config.
"""
filename = get_project_root("user_settings.ini")
if not filepath:
filepath = get_project_root("user_settings.ini")
else:
filepath = f"{filepath}/user_settings.ini"
base_config = configparser.ConfigParser()
base_config["translation"] = {
"enabledeepltranslate": False,
Expand All @@ -454,19 +458,19 @@ def load_user_config():
base_config["config"] = {"installdirectory": ""}

def create_base_config():
with open(filename, "w+") as configfile:
with open(filepath, "w+") as configfile:
base_config.write(configfile)

# Create the config if it doesn't exist
if not os.path.exists(filename):
if not os.path.exists(filepath):
create_base_config()

# Verify the integrity of the config. If a key is missing,
# trigger user_config_state and create a new one, backing
# up the old config.
user_config = configparser.ConfigParser()
user_config_state = 0
user_config.read(filename)
user_config.read(filepath)
for section in base_config.sections():
if section not in user_config.sections():
user_config_state = 1
Expand All @@ -478,17 +482,17 @@ def create_base_config():

# Notify user their config is busted
if user_config_state == 1:
shutil.copyfile(filename, f"{filename}.invalid")
shutil.copyfile(filepath, "user_settings.invalid")
create_base_config()
message_box(
title="New config created",
message=f"We found a missing config value in your {filename}.\n\nYour old config has been renamed to {filename}.invalid in case you need to reference it.\n\nPlease relaunch dqxclarity.",
message=f"We found a missing config value in your user_settings.ini.\n\nYour old config has been renamed to user_settings.invalid in case you need to reference it.\n\nPlease relaunch dqxclarity.",
exit_prog=True,
)

config_dict = {}
good_config = configparser.ConfigParser()
good_config.read(filename)
good_config.read(filepath)
for section in good_config.sections():
config_dict[section] = {}
for key, val in good_config.items(section):
Expand Down Expand Up @@ -641,11 +645,6 @@ def detect_lang(text: str) -> bool:
return False


def read_json_file(file):
with open(file, encoding="utf-8") as json_data:
return json.loads(json_data.read())


def transliterate_player_name(word: str) -> str:
"""Uses the pykakasi library to phonetically convert a Japanese word into
English.
Expand Down
4 changes: 2 additions & 2 deletions app/hooking/corner_text.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from common.db_ops import generate_m00_dict
from common.lib import encode_to_utf8, get_project_root, setup_logger
from common.lib import get_project_root, setup_logger
from common.memory import MemWriter
from common.translate import detect_lang
from json import dumps
Expand Down Expand Up @@ -60,4 +60,4 @@ def corner_text_shellcode(eax_address: int) -> str:
f.write(str(traceback.format_exc()))
"""

return encode_to_utf8(shellcode).decode()
return shellcode
3 changes: 1 addition & 2 deletions app/hooking/dialog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from common.db_ops import init_db, search_bad_strings, sql_read
from common.lib import encode_to_utf8
from common.memory import MemWriter
from common.translate import detect_lang, Translate
from json import dumps
Expand Down Expand Up @@ -129,4 +128,4 @@ def translate_shellcode(esi_address: int, esp_address: int) -> str:
f.write(str(traceback.format_exc()))
"""

return encode_to_utf8(shellcode).decode()
return shellcode
48 changes: 1 addition & 47 deletions app/hooking/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,14 @@
dialog_trigger,
integrity_check,
network_text_trigger,
party_ai_trigger,
player_sibling_name_trigger,
quest_text_trigger,
)
from hooking.easydetour import EasyDetour
from hooking.hide_hooks import load_hooks
from pymem import Pymem

import struct
import sys
import traceback


def inject_python_dll():
"""Injects a Python dll."""
writer = Pymem("DQXGame.exe")
try:
writer.inject_python_interpreter()
if writer._python_injected:
if writer.py_run_simple_string:
log.success(f"Python injected.")
return writer.py_run_simple_string
log.error(f"Python dll failed to inject. Details:\n{writer.__dict__}")
return False
except Exception:
log.error(f"Python dll failed to inject. Error: \n{str(traceback.print_exc())}\nDetails:\n{writer.__dict__}")
return False


def translate_detour(simple_str_addr: int):
Expand Down Expand Up @@ -121,29 +102,6 @@ def player_name_detour(simple_str_addr: int):
return hook_obj


# not in use until we can find a better function to hook.
# def party_name_detour(simple_str_addr: int):
# """Detours function when party names in the bottom right load and renames
# them into English."""
# from hooking.party import rename_party_members_shellcode

# writer = MemWriter()

# hook_obj = EasyDetour(
# hook_name="party_members",
# signature=party_ai_trigger,
# num_bytes_to_steal=6,
# simple_str_addr=simple_str_addr,
# )

# ebx = hook_obj.address_dict["attrs"]["ebx"]
# shellcode = rename_party_members_shellcode(ebx_address=ebx)
# shellcode_addr = hook_obj.address_dict["attrs"]["shellcode"]
# writer.write_string(address=shellcode_addr, text=shellcode)

# return hook_obj


def corner_text_detour(simple_str_addr: int):
"""Detours function when top-right corner text is about to happen and
replaces it with English."""
Expand Down Expand Up @@ -174,11 +132,7 @@ def activate_hooks(player_names: bool, communication_window: bool):
log = setup_logging()

writer = MemWriter()

simple_str_addr = inject_python_dll()
if not simple_str_addr:
log.exception("Since Python injection failed, we will not try to hook. Exiting.")
return False
simple_str_addr = writer.inject_python()

# activates all hooks. add any new hooks to this list
hooks = []
Expand Down
4 changes: 2 additions & 2 deletions app/hooking/network_text.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from common.db_ops import generate_m00_dict, sql_read
from common.lib import encode_to_utf8, get_project_root, setup_logger
from common.lib import get_project_root, setup_logger
from common.memory import MemWriter
from common.translate import detect_lang, transliterate_player_name
from json import dumps
Expand Down Expand Up @@ -207,4 +207,4 @@ def network_text_shellcode(ecx_address: int, esp_address) -> str:
f.write(str(traceback.format_exc()))
"""

return encode_to_utf8(shellcode).decode()
return shellcode
3 changes: 1 addition & 2 deletions app/hooking/party.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# from common.db_ops import generate_m00_dict
# from common.lib import encode_to_utf8
# from common.memory import MemWriter
# from common.translate import transliterate_player_name, detect_lang
# from json import dumps
Expand Down Expand Up @@ -72,4 +71,4 @@
# f.write(str(traceback.format_exc()))
# """

# return encode_to_utf8(shellcode).decode()
# return shellcode
4 changes: 2 additions & 2 deletions app/hooking/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""

from common.db_ops import db_query, generate_m00_dict, init_db
from common.lib import encode_to_utf8, get_project_root
from common.lib import get_project_root
from common.memory import MemWriter
from common.translate import transliterate_player_name
from json import dumps
Expand Down Expand Up @@ -222,4 +222,4 @@ def player_name_shellcode(eax_address: int) -> str:
f.write(str(traceback.format_exc()))
"""

return encode_to_utf8(shellcode).decode()
return shellcode
4 changes: 2 additions & 2 deletions app/hooking/quest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from common.db_ops import generate_m00_dict, sql_read, sql_write
from common.lib import encode_to_utf8, get_project_root
from common.lib import get_project_root
from common.memory import MemWriter
from common.translate import clean_up_and_return_items, detect_lang, Translate
from json import dumps, loads
Expand Down Expand Up @@ -147,4 +147,4 @@ def quest_text_shellcode(address: int) -> str:
f.write(str(traceback.format_exc()))
"""

return encode_to_utf8(shellcode).decode()
return shellcode
Loading

0 comments on commit ee88a9f

Please sign in to comment.