diff --git a/src/gamepatches/boot.cpp b/src/gamepatches/boot.cpp index 9b6fc2f..a3698b8 100644 --- a/src/gamepatches/boot.cpp +++ b/src/gamepatches/boot.cpp @@ -82,6 +82,8 @@ UINT64 PreprocessCommandLineHook(PVOID pGame) { g_timestampLogs = TRUE; } else if (lstrcmpW(arg, L"-upnp") == 0) { g_upnpEnabled = TRUE; + } else if (lstrcmpW(arg, L"-legacy-log-names") == 0) { + g_legacyLogNames = TRUE; } else if (lstrcmpW(arg, L"-config") == 0 || lstrcmpW(arg, L"-config-path") == 0) { if (i + 1 < argc) { WideCharToMultiByte(CP_UTF8, 0, argv[i + 1], -1, g_customConfigPath, MAX_PATH, NULL, NULL); diff --git a/src/gamepatches/cli.cpp b/src/gamepatches/cli.cpp index b65fbc7..6085817 100644 --- a/src/gamepatches/cli.cpp +++ b/src/gamepatches/cli.cpp @@ -5,6 +5,7 @@ BOOL g_isServer = FALSE; BOOL g_isOffline = FALSE; BOOL g_isWindowed = FALSE; +BOOL g_legacyLogNames = FALSE; CHAR g_customConfigPath[MAX_PATH] = {0}; CHAR g_regionOverride[64] = {0}; @@ -53,6 +54,9 @@ UINT64 BuildCmdLineSyntaxDefinitionsHook(PVOID pGame, PVOID pArgSyntax) { EchoVR::AddArgSyntax(pArgSyntax, "-upnp", 0, 0, FALSE); EchoVR::AddArgHelpString(pArgSyntax, "-upnp", "[NEVR] Enable UPnP port forwarding"); + EchoVR::AddArgSyntax(pArgSyntax, "-legacy-log-names", 0, 0, FALSE); + EchoVR::AddArgHelpString(pArgSyntax, "-legacy-log-names", "[NEVR] Keep EchoVR native log filename format ([r14(server)]-[date]_[time]_N.log)"); + // Backwards compat: accept deprecated flags without error (they're silently ignored) EchoVR::AddArgSyntax(pArgSyntax, "-timestep", 1, 1, FALSE); EchoVR::AddArgSyntax(pArgSyntax, "-fixedtimestep", 0, 0, FALSE); diff --git a/src/gamepatches/cli.h b/src/gamepatches/cli.h index 41f6e63..ce5a84f 100644 --- a/src/gamepatches/cli.h +++ b/src/gamepatches/cli.h @@ -18,6 +18,11 @@ extern CHAR g_customConfigPath[MAX_PATH]; /// Region override from -region or -serverregion CLI args. Empty = default. extern CHAR g_regionOverride[64]; +/// When TRUE, game log files in _local\r14logs\ keep the native EchoVR naming +/// convention ([r14(server)]-[MM-DD-YYYY]_...). When FALSE (default), NEVR +/// redirects them to nevr-server-YYYYMMDD-HHMMSS.log. +extern BOOL g_legacyLogNames; + /// A detour hook for the game's method it uses to build CLI argument definitions. /// Adds additional definitions to the structure, so that they may be parsed successfully without error. UINT64 BuildCmdLineSyntaxDefinitionsHook(PVOID pGame, PVOID pArgSyntax); diff --git a/src/gamepatches/mode_patches.cpp b/src/gamepatches/mode_patches.cpp index 4641cd7..40cc182 100644 --- a/src/gamepatches/mode_patches.cpp +++ b/src/gamepatches/mode_patches.cpp @@ -307,6 +307,49 @@ static VOID BugSplatCrashHandlerHook(INT64 exitCode) { OriginalBugSplatCrashHandler(exitCode); } +// ============================================================================ +// Game log file rename hook +// ============================================================================ +// +// The EchoVR logging system writes to _local\r14logs\[r14(server)]-[MM-DD-YYYY]_[HH-MM-SS]_N.log. +// This hook intercepts CreateFileA calls that target that directory and redirects +// them to nevr-server-YYYYMMDD-HHMMSS.log, which is sortable and grep-friendly. +// Active only in server mode and only when -legacy-log-names is not set. + +typedef HANDLE (WINAPI *CreateFileA_t)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE); +static CreateFileA_t orig_CreateFileA = nullptr; + +static bool IsGameLogPath(const char* path) { + if (!path) return false; + const char* p = strstr(path, "r14logs"); + if (!p) return false; + p += 7; + if (*p == '\\' || *p == '/') p++; + return p[0] == '['; // native EVR format starts with '[r14...' +} + +static HANDLE WINAPI CreateFileAHook( + LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) +{ + if (IsGameLogPath(lpFileName)) { + const char* r14 = strstr(lpFileName, "r14logs"); + std::string dir(lpFileName, static_cast(r14 - lpFileName) + 7); + + char ts[16]; + SYSTEMTIME st; + GetSystemTime(&st); + snprintf(ts, sizeof(ts), "%04d%02d%02d-%02d%02d%02d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + + std::string newPath = dir + "\\nevr-server-" + ts + ".log"; + Log(EchoVR::LogLevel::Info, + "[NEVR.PATCH] Renaming game log: %s -> %s", lpFileName, newPath.c_str()); + return orig_CreateFileA(newPath.c_str(), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } + return orig_CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); +} + // ============================================================================ // PatchEnableServer — force dedicated server mode // ============================================================================ @@ -354,6 +397,24 @@ VOID PatchEnableServer() { static_assert(sizeof(spectatorStreamCheck) == SPECTATORSTREAM_CHECK_SIZE, "SPECTATORSTREAM_CHECK patch size mismatch"); ApplyPatch(SPECTATORSTREAM_CHECK, spectatorStreamCheck, sizeof(spectatorStreamCheck)); + + // Redirect game log filenames from the native EchoVR bracketed format to + // nevr-server-YYYYMMDD-HHMMSS.log, unless -legacy-log-names was passed. + if (!g_legacyLogNames) { + PVOID pTarget = reinterpret_cast( + GetProcAddress(GetModuleHandleA("kernel32"), "CreateFileA")); + if (pTarget) { + orig_CreateFileA = reinterpret_cast(pTarget); + if (Hooking::Attach(&pTarget, reinterpret_cast(CreateFileAHook))) { + orig_CreateFileA = reinterpret_cast(pTarget); + Log(EchoVR::LogLevel::Info, + "[NEVR.PATCH] Game log rename active — files will use nevr-server-YYYYMMDD-HHMMSS.log"); + } else { + Log(EchoVR::LogLevel::Warning, "[NEVR.PATCH] Failed to hook CreateFileA for log rename"); + orig_CreateFileA = nullptr; + } + } + } } // ============================================================================ diff --git a/tests/system/patches_test.go b/tests/system/patches_test.go index 033fc1d..ad73704 100644 --- a/tests/system/patches_test.go +++ b/tests/system/patches_test.go @@ -58,7 +58,8 @@ func TestPatches_ServerMode(t *testing.T) { // - Server broadcast port is exposed // - Loading tips system is disabled (reduces log spam) // - "allow_incoming" is forced to true (accepts connections) - // - Log files use "r14(server)" subject instead of "r14netserver" + // - Game log files in _local\r14logs\ are named nevr-server-YYYYMMDD-HHMMSS.log + // (pass -legacy-log-names to revert to [r14(server)]-[MM-DD-YYYY]_[HH-MM-SS]_N.log) // // Verification approach: // 1. Start game with: echovr.exe -server -headless