diff --git a/native/pt_world_fix.c b/native/pt_world_fix.c new file mode 100644 index 0000000..95d0d33 --- /dev/null +++ b/native/pt_world_fix.c @@ -0,0 +1,829 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + + +#define GAME_DLL_MAP_ENTRY_RVA 0x007c0b40u +#define GAME_DLL_LOGIN_RETURN_RVA 0x0079f157u +#define GAME_DLL_CODE_CAVE_RVA 0x01195ce0u +#define GAME_DLL_LSTRCMPA_IAT_RVA 0x01196280u +#define GAME_DLL_VIRTUAL_PROTECT_IAT_RVA 0x01196108u +#define GAME_DLL_FLUSH_INSTRUCTION_CACHE_IAT_RVA 0x01196140u +#define GAME_DLL_GET_CURRENT_PROCESS_IAT_RVA 0x01196344u +#define GAME_DLL_MINIMUM_IMAGE_SIZE 0x01196348u + +#define REMOTE_ALLOCATION_SIZE 4096u +#define REMOTE_RECLAIM_OFFSET 0x0600u +#define REMOTE_MAP_JUMP_OFFSET 0x0680u +#define REMOTE_STATE_OFFSET 0x0700u +#define REMOTE_OLD_CAVE_PROTECT_OFFSET 0x0704u +#define REMOTE_OLD_ENTRY_PROTECT_OFFSET 0x0708u +#define REMOTE_TEMP_PROTECT_OFFSET 0x070cu + +#define WORLD_FIX_WAIT_MILLISECONDS 60000u + + +static const BYTE MAP_ENTRY_ORIGINAL[] = {0x55, 0x8b, 0xec, 0x6a, 0xff}; +static const BYTE LOGIN_COMPARE_ORIGINAL[] = { + 0x85, 0xc0, 0x0f, 0x85, 0x6b, 0x01, 0x00, 0x00, +}; +static const BYTE MAP_ENTRY_JUMP[] = {0xe9, 0x9b, 0x51, 0x9d, 0x00}; + +/* + * Exact 97-byte entity-surface reclaim routine verified for V21.75.83. + * Its relative calls and return jump are valid at GAME_DLL_CODE_CAVE_RVA. + */ +static const BYTE PROVEN_RECLAIM_HOOK[] = { + 0x9c, 0x60, 0xe8, 0x49, 0x01, 0x36, 0xff, 0x85, + 0xc0, 0x74, 0x4a, 0x89, 0xc3, 0x8d, 0x70, 0x08, + 0x8d, 0xbe, 0x50, 0x5d, 0x01, 0x00, 0x39, 0xfe, + 0x74, 0x3b, 0x80, 0xbe, 0x58, 0x03, 0x00, 0x00, + 0x00, 0x74, 0x2a, 0x8b, 0x06, 0x8b, 0x4c, 0x24, + 0x28, 0x85, 0xc9, 0x74, 0x08, 0x3b, 0x81, 0xd4, + 0x1f, 0x00, 0x00, 0x74, 0x18, 0x8b, 0x4c, 0x24, + 0x2c, 0x85, 0xc9, 0x74, 0x08, 0x3b, 0x81, 0xd4, + 0x1f, 0x00, 0x00, 0x74, 0x08, 0x50, 0x89, 0xd9, + 0xe8, 0xa3, 0x20, 0x36, 0xff, 0x81, 0xc6, 0x50, + 0x04, 0x00, 0x00, 0xeb, 0xc1, 0x61, 0x9d, 0x55, + 0x89, 0xe5, 0x6a, 0xff, 0xe9, 0x04, 0xae, 0x62, + 0xff, +}; + + +typedef struct CodeBuffer +{ + BYTE *data; + SIZE_T capacity; + SIZE_T length; + BOOL valid; +} CodeBuffer; + + +typedef struct WorldFixInstallation +{ + DWORD remote_state; + LPVOID remote_allocation; +} WorldFixInstallation; + + +static DWORD address_from_rva(DWORD base, DWORD rva) +{ + return base + rva; +} + + +static BOOL read_exact(HANDLE process, DWORD address, void *buffer, SIZE_T size) +{ + SIZE_T read_size = 0; + return ReadProcessMemory( + process, + (LPCVOID)(uintptr_t)address, + buffer, + size, + &read_size + ) && read_size == size; +} + + +static BOOL write_exact(HANDLE process, DWORD address, const void *buffer, SIZE_T size) +{ + SIZE_T written_size = 0; + return WriteProcessMemory( + process, + (LPVOID)(uintptr_t)address, + buffer, + size, + &written_size + ) && written_size == size; +} + + +static void emit_byte(CodeBuffer *code, BYTE value) +{ + if (!code->valid || code->length >= code->capacity) + { + code->valid = FALSE; + return; + } + code->data[code->length++] = value; +} + + +static void emit_dword(CodeBuffer *code, DWORD value) +{ + SIZE_T index; + for (index = 0; index < sizeof(value); ++index) + { + emit_byte(code, (BYTE)((value >> (index * 8u)) & 0xffu)); + } +} + + +static void emit_bytes(CodeBuffer *code, const BYTE *values, SIZE_T size) +{ + SIZE_T index; + for (index = 0; index < size; ++index) + { + emit_byte(code, values[index]); + } +} + + +static SIZE_T emit_near_jump_placeholder(CodeBuffer *code, BYTE condition) +{ + SIZE_T displacement_offset; + if (condition != 0) + { + emit_byte(code, 0x0f); + emit_byte(code, condition); + } + else + { + emit_byte(code, 0xe9); + } + displacement_offset = code->length; + emit_dword(code, 0); + return displacement_offset; +} + + +static void patch_near_jump(CodeBuffer *code, SIZE_T displacement_offset, SIZE_T target_offset) +{ + int32_t displacement; + if (!code->valid || displacement_offset + sizeof(displacement) > code->capacity) + { + code->valid = FALSE; + return; + } + displacement = (int32_t)(target_offset - (displacement_offset + sizeof(displacement))); + memcpy(code->data + displacement_offset, &displacement, sizeof(displacement)); +} + + +static void emit_push_immediate(CodeBuffer *code, DWORD value) +{ + emit_byte(code, 0x68); + emit_dword(code, value); +} + + +static void emit_call_iat(CodeBuffer *code, DWORD iat_address) +{ + emit_byte(code, 0xff); + emit_byte(code, 0x15); + emit_dword(code, iat_address); +} + + +static void emit_set_state(CodeBuffer *code, DWORD state_address, BYTE value) +{ + emit_byte(code, 0xc6); + emit_byte(code, 0x05); + emit_dword(code, state_address); + emit_byte(code, value); +} + + +static void emit_restore_iat( + CodeBuffer *code, + DWORD iat_address, + DWORD original_lstrcmp, + DWORD virtual_protect_iat, + DWORD original_iat_protect, + DWORD temporary_protect_address +) +{ + /* mov dword ptr [iat_address], original_lstrcmp */ + emit_byte(code, 0xc7); + emit_byte(code, 0x05); + emit_dword(code, iat_address); + emit_dword(code, original_lstrcmp); + + emit_push_immediate(code, temporary_protect_address); + emit_push_immediate(code, original_iat_protect); + emit_push_immediate(code, sizeof(DWORD)); + emit_push_immediate(code, iat_address); + emit_call_iat(code, virtual_protect_iat); +} + + +static BOOL build_remote_hook( + BYTE *image, + SIZE_T image_size, + DWORD remote_base, + DWORD game_dll_base, + DWORD original_lstrcmp, + DWORD original_iat_protect, + SIZE_T *code_size +) +{ + CodeBuffer code; + DWORD map_entry = address_from_rva(game_dll_base, GAME_DLL_MAP_ENTRY_RVA); + DWORD code_cave = address_from_rva(game_dll_base, GAME_DLL_CODE_CAVE_RVA); + DWORD login_return = address_from_rva(game_dll_base, GAME_DLL_LOGIN_RETURN_RVA); + DWORD lstrcmp_iat = address_from_rva(game_dll_base, GAME_DLL_LSTRCMPA_IAT_RVA); + DWORD virtual_protect_iat = address_from_rva( + game_dll_base, + GAME_DLL_VIRTUAL_PROTECT_IAT_RVA + ); + DWORD flush_iat = address_from_rva( + game_dll_base, + GAME_DLL_FLUSH_INSTRUCTION_CACHE_IAT_RVA + ); + DWORD get_process_iat = address_from_rva( + game_dll_base, + GAME_DLL_GET_CURRENT_PROCESS_IAT_RVA + ); + DWORD reclaim_source = remote_base + REMOTE_RECLAIM_OFFSET; + DWORD jump_source = remote_base + REMOTE_MAP_JUMP_OFFSET; + DWORD state_address = remote_base + REMOTE_STATE_OFFSET; + DWORD old_cave_protect = remote_base + REMOTE_OLD_CAVE_PROTECT_OFFSET; + DWORD old_entry_protect = remote_base + REMOTE_OLD_ENTRY_PROTECT_OFFSET; + DWORD temporary_protect = remote_base + REMOTE_TEMP_PROTECT_OFFSET; + SIZE_T nonzero_jump; + SIZE_T wrong_caller_jump; + SIZE_T already_triggered_jump; + SIZE_T cave_failure_jump; + SIZE_T entry_failure_jump; + SIZE_T success_jump; + SIZE_T finish_offset; + SIZE_T entry_failure_offset; + SIZE_T failure_offset; + SIZE_T success_offset; + + if (image_size < REMOTE_TEMP_PROTECT_OFFSET + sizeof(DWORD)) + { + return FALSE; + } + memset(image, 0, image_size); + memcpy( + image + REMOTE_RECLAIM_OFFSET, + PROVEN_RECLAIM_HOOK, + sizeof(PROVEN_RECLAIM_HOOK) + ); + memcpy(image + REMOTE_MAP_JUMP_OFFSET, MAP_ENTRY_JUMP, sizeof(MAP_ENTRY_JUMP)); + + code.data = image; + code.capacity = REMOTE_RECLAIM_OFFSET; + code.length = 0; + code.valid = TRUE; + + /* Standard-call lstrcmpA wrapper, preserving every nonvolatile register. */ + emit_bytes(&code, (const BYTE[]){0x55, 0x89, 0xe5, 0x53, 0x56, 0x57}, 6); + emit_bytes(&code, (const BYTE[]){0xff, 0x75, 0x0c, 0xff, 0x75, 0x08}, 6); + emit_byte(&code, 0xb8); + emit_dword(&code, original_lstrcmp); + emit_bytes(&code, (const BYTE[]){0xff, 0xd0, 0x85, 0xc0}, 4); + nonzero_jump = emit_near_jump_placeholder(&code, 0x85); /* jne finish */ + emit_bytes(&code, (const BYTE[]){0x81, 0x7d, 0x04}, 3); + emit_dword(&code, login_return); + wrong_caller_jump = emit_near_jump_placeholder(&code, 0x85); /* jne finish */ + emit_bytes(&code, (const BYTE[]){0x80, 0x3d}, 2); + emit_dword(&code, state_address); + emit_byte(&code, 0x00); + already_triggered_jump = emit_near_jump_placeholder(&code, 0x85); /* jne finish */ + emit_set_state(&code, state_address, 1); + + /* Make the two exact publisher-code ranges writable only after login. */ + emit_push_immediate(&code, old_cave_protect); + emit_push_immediate(&code, PAGE_EXECUTE_READWRITE); + emit_push_immediate(&code, sizeof(PROVEN_RECLAIM_HOOK)); + emit_push_immediate(&code, code_cave); + emit_call_iat(&code, virtual_protect_iat); + emit_bytes(&code, (const BYTE[]){0x85, 0xc0}, 2); + cave_failure_jump = emit_near_jump_placeholder(&code, 0x84); /* je failure */ + + emit_push_immediate(&code, old_entry_protect); + emit_push_immediate(&code, PAGE_EXECUTE_READWRITE); + emit_push_immediate(&code, sizeof(MAP_ENTRY_JUMP)); + emit_push_immediate(&code, map_entry); + emit_call_iat(&code, virtual_protect_iat); + emit_bytes(&code, (const BYTE[]){0x85, 0xc0}, 2); + entry_failure_jump = emit_near_jump_placeholder(&code, 0x84); /* je entry_failure */ + + /* Copy only after the exact account-ID comparison returned success. */ + emit_byte(&code, 0xfc); + emit_byte(&code, 0xbe); + emit_dword(&code, reclaim_source); + emit_byte(&code, 0xbf); + emit_dword(&code, code_cave); + emit_byte(&code, 0xb9); + emit_dword(&code, sizeof(PROVEN_RECLAIM_HOOK)); + emit_bytes(&code, (const BYTE[]){0xf3, 0xa4}, 2); + emit_byte(&code, 0xbe); + emit_dword(&code, jump_source); + emit_byte(&code, 0xbf); + emit_dword(&code, map_entry); + emit_byte(&code, 0xb9); + emit_dword(&code, sizeof(MAP_ENTRY_JUMP)); + emit_bytes(&code, (const BYTE[]){0xf3, 0xa4}, 2); + + /* x86 is coherent, but use the publisher's own imports for an explicit flush. */ + emit_call_iat(&code, get_process_iat); + emit_push_immediate(&code, 0); + emit_push_immediate(&code, 0); + emit_byte(&code, 0x50); + emit_call_iat(&code, flush_iat); + + /* Restore publisher section protections after the five-byte entry change. */ + emit_push_immediate(&code, temporary_protect); + emit_bytes(&code, (const BYTE[]){0xff, 0x35}, 2); + emit_dword(&code, old_entry_protect); + emit_push_immediate(&code, sizeof(MAP_ENTRY_JUMP)); + emit_push_immediate(&code, map_entry); + emit_call_iat(&code, virtual_protect_iat); + emit_push_immediate(&code, temporary_protect); + emit_bytes(&code, (const BYTE[]){0xff, 0x35}, 2); + emit_dword(&code, old_cave_protect); + emit_push_immediate(&code, sizeof(PROVEN_RECLAIM_HOOK)); + emit_push_immediate(&code, code_cave); + emit_call_iat(&code, virtual_protect_iat); + + emit_restore_iat( + &code, + lstrcmp_iat, + original_lstrcmp, + virtual_protect_iat, + original_iat_protect, + temporary_protect + ); + emit_set_state(&code, state_address, 2); + success_jump = emit_near_jump_placeholder(&code, 0); /* jmp finish */ + + entry_failure_offset = code.length; + emit_push_immediate(&code, temporary_protect); + emit_bytes(&code, (const BYTE[]){0xff, 0x35}, 2); + emit_dword(&code, old_cave_protect); + emit_push_immediate(&code, sizeof(PROVEN_RECLAIM_HOOK)); + emit_push_immediate(&code, code_cave); + emit_call_iat(&code, virtual_protect_iat); + + failure_offset = code.length; + emit_restore_iat( + &code, + lstrcmp_iat, + original_lstrcmp, + virtual_protect_iat, + original_iat_protect, + temporary_protect + ); + emit_set_state(&code, state_address, 3); + + success_offset = code.length; + emit_bytes(&code, (const BYTE[]){0x31, 0xc0}, 2); /* successful lstrcmpA result */ + finish_offset = code.length; + emit_bytes(&code, (const BYTE[]){0x5f, 0x5e, 0x5b, 0x89, 0xec, 0x5d, 0xc2, 0x08, 0x00}, 9); + + patch_near_jump(&code, nonzero_jump, finish_offset); + patch_near_jump(&code, wrong_caller_jump, finish_offset); + patch_near_jump(&code, already_triggered_jump, success_offset); + patch_near_jump(&code, cave_failure_jump, failure_offset); + patch_near_jump(&code, entry_failure_jump, entry_failure_offset); + patch_near_jump(&code, success_jump, success_offset); + + if (!code.valid) + { + return FALSE; + } + *code_size = code.length; + return TRUE; +} + + +static DWORD find_game_dll(DWORD process_id, DWORD *module_size) +{ + HANDLE snapshot; + MODULEENTRY32W module; + DWORD result = 0; + + snapshot = CreateToolhelp32Snapshot( + TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, + process_id + ); + if (snapshot == INVALID_HANDLE_VALUE) + { + return 0; + } + memset(&module, 0, sizeof(module)); + module.dwSize = sizeof(module); + if (Module32FirstW(snapshot, &module)) + { + do + { + if (_wcsicmp(module.szModule, L"game.dll") == 0) + { + result = (DWORD)(uintptr_t)module.modBaseAddr; + *module_size = module.modBaseSize; + break; + } + } + while (Module32NextW(snapshot, &module)); + } + CloseHandle(snapshot); + return result; +} + + +static DWORD wait_for_game_dll( + HANDLE process, + DWORD process_id, + DWORD *module_size +) +{ + ULONGLONG deadline = GetTickCount64() + WORLD_FIX_WAIT_MILLISECONDS; + DWORD base; + while (GetTickCount64() < deadline) + { + if (WaitForSingleObject(process, 25) == WAIT_OBJECT_0) + { + return 0; + } + base = find_game_dll(process_id, module_size); + if (base != 0) + { + return base; + } + Sleep(10); + } + return 0; +} + + +static BOOL verify_game_dll_memory(HANDLE process, DWORD base, DWORD module_size) +{ + BYTE map_entry[sizeof(MAP_ENTRY_ORIGINAL)]; + BYTE login_compare[sizeof(LOGIN_COMPARE_ORIGINAL)]; + BYTE code_cave[sizeof(PROVEN_RECLAIM_HOOK)]; + DWORD iat_values[4]; + + if (module_size < GAME_DLL_MINIMUM_IMAGE_SIZE) + { + return FALSE; + } + if (!read_exact( + process, + address_from_rva(base, GAME_DLL_MAP_ENTRY_RVA), + map_entry, + sizeof(map_entry) + ) || memcmp(map_entry, MAP_ENTRY_ORIGINAL, sizeof(map_entry)) != 0) + { + return FALSE; + } + if (!read_exact( + process, + address_from_rva(base, GAME_DLL_LOGIN_RETURN_RVA), + login_compare, + sizeof(login_compare) + ) || memcmp(login_compare, LOGIN_COMPARE_ORIGINAL, sizeof(login_compare)) != 0) + { + return FALSE; + } + if (!read_exact( + process, + address_from_rva(base, GAME_DLL_CODE_CAVE_RVA), + code_cave, + sizeof(code_cave) + )) + { + return FALSE; + } + for (SIZE_T index = 0; index < sizeof(code_cave); ++index) + { + if (code_cave[index] != 0) + { + return FALSE; + } + } + if (!read_exact( + process, + address_from_rva(base, GAME_DLL_VIRTUAL_PROTECT_IAT_RVA), + &iat_values[0], + sizeof(iat_values[0]) + ) || !read_exact( + process, + address_from_rva(base, GAME_DLL_FLUSH_INSTRUCTION_CACHE_IAT_RVA), + &iat_values[1], + sizeof(iat_values[1]) + ) || !read_exact( + process, + address_from_rva(base, GAME_DLL_GET_CURRENT_PROCESS_IAT_RVA), + &iat_values[2], + sizeof(iat_values[2]) + ) || !read_exact( + process, + address_from_rva(base, GAME_DLL_LSTRCMPA_IAT_RVA), + &iat_values[3], + sizeof(iat_values[3]) + )) + { + return FALSE; + } + return iat_values[0] != 0 && iat_values[1] != 0 + && iat_values[2] != 0 && iat_values[3] != 0; +} + + +static BOOL install_world_fix( + HANDLE process, + DWORD game_dll_base, + WorldFixInstallation *installation +) +{ + BYTE remote_image[REMOTE_ALLOCATION_SIZE]; + DWORD original_lstrcmp; + DWORD original_iat_protect; + DWORD hook_address; + DWORD lstrcmp_iat = address_from_rva( + game_dll_base, + GAME_DLL_LSTRCMPA_IAT_RVA + ); + LPVOID remote; + SIZE_T code_size; + + if (!read_exact( + process, + lstrcmp_iat, + &original_lstrcmp, + sizeof(original_lstrcmp) + )) + { + return FALSE; + } + remote = VirtualAllocEx( + process, + NULL, + REMOTE_ALLOCATION_SIZE, + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE + ); + if (remote == NULL) + { + return FALSE; + } + if (!build_remote_hook( + remote_image, + sizeof(remote_image), + (DWORD)(uintptr_t)remote, + game_dll_base, + original_lstrcmp, + PAGE_READONLY, + &code_size + )) + { + VirtualFreeEx(process, remote, 0, MEM_RELEASE); + return FALSE; + } + if (!write_exact( + process, + (DWORD)(uintptr_t)remote, + remote_image, + sizeof(remote_image) + )) + { + VirtualFreeEx(process, remote, 0, MEM_RELEASE); + return FALSE; + } + if (!VirtualProtectEx( + process, + (LPVOID)(uintptr_t)lstrcmp_iat, + sizeof(DWORD), + PAGE_READWRITE, + &original_iat_protect + )) + { + VirtualFreeEx(process, remote, 0, MEM_RELEASE); + return FALSE; + } + + /* Rebuild so the callback restores the exact protection returned above. */ + if (!build_remote_hook( + remote_image, + sizeof(remote_image), + (DWORD)(uintptr_t)remote, + game_dll_base, + original_lstrcmp, + original_iat_protect, + &code_size + ) || !write_exact( + process, + (DWORD)(uintptr_t)remote, + remote_image, + sizeof(remote_image) + )) + { + DWORD ignored; + VirtualProtectEx( + process, + (LPVOID)(uintptr_t)lstrcmp_iat, + sizeof(DWORD), + original_iat_protect, + &ignored + ); + VirtualFreeEx(process, remote, 0, MEM_RELEASE); + return FALSE; + } + hook_address = (DWORD)(uintptr_t)remote; + if (!write_exact(process, lstrcmp_iat, &hook_address, sizeof(hook_address))) + { + DWORD ignored; + VirtualProtectEx( + process, + (LPVOID)(uintptr_t)lstrcmp_iat, + sizeof(DWORD), + original_iat_protect, + &ignored + ); + VirtualFreeEx(process, remote, 0, MEM_RELEASE); + return FALSE; + } + FlushInstructionCache(process, remote, code_size); + installation->remote_state = (DWORD)(uintptr_t)remote + REMOTE_STATE_OFFSET; + installation->remote_allocation = remote; + return TRUE; +} + + +static DWORD wait_for_game_and_report( + HANDLE process, + const WorldFixInstallation *installation +) +{ + BOOL reported = FALSE; + for (;;) + { + DWORD wait_result = WaitForSingleObject(process, 100); + if (wait_result == WAIT_OBJECT_0) + { + DWORD exit_code = 1; + GetExitCodeProcess(process, &exit_code); + return exit_code; + } + if (!reported && installation->remote_state != 0) + { + BYTE state = 0; + if (read_exact(process, installation->remote_state, &state, sizeof(state))) + { + if (state == 2) + { + fprintf( + stderr, + "PTWORLD: login accepted; world-transition fix activated in memory.\n" + ); + reported = TRUE; + } + else if (state == 3) + { + fprintf( + stderr, + "PTWORLD: login succeeded, but the verified memory fix could not be activated.\n" + ); + reported = TRUE; + } + } + } + } +} + + +static int run_self_test(void) +{ + BYTE image[REMOTE_ALLOCATION_SIZE]; + SIZE_T code_size = 0; + if (!build_remote_hook( + image, + sizeof(image), + 0x50000000u, + 0x10000000u, + 0x7b621000u, + PAGE_READONLY, + &code_size + ) || code_size == 0 || code_size >= REMOTE_RECLAIM_OFFSET) + { + fprintf(stderr, "PTWORLD-SELFTEST: remote hook construction failed.\n"); + return 1; + } + if (memcmp( + image + REMOTE_RECLAIM_OFFSET, + PROVEN_RECLAIM_HOOK, + sizeof(PROVEN_RECLAIM_HOOK) + ) != 0 || memcmp( + image + REMOTE_MAP_JUMP_OFFSET, + MAP_ENTRY_JUMP, + sizeof(MAP_ENTRY_JUMP) + ) != 0 || image[REMOTE_STATE_OFFSET] != 0) + { + fprintf(stderr, "PTWORLD-SELFTEST: remote payload layout failed.\n"); + return 1; + } + fprintf( + stdout, + "PTWORLD-SELFTEST: ok; remote code=%lu bytes.\n", + (unsigned long)code_size + ); + return 0; +} + + +int wmain(int argument_count, wchar_t **arguments) +{ + STARTUPINFOW startup; + PROCESS_INFORMATION process; + WorldFixInstallation installation; + WCHAR game_path[MAX_PATH]; + WCHAR command_line[MAX_PATH + 64]; + DWORD game_dll_base; + DWORD game_dll_size = 0; + DWORD exit_code; + DWORD path_length; + + if ( + argument_count == 2 + && wcscmp(arguments[1], L"--self-test") == 0 + ) + { + return run_self_test(); + } + memset(&startup, 0, sizeof(startup)); + memset(&process, 0, sizeof(process)); + memset(&installation, 0, sizeof(installation)); + startup.cb = sizeof(startup); + path_length = GetFullPathNameW(L"Game.exe", MAX_PATH, game_path, NULL); + if (path_length == 0 || path_length >= MAX_PATH) + { + fprintf(stderr, "PTWORLD: Game.exe is not available in the working directory.\n"); + return 111; + } + if (swprintf( + command_line, + MAX_PATH + 64, + L"\"%ls\" PTEU_REBORN", + game_path + ) < 0) + { + fprintf(stderr, "PTWORLD: could not construct the game command line.\n"); + return 111; + } + if (!CreateProcessW( + game_path, + command_line, + NULL, + NULL, + TRUE, + 0, + NULL, + NULL, + &startup, + &process + )) + { + fprintf( + stderr, + "PTWORLD: could not start Game.exe (Windows error %lu).\n", + (unsigned long)GetLastError() + ); + return 111; + } + CloseHandle(process.hThread); + + game_dll_base = wait_for_game_dll( + process.hProcess, + process.dwProcessId, + &game_dll_size + ); + if (game_dll_base == 0) + { + fprintf( + stderr, + "PTWORLD: game.dll was not available for verification; continuing pristine.\n" + ); + } + else if (!verify_game_dll_memory(process.hProcess, game_dll_base, game_dll_size)) + { + fprintf( + stderr, + "PTWORLD: game.dll memory did not match V21.75.83; continuing pristine.\n" + ); + } + else if (!install_world_fix(process.hProcess, game_dll_base, &installation)) + { + fprintf( + stderr, + "PTWORLD: the post-login hook could not be armed; continuing pristine.\n" + ); + } + else + { + fprintf( + stderr, + "PTWORLD: publisher game.dll verified; post-login world fix armed.\n" + ); + } + + exit_code = wait_for_game_and_report(process.hProcess, &installation); + CloseHandle(process.hProcess); + return (int)exit_code; +} diff --git a/ptlinux/native/pt-world-fix.exe b/ptlinux/native/pt-world-fix.exe new file mode 100755 index 0000000..cbe14c0 Binary files /dev/null and b/ptlinux/native/pt-world-fix.exe differ