diff --git a/common/unityhook/config.c b/common/unityhook/config.c index fe4e062..8bc41bc 100644 --- a/common/unityhook/config.c +++ b/common/unityhook/config.c @@ -4,6 +4,8 @@ void unity_config_load(struct unity_config *cfg, const wchar_t *filename) { cfg->enable = GetPrivateProfileIntW(L"unity", L"enable", 1, filename); + cfg->debug_enable = GetPrivateProfileIntW(L"unity", L"enableDebug", 0, filename); + cfg->debug_suspend = GetPrivateProfileIntW(L"unity", L"debugSuspend", 0, filename); GetPrivateProfileStringW( L"unity", @@ -13,4 +15,13 @@ void unity_config_load(struct unity_config *cfg, const wchar_t *filename) { _countof(cfg->target_assembly), filename ); + + GetPrivateProfileStringW( + L"unity", + L"debugAddress", + L"127.0.0.1:55555", + cfg->debug_address, + _countof(cfg->debug_address), + filename + ); } diff --git a/common/unityhook/config.h b/common/unityhook/config.h index 6857c4b..f45245b 100644 --- a/common/unityhook/config.h +++ b/common/unityhook/config.h @@ -6,7 +6,10 @@ struct unity_config { bool enable; + bool debug_enable; + bool debug_suspend; wchar_t target_assembly[MAX_PATH]; + wchar_t debug_address[MAX_PATH]; }; void unity_config_load(struct unity_config *cfg, const wchar_t *filename); diff --git a/common/unityhook/doorstop.c b/common/unityhook/doorstop.c index 04dee02..3ca60d4 100644 --- a/common/unityhook/doorstop.c +++ b/common/unityhook/doorstop.c @@ -18,21 +18,38 @@ #include "mono.h" #include "util.h" -static void * my_mono_jit_init_version(const char *root_domain_name, const char *runtime_version); -void doorstop_invoke(void *domain); +#define MONO_DEBUG_ARG_START "--debugger-agent=transport=dt_socket,server=y,address=" +#define MONO_DEBUG_NO_SUSPEND ",suspend=n" +#define MONO_DEBUG_NO_SUSPEND_NET35 ",suspend=n,defer=y" + +static void* my_mono_jit_init_version(const char* root_domain_name, + const char* runtime_version); +static void my_mono_jit_parse_options(int argc, const char** argv); +static void* my_mono_debug_init(MonoDebugFormat format); + +void doorstop_invoke(void* domain); static char module_name[MAX_PATH]; static bool doorstop_hook_initted; +static bool mono_debug_init_called; +static bool mono_is_net35; static struct unity_config unity_config; static struct hook_symbol unity_mono_syms[] = { -{ + { .name = "mono_jit_init_version", .patch = my_mono_jit_init_version, - } -}; + }, + { + .name = "mono_debug_init", + .patch = my_mono_debug_init, + }, + { + .name = "mono_jit_parse_options", + .patch = my_mono_jit_parse_options, + }}; -void doorstop_mono_hook_init(const struct unity_config *cfg, HINSTANCE module) { - if (doorstop_hook_initted || cfg->target_assembly[0] == 0) { +void doorstop_mono_hook_init(const struct unity_config* cfg, HINSTANCE module) { + if (doorstop_hook_initted) { return; } @@ -40,22 +57,104 @@ void doorstop_mono_hook_init(const struct unity_config *cfg, HINSTANCE module) { memcpy(&unity_config, cfg, sizeof(*cfg)); load_mono_functions(module); - proc_addr_table_push(NULL, module_name, unity_mono_syms, _countof(unity_mono_syms)); + proc_addr_table_push(NULL, module_name, unity_mono_syms, + _countof(unity_mono_syms)); doorstop_hook_initted = true; } static void * my_mono_jit_init_version(const char *root_domain_name, const char *runtime_version) { - dprintf("Unity: Starting Mono domain \"%s\"\n", root_domain_name); + dprintf("Unity: Starting Mono domain \"%s\", runtime version %s\n", root_domain_name, runtime_version); + if (strlen(runtime_version) > 2 && (runtime_version[1] == L'2' || runtime_version[1] == L'1')) { + mono_is_net35 = TRUE; + } SetEnvironmentVariableW(L"DOORSTOP_DLL_SEARCH_DIRS", widen(mono_assembly_getrootdir())); + my_mono_jit_parse_options(0, NULL); + bool debugger_already_enabled = mono_debug_init_called; + if (mono_debug_enabled) { + debugger_already_enabled |= mono_debug_enabled(); + } + + if (!debugger_already_enabled) { + dprintf("Unity: Detected mono debugger is not initialized; initialized it\n"); + mono_debug_init(MONO_DEBUG_FORMAT_MONO); + } void* domain = mono_jit_init_version(root_domain_name, runtime_version); doorstop_invoke(domain); return domain; } +static void my_mono_jit_parse_options(int argc, const char** argv) { + char* debug_options = getenv(TEXT("DNSPY_UNITY_DBG2")); + if (debug_options) { + unity_config.debug_enable = TRUE; + } + + if (unity_config.debug_enable) { + dprintf("Unity: Configuring mono debug server\n"); + + int size = argc + 1; + const char** new_argv = calloc(size, sizeof(char*)); + memcpy(new_argv, argv, argc * sizeof(char*)); + + //check if debug options are already set by env variable, if not, build it from config + if (!debug_options) { + size_t debug_args_len = strlen(MONO_DEBUG_ARG_START) + + wcslen(unity_config.debug_address); + if (!unity_config.debug_suspend) { + if (mono_is_net35) { + debug_args_len += strlen(MONO_DEBUG_NO_SUSPEND_NET35); + } else { + debug_args_len += strlen(MONO_DEBUG_NO_SUSPEND); + } + } + + debug_options = calloc(debug_args_len + 1, sizeof(wchar_t)); + strcat(debug_options, MONO_DEBUG_ARG_START); + + char* temp_address = narrow(unity_config.debug_address); + strcat(debug_options, temp_address); + free(temp_address); + + if (!unity_config.debug_suspend) { + if (mono_is_net35) { + strcat(debug_options, MONO_DEBUG_NO_SUSPEND_NET35); + } else { + strcat(debug_options, MONO_DEBUG_NO_SUSPEND); + } + } + dprintf("Unity: Debug options from config: %s\n", debug_options); + } else + dprintf("Unity: Debug options from env DNSPY_UNITY_DBG2: %s\n", + debug_options); + + new_argv[argc] = debug_options; + /* + dprintf("Unity: mono_jit_parse_options argc: %d\n", size); + dprintf("Unity: mono_jit_parse_options argv:\n"); + for (int i = 0; i < size; i++) { + dprintf(" %d: %s\n", i, new_argv[i]); + } + dprintf("\n"); + */ + mono_jit_parse_options(size, new_argv); + free(debug_options); + free(new_argv); + } else { + mono_jit_parse_options(argc, argv); + } +} + +static void* my_mono_debug_init(MonoDebugFormat format) { + dprintf("Unity: Start mono debug init with format %d\n",format); + mono_debug_init_called = true; + void* domain = mono_debug_init(format); + return domain; +} + void doorstop_invoke(void* domain) { if (GetEnvironmentVariableW(L"DOORSTOP_INITIALIZED", NULL, 0) != 0) { dprintf("Unity: Doorstop is already initialized.\n"); @@ -90,6 +189,11 @@ void doorstop_invoke(void* domain) { #undef CONFIG_EXT } + if(unity_config.target_assembly[0] == 0) { + dprintf("Unity: No target assembly file specified, skipping doorstop invocation.\n"); + return; + } + SetEnvironmentVariableW(L"DOORSTOP_INVOKE_DLL_PATH", unity_config.target_assembly); char *assembly_dir = mono_assembly_getrootdir(); @@ -108,7 +212,7 @@ void doorstop_invoke(void* domain) { void* assembly = mono_domain_assembly_open(domain, dll_path); if (!assembly) { - dprintf("Unity: Failed to load assembly\n"); + dprintf("Unity: Failed to load target assembly\n"); free(dll_path); return; } @@ -116,7 +220,7 @@ void doorstop_invoke(void* domain) { void *image = mono_assembly_get_image(assembly); if (!image) { - dprintf("Unity: Assembly image doesn't exist\n"); + dprintf("Unity: Target assembly image doesn't exist\n"); free(dll_path); return; } @@ -135,7 +239,7 @@ void doorstop_invoke(void* domain) { } if (!method) { - dprintf("Unity: Assembly does not have a valid entrypoint.\n"); + dprintf("Unity: Target assembly does not have a valid entrypoint.\n"); free(dll_path); return; } diff --git a/common/unityhook/mono.h b/common/unityhook/mono.h index 39fdc01..d97045c 100644 --- a/common/unityhook/mono.h +++ b/common/unityhook/mono.h @@ -47,6 +47,7 @@ void* (*mono_get_exception_class)(); void* (*mono_object_get_virtual_method)(void* obj_raw, void* method); void* (*mono_jit_parse_options)(int argc, const char** argv); +UINT32 (*mono_debug_enabled)(); typedef enum { MONO_DEBUG_FORMAT_NONE, @@ -95,6 +96,7 @@ void load_mono_functions(HMODULE mono_lib) { GET_MONO_PROC(mono_jit_parse_options); GET_MONO_PROC(mono_debug_init); GET_MONO_PROC(mono_debug_domain_create); + GET_MONO_PROC(mono_debug_enabled); #undef GET_MONO_PROC } \ No newline at end of file diff --git a/doc/config/common.md b/doc/config/common.md index 62e6bbb..77fd5a8 100644 --- a/doc/config/common.md +++ b/doc/config/common.md @@ -718,6 +718,46 @@ Default `DEVICE\sram.bin` Path to the storage file for SRAM emulation. +## `[unity]` + +Configure the Unity hook and optional Mono debugger integration. + +### `enable` + +Default: `1` + +Enable the Unity hook. This allows custom .NET code to run before the game. + +### `targetAssembly` + +Default: Empty string + +Path to a .NET DLL to load before the game. This is useful for modding +frameworks such as BepInEx. If it is empty, no assembly is loaded. + +### `enableDebug` + +Default: `0` + +Enable the Mono debugger server without patching Mono. Setting the +`DNSPY_UNITY_DBG2` environment variable also enables the debugger and uses its +value as the debugger options. See the +[dnSpy Unity debugging documentation](https://github.com/dnSpyEx/dnSpy/wiki/Debugging-Unity-Games#debugging-release-builds) +for details. + +### `debugAddress` + +Default: `127.0.0.1:55555` + +Address and port for the Mono debugger server when `enableDebug` is enabled. + +### `debugSuspend` + +Default: `0` + +Set to `1` to suspend the game until a debugger attaches. The default value +allows the game to continue running while waiting for a debugger. + ### `[touch]` Configure WinTouch emulation for mouse input. @@ -879,6 +919,6 @@ Default: `DEVICE\NextProcess.txt` This is a file that will be set to the content of what would be written to the `NextProcess` registry key when the game is terminated. -This allows whatever executed the game process to react what should happen next (System Test Mode selected, network delivery completed, ...) without requiring admin permissions. +This allows whatever executed the game process to react what should happen next (System Test Mode selected, network delivery completed, ...) without requiring admin permissions. The file is deleted on startup of segatools.