Skip to content

Commit

Permalink
Merge pull request #164 from SnowyWhite/feat/improve-exceptions
Browse files Browse the repository at this point in the history
Feat/improve exceptions
  • Loading branch information
Rackover authored Dec 3, 2024
2 parents 1ec8b27 + 144381d commit 4d79e89
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 22 deletions.
6 changes: 3 additions & 3 deletions src/Components/Modules/Dedicated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ namespace Components

bool Dedicated::IsRunning()
{
assert(*Game::com_sv_running);
return *Game::com_sv_running && (*Game::com_sv_running)->current.enabled;
assert(*Game::sv_running);
return *Game::sv_running && (*Game::sv_running)->current.enabled;
}

void Dedicated::InitDedicatedServer()
Expand Down Expand Up @@ -114,7 +114,7 @@ namespace Components
popad

// Game's code
mov edx, dword ptr com_sv_running
mov edx, dword ptr sv_running

push 0x47DDB8
ret
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Modules/Discord.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Components
public:
Discord();

static std::string GetDiscordServerLink() { return "https://discord.gg/2ETE8engZM"; }

void preDestroy() override;

private:
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Modules/Download.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ namespace Components

std::optional<std::string> Download::InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
if (!(*Game::com_sv_running)->current.enabled)
if (!(*Game::sv_running)->current.enabled)
{
// Game is not running ,cannot return info
return std::nullopt;
Expand Down
1 change: 1 addition & 0 deletions src/Components/Modules/Events.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace Components
static void OnSVInit(const std::function<void()>& callback);

// Client & Server (triggered once)
// Required for String Dvars (game will crash if the dvar subsystem wasn't initialised)
static void OnDvarInit(const std::function<void()>& callback);

// Client & Server (triggered once)
Expand Down
176 changes: 168 additions & 8 deletions src/Components/Modules/Exception.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
#include "Console.hpp"
#include "Exception.hpp"
#include "Window.hpp"
#include "Party.hpp"
#include "TextRenderer.hpp"
#include "Discord.hpp"

#include <cwctype>
#include <version.hpp>

namespace Components
{
constexpr auto CLIPBOARD_MSG = "Do you want to copy this message to the clipboard?";

Utils::Hook Exception::SetFilterHook;
int Exception::MiniDumpType;

Expand Down Expand Up @@ -53,6 +59,155 @@ namespace Components
Game::Sys_SuspendOtherThreads();
}

std::wstring Exception::GetErrorMessage()
{
const auto clientVersion = (*Game::shortversion)->current.string;
const auto osVersion = Utils::IsWineEnvironment() ? "Wine" : Utils::GetWindowsVersion();
const auto launchParams = Utils::String::Convert(Utils::GetLaunchParameters());

std::string clientInfo = std::format(R"(
Client Info:
IW4x Version: {}
OS Version: {}
Parameters: {})",
clientVersion, osVersion, launchParams);

if (!Game::CL_IsCgameInitialized())
{
std::string msg = std::format("{}\n\n{}", clientInfo, CLIPBOARD_MSG);
std::wstring message(msg.begin(), msg.end());
return message;
}

const auto* gameType = (*Game::sv_gametype)->current.string;
const auto* mapName = (*Game::sv_mapname)->current.string;
const auto* fsGame = (*Game::fs_gameDirVar)->current.string[0] != '\0' ? (*Game::fs_gameDirVar)->current.string : "None";

char modName[256]{ 0 };
TextRenderer::StripColors(fsGame, modName, sizeof(modName));
TextRenderer::StripAllTextIcons(modName, modName, sizeof(modName));

// Get info for a private match
{
if (Dedicated::IsRunning())
{
std::string privateMatchInfo = std::format(R"(
Host Info:
Type: Private Match
Gametype: {}
Map Name: {}
Mod Name: {})",
gameType, mapName, modName);

std::string msg = std::format("{}\n{}\n\n{}", clientInfo, privateMatchInfo, CLIPBOARD_MSG);
std::wstring message(msg.begin(), msg.end());
return message;
}
}

// Get info for a dedicated server
{
const auto serverVersion = Dvar::Var("sv_version").get<std::string>();
const auto ipAddress = Network::Address(*Game::connectedHost).getString();

char serverName[256]{ 0 };
TextRenderer::StripColors(Party::GetHostName().data(), serverName, sizeof(serverName));
TextRenderer::StripAllTextIcons(serverName, serverName, sizeof(serverName));

std::string serverInfo = std::format(R"(
Server Info:
Type: Dedicated Server
IW4x Version: {}
Server Name: {}
IP Address: {}
Gametype: {}
Map Name: {}
Mod Name: {})",
serverVersion, serverName, ipAddress, gameType, mapName, modName);

std::string msg = std::format("{}\n{}\n\n{}", clientInfo, serverInfo, CLIPBOARD_MSG);
std::wstring message(msg.begin(), msg.end());
return message;
}
}

std::string Exception::FormatMessageForClipboard(const std::wstring& message)
{
std::wstringstream ss(message);
std::wstring line;
std::wostringstream result;

// Trim all the whitespaces from the message
auto trim = [](std::wstring& s)
{
s.erase(s.begin(), std::find_if_not(s.begin(), s.end(), [](wchar_t ch) { return std::iswspace(ch); })); // trim left
s.erase(std::find_if_not(s.rbegin(), s.rend(), [](wchar_t ch) { return std::iswspace(ch); }).base()); // trim right
};

// Construct a corrected version and get rid of the last line
while (std::getline(ss, line))
{
trim(line);

if (line != Utils::String::Convert(CLIPBOARD_MSG))
{
if (!line.empty())
{
result << line << L'\n';
}
}
}

// Enough wide strings for today
return Utils::String::Convert(result.str());
}

void Exception::DisplayErrorMessage(const std::wstring& title, const std::wstring& message)
{
const std::wstring footerText = std::format(
L"Join the official <a href=\"{}\">Discord Server</a> for additional support.",
Utils::String::Convert(Discord::GetDiscordServerLink()));

TASKDIALOGCONFIG taskDialogConfig = { 0 };
taskDialogConfig.cbSize = sizeof(taskDialogConfig);
taskDialogConfig.hInstance = GetModuleHandleA(nullptr);
taskDialogConfig.hwndParent = Window::GetWindow();
taskDialogConfig.pszWindowTitle = L"Unhandled Exception";
taskDialogConfig.pszMainIcon = MAKEINTRESOURCEW(-7); // Red bar with a shield icon
taskDialogConfig.pszMainInstruction = title.c_str();
taskDialogConfig.pszContent = message.c_str();
taskDialogConfig.dwCommonButtons = TDCBF_YES_BUTTON | TDCBF_NO_BUTTON;
taskDialogConfig.nDefaultButton = IDYES;
taskDialogConfig.pszFooterIcon = TD_INFORMATION_ICON;
taskDialogConfig.pszFooter = footerText.c_str();
taskDialogConfig.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_POSITION_RELATIVE_TO_WINDOW | TDF_SIZE_TO_CONTENT;
taskDialogConfig.lpCallbackData = reinterpret_cast<LONG_PTR>(&message);
taskDialogConfig.pfCallback = Exception::TaskDialogCallbackProc;

::TaskDialogIndirect(&taskDialogConfig, nullptr, nullptr, nullptr);
}

HRESULT CALLBACK Exception::TaskDialogCallbackProc(HWND, UINT notification, WPARAM clickedButton, LPARAM, LONG_PTR data)
{
const auto* msg = reinterpret_cast<const std::wstring*>(data);

if (notification == TDN_HYPERLINK_CLICKED)
{
Utils::OpenUrl(Discord::GetDiscordServerLink());
}

if (notification == TDN_BUTTON_CLICKED)
{
if (clickedButton == IDYES)
{
std::string formattedMessage = Exception::FormatMessageForClipboard(*msg);
Exception::CopyMessageToClipboard(formattedMessage.c_str());
}
}

return S_OK;
}

void Exception::CopyMessageToClipboard(const char* error)
{
const auto hWndNewOwner = GetDesktopWindow();
Expand Down Expand Up @@ -98,20 +253,25 @@ namespace Components
return EXCEPTION_CONTINUE_EXECUTION;
}

const char* error;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)
{
error = "Termination because of a stack overflow.\nCopy exception address to clipboard?";
const auto error = std::format("Termination because of a stack overflow.\n{}", CLIPBOARD_MSG);

// Message should be copied to the clipboard if no button is pressed
if (MessageBoxA(nullptr, error.c_str(), nullptr, MB_YESNO | MB_ICONERROR) == IDYES)
{
CopyMessageToClipboard(Utils::String::VA("0x%08X", ExceptionInfo->ExceptionRecord->ExceptionAddress));
}
}
else
{
error = Utils::String::VA("Fatal error (0x%08X) at 0x%08X.\nCopy exception address to clipboard?", ExceptionInfo->ExceptionRecord->ExceptionCode, ExceptionInfo->ExceptionRecord->ExceptionAddress);
}
const auto code = ExceptionInfo->ExceptionRecord->ExceptionCode;
const auto address = reinterpret_cast<std::uintptr_t>(ExceptionInfo->ExceptionRecord->ExceptionAddress);

// Message should be copied to the keyboard if no button is pressed
if (MessageBoxA(nullptr, error, nullptr, MB_YESNO | MB_ICONERROR) == IDYES)
{
CopyMessageToClipboard(Utils::String::VA("0x%08X", ExceptionInfo->ExceptionRecord->ExceptionAddress));
std::wstring title = Utils::String::Convert(std::format("Fatal error (0x{:X}) at 0x{:X}", code, address));
std::wstring message = Exception::GetErrorMessage();

Exception::DisplayErrorMessage(title, message);
}

if (Flags::HasFlag("bigminidumps"))
Expand Down
5 changes: 5 additions & 0 deletions src/Components/Modules/Exception.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ namespace Components
static LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS ExceptionInfo);
static __declspec(noreturn) void LongJmp_Internal_Stub(jmp_buf env, int status);

static std::wstring GetErrorMessage();
static std::string FormatMessageForClipboard(const std::wstring& message);

static void DisplayErrorMessage(const std::wstring& title, const std::wstring& message);
static void CopyMessageToClipboard(const char* error);

static LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter_Stub(LPTOP_LEVEL_EXCEPTION_FILTER);
static HRESULT CALLBACK TaskDialogCallbackProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, LONG_PTR lpRefData);

static int MiniDumpType;
static Utils::Hook SetFilterHook;
Expand Down
14 changes: 11 additions & 3 deletions src/Components/Modules/Party.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "Stats.hpp"
#include "TextRenderer.hpp"
#include "Voice.hpp"
#include "Events.hpp"

#include <version.hpp>

Expand Down Expand Up @@ -40,6 +41,7 @@ namespace Components
std::map<std::uint64_t, Network::Address> Party::LobbyMap;

Dvar::Var Party::PartyEnable;
Dvar::Var Party::ServerVersion;

SteamID Party::GenerateLobbyId()
{
Expand Down Expand Up @@ -208,8 +210,12 @@ namespace Components
return;
}

PartyEnable = Dvar::Register<bool>("party_enable", Dedicated::IsEnabled(), Game::DVAR_NONE, "Enable party system");
Dvar::Register<bool>("xblive_privatematch", true, Game::DVAR_INIT, "");
Events::OnDvarInit([]
{
ServerVersion = Dvar::Register<const char*>("sv_version", "", Game::DVAR_SERVERINFO | Game::DVAR_INIT, "Server version");
PartyEnable = Dvar::Register<bool>("party_enable", Dedicated::IsEnabled(), Game::DVAR_NONE, "Enable party system");
Dvar::Register<bool>("xblive_privatematch", true, Game::DVAR_INIT, "");
});

// Kill the party migrate handler - it's not necessary and has apparently been used in the past for trickery?
Utils::Hook(0x46AB70, PartyMigrate_HandlePacket, HOOK_JUMP).install()->quick();
Expand Down Expand Up @@ -563,11 +569,13 @@ namespace Components
{
int clients;
int maxClients;
std::string version;

try
{
clients = std::stoi(Container.info.get("clients"));
maxClients = std::stoi(Container.info.get("sv_maxclients"));
version = Container.info.get("version");
}
catch ([[maybe_unused]] const std::exception& ex)
{
Expand All @@ -582,7 +590,7 @@ namespace Components
else
{
Dvar::Var("xblive_privateserver").set(true);

ServerVersion.set(version);
Game::Menus_CloseAll(Game::uiContext);

Game::_XSESSION_INFO hostInfo;
Expand Down
1 change: 1 addition & 0 deletions src/Components/Modules/Party.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace Components
static std::map<std::uint64_t, Network::Address> LobbyMap;

static Dvar::Var PartyEnable;
static Dvar::Var ServerVersion;

static SteamID GenerateLobbyId();

Expand Down
6 changes: 3 additions & 3 deletions src/Components/Modules/QuickPatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,9 @@ namespace Components
// disable bind protection
Utils::Hook::Set<BYTE>(0x4DACA2, 0xEB);

// require Windows 5
Utils::Hook::Set<BYTE>(0x467ADF, 5);
Utils::Hook::Set<char>(0x6DF5D6, '5');
// require Windows 6 (Vista)
Utils::Hook::Set<BYTE>(0x467ADF, 6);
Utils::Hook::Set<char>(0x6DF5D6, '6');

// disable 'ignoring asset' notices
Utils::Hook::Nop(0x5BB902, 5);
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Modules/Rumble.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ namespace Components

const auto logError = [&](const std::string& view)
{
if ((*Game::com_sv_running)->current.value)
if ((*Game::sv_running)->current.value)
{
Components::Logger::Error(Game::ERR_DROP, view);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Modules/Threading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ namespace Components
pop eax
mov ecx, eax

mov edx, 1AD7934h // com_sv_running
mov edx, 1AD7934h // sv_running
cmp byte ptr [edx + 10h], 0

push 47DDC1h
Expand Down
3 changes: 2 additions & 1 deletion src/Game/Dvars.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ namespace Game
const dvar_t** com_developer_script = reinterpret_cast<const dvar_t**>(0x1AD8F10);
const dvar_t** com_timescale = reinterpret_cast<const dvar_t**>(0x1AD7920);
const dvar_t** com_maxFrameTime = reinterpret_cast<const dvar_t**>(0x1AD78F4);
const dvar_t** com_sv_running = reinterpret_cast<const dvar_t**>(0x1AD7934);
const dvar_t** com_masterServerName = reinterpret_cast<const dvar_t**>(0x1AD8F48);
const dvar_t** com_masterPort = reinterpret_cast<const dvar_t**>(0x1AD8F30);

Expand All @@ -50,6 +49,7 @@ namespace Game
const dvar_t** fs_homepath = reinterpret_cast<const dvar_t**>(0x63D4FD8);

const dvar_t** sv_privatePassword = reinterpret_cast<const dvar_t**>(0x62C7C14);
const dvar_t** sv_running = reinterpret_cast<const dvar_t**>(0x1AD7934);
const dvar_t** sv_hostname = reinterpret_cast<const dvar_t**>(0x2098D98);
const dvar_t** sv_gametype = reinterpret_cast<const dvar_t**>(0x2098DD4);
const dvar_t** sv_mapname = reinterpret_cast<const dvar_t**>(0x2098DDC);
Expand Down Expand Up @@ -80,6 +80,7 @@ namespace Game
const dvar_t** cg_scoreboardWidth = reinterpret_cast<const dvar_t**>(0x9FD0AC);

const dvar_t** version = reinterpret_cast<const dvar_t**>(0x1AD7930);
const dvar_t** shortversion = reinterpret_cast<const dvar_t**>(0x01AD79D0);

const dvar_t** viewposNow = reinterpret_cast<const dvar_t**>(0x9FD30C);

Expand Down
Loading

0 comments on commit 4d79e89

Please sign in to comment.