Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/improve exceptions #164

Merged
merged 23 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6fdd796
doc: Add a hint to OnDvarInit
SnowyWhite Nov 2, 2024
e1b61b1
feat: Add UI_GetGameTypeDisplayName
SnowyWhite Nov 2, 2024
d17fd51
feat: Add new dvar to save the server version
SnowyWhite Nov 2, 2024
28cf8b0
feat: Add dvar for the client version
SnowyWhite Nov 2, 2024
0859f4e
feat: Add method to get the Windows version
SnowyWhite Nov 2, 2024
5d546d0
feat: Include Windows Controls library
SnowyWhite Nov 2, 2024
ccadb19
feat: Limit Windows to the minimum supported version
SnowyWhite Nov 2, 2024
a9da13f
fix: Actually set sv_version value
SnowyWhite Nov 2, 2024
11f89ba
feat: Implement improved exception message
SnowyWhite Nov 2, 2024
3e34e46
feat: Apply formatting before copying the message to clipboard
SnowyWhite Nov 3, 2024
d99ff56
fix: Display MessageBox in case of a stack overflow
SnowyWhite Nov 3, 2024
c3602ad
Merge branch 'develop' into feat/improve-exceptions
Rackover Nov 4, 2024
4665b5a
Merge branch 'develop' of https://github.com/iw4x/iw4x-client into fe…
SnowyWhite Nov 5, 2024
d0d799a
feat: Add method to get system architecture
SnowyWhite Nov 5, 2024
96d22ec
fix: Use macros for duplicate strings
SnowyWhite Nov 5, 2024
d2e6ef2
Merge branch 'feat/improve-exceptions' of https://github.com/SnowyWhi…
SnowyWhite Nov 5, 2024
955b562
fix: Replace macro with constexpr
SnowyWhite Nov 5, 2024
e9c4ae2
fix: Implement requested changes
SnowyWhite Nov 7, 2024
4f9220f
fix: Use proper check for private match
SnowyWhite Nov 7, 2024
3858c31
fix: Rename com_sv_running to sv_running
SnowyWhite Nov 7, 2024
148e29a
feat: Add Mod Name to message
SnowyWhite Nov 12, 2024
62c3175
Merge branch 'develop' into feat/improve-exceptions
SnowyWhite Dec 3, 2024
144381d
Merge branch 'develop' into feat/improve-exceptions
Rackover Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-constant bound is dangerous

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to accompany your comment with a snippet or a suggestion for something that the offending code should be replaced with

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None that come to mind right now without more context. I believe the first step would be to look into StripColors and StripAllTextIcons and see how we can handle it better; perhaps leave a TODO for now or a FIXME;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened issue #165 for this.

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