aboutsummaryrefslogtreecommitdiff
path: root/primedev/logging
diff options
context:
space:
mode:
Diffstat (limited to 'primedev/logging')
-rw-r--r--primedev/logging/crashhandler.cpp590
-rw-r--r--primedev/logging/crashhandler.h97
-rw-r--r--primedev/logging/logging.cpp302
-rw-r--r--primedev/logging/logging.h136
-rw-r--r--primedev/logging/loghooks.cpp261
-rw-r--r--primedev/logging/loghooks.h1
-rw-r--r--primedev/logging/sourceconsole.cpp91
-rw-r--r--primedev/logging/sourceconsole.h85
8 files changed, 1563 insertions, 0 deletions
diff --git a/primedev/logging/crashhandler.cpp b/primedev/logging/crashhandler.cpp
new file mode 100644
index 00000000..a01de5a1
--- /dev/null
+++ b/primedev/logging/crashhandler.cpp
@@ -0,0 +1,590 @@
+#include "crashhandler.h"
+#include "config/profile.h"
+#include "dedicated/dedicated.h"
+#include "util/version.h"
+#include "mods/modmanager.h"
+#include "plugins/plugins.h"
+
+#include <minidumpapiset.h>
+
+#define CRASHHANDLER_MAX_FRAMES 32
+#define CRASHHANDLER_GETMODULEHANDLE_FAIL "GetModuleHandleExA failed!"
+
+//-----------------------------------------------------------------------------
+// Purpose: Vectored exception callback
+//-----------------------------------------------------------------------------
+LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* pExceptionInfo)
+{
+ g_pCrashHandler->Lock();
+
+ g_pCrashHandler->SetExceptionInfos(pExceptionInfo);
+
+ // Check if we should handle this
+ // NOTE [Fifty]: This gets called before even a try{} catch() {} can handle an exception
+ // we don't handle these unless "-crash_handle_all" is passed as a launch arg
+ if (!g_pCrashHandler->IsExceptionFatal() && !g_pCrashHandler->GetAllFatal())
+ {
+ g_pCrashHandler->Unlock();
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+
+ // Don't run if a debbuger is attached
+ if (IsDebuggerPresent())
+ {
+ g_pCrashHandler->Unlock();
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+
+ // Prevent recursive calls
+ if (g_pCrashHandler->GetState())
+ {
+ g_pCrashHandler->Unlock();
+ ExitProcess(1);
+ }
+
+ g_pCrashHandler->SetState(true);
+
+ // Needs to be called first as we use the members this sets later on
+ g_pCrashHandler->SetCrashedModule();
+
+ // Format
+ g_pCrashHandler->FormatException();
+ g_pCrashHandler->FormatCallstack();
+ g_pCrashHandler->FormatRegisters();
+ g_pCrashHandler->FormatLoadedMods();
+ g_pCrashHandler->FormatLoadedPlugins();
+ g_pCrashHandler->FormatModules();
+
+ // Flush
+ NS::log::FlushLoggers();
+
+ // Write minidump
+ g_pCrashHandler->WriteMinidump();
+
+ // Show message box
+ g_pCrashHandler->ShowPopUpMessage();
+
+ g_pCrashHandler->Unlock();
+
+ // We showed the "Northstar has crashed" message box
+ // make sure we terminate
+ if (!g_pCrashHandler->IsExceptionFatal())
+ ExitProcess(1);
+
+ return EXCEPTION_EXECUTE_HANDLER;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: console control signal handler
+//-----------------------------------------------------------------------------
+BOOL WINAPI ConsoleCtrlRoutine(DWORD dwCtrlType)
+{
+ // NOTE [Fifty]: When closing the process by closing the console we don't want
+ // to trigger the crash handler so we remove it
+ switch (dwCtrlType)
+ {
+ case CTRL_CLOSE_EVENT:
+ spdlog::info("Exiting due to console close...");
+ delete g_pCrashHandler;
+ g_pCrashHandler = nullptr;
+ std::exit(EXIT_SUCCESS);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Constructor
+//-----------------------------------------------------------------------------
+CCrashHandler::CCrashHandler()
+ : m_hExceptionFilter(nullptr)
+ , m_pExceptionInfos(nullptr)
+ , m_bHasSetConsolehandler(false)
+ , m_bAllExceptionsFatal(false)
+ , m_bHasShownCrashMsg(false)
+ , m_bState(false)
+{
+ Init();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Destructor
+//-----------------------------------------------------------------------------
+CCrashHandler::~CCrashHandler()
+{
+ Shutdown();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Initilazes crash handler
+//-----------------------------------------------------------------------------
+void CCrashHandler::Init()
+{
+ m_hExceptionFilter = AddVectoredExceptionHandler(TRUE, ExceptionFilter);
+ m_bHasSetConsolehandler = SetConsoleCtrlHandler(ConsoleCtrlRoutine, TRUE);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Shutdowns crash handler
+//-----------------------------------------------------------------------------
+void CCrashHandler::Shutdown()
+{
+ if (m_hExceptionFilter)
+ {
+ RemoveVectoredExceptionHandler(m_hExceptionFilter);
+ m_hExceptionFilter = nullptr;
+ }
+
+ if (m_bHasSetConsolehandler)
+ {
+ SetConsoleCtrlHandler(ConsoleCtrlRoutine, FALSE);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Sets the exception info
+//-----------------------------------------------------------------------------
+void CCrashHandler::SetExceptionInfos(EXCEPTION_POINTERS* pExceptionPointers)
+{
+ m_pExceptionInfos = pExceptionPointers;
+}
+//-----------------------------------------------------------------------------
+// Purpose: Sets the exception stirngs for message box
+//-----------------------------------------------------------------------------
+void CCrashHandler::SetCrashedModule()
+{
+ LPCSTR pCrashAddress = static_cast<LPCSTR>(m_pExceptionInfos->ExceptionRecord->ExceptionAddress);
+ HMODULE hCrashedModule;
+ if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, pCrashAddress, &hCrashedModule))
+ {
+ m_svCrashedModule = CRASHHANDLER_GETMODULEHANDLE_FAIL;
+ m_svCrashedOffset = "";
+
+ DWORD dwErrorID = GetLastError();
+ if (dwErrorID != 0)
+ {
+ LPSTR pszBuffer;
+ DWORD dwSize = FormatMessageA(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL,
+ dwErrorID,
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (LPSTR)&pszBuffer,
+ 0,
+ NULL);
+
+ if (dwSize > 0)
+ {
+ m_svError = pszBuffer;
+ LocalFree(pszBuffer);
+ }
+ }
+
+ return;
+ }
+
+ // Get module filename
+ CHAR szCrashedModulePath[MAX_PATH];
+ GetModuleFileNameExA(GetCurrentProcess(), hCrashedModule, szCrashedModulePath, sizeof(szCrashedModulePath));
+
+ const CHAR* pszCrashedModuleFileName = strrchr(szCrashedModulePath, '\\') + 1;
+
+ // Get relative address
+ LPCSTR pModuleBase = reinterpret_cast<LPCSTR>(pCrashAddress - reinterpret_cast<LPCSTR>(hCrashedModule));
+
+ m_svCrashedModule = pszCrashedModuleFileName;
+ m_svCrashedOffset = fmt::format("{:#x}", reinterpret_cast<DWORD64>(pModuleBase));
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Gets the exception null terminated stirng
+//-----------------------------------------------------------------------------
+
+const CHAR* CCrashHandler::GetExceptionString() const
+{
+ return GetExceptionString(m_pExceptionInfos->ExceptionRecord->ExceptionCode);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Gets the exception null terminated stirng
+//-----------------------------------------------------------------------------
+const CHAR* CCrashHandler::GetExceptionString(DWORD dwExceptionCode) const
+{
+ // clang-format off
+ switch (dwExceptionCode)
+ {
+ case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION";
+ case EXCEPTION_DATATYPE_MISALIGNMENT: return "EXCEPTION_DATATYPE_MISALIGNMENT";
+ case EXCEPTION_BREAKPOINT: return "EXCEPTION_BREAKPOINT";
+ case EXCEPTION_SINGLE_STEP: return "EXCEPTION_SINGLE_STEP";
+ case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
+ case EXCEPTION_FLT_DENORMAL_OPERAND: return "EXCEPTION_FLT_DENORMAL_OPERAND";
+ case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
+ case EXCEPTION_FLT_INEXACT_RESULT: return "EXCEPTION_FLT_INEXACT_RESULT";
+ case EXCEPTION_FLT_INVALID_OPERATION: return "EXCEPTION_FLT_INVALID_OPERATION";
+ case EXCEPTION_FLT_OVERFLOW: return "EXCEPTION_FLT_OVERFLOW";
+ case EXCEPTION_FLT_STACK_CHECK: return "EXCEPTION_FLT_STACK_CHECK";
+ case EXCEPTION_FLT_UNDERFLOW: return "EXCEPTION_FLT_UNDERFLOW";
+ case EXCEPTION_INT_DIVIDE_BY_ZERO: return "EXCEPTION_INT_DIVIDE_BY_ZERO";
+ case EXCEPTION_INT_OVERFLOW: return "EXCEPTION_INT_OVERFLOW";
+ case EXCEPTION_PRIV_INSTRUCTION: return "EXCEPTION_PRIV_INSTRUCTION";
+ case EXCEPTION_IN_PAGE_ERROR: return "EXCEPTION_IN_PAGE_ERROR";
+ case EXCEPTION_ILLEGAL_INSTRUCTION: return "EXCEPTION_ILLEGAL_INSTRUCTION";
+ case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
+ case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW";
+ case EXCEPTION_INVALID_DISPOSITION: return "EXCEPTION_INVALID_DISPOSITION";
+ case EXCEPTION_GUARD_PAGE: return "EXCEPTION_GUARD_PAGE";
+ case EXCEPTION_INVALID_HANDLE: return "EXCEPTION_INVALID_HANDLE";
+ case 3765269347: return "RUNTIME_EXCEPTION";
+ }
+ // clang-format on
+ return "UNKNOWN_EXCEPTION";
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns true if exception is known
+//-----------------------------------------------------------------------------
+bool CCrashHandler::IsExceptionFatal() const
+{
+ return IsExceptionFatal(m_pExceptionInfos->ExceptionRecord->ExceptionCode);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns true if exception is known
+//-----------------------------------------------------------------------------
+bool CCrashHandler::IsExceptionFatal(DWORD dwExceptionCode) const
+{
+ // clang-format off
+ switch (dwExceptionCode)
+ {
+ case EXCEPTION_ACCESS_VIOLATION:
+ case EXCEPTION_DATATYPE_MISALIGNMENT:
+ case EXCEPTION_BREAKPOINT:
+ case EXCEPTION_SINGLE_STEP:
+ case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
+ case EXCEPTION_FLT_DENORMAL_OPERAND:
+ case EXCEPTION_FLT_DIVIDE_BY_ZERO:
+ case EXCEPTION_FLT_INEXACT_RESULT:
+ case EXCEPTION_FLT_INVALID_OPERATION:
+ case EXCEPTION_FLT_OVERFLOW:
+ case EXCEPTION_FLT_STACK_CHECK:
+ case EXCEPTION_FLT_UNDERFLOW:
+ case EXCEPTION_INT_DIVIDE_BY_ZERO:
+ case EXCEPTION_INT_OVERFLOW:
+ case EXCEPTION_PRIV_INSTRUCTION:
+ case EXCEPTION_IN_PAGE_ERROR:
+ case EXCEPTION_ILLEGAL_INSTRUCTION:
+ case EXCEPTION_NONCONTINUABLE_EXCEPTION:
+ case EXCEPTION_STACK_OVERFLOW:
+ case EXCEPTION_INVALID_DISPOSITION:
+ case EXCEPTION_GUARD_PAGE:
+ case EXCEPTION_INVALID_HANDLE:
+ return true;
+ }
+ // clang-format on
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Shows a message box
+//-----------------------------------------------------------------------------
+void CCrashHandler::ShowPopUpMessage()
+{
+ if (m_bHasShownCrashMsg)
+ return;
+
+ m_bHasShownCrashMsg = true;
+
+ if (!IsDedicatedServer())
+ {
+ std::string svMessage = fmt::format(
+ "Northstar has crashed! Crash info can be found at {}/logs!\n\n{}\n{} + {}",
+ GetNorthstarPrefix(),
+ GetExceptionString(),
+ m_svCrashedModule,
+ m_svCrashedOffset);
+
+ MessageBoxA(GetForegroundWindow(), svMessage.c_str(), "Northstar has crashed!", MB_ICONERROR | MB_OK);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatException()
+{
+ spdlog::error("-------------------------------------------");
+ spdlog::error("Northstar has crashed!");
+ spdlog::error("\tVersion: {}", version);
+ if (!m_svError.empty())
+ {
+ spdlog::info("\tEncountered an error when gathering crash information!");
+ spdlog::info("\tWinApi Error: {}", m_svError.c_str());
+ }
+ spdlog::error("\t{}", GetExceptionString());
+
+ DWORD dwExceptionCode = m_pExceptionInfos->ExceptionRecord->ExceptionCode;
+ if (dwExceptionCode == EXCEPTION_ACCESS_VIOLATION || dwExceptionCode == EXCEPTION_IN_PAGE_ERROR)
+ {
+ ULONG_PTR uExceptionInfo0 = m_pExceptionInfos->ExceptionRecord->ExceptionInformation[0];
+ ULONG_PTR uExceptionInfo1 = m_pExceptionInfos->ExceptionRecord->ExceptionInformation[1];
+
+ if (!uExceptionInfo0)
+ spdlog::error("\tAttempted to read from: {:#x}", uExceptionInfo1);
+ else if (uExceptionInfo0 == 1)
+ spdlog::error("\tAttempted to write to: {:#x}", uExceptionInfo1);
+ else if (uExceptionInfo0 == 8)
+ spdlog::error("\tData Execution Prevention (DEP) at: {:#x}", uExceptionInfo1);
+ else
+ spdlog::error("\tUnknown access violation at: {:#x}", uExceptionInfo1);
+ }
+
+ spdlog::error("\tAt: {} + {}", m_svCrashedModule, m_svCrashedOffset);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatCallstack()
+{
+ spdlog::error("Callstack:");
+
+ PVOID pFrames[CRASHHANDLER_MAX_FRAMES];
+
+ int iFrames = RtlCaptureStackBackTrace(0, CRASHHANDLER_MAX_FRAMES, pFrames, NULL);
+
+ // Above call gives us frames after the crash occured, we only want to print the ones starting from where
+ // the exception was called
+ bool bSkipExceptionHandlingFrames = true;
+
+ // We ran into an error when getting the offset, just print all frames
+ if (m_svCrashedOffset.empty())
+ bSkipExceptionHandlingFrames = false;
+
+ for (int i = 0; i < iFrames; i++)
+ {
+ const CHAR* pszModuleFileName;
+
+ LPCSTR pAddress = static_cast<LPCSTR>(pFrames[i]);
+ HMODULE hModule;
+ if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, pAddress, &hModule))
+ {
+ pszModuleFileName = CRASHHANDLER_GETMODULEHANDLE_FAIL;
+ // If we fail here it's too late to do any damage control
+ }
+ else
+ {
+ CHAR szModulePath[MAX_PATH];
+ GetModuleFileNameExA(GetCurrentProcess(), hModule, szModulePath, sizeof(szModulePath));
+ pszModuleFileName = strrchr(szModulePath, '\\') + 1;
+ }
+
+ // Get relative address
+ LPCSTR pCrashOffset = reinterpret_cast<LPCSTR>(pAddress - reinterpret_cast<LPCSTR>(hModule));
+ std::string svCrashOffset = fmt::format("{:#x}", reinterpret_cast<DWORD64>(pCrashOffset));
+
+ // Should we log this frame
+ if (bSkipExceptionHandlingFrames)
+ {
+ if (m_svCrashedModule == pszModuleFileName && m_svCrashedOffset == svCrashOffset)
+ {
+ bSkipExceptionHandlingFrames = false;
+ }
+ else
+ {
+ continue;
+ }
+ }
+
+ // Log module + offset
+ spdlog::error("\t{} + {:#x}", pszModuleFileName, reinterpret_cast<DWORD64>(pCrashOffset));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatFlags(const CHAR* pszRegister, DWORD nValue)
+{
+ spdlog::error("\t{}: {:#b}", pszRegister, nValue);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatIntReg(const CHAR* pszRegister, DWORD64 nValue)
+{
+ spdlog::error("\t{}: {:#x}", pszRegister, nValue);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatFloatReg(const CHAR* pszRegister, M128A nValue)
+{
+ DWORD nVec[4] = {
+ static_cast<DWORD>(nValue.Low & UINT_MAX),
+ static_cast<DWORD>(nValue.Low >> 32),
+ static_cast<DWORD>(nValue.High & UINT_MAX),
+ static_cast<DWORD>(nValue.High >> 32)};
+
+ spdlog::error(
+ "\t{}: [ {:G}, {:G}, {:G}, {:G} ]; [ {:#x}, {:#x}, {:#x}, {:#x} ]",
+ pszRegister,
+ static_cast<float>(nVec[0]),
+ static_cast<float>(nVec[1]),
+ static_cast<float>(nVec[2]),
+ static_cast<float>(nVec[3]),
+ nVec[0],
+ nVec[1],
+ nVec[2],
+ nVec[3]);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatRegisters()
+{
+ spdlog::error("Registers:");
+
+ PCONTEXT pContext = m_pExceptionInfos->ContextRecord;
+
+ FormatFlags("Flags:", pContext->ContextFlags);
+
+ FormatIntReg("Rax", pContext->Rax);
+ FormatIntReg("Rcx", pContext->Rcx);
+ FormatIntReg("Rdx", pContext->Rdx);
+ FormatIntReg("Rbx", pContext->Rbx);
+ FormatIntReg("Rsp", pContext->Rsp);
+ FormatIntReg("Rbp", pContext->Rbp);
+ FormatIntReg("Rsi", pContext->Rsi);
+ FormatIntReg("Rdi", pContext->Rdi);
+ FormatIntReg("R8 ", pContext->R8);
+ FormatIntReg("R9 ", pContext->R9);
+ FormatIntReg("R10", pContext->R10);
+ FormatIntReg("R11", pContext->R11);
+ FormatIntReg("R12", pContext->R12);
+ FormatIntReg("R13", pContext->R13);
+ FormatIntReg("R14", pContext->R14);
+ FormatIntReg("R15", pContext->R15);
+ FormatIntReg("Rip", pContext->Rip);
+
+ FormatFloatReg("Xmm0 ", pContext->Xmm0);
+ FormatFloatReg("Xmm1 ", pContext->Xmm1);
+ FormatFloatReg("Xmm2 ", pContext->Xmm2);
+ FormatFloatReg("Xmm3 ", pContext->Xmm3);
+ FormatFloatReg("Xmm4 ", pContext->Xmm4);
+ FormatFloatReg("Xmm5 ", pContext->Xmm5);
+ FormatFloatReg("Xmm6 ", pContext->Xmm6);
+ FormatFloatReg("Xmm7 ", pContext->Xmm7);
+ FormatFloatReg("Xmm8 ", pContext->Xmm8);
+ FormatFloatReg("Xmm9 ", pContext->Xmm9);
+ FormatFloatReg("Xmm10", pContext->Xmm10);
+ FormatFloatReg("Xmm11", pContext->Xmm11);
+ FormatFloatReg("Xmm12", pContext->Xmm12);
+ FormatFloatReg("Xmm13", pContext->Xmm13);
+ FormatFloatReg("Xmm14", pContext->Xmm14);
+ FormatFloatReg("Xmm15", pContext->Xmm15);
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatLoadedMods()
+{
+ if (g_pModManager)
+ {
+ spdlog::error("Enabled mods:");
+ for (const Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (!mod.m_bEnabled)
+ continue;
+
+ spdlog::error("\t{}", mod.Name);
+ }
+
+ spdlog::error("Disabled mods:");
+ for (const Mod& mod : g_pModManager->m_LoadedMods)
+ {
+ if (mod.m_bEnabled)
+ continue;
+
+ spdlog::error("\t{}", mod.Name);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatLoadedPlugins()
+{
+ if (g_pPluginManager)
+ {
+ spdlog::error("Loaded Plugins:");
+ for (const Plugin& plugin : g_pPluginManager->m_vLoadedPlugins)
+ {
+ spdlog::error("\t{}", plugin.name);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CCrashHandler::FormatModules()
+{
+ spdlog::error("Loaded modules:");
+ HMODULE hModules[1024];
+ DWORD cbNeeded;
+
+ if (EnumProcessModules(GetCurrentProcess(), hModules, sizeof(hModules), &cbNeeded))
+ {
+ for (DWORD i = 0; i < (cbNeeded / sizeof(HMODULE)); i++)
+ {
+ CHAR szModulePath[MAX_PATH];
+ if (GetModuleFileNameExA(GetCurrentProcess(), hModules[i], szModulePath, sizeof(szModulePath)))
+ {
+ const CHAR* pszModuleFileName = strrchr(szModulePath, '\\') + 1;
+ spdlog::error("\t{}", pszModuleFileName);
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Writes minidump to disk
+//-----------------------------------------------------------------------------
+void CCrashHandler::WriteMinidump()
+{
+ time_t time = std::time(nullptr);
+ tm currentTime = *std::localtime(&time);
+ std::stringstream stream;
+ stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nsdump%Y-%m-%d %H-%M-%S.dmp").c_str());
+
+ HANDLE hMinidumpFile = CreateFileA(stream.str().c_str(), GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
+ if (hMinidumpFile)
+ {
+ MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo;
+ dumpExceptionInfo.ThreadId = GetCurrentThreadId();
+ dumpExceptionInfo.ExceptionPointers = m_pExceptionInfos;
+ dumpExceptionInfo.ClientPointers = false;
+
+ MiniDumpWriteDump(
+ GetCurrentProcess(),
+ GetCurrentProcessId(),
+ hMinidumpFile,
+ MINIDUMP_TYPE(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory),
+ &dumpExceptionInfo,
+ nullptr,
+ nullptr);
+ CloseHandle(hMinidumpFile);
+ }
+ else
+ spdlog::error("Failed to write minidump file {}!", stream.str());
+}
+
+//-----------------------------------------------------------------------------
+CCrashHandler* g_pCrashHandler = nullptr;
diff --git a/primedev/logging/crashhandler.h b/primedev/logging/crashhandler.h
new file mode 100644
index 00000000..c059a8ca
--- /dev/null
+++ b/primedev/logging/crashhandler.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include <mutex>
+
+//-----------------------------------------------------------------------------
+// Purpose: Exception handling
+//-----------------------------------------------------------------------------
+class CCrashHandler
+{
+public:
+ CCrashHandler();
+ ~CCrashHandler();
+
+ void Init();
+ void Shutdown();
+
+ void Lock()
+ {
+ m_Mutex.lock();
+ }
+
+ void Unlock()
+ {
+ m_Mutex.unlock();
+ }
+
+ void SetState(bool bState)
+ {
+ m_bState = bState;
+ }
+
+ bool GetState() const
+ {
+ return m_bState;
+ }
+
+ void SetAllFatal(bool bState)
+ {
+ m_bAllExceptionsFatal = bState;
+ }
+
+ bool GetAllFatal() const
+ {
+ return m_bAllExceptionsFatal;
+ }
+
+ //-----------------------------------------------------------------------------
+ // Exception helpers
+ //-----------------------------------------------------------------------------
+ void SetExceptionInfos(EXCEPTION_POINTERS* pExceptionPointers);
+
+ void SetCrashedModule();
+
+ const CHAR* GetExceptionString() const;
+ const CHAR* GetExceptionString(DWORD dwExceptionCode) const;
+
+ bool IsExceptionFatal() const;
+ bool IsExceptionFatal(DWORD dwExceptionCode) const;
+
+ //-----------------------------------------------------------------------------
+ // Formatting
+ //-----------------------------------------------------------------------------
+ void ShowPopUpMessage();
+
+ void FormatException();
+ void FormatCallstack();
+ void FormatFlags(const CHAR* pszRegister, DWORD nValue);
+ void FormatIntReg(const CHAR* pszRegister, DWORD64 nValue);
+ void FormatFloatReg(const CHAR* pszRegister, M128A nValue);
+ void FormatRegisters();
+ void FormatLoadedMods();
+ void FormatLoadedPlugins();
+ void FormatModules();
+
+ //-----------------------------------------------------------------------------
+ // Minidump
+ //-----------------------------------------------------------------------------
+ void WriteMinidump();
+
+private:
+ PVOID m_hExceptionFilter;
+ EXCEPTION_POINTERS* m_pExceptionInfos;
+
+ bool m_bHasSetConsolehandler;
+ bool m_bAllExceptionsFatal;
+ bool m_bHasShownCrashMsg;
+ bool m_bState;
+
+ std::string m_svCrashedModule;
+ std::string m_svCrashedOffset;
+
+ std::string m_svError;
+
+ std::mutex m_Mutex;
+};
+
+extern CCrashHandler* g_pCrashHandler;
diff --git a/primedev/logging/logging.cpp b/primedev/logging/logging.cpp
new file mode 100644
index 00000000..3416bb8c
--- /dev/null
+++ b/primedev/logging/logging.cpp
@@ -0,0 +1,302 @@
+#include "logging.h"
+#include "core/convar/convar.h"
+#include "core/convar/concommand.h"
+#include "config/profile.h"
+#include "core/tier0.h"
+#include "util/version.h"
+#include "spdlog/sinks/basic_file_sink.h"
+
+#include <winternl.h>
+#include <cstdlib>
+#include <iomanip>
+#include <sstream>
+
+AUTOHOOK_INIT()
+
+std::vector<std::shared_ptr<ColoredLogger>> loggers {};
+
+namespace NS::log
+{
+ std::shared_ptr<ColoredLogger> SCRIPT_UI;
+ std::shared_ptr<ColoredLogger> SCRIPT_CL;
+ std::shared_ptr<ColoredLogger> SCRIPT_SV;
+
+ std::shared_ptr<ColoredLogger> NATIVE_UI;
+ std::shared_ptr<ColoredLogger> NATIVE_CL;
+ std::shared_ptr<ColoredLogger> NATIVE_SV;
+ std::shared_ptr<ColoredLogger> NATIVE_EN;
+
+ std::shared_ptr<ColoredLogger> fs;
+ std::shared_ptr<ColoredLogger> rpak;
+ std::shared_ptr<ColoredLogger> echo;
+
+ std::shared_ptr<ColoredLogger> NORTHSTAR;
+ std::shared_ptr<ColoredLogger> PLUGINSYS;
+}; // namespace NS::log
+
+// This needs to be called after hooks are loaded so we can access the command line args
+void CreateLogFiles()
+{
+ if (strstr(GetCommandLineA(), "-disablelogs"))
+ {
+ spdlog::default_logger()->set_level(spdlog::level::off);
+ }
+ else
+ {
+ try
+ {
+ // todo: might be good to delete logs that are too old
+ time_t time = std::time(nullptr);
+ tm currentTime = *std::localtime(&time);
+ std::stringstream stream;
+
+ stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nslog%Y-%m-%d %H-%M-%S.txt").c_str());
+ auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(stream.str(), false);
+ sink->set_pattern("[%Y-%m-%d] [%H:%M:%S] [%n] [%l] %v");
+ for (auto& logger : loggers)
+ {
+ logger->sinks().push_back(sink);
+ }
+ spdlog::flush_on(spdlog::level::info);
+ }
+ catch (...)
+ {
+ spdlog::error("Failed creating log file!");
+ MessageBoxA(
+ 0, "Failed creating log file! Make sure the profile directory is writable.", "Northstar Warning", MB_ICONWARNING | MB_OK);
+ }
+ }
+}
+
+void ExternalConsoleSink::sink_it_(const spdlog::details::log_msg& msg)
+{
+ throw std::runtime_error("sink_it_ called on SourceConsoleSink with pure log_msg. This is an error!");
+}
+
+void ExternalConsoleSink::custom_sink_it_(const custom_log_msg& msg)
+{
+ spdlog::memory_buf_t formatted;
+ spdlog::sinks::base_sink<std::mutex>::formatter_->format(msg, formatted);
+
+ std::string out = "";
+ // if ansi colour is turned off, just use WriteConsoleA and return
+ if (!g_bSpdLog_UseAnsiColor)
+ {
+ out += fmt::to_string(formatted);
+ }
+
+ // print to the console with colours
+ else
+ {
+ // get message string
+ std::string str = fmt::to_string(formatted);
+
+ std::string levelColor = m_LogColours[msg.level];
+ std::string name {msg.logger_name.begin(), msg.logger_name.end()};
+
+ std::string name_str = "[NAME]";
+ int name_pos = str.find(name_str);
+ str.replace(name_pos, name_str.length(), msg.origin->ANSIColor + "[" + name + "]" + default_color);
+
+ std::string level_str = "[LVL]";
+ int level_pos = str.find(level_str);
+ str.replace(level_pos, level_str.length(), levelColor + "[" + std::string(level_names[msg.level]) + "]" + default_color);
+
+ out += str;
+ }
+ // print the string to the console - this is definitely bad i think
+ HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
+ auto ignored = WriteConsoleA(handle, out.c_str(), std::strlen(out.c_str()), nullptr, nullptr);
+ (void)ignored;
+}
+
+void ExternalConsoleSink::flush_()
+{
+ std::cout << std::flush;
+}
+
+void CustomSink::custom_log(const custom_log_msg& msg)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ custom_sink_it_(msg);
+}
+
+void InitialiseConsole()
+{
+ if (AllocConsole() == FALSE)
+ {
+ std::cout << "[*] Failed to create a console window, maybe a console already exists?" << std::endl;
+ }
+ else
+ {
+ freopen("CONOUT$", "w", stdout);
+ freopen("CONOUT$", "w", stderr);
+ }
+
+ // this if statement is adapted from r5sdk
+ if (!strstr(GetCommandLineA(), "-noansiclr"))
+ {
+ g_bSpdLog_UseAnsiColor = true;
+ DWORD dwMode = 0;
+ HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+
+ GetConsoleMode(hOutput, &dwMode);
+ dwMode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+
+ if (!SetConsoleMode(hOutput, dwMode)) // Some editions of Windows have 'VirtualTerminalLevel' disabled by default.
+ {
+ // If 'VirtualTerminalLevel' can't be set, just disable ANSI color, since it wouldnt work anyway.
+ spdlog::warn("could not set VirtualTerminalLevel. Disabling color output");
+ g_bSpdLog_UseAnsiColor = false;
+ }
+ }
+}
+
+void RegisterLogger(std::shared_ptr<ColoredLogger> logger)
+{
+ loggers.push_back(logger);
+}
+
+void RegisterCustomSink(std::shared_ptr<CustomSink> sink)
+{
+ for (auto& logger : loggers)
+ {
+ logger->custom_sinks_.push_back(sink);
+ }
+};
+
+void InitialiseLogging()
+{
+ // create a logger, and set it to default
+ NS::log::NORTHSTAR = std::make_shared<ColoredLogger>("NORTHSTAR", NS::Colors::NORTHSTAR, true);
+ NS::log::NORTHSTAR->sinks().clear();
+ loggers.push_back(NS::log::NORTHSTAR);
+ spdlog::set_default_logger(NS::log::NORTHSTAR);
+
+ // create our console sink
+ auto sink = std::make_shared<ExternalConsoleSink>();
+ // set the pattern
+ if (g_bSpdLog_UseAnsiColor)
+ // dont put the log level in the pattern if we are using colours, as the colour will show the log level
+ sink->set_pattern("[%H:%M:%S] [NAME] [LVL] %v");
+ else
+ sink->set_pattern("[%H:%M:%S] [%n] [%l] %v");
+
+ // add our sink to the logger
+ NS::log::NORTHSTAR->custom_sinks_.push_back(sink);
+
+ NS::log::SCRIPT_UI = std::make_shared<ColoredLogger>("SCRIPT UI", NS::Colors::SCRIPT_UI);
+ NS::log::SCRIPT_CL = std::make_shared<ColoredLogger>("SCRIPT CL", NS::Colors::SCRIPT_CL);
+ NS::log::SCRIPT_SV = std::make_shared<ColoredLogger>("SCRIPT SV", NS::Colors::SCRIPT_SV);
+
+ NS::log::NATIVE_UI = std::make_shared<ColoredLogger>("NATIVE UI", NS::Colors::NATIVE_UI);
+ NS::log::NATIVE_CL = std::make_shared<ColoredLogger>("NATIVE CL", NS::Colors::NATIVE_CL);
+ NS::log::NATIVE_SV = std::make_shared<ColoredLogger>("NATIVE SV", NS::Colors::NATIVE_SV);
+ NS::log::NATIVE_EN = std::make_shared<ColoredLogger>("NATIVE EN", NS::Colors::NATIVE_ENGINE);
+
+ NS::log::fs = std::make_shared<ColoredLogger>("FILESYSTM", NS::Colors::FILESYSTEM);
+ NS::log::rpak = std::make_shared<ColoredLogger>("RPAK_FSYS", NS::Colors::RPAK);
+ NS::log::echo = std::make_shared<ColoredLogger>("ECHO", NS::Colors::ECHO);
+
+ NS::log::PLUGINSYS = std::make_shared<ColoredLogger>("PLUGINSYS", NS::Colors::PLUGINSYS);
+
+ loggers.push_back(NS::log::SCRIPT_UI);
+ loggers.push_back(NS::log::SCRIPT_CL);
+ loggers.push_back(NS::log::SCRIPT_SV);
+
+ loggers.push_back(NS::log::NATIVE_UI);
+ loggers.push_back(NS::log::NATIVE_CL);
+ loggers.push_back(NS::log::NATIVE_SV);
+ loggers.push_back(NS::log::NATIVE_EN);
+
+ loggers.push_back(NS::log::PLUGINSYS);
+
+ loggers.push_back(NS::log::fs);
+ loggers.push_back(NS::log::rpak);
+ loggers.push_back(NS::log::echo);
+}
+
+void NS::log::FlushLoggers()
+{
+ for (auto& logger : loggers)
+ logger->flush();
+
+ spdlog::default_logger()->flush();
+}
+
+// Wine specific functions
+typedef const char*(CDECL* wine_get_host_version_type)(const char**, const char**);
+wine_get_host_version_type wine_get_host_version;
+
+typedef const char*(CDECL* wine_get_build_id_type)(void);
+wine_get_build_id_type wine_get_build_id;
+
+// Not exported Winapi methods
+typedef NTSTATUS(WINAPI* RtlGetVersion_type)(PRTL_OSVERSIONINFOW);
+RtlGetVersion_type RtlGetVersion;
+
+void StartupLog()
+{
+ spdlog::info("NorthstarLauncher version: {}", version);
+ spdlog::info("Command line: {}", GetCommandLineA());
+ spdlog::info("Using profile: {}", GetNorthstarPrefix());
+
+ HMODULE ntdll = GetModuleHandleA("ntdll.dll");
+ if (!ntdll)
+ {
+ // How did we get here
+ spdlog::info("Operating System: Unknown");
+ return;
+ }
+
+ wine_get_host_version = (wine_get_host_version_type)GetProcAddress(ntdll, "wine_get_host_version");
+ if (wine_get_host_version)
+ {
+ // Load the rest of the functions we need
+ wine_get_build_id = (wine_get_build_id_type)GetProcAddress(ntdll, "wine_get_build_id");
+
+ const char* sysname;
+ wine_get_host_version(&sysname, NULL);
+
+ spdlog::info("Operating System: {} (Wine)", sysname);
+ spdlog::info("Wine build: {}", wine_get_build_id());
+
+ // STEAM_COMPAT_TOOL_PATHS is a colon separated lists of all compat tool paths used
+ // The first one tends to be the Proton path itself
+ // We extract the basename out of it to get the name used
+ char* compatToolPtr = std::getenv("STEAM_COMPAT_TOOL_PATHS");
+ if (compatToolPtr)
+ {
+ std::string_view compatToolPath(compatToolPtr);
+
+ auto protonBasenameEnd = compatToolPath.find(":");
+ if (protonBasenameEnd == std::string_view::npos)
+ protonBasenameEnd = 0;
+ auto protonBasenameStart = compatToolPath.rfind("/", protonBasenameEnd) + 1;
+ if (protonBasenameStart == std::string_view::npos)
+ protonBasenameStart = 0;
+
+ spdlog::info("Proton build: {}", compatToolPath.substr(protonBasenameStart, protonBasenameEnd - protonBasenameStart));
+ }
+ }
+ else
+ {
+ // We are real Windows (hopefully)
+ const char* win_ver = "Unknown";
+
+ RTL_OSVERSIONINFOW osvi;
+ osvi.dwOSVersionInfoSize = sizeof(osvi);
+
+ RtlGetVersion = (RtlGetVersion_type)GetProcAddress(ntdll, "RtlGetVersion");
+ if (RtlGetVersion && !RtlGetVersion(&osvi))
+ {
+ // Version reference table
+ // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa#remarks
+ spdlog::info("Operating System: Windows (NT{}.{})", osvi.dwMajorVersion, osvi.dwMinorVersion);
+ }
+ else
+ {
+ spdlog::info("Operating System: Windows");
+ }
+ }
+}
diff --git a/primedev/logging/logging.h b/primedev/logging/logging.h
new file mode 100644
index 00000000..5056af27
--- /dev/null
+++ b/primedev/logging/logging.h
@@ -0,0 +1,136 @@
+#pragma once
+#include "spdlog/sinks/base_sink.h"
+#include "spdlog/logger.h"
+#include "squirrel/squirrel.h"
+#include "core/math/color.h"
+
+void CreateLogFiles();
+void InitialiseLogging();
+void InitialiseConsole();
+void StartupLog();
+
+class ColoredLogger;
+
+struct custom_log_msg : spdlog::details::log_msg
+{
+public:
+ custom_log_msg(ColoredLogger* origin, spdlog::details::log_msg msg) : origin(origin), spdlog::details::log_msg(msg) {}
+
+ ColoredLogger* origin;
+};
+
+class CustomSink : public spdlog::sinks::base_sink<std::mutex>
+{
+public:
+ void custom_log(const custom_log_msg& msg);
+ virtual void custom_sink_it_(const custom_log_msg& msg)
+ {
+ throw std::runtime_error("Pure virtual call to CustomSink::custom_sink_it_");
+ }
+};
+
+class ColoredLogger : public spdlog::logger
+{
+public:
+ std::string ANSIColor;
+ SourceColor SRCColor;
+
+ std::vector<std::shared_ptr<CustomSink>> custom_sinks_;
+
+ ColoredLogger(std::string name, Color color, bool first = false) : spdlog::logger(*spdlog::default_logger())
+ {
+ name_ = std::move(name);
+ if (!first)
+ {
+ custom_sinks_ = dynamic_pointer_cast<ColoredLogger>(spdlog::default_logger())->custom_sinks_;
+ }
+
+ ANSIColor = color.ToANSIColor();
+ SRCColor = color.ToSourceColor();
+ }
+
+ void sink_it_(const spdlog::details::log_msg& msg)
+ {
+ custom_log_msg custom_msg {this, msg};
+
+ // Ugh
+ for (auto& sink : sinks_)
+ {
+ SPDLOG_TRY
+ {
+ sink->log(custom_msg);
+ }
+ SPDLOG_LOGGER_CATCH()
+ }
+
+ for (auto& sink : custom_sinks_)
+ {
+ SPDLOG_TRY
+ {
+ sink->custom_log(custom_msg);
+ }
+ SPDLOG_LOGGER_CATCH()
+ }
+
+ if (should_flush_(custom_msg))
+ {
+ flush_();
+ }
+ }
+};
+
+namespace NS::log
+{
+ // Squirrel
+ extern std::shared_ptr<ColoredLogger> SCRIPT_UI;
+ extern std::shared_ptr<ColoredLogger> SCRIPT_CL;
+ extern std::shared_ptr<ColoredLogger> SCRIPT_SV;
+
+ // Native code
+ extern std::shared_ptr<ColoredLogger> NATIVE_UI;
+ extern std::shared_ptr<ColoredLogger> NATIVE_CL;
+ extern std::shared_ptr<ColoredLogger> NATIVE_SV;
+ extern std::shared_ptr<ColoredLogger> NATIVE_EN;
+
+ // File system
+ extern std::shared_ptr<ColoredLogger> fs;
+ // RPak
+ extern std::shared_ptr<ColoredLogger> rpak;
+ // Echo
+ extern std::shared_ptr<ColoredLogger> echo;
+
+ extern std::shared_ptr<ColoredLogger> NORTHSTAR;
+
+ extern std::shared_ptr<ColoredLogger> PLUGINSYS;
+
+ void FlushLoggers();
+}; // namespace NS::log
+
+void RegisterCustomSink(std::shared_ptr<CustomSink> sink);
+void RegisterLogger(std::shared_ptr<ColoredLogger> logger);
+
+inline bool g_bSpdLog_UseAnsiColor = true;
+
+// Could maybe use some different names here, idk
+static const char* level_names[] {"trac", "dbug", "info", "warn", "errr", "crit", "off"};
+
+// spdlog logger, for cool colour things
+class ExternalConsoleSink : public CustomSink
+{
+private:
+ std::map<spdlog::level::level_enum, std::string> m_LogColours = {
+ {spdlog::level::trace, NS::Colors::TRACE.ToANSIColor()},
+ {spdlog::level::debug, NS::Colors::DEBUG.ToANSIColor()},
+ {spdlog::level::info, NS::Colors::INFO.ToANSIColor()},
+ {spdlog::level::warn, NS::Colors::WARN.ToANSIColor()},
+ {spdlog::level::err, NS::Colors::ERR.ToANSIColor()},
+ {spdlog::level::critical, NS::Colors::CRIT.ToANSIColor()},
+ {spdlog::level::off, NS::Colors::OFF.ToANSIColor()}};
+
+ std::string default_color = "\033[39;49m";
+
+protected:
+ void sink_it_(const spdlog::details::log_msg& msg) override;
+ void custom_sink_it_(const custom_log_msg& msg);
+ void flush_() override;
+};
diff --git a/primedev/logging/loghooks.cpp b/primedev/logging/loghooks.cpp
new file mode 100644
index 00000000..7efb5b99
--- /dev/null
+++ b/primedev/logging/loghooks.cpp
@@ -0,0 +1,261 @@
+#include "logging.h"
+#include "loghooks.h"
+#include "core/convar/convar.h"
+#include "core/convar/concommand.h"
+#include "core/math/bitbuf.h"
+#include "config/profile.h"
+#include "core/tier0.h"
+#include "squirrel/squirrel.h"
+#include <iomanip>
+#include <sstream>
+
+AUTOHOOK_INIT()
+
+ConVar* Cvar_spewlog_enable;
+ConVar* Cvar_cl_showtextmsg;
+
+enum class TextMsgPrintType_t
+{
+ HUD_PRINTNOTIFY = 1,
+ HUD_PRINTCONSOLE,
+ HUD_PRINTTALK,
+ HUD_PRINTCENTER
+};
+
+class ICenterPrint
+{
+public:
+ virtual void ctor() = 0;
+ virtual void Clear(void) = 0;
+ virtual void ColorPrint(int r, int g, int b, int a, wchar_t* text) = 0;
+ virtual void ColorPrint(int r, int g, int b, int a, char* text) = 0;
+ virtual void Print(wchar_t* text) = 0;
+ virtual void Print(char* text) = 0;
+ virtual void SetTextColor(int r, int g, int b, int a) = 0;
+};
+
+enum class SpewType_t
+{
+ SPEW_MESSAGE = 0,
+
+ SPEW_WARNING,
+ SPEW_ASSERT,
+ SPEW_ERROR,
+ SPEW_LOG,
+
+ SPEW_TYPE_COUNT
+};
+
+const std::unordered_map<SpewType_t, const char*> PrintSpewTypes = {
+ {SpewType_t::SPEW_MESSAGE, "SPEW_MESSAGE"},
+ {SpewType_t::SPEW_WARNING, "SPEW_WARNING"},
+ {SpewType_t::SPEW_ASSERT, "SPEW_ASSERT"},
+ {SpewType_t::SPEW_ERROR, "SPEW_ERROR"},
+ {SpewType_t::SPEW_LOG, "SPEW_LOG"}};
+
+// these are used to define the base text colour for these things
+const std::unordered_map<SpewType_t, spdlog::level::level_enum> PrintSpewLevels = {
+ {SpewType_t::SPEW_MESSAGE, spdlog::level::level_enum::info},
+ {SpewType_t::SPEW_WARNING, spdlog::level::level_enum::warn},
+ {SpewType_t::SPEW_ASSERT, spdlog::level::level_enum::err},
+ {SpewType_t::SPEW_ERROR, spdlog::level::level_enum::err},
+ {SpewType_t::SPEW_LOG, spdlog::level::level_enum::info}};
+
+const std::unordered_map<SpewType_t, const char> PrintSpewTypes_Short = {
+ {SpewType_t::SPEW_MESSAGE, 'M'},
+ {SpewType_t::SPEW_WARNING, 'W'},
+ {SpewType_t::SPEW_ASSERT, 'A'},
+ {SpewType_t::SPEW_ERROR, 'E'},
+ {SpewType_t::SPEW_LOG, 'L'}};
+
+ICenterPrint* pInternalCenterPrint = NULL;
+
+// clang-format off
+AUTOHOOK(TextMsg, client.dll + 0x198710,
+void,, (BFRead* msg))
+// clang-format on
+{
+ TextMsgPrintType_t msg_dest = (TextMsgPrintType_t)msg->ReadByte();
+
+ char text[256];
+ msg->ReadString(text, sizeof(text));
+
+ if (!Cvar_cl_showtextmsg->GetBool())
+ return;
+
+ switch (msg_dest)
+ {
+ case TextMsgPrintType_t::HUD_PRINTCENTER:
+ pInternalCenterPrint->Print(text);
+ break;
+
+ default:
+ spdlog::warn("Unimplemented TextMsg type {}! printing to console", msg_dest);
+ [[fallthrough]];
+
+ case TextMsgPrintType_t::HUD_PRINTCONSOLE:
+ auto endpos = strlen(text);
+ if (text[endpos - 1] == '\n')
+ text[endpos - 1] = '\0'; // cut off repeated newline
+
+ spdlog::info(text);
+ break;
+ }
+}
+
+// clang-format off
+AUTOHOOK(Hook_fprintf, engine.dll + 0x51B1F0,
+int,, (void* const stream, const char* const format, ...))
+// clang-format on
+{
+ va_list va;
+ va_start(va, format);
+
+ SQChar buf[1024];
+ int charsWritten = vsnprintf_s(buf, _TRUNCATE, format, va);
+
+ if (charsWritten > 0)
+ {
+ if (buf[charsWritten - 1] == '\n')
+ buf[charsWritten - 1] = '\0';
+ NS::log::NATIVE_EN->info("{}", buf);
+ }
+
+ va_end(va);
+ return 0;
+}
+
+// clang-format off
+AUTOHOOK(ConCommand_echo, engine.dll + 0x123680,
+void,, (const CCommand& arg))
+// clang-format on
+{
+ if (arg.ArgC() >= 2)
+ NS::log::echo->info("{}", arg.ArgS());
+}
+
+// clang-format off
+AUTOHOOK(EngineSpewFunc, engine.dll + 0x11CA80,
+void, __fastcall, (void* pEngineServer, SpewType_t type, const char* format, va_list args))
+// clang-format on
+{
+ if (!Cvar_spewlog_enable->GetBool())
+ return;
+
+ const char* typeStr = PrintSpewTypes.at(type);
+ char formatted[2048] = {0};
+ bool bShouldFormat = true;
+
+ // because titanfall 2 is quite possibly the worst thing to yet exist, it sometimes gives invalid specifiers which will crash
+ // ttf2sdk had a way to prevent them from crashing but it doesnt work in debug builds
+ // so we use this instead
+ for (int i = 0; format[i]; i++)
+ {
+ if (format[i] == '%')
+ {
+ switch (format[i + 1])
+ {
+ // this is fucking awful lol
+ case 'd':
+ case 'i':
+ case 'u':
+ case 'x':
+ case 'X':
+ case 'f':
+ case 'F':
+ case 'g':
+ case 'G':
+ case 'a':
+ case 'A':
+ case 'c':
+ case 's':
+ case 'p':
+ case 'n':
+ case '%':
+ case '-':
+ case '+':
+ case ' ':
+ case '#':
+ case '*':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ break;
+
+ default:
+ {
+ bShouldFormat = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (bShouldFormat)
+ vsnprintf(formatted, sizeof(formatted), format, args);
+ else
+ spdlog::warn("Failed to format {} \"{}\"", typeStr, format);
+
+ auto endpos = strlen(formatted);
+ if (formatted[endpos - 1] == '\n')
+ formatted[endpos - 1] = '\0'; // cut off repeated newline
+
+ NS::log::NATIVE_SV->log(PrintSpewLevels.at(type), "{}", formatted);
+}
+
+// used for printing the output of status
+// clang-format off
+AUTOHOOK(Status_ConMsg, engine.dll + 0x15ABD0,
+void,, (const char* text, ...))
+// clang-format on
+{
+ char formatted[2048];
+ va_list list;
+
+ va_start(list, text);
+ vsprintf_s(formatted, text, list);
+ va_end(list);
+
+ auto endpos = strlen(formatted);
+ if (formatted[endpos - 1] == '\n')
+ formatted[endpos - 1] = '\0'; // cut off repeated newline
+
+ spdlog::info(formatted);
+}
+
+// clang-format off
+AUTOHOOK(CClientState_ProcessPrint, engine.dll + 0x1A1530,
+bool,, (void* thisptr, uintptr_t msg))
+// clang-format on
+{
+ char* text = *(char**)(msg + 0x20);
+
+ auto endpos = strlen(text);
+ if (text[endpos - 1] == '\n')
+ text[endpos - 1] = '\0'; // cut off repeated newline
+
+ spdlog::info(text);
+ return true;
+}
+
+ON_DLL_LOAD_RELIESON("engine.dll", EngineSpewFuncHooks, ConVar, (CModule module))
+{
+ AUTOHOOK_DISPATCH_MODULE(engine.dll)
+
+ Cvar_spewlog_enable = new ConVar("spewlog_enable", "0", FCVAR_NONE, "Enables/disables whether the engine spewfunc should be logged");
+}
+
+ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ClientPrintHooks, ConVar, (CModule module))
+{
+ AUTOHOOK_DISPATCH_MODULE(client.dll)
+
+ Cvar_cl_showtextmsg = new ConVar("cl_showtextmsg", "1", FCVAR_NONE, "Enable/disable text messages printing on the screen.");
+ pInternalCenterPrint = module.Offset(0x216E940).RCast<ICenterPrint*>();
+}
diff --git a/primedev/logging/loghooks.h b/primedev/logging/loghooks.h
new file mode 100644
index 00000000..6f70f09b
--- /dev/null
+++ b/primedev/logging/loghooks.h
@@ -0,0 +1 @@
+#pragma once
diff --git a/primedev/logging/sourceconsole.cpp b/primedev/logging/sourceconsole.cpp
new file mode 100644
index 00000000..e436d1d4
--- /dev/null
+++ b/primedev/logging/sourceconsole.cpp
@@ -0,0 +1,91 @@
+#include "core/convar/convar.h"
+#include "sourceconsole.h"
+#include "core/sourceinterface.h"
+#include "core/convar/concommand.h"
+#include "util/printcommands.h"
+
+SourceInterface<CGameConsole>* g_pSourceGameConsole;
+
+void ConCommand_toggleconsole(const CCommand& arg)
+{
+ if ((*g_pSourceGameConsole)->IsConsoleVisible())
+ (*g_pSourceGameConsole)->Hide();
+ else
+ (*g_pSourceGameConsole)->Activate();
+}
+
+void ConCommand_showconsole(const CCommand& arg)
+{
+ (*g_pSourceGameConsole)->Activate();
+}
+
+void ConCommand_hideconsole(const CCommand& arg)
+{
+ (*g_pSourceGameConsole)->Hide();
+}
+
+void SourceConsoleSink::custom_sink_it_(const custom_log_msg& msg)
+{
+ if (!(*g_pSourceGameConsole)->m_bInitialized)
+ return;
+
+ spdlog::memory_buf_t formatted;
+ spdlog::sinks::base_sink<std::mutex>::formatter_->format(msg, formatted);
+
+ // get message string
+ std::string str = fmt::to_string(formatted);
+
+ SourceColor levelColor = m_LogColours[msg.level];
+ std::string name {msg.logger_name.begin(), msg.logger_name.end()};
+
+ (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->ColorPrint(msg.origin->SRCColor, ("[" + name + "]").c_str());
+ (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(" ");
+ (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->ColorPrint(levelColor, ("[" + std::string(level_names[msg.level]) + "]").c_str());
+ (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(" ");
+ (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(fmt::to_string(formatted).c_str());
+}
+
+void SourceConsoleSink::sink_it_(const spdlog::details::log_msg& msg)
+{
+ throw std::runtime_error("sink_it_ called on SourceConsoleSink with pure log_msg. This is an error!");
+}
+
+void SourceConsoleSink::flush_() {}
+
+// clang-format off
+HOOK(OnCommandSubmittedHook, OnCommandSubmitted,
+void, __fastcall, (CConsoleDialog* consoleDialog, const char* pCommand))
+// clang-format on
+{
+ consoleDialog->m_pConsolePanel->Print("] ");
+ consoleDialog->m_pConsolePanel->Print(pCommand);
+ consoleDialog->m_pConsolePanel->Print("\n");
+
+ TryPrintCvarHelpForCommand(pCommand);
+
+ OnCommandSubmitted(consoleDialog, pCommand);
+}
+
+// called from sourceinterface.cpp in client createinterface hooks, on GameClientExports001
+void InitialiseConsoleOnInterfaceCreation()
+{
+ (*g_pSourceGameConsole)->Initialize();
+ // hook OnCommandSubmitted so we print inputted commands
+ OnCommandSubmittedHook.Dispatch((LPVOID)(*g_pSourceGameConsole)->m_pConsole->m_vtable->OnCommandSubmitted);
+
+ auto consoleSink = std::make_shared<SourceConsoleSink>();
+ if (g_bSpdLog_UseAnsiColor)
+ consoleSink->set_pattern("%v"); // no need to include the level in the game console, the text colour signifies it anyway
+ else
+ consoleSink->set_pattern("[%n] [%l] %v"); // no colour, so we should show the level for colourblind people
+ RegisterCustomSink(consoleSink);
+}
+
+ON_DLL_LOAD_CLIENT_RELIESON("client.dll", SourceConsole, ConCommand, (CModule module))
+{
+ g_pSourceGameConsole = new SourceInterface<CGameConsole>("client.dll", "GameConsole004");
+
+ RegisterConCommand("toggleconsole", ConCommand_toggleconsole, "Show/hide the console.", FCVAR_DONTRECORD);
+ RegisterConCommand("showconsole", ConCommand_showconsole, "Show the console.", FCVAR_DONTRECORD);
+ RegisterConCommand("hideconsole", ConCommand_hideconsole, "Hide the console.", FCVAR_DONTRECORD);
+}
diff --git a/primedev/logging/sourceconsole.h b/primedev/logging/sourceconsole.h
new file mode 100644
index 00000000..44d73843
--- /dev/null
+++ b/primedev/logging/sourceconsole.h
@@ -0,0 +1,85 @@
+#pragma once
+#include "core/sourceinterface.h"
+#include "spdlog/sinks/base_sink.h"
+#include <map>
+
+class EditablePanel
+{
+public:
+ virtual ~EditablePanel() = 0;
+ unsigned char unknown[0x2B0];
+};
+
+class IConsoleDisplayFunc
+{
+public:
+ virtual void ColorPrint(const SourceColor& clr, const char* pMessage) = 0;
+ virtual void Print(const char* pMessage) = 0;
+ virtual void DPrint(const char* pMessage) = 0;
+};
+
+class CConsolePanel : public EditablePanel, public IConsoleDisplayFunc
+{
+};
+
+class CConsoleDialog
+{
+public:
+ struct VTable
+ {
+ void* unknown[298];
+ void (*OnCommandSubmitted)(CConsoleDialog* consoleDialog, const char* pCommand);
+ };
+
+ VTable* m_vtable;
+ unsigned char unknown[0x398];
+ CConsolePanel* m_pConsolePanel;
+};
+
+class CGameConsole
+{
+public:
+ virtual ~CGameConsole() = 0;
+
+ // activates the console, makes it visible and brings it to the foreground
+ virtual void Activate() = 0;
+
+ virtual void Initialize() = 0;
+
+ // hides the console
+ virtual void Hide() = 0;
+
+ // clears the console
+ virtual void Clear() = 0;
+
+ // return true if the console has focus
+ virtual bool IsConsoleVisible() = 0;
+
+ virtual void SetParent(int parent) = 0;
+
+ bool m_bInitialized;
+ CConsoleDialog* m_pConsole;
+};
+
+extern SourceInterface<CGameConsole>* g_pSourceGameConsole;
+
+// spdlog logger
+class SourceConsoleSink : public CustomSink
+{
+private:
+ std::map<spdlog::level::level_enum, SourceColor> m_LogColours = {
+ {spdlog::level::trace, NS::Colors::TRACE.ToSourceColor()},
+ {spdlog::level::debug, NS::Colors::DEBUG.ToSourceColor()},
+ {spdlog::level::info, NS::Colors::INFO.ToSourceColor()},
+ {spdlog::level::warn, NS::Colors::WARN.ToSourceColor()},
+ {spdlog::level::err, NS::Colors::ERR.ToSourceColor()},
+ {spdlog::level::critical, NS::Colors::CRIT.ToSourceColor()},
+ {spdlog::level::off, NS::Colors::OFF.ToSourceColor()}};
+
+protected:
+ void custom_sink_it_(const custom_log_msg& msg);
+ void sink_it_(const spdlog::details::log_msg& msg) override;
+ void flush_() override;
+};
+
+void InitialiseConsoleOnInterfaceCreation();