Added more diagnostics to the native world fixer and py logs system.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <tlhelp32.h>
|
#include <tlhelp32.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <stdarg.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <wchar.h>
|
#include <wchar.h>
|
||||||
@@ -70,12 +71,131 @@ typedef struct WorldFixInstallation
|
|||||||
} WorldFixInstallation;
|
} WorldFixInstallation;
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct StatusLog
|
||||||
|
{
|
||||||
|
HANDLE handle;
|
||||||
|
} StatusLog;
|
||||||
|
|
||||||
|
|
||||||
|
static void initialize_status_log(StatusLog *status)
|
||||||
|
{
|
||||||
|
status->handle = INVALID_HANDLE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BOOL status_path_is_allowed(const wchar_t *path)
|
||||||
|
{
|
||||||
|
static const wchar_t prefix[] = L"save\\launcher\\ptworld-";
|
||||||
|
static const wchar_t suffix[] = L".jsonl";
|
||||||
|
SIZE_T path_length;
|
||||||
|
SIZE_T prefix_length = (sizeof(prefix) / sizeof(prefix[0])) - 1;
|
||||||
|
SIZE_T suffix_length = (sizeof(suffix) / sizeof(suffix[0])) - 1;
|
||||||
|
|
||||||
|
if (path == NULL)
|
||||||
|
{
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
path_length = wcslen(path);
|
||||||
|
if (
|
||||||
|
path_length <= prefix_length + suffix_length
|
||||||
|
|| path_length >= MAX_PATH
|
||||||
|
|| wcsncmp(path, prefix, prefix_length) != 0
|
||||||
|
|| wcscmp(path + path_length - suffix_length, suffix) != 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
return wcschr(path, L'/') == NULL
|
||||||
|
&& wcschr(path, L':') == NULL
|
||||||
|
&& wcsstr(path, L"..") == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BOOL open_status_log(StatusLog *status, const wchar_t *path)
|
||||||
|
{
|
||||||
|
if (!status_path_is_allowed(path))
|
||||||
|
{
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
status->handle = CreateFileW(
|
||||||
|
path,
|
||||||
|
GENERIC_WRITE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
NULL,
|
||||||
|
CREATE_NEW,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
return status->handle != INVALID_HANDLE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void write_status_event(
|
||||||
|
StatusLog *status,
|
||||||
|
const char *event,
|
||||||
|
const char *detail_format,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
{
|
||||||
|
char details[512];
|
||||||
|
char line[768];
|
||||||
|
int line_length;
|
||||||
|
DWORD written = 0;
|
||||||
|
va_list arguments;
|
||||||
|
|
||||||
|
if (status->handle == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
details[0] = '\0';
|
||||||
|
if (detail_format != NULL)
|
||||||
|
{
|
||||||
|
va_start(arguments, detail_format);
|
||||||
|
vsnprintf(details, sizeof(details), detail_format, arguments);
|
||||||
|
va_end(arguments);
|
||||||
|
details[sizeof(details) - 1] = '\0';
|
||||||
|
}
|
||||||
|
line_length = snprintf(
|
||||||
|
line,
|
||||||
|
sizeof(line),
|
||||||
|
"{\"protocol\":1,\"event\":\"%s\",\"tick_ms\":%llu%s}\r\n",
|
||||||
|
event,
|
||||||
|
(unsigned long long)GetTickCount64(),
|
||||||
|
details
|
||||||
|
);
|
||||||
|
if (line_length <= 0 || (SIZE_T)line_length >= sizeof(line))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WriteFile(status->handle, line, (DWORD)line_length, &written, NULL);
|
||||||
|
FlushFileBuffers(status->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void close_status_log(StatusLog *status)
|
||||||
|
{
|
||||||
|
if (status->handle != INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
CloseHandle(status->handle);
|
||||||
|
status->handle = INVALID_HANDLE_VALUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static DWORD address_from_rva(DWORD base, DWORD rva)
|
static DWORD address_from_rva(DWORD base, DWORD rva)
|
||||||
{
|
{
|
||||||
return base + rva;
|
return base + rva;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static DWORD image_dword(const BYTE *image, SIZE_T offset)
|
||||||
|
{
|
||||||
|
DWORD value;
|
||||||
|
memcpy(&value, image + offset, sizeof(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static BOOL read_exact(HANDLE process, DWORD address, void *buffer, SIZE_T size)
|
static BOOL read_exact(HANDLE process, DWORD address, void *buffer, SIZE_T size)
|
||||||
{
|
{
|
||||||
SIZE_T read_size = 0;
|
SIZE_T read_size = 0;
|
||||||
@@ -188,7 +308,7 @@ static void emit_set_state(CodeBuffer *code, DWORD state_address, BYTE value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void emit_restore_iat(
|
static SIZE_T emit_restore_iat(
|
||||||
CodeBuffer *code,
|
CodeBuffer *code,
|
||||||
DWORD iat_address,
|
DWORD iat_address,
|
||||||
DWORD original_lstrcmp,
|
DWORD original_lstrcmp,
|
||||||
@@ -197,6 +317,17 @@ static void emit_restore_iat(
|
|||||||
DWORD temporary_protect_address
|
DWORD temporary_protect_address
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
SIZE_T protect_failure_jump;
|
||||||
|
|
||||||
|
/* The import table is in publisher .rdata. Make it writable before MOV. */
|
||||||
|
emit_push_immediate(code, temporary_protect_address);
|
||||||
|
emit_push_immediate(code, PAGE_READWRITE);
|
||||||
|
emit_push_immediate(code, sizeof(DWORD));
|
||||||
|
emit_push_immediate(code, iat_address);
|
||||||
|
emit_call_iat(code, virtual_protect_iat);
|
||||||
|
emit_bytes(code, (const BYTE[]){0x85, 0xc0}, 2);
|
||||||
|
protect_failure_jump = emit_near_jump_placeholder(code, 0x84); /* je finish */
|
||||||
|
|
||||||
/* mov dword ptr [iat_address], original_lstrcmp */
|
/* mov dword ptr [iat_address], original_lstrcmp */
|
||||||
emit_byte(code, 0xc7);
|
emit_byte(code, 0xc7);
|
||||||
emit_byte(code, 0x05);
|
emit_byte(code, 0x05);
|
||||||
@@ -208,6 +339,7 @@ static void emit_restore_iat(
|
|||||||
emit_push_immediate(code, sizeof(DWORD));
|
emit_push_immediate(code, sizeof(DWORD));
|
||||||
emit_push_immediate(code, iat_address);
|
emit_push_immediate(code, iat_address);
|
||||||
emit_call_iat(code, virtual_protect_iat);
|
emit_call_iat(code, virtual_protect_iat);
|
||||||
|
return protect_failure_jump;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -249,6 +381,8 @@ static BOOL build_remote_hook(
|
|||||||
SIZE_T already_triggered_jump;
|
SIZE_T already_triggered_jump;
|
||||||
SIZE_T cave_failure_jump;
|
SIZE_T cave_failure_jump;
|
||||||
SIZE_T entry_failure_jump;
|
SIZE_T entry_failure_jump;
|
||||||
|
SIZE_T success_restore_failure_jump;
|
||||||
|
SIZE_T failure_restore_failure_jump;
|
||||||
SIZE_T success_jump;
|
SIZE_T success_jump;
|
||||||
SIZE_T finish_offset;
|
SIZE_T finish_offset;
|
||||||
SIZE_T entry_failure_offset;
|
SIZE_T entry_failure_offset;
|
||||||
@@ -343,7 +477,8 @@ static BOOL build_remote_hook(
|
|||||||
emit_push_immediate(&code, code_cave);
|
emit_push_immediate(&code, code_cave);
|
||||||
emit_call_iat(&code, virtual_protect_iat);
|
emit_call_iat(&code, virtual_protect_iat);
|
||||||
|
|
||||||
emit_restore_iat(
|
emit_set_state(&code, state_address, 2);
|
||||||
|
success_restore_failure_jump = emit_restore_iat(
|
||||||
&code,
|
&code,
|
||||||
lstrcmp_iat,
|
lstrcmp_iat,
|
||||||
original_lstrcmp,
|
original_lstrcmp,
|
||||||
@@ -351,7 +486,6 @@ static BOOL build_remote_hook(
|
|||||||
original_iat_protect,
|
original_iat_protect,
|
||||||
temporary_protect
|
temporary_protect
|
||||||
);
|
);
|
||||||
emit_set_state(&code, state_address, 2);
|
|
||||||
success_jump = emit_near_jump_placeholder(&code, 0); /* jmp finish */
|
success_jump = emit_near_jump_placeholder(&code, 0); /* jmp finish */
|
||||||
|
|
||||||
entry_failure_offset = code.length;
|
entry_failure_offset = code.length;
|
||||||
@@ -363,7 +497,8 @@ static BOOL build_remote_hook(
|
|||||||
emit_call_iat(&code, virtual_protect_iat);
|
emit_call_iat(&code, virtual_protect_iat);
|
||||||
|
|
||||||
failure_offset = code.length;
|
failure_offset = code.length;
|
||||||
emit_restore_iat(
|
emit_set_state(&code, state_address, 3);
|
||||||
|
failure_restore_failure_jump = emit_restore_iat(
|
||||||
&code,
|
&code,
|
||||||
lstrcmp_iat,
|
lstrcmp_iat,
|
||||||
original_lstrcmp,
|
original_lstrcmp,
|
||||||
@@ -371,7 +506,6 @@ static BOOL build_remote_hook(
|
|||||||
original_iat_protect,
|
original_iat_protect,
|
||||||
temporary_protect
|
temporary_protect
|
||||||
);
|
);
|
||||||
emit_set_state(&code, state_address, 3);
|
|
||||||
|
|
||||||
success_offset = code.length;
|
success_offset = code.length;
|
||||||
emit_bytes(&code, (const BYTE[]){0x31, 0xc0}, 2); /* successful lstrcmpA result */
|
emit_bytes(&code, (const BYTE[]){0x31, 0xc0}, 2); /* successful lstrcmpA result */
|
||||||
@@ -383,6 +517,8 @@ static BOOL build_remote_hook(
|
|||||||
patch_near_jump(&code, already_triggered_jump, success_offset);
|
patch_near_jump(&code, already_triggered_jump, success_offset);
|
||||||
patch_near_jump(&code, cave_failure_jump, failure_offset);
|
patch_near_jump(&code, cave_failure_jump, failure_offset);
|
||||||
patch_near_jump(&code, entry_failure_jump, entry_failure_offset);
|
patch_near_jump(&code, entry_failure_jump, entry_failure_offset);
|
||||||
|
patch_near_jump(&code, success_restore_failure_jump, success_offset);
|
||||||
|
patch_near_jump(&code, failure_restore_failure_jump, success_offset);
|
||||||
patch_near_jump(&code, success_jump, success_offset);
|
patch_near_jump(&code, success_jump, success_offset);
|
||||||
|
|
||||||
if (!code.valid)
|
if (!code.valid)
|
||||||
@@ -394,6 +530,64 @@ static BOOL build_remote_hook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BOOL remote_iat_restore_is_safe(
|
||||||
|
const BYTE *image,
|
||||||
|
SIZE_T code_size,
|
||||||
|
DWORD game_dll_base,
|
||||||
|
DWORD original_lstrcmp
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DWORD iat_address = 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
|
||||||
|
);
|
||||||
|
SIZE_T restore_count = 0;
|
||||||
|
SIZE_T index;
|
||||||
|
|
||||||
|
for (index = 0; index + 10 <= code_size; ++index)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
image[index] != 0xc7
|
||||||
|
|| image[index + 1] != 0x05
|
||||||
|
|| image_dword(image, index + 2) != iat_address
|
||||||
|
|| image_dword(image, index + 6) != original_lstrcmp
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++restore_count;
|
||||||
|
/*
|
||||||
|
* Immediately before every IAT MOV, require:
|
||||||
|
* VirtualProtect(iat, 4, PAGE_READWRITE, ...), TEST, JE.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
index < 29
|
||||||
|
|| image[index - 29] != 0x68
|
||||||
|
|| image_dword(image, index - 28) != PAGE_READWRITE
|
||||||
|
|| image[index - 24] != 0x68
|
||||||
|
|| image_dword(image, index - 23) != sizeof(DWORD)
|
||||||
|
|| image[index - 19] != 0x68
|
||||||
|
|| image_dword(image, index - 18) != iat_address
|
||||||
|
|| image[index - 14] != 0xff
|
||||||
|
|| image[index - 13] != 0x15
|
||||||
|
|| image_dword(image, index - 12) != virtual_protect_iat
|
||||||
|
|| image[index - 8] != 0x85
|
||||||
|
|| image[index - 7] != 0xc0
|
||||||
|
|| image[index - 6] != 0x0f
|
||||||
|
|| image[index - 5] != 0x84
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return restore_count == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static DWORD find_game_dll(DWORD process_id, DWORD *module_size)
|
static DWORD find_game_dll(DWORD process_id, DWORD *module_size)
|
||||||
{
|
{
|
||||||
HANDLE snapshot;
|
HANDLE snapshot;
|
||||||
@@ -647,18 +841,78 @@ static BOOL install_world_fix(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void report_login_accepted(StatusLog *status, BOOL *reported)
|
||||||
|
{
|
||||||
|
if (!*reported)
|
||||||
|
{
|
||||||
|
write_status_event(status, "login_accepted", NULL);
|
||||||
|
*reported = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BOOL report_world_fix_state(
|
||||||
|
StatusLog *status,
|
||||||
|
BYTE state,
|
||||||
|
BOOL *login_reported
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (state >= 1 && state <= 3)
|
||||||
|
{
|
||||||
|
report_login_accepted(status, login_reported);
|
||||||
|
}
|
||||||
|
if (state == 2)
|
||||||
|
{
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"PTWORLD: login accepted; world-transition fix activated in memory.\n"
|
||||||
|
);
|
||||||
|
write_status_event(status, "world_fix_activated", NULL);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if (state == 3)
|
||||||
|
{
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"PTWORLD: login succeeded, but the verified memory fix could not be activated.\n"
|
||||||
|
);
|
||||||
|
write_status_event(status, "world_fix_activation_failed", NULL);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static DWORD wait_for_game_and_report(
|
static DWORD wait_for_game_and_report(
|
||||||
HANDLE process,
|
HANDLE process,
|
||||||
const WorldFixInstallation *installation
|
const WorldFixInstallation *installation,
|
||||||
|
StatusLog *status
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
BOOL reported = FALSE;
|
BOOL reported = FALSE;
|
||||||
|
BOOL login_reported = FALSE;
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
DWORD wait_result = WaitForSingleObject(process, 100);
|
DWORD wait_result = WaitForSingleObject(process, 100);
|
||||||
if (wait_result == WAIT_OBJECT_0)
|
if (wait_result == WAIT_OBJECT_0)
|
||||||
{
|
{
|
||||||
DWORD exit_code = 1;
|
DWORD exit_code = 1;
|
||||||
|
if (!reported && installation->remote_state != 0)
|
||||||
|
{
|
||||||
|
BYTE state = 0;
|
||||||
|
if (read_exact(process, installation->remote_state, &state, sizeof(state)))
|
||||||
|
{
|
||||||
|
reported = report_world_fix_state(
|
||||||
|
status,
|
||||||
|
state,
|
||||||
|
&login_reported
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!reported && installation->remote_state != 0)
|
||||||
|
{
|
||||||
|
write_status_event(status, "world_fix_not_activated", NULL);
|
||||||
|
}
|
||||||
GetExitCodeProcess(process, &exit_code);
|
GetExitCodeProcess(process, &exit_code);
|
||||||
return exit_code;
|
return exit_code;
|
||||||
}
|
}
|
||||||
@@ -667,22 +921,11 @@ static DWORD wait_for_game_and_report(
|
|||||||
BYTE state = 0;
|
BYTE state = 0;
|
||||||
if (read_exact(process, installation->remote_state, &state, sizeof(state)))
|
if (read_exact(process, installation->remote_state, &state, sizeof(state)))
|
||||||
{
|
{
|
||||||
if (state == 2)
|
reported = report_world_fix_state(
|
||||||
{
|
status,
|
||||||
fprintf(
|
state,
|
||||||
stderr,
|
&login_reported
|
||||||
"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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,6 +949,19 @@ static int run_self_test(void)
|
|||||||
fprintf(stderr, "PTWORLD-SELFTEST: remote hook construction failed.\n");
|
fprintf(stderr, "PTWORLD-SELFTEST: remote hook construction failed.\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if (!remote_iat_restore_is_safe(
|
||||||
|
image,
|
||||||
|
code_size,
|
||||||
|
0x10000000u,
|
||||||
|
0x7b621000u
|
||||||
|
))
|
||||||
|
{
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"PTWORLD-SELFTEST: import-table restore is not write-safe.\n"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (memcmp(
|
if (memcmp(
|
||||||
image + REMOTE_RECLAIM_OFFSET,
|
image + REMOTE_RECLAIM_OFFSET,
|
||||||
PROVEN_RECLAIM_HOOK,
|
PROVEN_RECLAIM_HOOK,
|
||||||
@@ -733,6 +989,8 @@ int wmain(int argument_count, wchar_t **arguments)
|
|||||||
STARTUPINFOW startup;
|
STARTUPINFOW startup;
|
||||||
PROCESS_INFORMATION process;
|
PROCESS_INFORMATION process;
|
||||||
WorldFixInstallation installation;
|
WorldFixInstallation installation;
|
||||||
|
StatusLog status;
|
||||||
|
const wchar_t *status_path = NULL;
|
||||||
WCHAR game_path[MAX_PATH];
|
WCHAR game_path[MAX_PATH];
|
||||||
WCHAR command_line[MAX_PATH + 64];
|
WCHAR command_line[MAX_PATH + 64];
|
||||||
DWORD game_dll_base;
|
DWORD game_dll_base;
|
||||||
@@ -747,6 +1005,33 @@ int wmain(int argument_count, wchar_t **arguments)
|
|||||||
{
|
{
|
||||||
return run_self_test();
|
return run_self_test();
|
||||||
}
|
}
|
||||||
|
initialize_status_log(&status);
|
||||||
|
if (
|
||||||
|
argument_count == 3
|
||||||
|
&& wcscmp(arguments[1], L"--status-file") == 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
status_path = arguments[2];
|
||||||
|
if (!open_status_log(&status, status_path))
|
||||||
|
{
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"PTWORLD: could not create the per-run status file (Windows error %lu).\n",
|
||||||
|
(unsigned long)GetLastError()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (argument_count != 1)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "PTWORLD: invalid helper arguments.\n");
|
||||||
|
return 111;
|
||||||
|
}
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"helper_started",
|
||||||
|
",\"process_id\":%lu",
|
||||||
|
(unsigned long)GetCurrentProcessId()
|
||||||
|
);
|
||||||
memset(&startup, 0, sizeof(startup));
|
memset(&startup, 0, sizeof(startup));
|
||||||
memset(&process, 0, sizeof(process));
|
memset(&process, 0, sizeof(process));
|
||||||
memset(&installation, 0, sizeof(installation));
|
memset(&installation, 0, sizeof(installation));
|
||||||
@@ -755,6 +1040,8 @@ int wmain(int argument_count, wchar_t **arguments)
|
|||||||
if (path_length == 0 || path_length >= MAX_PATH)
|
if (path_length == 0 || path_length >= MAX_PATH)
|
||||||
{
|
{
|
||||||
fprintf(stderr, "PTWORLD: Game.exe is not available in the working directory.\n");
|
fprintf(stderr, "PTWORLD: Game.exe is not available in the working directory.\n");
|
||||||
|
write_status_event(&status, "game_path_failed", NULL);
|
||||||
|
close_status_log(&status);
|
||||||
return 111;
|
return 111;
|
||||||
}
|
}
|
||||||
if (swprintf(
|
if (swprintf(
|
||||||
@@ -765,6 +1052,8 @@ int wmain(int argument_count, wchar_t **arguments)
|
|||||||
) < 0)
|
) < 0)
|
||||||
{
|
{
|
||||||
fprintf(stderr, "PTWORLD: could not construct the game command line.\n");
|
fprintf(stderr, "PTWORLD: could not construct the game command line.\n");
|
||||||
|
write_status_event(&status, "game_command_failed", NULL);
|
||||||
|
close_status_log(&status);
|
||||||
return 111;
|
return 111;
|
||||||
}
|
}
|
||||||
if (!CreateProcessW(
|
if (!CreateProcessW(
|
||||||
@@ -780,26 +1069,51 @@ int wmain(int argument_count, wchar_t **arguments)
|
|||||||
&process
|
&process
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
fprintf(
|
fprintf(
|
||||||
stderr,
|
stderr,
|
||||||
"PTWORLD: could not start Game.exe (Windows error %lu).\n",
|
"PTWORLD: could not start Game.exe (Windows error %lu).\n",
|
||||||
(unsigned long)GetLastError()
|
(unsigned long)error
|
||||||
);
|
);
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"game_start_failed",
|
||||||
|
",\"windows_error\":%lu",
|
||||||
|
(unsigned long)error
|
||||||
|
);
|
||||||
|
close_status_log(&status);
|
||||||
return 111;
|
return 111;
|
||||||
}
|
}
|
||||||
CloseHandle(process.hThread);
|
CloseHandle(process.hThread);
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"game_started",
|
||||||
|
",\"process_id\":%lu",
|
||||||
|
(unsigned long)process.dwProcessId
|
||||||
|
);
|
||||||
|
|
||||||
game_dll_base = wait_for_game_dll(
|
game_dll_base = wait_for_game_dll(
|
||||||
process.hProcess,
|
process.hProcess,
|
||||||
process.dwProcessId,
|
process.dwProcessId,
|
||||||
&game_dll_size
|
&game_dll_size
|
||||||
);
|
);
|
||||||
|
if (game_dll_base != 0)
|
||||||
|
{
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"game_dll_found",
|
||||||
|
",\"base\":%lu,\"size\":%lu",
|
||||||
|
(unsigned long)game_dll_base,
|
||||||
|
(unsigned long)game_dll_size
|
||||||
|
);
|
||||||
|
}
|
||||||
if (game_dll_base == 0)
|
if (game_dll_base == 0)
|
||||||
{
|
{
|
||||||
fprintf(
|
fprintf(
|
||||||
stderr,
|
stderr,
|
||||||
"PTWORLD: game.dll was not available for verification; continuing pristine.\n"
|
"PTWORLD: game.dll was not available for verification; continuing pristine.\n"
|
||||||
);
|
);
|
||||||
|
write_status_event(&status, "game_dll_unavailable", NULL);
|
||||||
}
|
}
|
||||||
else if (!verify_game_dll_memory(process.hProcess, game_dll_base, game_dll_size))
|
else if (!verify_game_dll_memory(process.hProcess, game_dll_base, game_dll_size))
|
||||||
{
|
{
|
||||||
@@ -807,23 +1121,59 @@ int wmain(int argument_count, wchar_t **arguments)
|
|||||||
stderr,
|
stderr,
|
||||||
"PTWORLD: game.dll memory did not match V21.75.83; continuing pristine.\n"
|
"PTWORLD: game.dll memory did not match V21.75.83; continuing pristine.\n"
|
||||||
);
|
);
|
||||||
}
|
write_status_event(
|
||||||
else if (!install_world_fix(process.hProcess, game_dll_base, &installation))
|
&status,
|
||||||
{
|
"game_dll_rejected",
|
||||||
fprintf(
|
",\"base\":%lu,\"size\":%lu",
|
||||||
stderr,
|
(unsigned long)game_dll_base,
|
||||||
"PTWORLD: the post-login hook could not be armed; continuing pristine.\n"
|
(unsigned long)game_dll_size
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fprintf(
|
write_status_event(
|
||||||
stderr,
|
&status,
|
||||||
"PTWORLD: publisher game.dll verified; post-login world fix armed.\n"
|
"game_dll_verified",
|
||||||
|
NULL
|
||||||
);
|
);
|
||||||
|
if (!install_world_fix(process.hProcess, game_dll_base, &installation))
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"PTWORLD: the post-login hook could not be armed; continuing pristine.\n"
|
||||||
|
);
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"hook_arm_failed",
|
||||||
|
",\"windows_error\":%lu",
|
||||||
|
(unsigned long)error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"PTWORLD: publisher game.dll verified; post-login world fix armed.\n"
|
||||||
|
);
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"hook_armed",
|
||||||
|
",\"base\":%lu,\"size\":%lu",
|
||||||
|
(unsigned long)game_dll_base,
|
||||||
|
(unsigned long)game_dll_size
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_code = wait_for_game_and_report(process.hProcess, &installation);
|
exit_code = wait_for_game_and_report(process.hProcess, &installation, &status);
|
||||||
|
write_status_event(
|
||||||
|
&status,
|
||||||
|
"game_exited",
|
||||||
|
",\"exit_code\":%lu",
|
||||||
|
(unsigned long)exit_code
|
||||||
|
);
|
||||||
CloseHandle(process.hProcess);
|
CloseHandle(process.hProcess);
|
||||||
|
close_status_log(&status);
|
||||||
return (int)exit_code;
|
return (int)exit_code;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ from .paths import AppPaths
|
|||||||
|
|
||||||
__all__ = ["AppPaths", "__version__"]
|
__all__ = ["AppPaths", "__version__"]
|
||||||
|
|
||||||
__version__ = "0.5.9"
|
__version__ = "0.5.10"
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ from .self_update import (
|
|||||||
rollback_launcher_update,
|
rollback_launcher_update,
|
||||||
update_check_to_dict,
|
update_check_to_dict,
|
||||||
)
|
)
|
||||||
|
from .world_fix import (
|
||||||
|
create_world_fix_status_target,
|
||||||
|
read_world_fix_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
@@ -650,6 +654,12 @@ def _play_locked(
|
|||||||
patterns=("log_*.txt",),
|
patterns=("log_*.txt",),
|
||||||
)
|
)
|
||||||
game_started_ns = time.time_ns()
|
game_started_ns = time.time_ns()
|
||||||
|
world_fix_status_target = None
|
||||||
|
if not dry_run and not publisher_dll:
|
||||||
|
world_fix_status_target = create_world_fix_status_target(
|
||||||
|
paths.game_save,
|
||||||
|
game_started_ns,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
outcome = run_game(
|
outcome = run_game(
|
||||||
paths,
|
paths,
|
||||||
@@ -658,6 +668,7 @@ def _play_locked(
|
|||||||
runner,
|
runner,
|
||||||
use_gamebox=not no_box,
|
use_gamebox=not no_box,
|
||||||
use_world_fix=not publisher_dll,
|
use_world_fix=not publisher_dll,
|
||||||
|
world_fix_status_target=world_fix_status_target,
|
||||||
)
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
current_game_log = _current_game_log(
|
current_game_log = _current_game_log(
|
||||||
@@ -667,12 +678,26 @@ def _play_locked(
|
|||||||
)
|
)
|
||||||
if current_game_log is not None and not as_json:
|
if current_game_log is not None and not as_json:
|
||||||
_print_interrupted_game_log(current_game_log)
|
_print_interrupted_game_log(current_game_log)
|
||||||
|
world_fix_status = read_world_fix_status(
|
||||||
|
world_fix_status_target,
|
||||||
|
game_started_ns,
|
||||||
|
)
|
||||||
|
if world_fix_status is not None and not as_json:
|
||||||
|
print(
|
||||||
|
f"World-fix status: {str(world_fix_status['status'])} "
|
||||||
|
f"({str(world_fix_status['path'])})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
current_game_log = _current_game_log(
|
current_game_log = _current_game_log(
|
||||||
native_log_directory,
|
native_log_directory,
|
||||||
native_log_baseline,
|
native_log_baseline,
|
||||||
game_started_ns,
|
game_started_ns,
|
||||||
)
|
)
|
||||||
|
world_fix_status = read_world_fix_status(
|
||||||
|
world_fix_status_target,
|
||||||
|
game_started_ns,
|
||||||
|
)
|
||||||
_emit(
|
_emit(
|
||||||
{
|
{
|
||||||
"ok": outcome.returncode == 0,
|
"ok": outcome.returncode == 0,
|
||||||
@@ -682,6 +707,7 @@ def _play_locked(
|
|||||||
"setup_commands": [asdict(result) for result in setup_results],
|
"setup_commands": [asdict(result) for result in setup_results],
|
||||||
"game": asdict(outcome),
|
"game": asdict(outcome),
|
||||||
"current_game_log": current_game_log,
|
"current_game_log": current_game_log,
|
||||||
|
"world_fix": world_fix_status,
|
||||||
},
|
},
|
||||||
as_json,
|
as_json,
|
||||||
)
|
)
|
||||||
@@ -712,7 +738,17 @@ def _current_game_log(
|
|||||||
"import_scene_threw": analysis.import_scene_threw,
|
"import_scene_threw": analysis.import_scene_threw,
|
||||||
"failed_import_scenes": analysis.failed_import_scenes,
|
"failed_import_scenes": analysis.failed_import_scenes,
|
||||||
"stage_without_new_stage": analysis.stage_without_new_stage,
|
"stage_without_new_stage": analysis.stage_without_new_stage,
|
||||||
|
"stage_without_new_stage_events": [
|
||||||
|
event.to_dict()
|
||||||
|
for event in analysis.events
|
||||||
|
if event.kind == "stage_without_new_stage"
|
||||||
|
],
|
||||||
|
"scene_imports": {
|
||||||
|
name: [scene.to_dict() for scene in imports]
|
||||||
|
for name, imports in sorted(analysis.scenes.items())
|
||||||
|
},
|
||||||
"failed_file_count": len(analysis.failed_files),
|
"failed_file_count": len(analysis.failed_files),
|
||||||
|
"failed_files": [event.to_dict() for event in analysis.failed_files[:64]],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -16,7 +16,11 @@ from .command import CommandOutcome, CommandRunner, OutputLineFilter
|
|||||||
from .gamebox import build_gamebox_command
|
from .gamebox import build_gamebox_command
|
||||||
from .paths import AppPaths
|
from .paths import AppPaths
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
from .world_fix import WorldFixError, resolve_world_fix_helper
|
from .world_fix import (
|
||||||
|
WorldFixError,
|
||||||
|
WorldFixStatusTarget,
|
||||||
|
resolve_world_fix_helper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
|
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
|
||||||
@@ -252,6 +256,7 @@ def run_game(
|
|||||||
*,
|
*,
|
||||||
use_gamebox: bool = True,
|
use_gamebox: bool = True,
|
||||||
use_world_fix: bool = False,
|
use_world_fix: bool = False,
|
||||||
|
world_fix_status_target: WorldFixStatusTarget | None = None,
|
||||||
timeout_seconds: float = 12 * 60 * 60,
|
timeout_seconds: float = 12 * 60 * 60,
|
||||||
check: bool = True,
|
check: bool = True,
|
||||||
extra_environment: Mapping[str, str] | None = None,
|
extra_environment: Mapping[str, str] | None = None,
|
||||||
@@ -273,6 +278,10 @@ def run_game(
|
|||||||
]
|
]
|
||||||
if world_fix_helper is None:
|
if world_fix_helper is None:
|
||||||
game_command.append("PTEU_REBORN")
|
game_command.append("PTEU_REBORN")
|
||||||
|
elif world_fix_status_target is not None:
|
||||||
|
game_command.extend(
|
||||||
|
["--status-file", world_fix_status_target.windows_path]
|
||||||
|
)
|
||||||
environment = _umu_environment(paths)
|
environment = _umu_environment(paths)
|
||||||
elif selection.kind == "proton7-container":
|
elif selection.kind == "proton7-container":
|
||||||
if not selection.proton_path:
|
if not selection.proton_path:
|
||||||
@@ -283,6 +292,10 @@ def run_game(
|
|||||||
]
|
]
|
||||||
if world_fix_helper is None:
|
if world_fix_helper is None:
|
||||||
game_command.append("PTEU_REBORN")
|
game_command.append("PTEU_REBORN")
|
||||||
|
elif world_fix_status_target is not None:
|
||||||
|
game_command.extend(
|
||||||
|
["--status-file", world_fix_status_target.windows_path]
|
||||||
|
)
|
||||||
environment = _umu_proton7_environment(
|
environment = _umu_proton7_environment(
|
||||||
paths,
|
paths,
|
||||||
selection,
|
selection,
|
||||||
@@ -301,6 +314,10 @@ def run_game(
|
|||||||
]
|
]
|
||||||
if world_fix_helper is None:
|
if world_fix_helper is None:
|
||||||
game_command.append("PTEU_REBORN")
|
game_command.append("PTEU_REBORN")
|
||||||
|
elif world_fix_status_target is not None:
|
||||||
|
game_command.extend(
|
||||||
|
["--status-file", world_fix_status_target.windows_path]
|
||||||
|
)
|
||||||
environment = _proton_environment(
|
environment = _proton_environment(
|
||||||
paths,
|
paths,
|
||||||
selection,
|
selection,
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path, PureWindowsPath
|
||||||
|
|
||||||
|
|
||||||
V21_BINARY_HASHES = {
|
V21_BINARY_HASHES = {
|
||||||
@@ -13,8 +16,16 @@ V21_BINARY_HASHES = {
|
|||||||
WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
|
WORLD_FIX_HELPER_FILENAME = "pt-world-fix.exe"
|
||||||
# Filled from the reproducible stripped PE after the native helper is built.
|
# Filled from the reproducible stripped PE after the native helper is built.
|
||||||
WORLD_FIX_HELPER_SHA256 = (
|
WORLD_FIX_HELPER_SHA256 = (
|
||||||
"d5bdf655f62dae429eff627cf8960f670ec59c12a9759dcd14342786f53e4019"
|
"a7a17d29ef4c11f9603790f0110a4d6823b91cce03f70e466b7e0bdbd6fa313e"
|
||||||
)
|
)
|
||||||
|
WORLD_FIX_STATUS_PROTOCOL = 1
|
||||||
|
WORLD_FIX_STATUS_MAX_BYTES = 64 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WorldFixStatusTarget:
|
||||||
|
path: Path
|
||||||
|
windows_path: str
|
||||||
|
|
||||||
|
|
||||||
class WorldFixError(RuntimeError):
|
class WorldFixError(RuntimeError):
|
||||||
@@ -58,6 +69,145 @@ def resolve_world_fix_helper(
|
|||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def create_world_fix_status_target(
|
||||||
|
game_save: Path,
|
||||||
|
started_ns: int,
|
||||||
|
) -> WorldFixStatusTarget:
|
||||||
|
"""Allocate a unique launcher-owned status path without pre-creating it."""
|
||||||
|
|
||||||
|
if started_ns <= 0:
|
||||||
|
raise WorldFixError("world-fix status start time must be positive")
|
||||||
|
directory = game_save / "launcher"
|
||||||
|
if directory.is_symlink():
|
||||||
|
raise WorldFixError(
|
||||||
|
"the world-fix status directory must not be a symbolic link"
|
||||||
|
)
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not directory.is_dir():
|
||||||
|
raise WorldFixError("the world-fix status directory is unavailable")
|
||||||
|
filename = f"ptworld-{started_ns}-{os.getpid()}.jsonl"
|
||||||
|
path = directory / filename
|
||||||
|
if path.exists() or path.is_symlink():
|
||||||
|
raise WorldFixError(f"world-fix status target already exists: {path}")
|
||||||
|
windows_path = str(PureWindowsPath("save", "launcher", filename))
|
||||||
|
return WorldFixStatusTarget(path=path, windows_path=windows_path)
|
||||||
|
|
||||||
|
|
||||||
|
def read_world_fix_status(
|
||||||
|
target: WorldFixStatusTarget | None,
|
||||||
|
started_ns: int,
|
||||||
|
) -> dict[str, object] | None:
|
||||||
|
"""Read only the bounded, fresh status stream belonging to this game run."""
|
||||||
|
|
||||||
|
if target is None:
|
||||||
|
return None
|
||||||
|
report: dict[str, object] = {
|
||||||
|
"path": str(target.path),
|
||||||
|
"status": "missing",
|
||||||
|
"fresh": False,
|
||||||
|
"events": [],
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
if target.path.is_symlink() or not target.path.is_file():
|
||||||
|
return report
|
||||||
|
state = target.path.stat()
|
||||||
|
except OSError as exc:
|
||||||
|
report["status"] = "unreadable"
|
||||||
|
report["error"] = str(exc)
|
||||||
|
return report
|
||||||
|
report["modified_at_ns"] = state.st_mtime_ns
|
||||||
|
report["size"] = state.st_size
|
||||||
|
if state.st_mtime_ns < started_ns:
|
||||||
|
report["status"] = "stale"
|
||||||
|
return report
|
||||||
|
report["fresh"] = True
|
||||||
|
if state.st_size > WORLD_FIX_STATUS_MAX_BYTES:
|
||||||
|
report["status"] = "oversized"
|
||||||
|
return report
|
||||||
|
try:
|
||||||
|
lines = target.path.read_text(encoding="utf-8").splitlines()
|
||||||
|
except OSError as exc:
|
||||||
|
report["status"] = "unreadable"
|
||||||
|
report["error"] = str(exc)
|
||||||
|
return report
|
||||||
|
|
||||||
|
events: list[dict[str, object]] = []
|
||||||
|
invalid_lines = 0
|
||||||
|
for line in lines[:64]:
|
||||||
|
try:
|
||||||
|
value = json.loads(line)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
invalid_lines += 1
|
||||||
|
continue
|
||||||
|
clean = _validated_status_event(value)
|
||||||
|
if clean is None:
|
||||||
|
invalid_lines += 1
|
||||||
|
continue
|
||||||
|
events.append(clean)
|
||||||
|
report["events"] = events
|
||||||
|
if invalid_lines:
|
||||||
|
report["invalid_lines"] = invalid_lines
|
||||||
|
event_names = {
|
||||||
|
event["event"]
|
||||||
|
for event in events
|
||||||
|
if isinstance(event.get("event"), str)
|
||||||
|
}
|
||||||
|
for name in (
|
||||||
|
"helper_started",
|
||||||
|
"game_started",
|
||||||
|
"game_dll_verified",
|
||||||
|
"hook_armed",
|
||||||
|
"login_accepted",
|
||||||
|
"world_fix_activated",
|
||||||
|
):
|
||||||
|
report[name] = name in event_names
|
||||||
|
if "world_fix_activated" in event_names:
|
||||||
|
report["status"] = "activated"
|
||||||
|
elif "world_fix_activation_failed" in event_names:
|
||||||
|
report["status"] = "activation_failed"
|
||||||
|
elif "world_fix_not_activated" in event_names:
|
||||||
|
report["status"] = "not_activated"
|
||||||
|
elif "hook_armed" in event_names:
|
||||||
|
report["status"] = "armed"
|
||||||
|
elif event_names & {
|
||||||
|
"game_path_failed",
|
||||||
|
"game_command_failed",
|
||||||
|
"game_start_failed",
|
||||||
|
"game_dll_unavailable",
|
||||||
|
"game_dll_rejected",
|
||||||
|
"hook_arm_failed",
|
||||||
|
}:
|
||||||
|
report["status"] = "helper_failed"
|
||||||
|
elif events:
|
||||||
|
report["status"] = "started"
|
||||||
|
else:
|
||||||
|
report["status"] = "invalid"
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_status_event(value: object) -> dict[str, object] | None:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return None
|
||||||
|
if value.get("protocol") != WORLD_FIX_STATUS_PROTOCOL:
|
||||||
|
return None
|
||||||
|
event = value.get("event")
|
||||||
|
tick_ms = value.get("tick_ms")
|
||||||
|
if not isinstance(event, str) or not event or len(event) > 64:
|
||||||
|
return None
|
||||||
|
if isinstance(tick_ms, bool) or not isinstance(tick_ms, int) or tick_ms < 0:
|
||||||
|
return None
|
||||||
|
clean: dict[str, object] = {
|
||||||
|
"event": event,
|
||||||
|
"tick_ms": tick_ms,
|
||||||
|
}
|
||||||
|
for key in ("process_id", "base", "size", "windows_error", "exit_code"):
|
||||||
|
item = value.get(key)
|
||||||
|
if isinstance(item, bool) or not isinstance(item, int) or item < 0:
|
||||||
|
continue
|
||||||
|
clean[key] = item
|
||||||
|
return clean
|
||||||
|
|
||||||
|
|
||||||
def _sha256(path: Path) -> str:
|
def _sha256(path: Path) -> str:
|
||||||
digest = hashlib.sha256()
|
digest = hashlib.sha256()
|
||||||
with path.open("rb") as handle:
|
with path.open("rb") as handle:
|
||||||
|
|||||||
Reference in New Issue
Block a user