diff options
Diffstat (limited to 'primedev/logging')
-rw-r--r-- | primedev/logging/crashhandler.cpp | 590 | ||||
-rw-r--r-- | primedev/logging/crashhandler.h | 97 | ||||
-rw-r--r-- | primedev/logging/logging.cpp | 302 | ||||
-rw-r--r-- | primedev/logging/logging.h | 136 | ||||
-rw-r--r-- | primedev/logging/loghooks.cpp | 261 | ||||
-rw-r--r-- | primedev/logging/loghooks.h | 1 | ||||
-rw-r--r-- | primedev/logging/sourceconsole.cpp | 91 | ||||
-rw-r--r-- | primedev/logging/sourceconsole.h | 85 |
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(¤tTime, (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(¤tTime, (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(); |