diff --git a/CarbonLauncher/include/guimanager.h b/CarbonLauncher/include/guimanager.h index 8f99104..6c9d702 100644 --- a/CarbonLauncher/include/guimanager.h +++ b/CarbonLauncher/include/guimanager.h @@ -10,6 +10,21 @@ #include namespace Carbon { + enum LogColour : WORD { + DARKGREEN = 2, + BLUE = 3, + PURPLE = 5, + GOLD = 6, + WHITE = 7, + DARKGRAY = 8, + DARKBLUE = 9, + GREEN = 10, + CYAN = 11, + RED = 12, + PINK = 13, + YELLOW = 14, + }; + class GUIManager { public: // Initializes the GUI manager diff --git a/CarbonLauncher/include/state.h b/CarbonLauncher/include/state.h index 598a158..34e59cc 100644 --- a/CarbonLauncher/include/state.h +++ b/CarbonLauncher/include/state.h @@ -11,6 +11,7 @@ namespace Carbon { struct LogMessage { + int colour; std::string message; std::string time; }; diff --git a/CarbonLauncher/src/guimanager.cpp b/CarbonLauncher/src/guimanager.cpp index 9c1d994..9f07557 100644 --- a/CarbonLauncher/src/guimanager.cpp +++ b/CarbonLauncher/src/guimanager.cpp @@ -410,7 +410,47 @@ void _GUI() { auto& message = *it; ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), message.time.c_str()); ImGui::SameLine(); - ImGui::TextWrapped(message.message.c_str()); + + switch (message.colour) { + case LogColour::DARKGREEN: + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), message.message.c_str()); + break; + case LogColour::BLUE: + ImGui::TextColored(ImVec4(0.0f, 0.0f, 1.0f, 1.0f), message.message.c_str()); + break; + case LogColour::PURPLE: + ImGui::TextColored(ImVec4(1.0f, 0.0f, 1.0f, 1.0f), message.message.c_str()); + break; + case LogColour::GOLD: + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), message.message.c_str()); + break; + case LogColour::WHITE: + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), message.message.c_str()); + break; + case LogColour::DARKGRAY: + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), message.message.c_str()); + break; + case LogColour::DARKBLUE: + ImGui::TextColored(ImVec4(0.0f, 0.0f, 0.5f, 1.0f), message.message.c_str()); + break; + case LogColour::GREEN: + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), message.message.c_str()); + break; + case LogColour::CYAN: + ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), message.message.c_str()); + break; + case LogColour::RED: + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), message.message.c_str()); + break; + case LogColour::PINK: + ImGui::TextColored(ImVec4(1.0f, 0.0f, 1.0f, 1.0f), message.message.c_str()); + break; + case LogColour::YELLOW: + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), message.message.c_str()); + break; + } + + //ImGui::TextWrapped(message.message.c_str()); } // If we were scrolled to the bottom, scroll down diff --git a/CarbonLauncher/src/pipemanager.cpp b/CarbonLauncher/src/pipemanager.cpp index 5e9b518..6bcac53 100644 --- a/CarbonLauncher/src/pipemanager.cpp +++ b/CarbonLauncher/src/pipemanager.cpp @@ -113,7 +113,13 @@ PipeManager::PipeManager() { std::string time = fmt::format("{:02}:{:02}:{:02}", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec); + // Split on -|- to get the log colour + auto colourDelimiter = data.find("-|-"); + auto colour = data.substr(0, colourDelimiter); + data = data.substr(colourDelimiter + 3); + LogMessage logMessage; + logMessage.colour = std::stoi(colour); logMessage.message = data; logMessage.time = time; C.logMessages.emplace_back(logMessage); diff --git a/CarbonSupervisor/CarbonSupervisor.vcxproj b/CarbonSupervisor/CarbonSupervisor.vcxproj index 73b51c5..ac8bb1a 100644 --- a/CarbonSupervisor/CarbonSupervisor.vcxproj +++ b/CarbonSupervisor/CarbonSupervisor.vcxproj @@ -1,4 +1,4 @@ - + @@ -158,7 +158,14 @@ + + + + + + + diff --git a/CarbonSupervisor/include/globals.h b/CarbonSupervisor/include/globals.h new file mode 100644 index 0000000..5f3bdbd --- /dev/null +++ b/CarbonSupervisor/include/globals.h @@ -0,0 +1,22 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include + +struct LogMessage { + WORD colour; + std::string message; +}; + +class Globals_t { +public: + std::queue logMessages = {}; + + static Globals_t* GetInstance() { + static Globals_t instance; + return &instance; + } +}; diff --git a/CarbonSupervisor/include/sm.h b/CarbonSupervisor/include/sm.h new file mode 100644 index 0000000..2974a7e --- /dev/null +++ b/CarbonSupervisor/include/sm.h @@ -0,0 +1,77 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +#include + +#include + +enum ContraptionState { + LOADING = 1, + IN_GAME = 2, + MAIN_MENU = 3 +}; + +namespace SM { + const uintptr_t ContraptionOffset = 0x1267538; + + struct LogMessage { + int colour; + std::string message; + }; + + class Console { + virtual ~Console() {} + virtual void Log(const std::string&, WORD colour, WORD LogType) = 0; + virtual void LogNoRepeat(const std::string&, WORD colour, WORD LogType) = 0; + + public: + void Hook(); + }; + + class Contraption { + private: + /* 0x0000 */ char pad_0x0000[0x58]; + public: + /* 0x0058 */ Console* console; + private: + /* 0x0060 */ char pad_0x0060[0x11C]; + public: + /* 0x017C */ int state; + + public: + static Contraption* GetInstance() { + // TODO sig scan for this (90 48 89 05 ? ? ? ? + 0x4 @ Contraption) + auto contraption = *reinterpret_cast((uintptr_t)GetModuleHandle(nullptr) + ContraptionOffset); + while (contraption == nullptr || contraption->state < LOADING || contraption->state > 3) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + contraption = SM::Contraption::GetInstance(); + + static int i = 0; + if (i++ % 30 == 0) { // Every 3 seconds + spdlog::warn("Waiting for Contraption..."); + } + } + + return contraption; + } + + void WaitForStateEgress(ContraptionState state) const { + while (this->state == (int)state) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + void OnStateChange(std::function callback) const { + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + static int lastState = 0; + if (lastState != this->state) { + lastState = this->state; + callback((ContraptionState)lastState); + } + } + } + }; +} diff --git a/CarbonSupervisor/include/utils.h b/CarbonSupervisor/include/utils.h new file mode 100644 index 0000000..772ae79 --- /dev/null +++ b/CarbonSupervisor/include/utils.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +enum PacketType { + LOADED, + STATECHANGE, + LOG, + UNKNOWNTYPE +}; + +namespace Utils { + void InitLogging(); + + class Pipe { + public: + Pipe() = default; + + static Pipe* GetInstance() { + static Pipe instance; + return &instance; + } + + // Delete copy constructor and assignment operator + Pipe(Pipe const&) = delete; + void operator=(Pipe const&) = delete; + + void ResetPipe(bool reInformLauncher = false); + + void SendPacket(PacketType packetType, const std::string& data); + void SendPacket(PacketType packetType, int data); + void SendPacket(PacketType packetType); + + void ValidatePipe(); + + private: + std::mutex logMutex{}; + HANDLE pipe = INVALID_HANDLE_VALUE; + }; +} // namespace Utils diff --git a/CarbonSupervisor/src/console.cpp b/CarbonSupervisor/src/console.cpp new file mode 100644 index 0000000..3f1be92 --- /dev/null +++ b/CarbonSupervisor/src/console.cpp @@ -0,0 +1,62 @@ +#include "sm.h" +#include "utils.h" +#include "globals.h" + +using LogFunction = void(SM::Console::*)(const std::string&, WORD, WORD); +LogFunction oLogFunction; +LogFunction oLogNoRepeatFunction; + +static void LogPacket(const std::string& message, WORD colour, WORD type) { + static Utils::Pipe* pipe = Utils::Pipe::GetInstance(); + pipe->SendPacket(LOG, fmt::format("{}-|-{}", colour, message)); +} + +static void HookedLog(SM::Console* console, const std::string& message, WORD colour, WORD type) { + static Globals_t* globals = Globals_t::GetInstance(); + globals->logMessages.push({ colour, message }); + (console->*oLogFunction)(message, colour, type); +} + +static void HookVTableFunc(void** vtable, int index, void* newFunction, void** originalFunction) { + DWORD oldProtect; + VirtualProtect(&vtable[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &oldProtect); + *originalFunction = vtable[index]; + vtable[index] = newFunction; + VirtualProtect(&vtable[index], sizeof(void*), oldProtect, &oldProtect); +} + +void SM::Console::Hook() { + spdlog::info("Hooking console functions"); + + // Hook the consoles `Log` and `LogNoRepeat` functions (to the same thing) + // Make them send a packet to the pipe with LOG:-: + void** vtable = *reinterpret_cast(this); + HookVTableFunc(vtable, 1, HookedLog, reinterpret_cast(&oLogFunction)); + HookVTableFunc(vtable, 2, HookedLog, nullptr); + + // Start a thread to send log messages to the launcher + std::thread([]() { + auto pipe = Utils::Pipe::GetInstance(); + static Globals_t* globals = Globals_t::GetInstance(); + auto& logMessages = globals->logMessages; + + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!logMessages.empty()) { + auto& message = logMessages.front(); + pipe->SendPacket(LOG, fmt::format("{}-|-{}", message.colour, message.message)); + logMessages.pop(); + } + } + }).detach(); +} + +void Utils::InitLogging() { + AllocConsole(); + freopen_s(reinterpret_cast(stdout), "CONOUT$", "w", stdout); + + auto console = spdlog::stdout_color_mt("carbon"); + spdlog::set_default_logger(console); + spdlog::set_level(spdlog::level::trace); + spdlog::set_pattern("%^[ %H:%M:%S | %-8l] %n: %v%$"); +} diff --git a/CarbonSupervisor/src/main.cpp b/CarbonSupervisor/src/main.cpp index 4a6671d..c6cb44a 100644 --- a/CarbonSupervisor/src/main.cpp +++ b/CarbonSupervisor/src/main.cpp @@ -1,177 +1,9 @@ -#define WIN32_LEAN_AND_MEAN +#define WINDOWS_LEAN_AND_MEAN #include -#include -#include -#include -#include - -#include -#include - -#include - -// TODO: Send packets in a separate thread -// TODO: Fix messages too large causing the pipe to break - -const uintptr_t ContraptionOffset = 0x1267538; - -class Console { - virtual ~Console() {} - virtual void Log(const std::string&, WORD colour, WORD LogType) = 0; - virtual void LogNoRepeat(const std::string&, WORD colour, WORD LogType) = 0; -}; - -class Contraption { -private: - /* 0x0000 */ char pad_0x0000[0x58]; -public: - /* 0x0058 */ Console* console; -private: - /* 0x0060 */ char pad_0x0060[0x11C]; -public: - /* 0x017C */ int state; - -public: - static Contraption* GetInstance() { - // TODO sig scan for this (90 48 89 05 ? ? ? ? + 0x4 @ Contraption) - return *reinterpret_cast((uintptr_t)GetModuleHandle(nullptr) + ContraptionOffset); - } -}; - -using LogFunction = void(Console::*)(const std::string&, WORD, WORD); - -HANDLE gPipe = nullptr; -LogFunction oLogFunction = nullptr; -LogFunction oLogNoRepeatFunction = nullptr; -std::mutex gLogMutex; - -enum PacketType { - LOADED, - STATECHANGE, - LOG, - UNKNOWNTYPE -}; - -static void ResetPipe() { - if (gPipe != nullptr && gPipe != INVALID_HANDLE_VALUE) { - CloseHandle(gPipe); - gPipe = nullptr; - } - - gPipe = CreateFile( - "\\\\.\\pipe\\CarbonPipe", - GENERIC_WRITE, - 0, - nullptr, - OPEN_EXISTING, - 0, - nullptr - ); - - if (gPipe == INVALID_HANDLE_VALUE) { - spdlog::error("Failed to create pipe"); - } - else { - spdlog::info("Reconnected to pipe"); - } -} - -static void ValidatePipe() { - if (gPipe != nullptr && gPipe != INVALID_HANDLE_VALUE) { - return; - } - - int lastError = GetLastError(); - bool isPipeBroken = lastError != ERROR_SUCCESS; - - // Continuously attempt to reconnect to the pipe - while (gPipe == nullptr || gPipe == INVALID_HANDLE_VALUE || isPipeBroken) { - ResetPipe(); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - spdlog::info("Reconnected to pipe"); -} - -static void SendPacket(PacketType packetType, const std::string& data = "") { - std::lock_guard lock(gLogMutex); - ValidatePipe(); - - auto send = [&](const std::string& packet) { - DWORD bytesWritten = 0; - BOOL res = false; - res = WriteFile(gPipe, packet.c_str(), (DWORD)packet.size(), &bytesWritten, nullptr); - if (!res) { - int error = GetLastError(); - char* errorStr = nullptr; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorStr, 0, nullptr); - // Trim the \n off the end - errorStr[strlen(errorStr) - 2] = '\0'; - spdlog::error("Failed to write to pipe: {} ({})", errorStr, error); - LocalFree((HLOCAL)errorStr); - ResetPipe(); - return; - } - spdlog::info("Sent packet: {}", packet); - }; - - // Check if there are new lines in the data - auto delimiter = data.find('\n'); - std::vector packets; - if (delimiter != std::string::npos) { - size_t start = 0; - while (delimiter != std::string::npos) { - packets.push_back(data.substr(start, delimiter - start)); - start = delimiter + 1; - delimiter = data.find('\n', start); - } - packets.push_back(data.substr(start)); - } - else { - packets.push_back(data); - } - - if (packets.size() == 0) { - return; - } - else if (packets.size() > 1) { - spdlog::warn("Splitting packet into {} parts", packets.size()); - } - - for (auto& packet : packets) { - std::string packetStr; - switch (packetType) { - case LOADED: - packetStr = "LOADED-:-"; - break; - case STATECHANGE: - packetStr = "STATECHANGE-:-" + packet; - break; - case LOG: - packetStr = "LOG-:-" + packet; - break; - default: - packetStr = "UNKNOWNTYPE-:-" + packet; - break; - } - send(packetStr); - } -} - -static void HookedLog(Console* console, const std::string& message, WORD color1, WORD color2) { - SendPacket(LOG, message); - (console->*oLogFunction)(message, color1, color2); -} - -static void HookVTableFunc(void** vtable, int index, void* newFunction, void** originalFunction) { - DWORD oldProtect; - VirtualProtect(&vtable[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &oldProtect); - *originalFunction = vtable[index]; - vtable[index] = newFunction; - VirtualProtect(&vtable[index], sizeof(void*), oldProtect, &oldProtect); -} +#include "sm.h" +#include "utils.h" +#include "globals.h" /* * DLL main thread @@ -179,70 +11,24 @@ static void HookVTableFunc(void** vtable, int index, void* newFunction, void** o * @param lpParam The parameter, in this case the module handle */ DWORD WINAPI DllMainThread(LPVOID lpParam) { - AllocConsole(); - freopen_s(reinterpret_cast(stdout), "CONOUT$", "w", stdout); - - auto console = spdlog::stdout_color_mt("carbon"); - spdlog::set_default_logger(console); - spdlog::set_level(spdlog::level::trace); - - auto hModule = static_cast(lpParam); - - // Get contraption instance - auto contraption = Contraption::GetInstance(); - while (contraption == nullptr || contraption->state < 1 || contraption->state > 3) { - contraption = Contraption::GetInstance(); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - static int i = 0; - if (i++ % 30 == 0) { // Every 3 seconds - spdlog::warn("Waiting for Contraption..."); - } - } + HMODULE hModule = static_cast(lpParam); + Utils::InitLogging(); - // Check for the state changing off of 1 (not loading anymore) - while (contraption->state == 1) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - spdlog::info("Contraption state: {}", contraption->state); - - gPipe = CreateFile( - "\\\\.\\pipe\\CarbonPipe", - GENERIC_WRITE, - 0, - nullptr, - OPEN_EXISTING, - 0, - nullptr - ); + auto contraption = SM::Contraption::GetInstance(); + auto pipe = Utils::Pipe::GetInstance(); // Connection to launcher - ValidatePipe(); - spdlog::info("Connected to pipe"); + contraption->console->Hook(); // Hook the console, sending log messages to the launcher + contraption->WaitForStateEgress(LOADING); // Wait for the game to finish loading - // Send the loaded packet - SendPacket(LOADED); + pipe->SendPacket(LOADED); // Inform the launcher we have loaded - // Hook the consoles `Log` and `LogNoRepeat` functions (to the same thing) - // Make them send a packet to the pipe with LOG:-: - void** vtable = *reinterpret_cast(contraption->console); - // ~Console, Log, LogNoRepeat - // 0x0, 0x8, 0x10 - HookVTableFunc(vtable, 0x8 / sizeof(void*), HookedLog, reinterpret_cast(&oLogFunction)); - HookVTableFunc(vtable, 0x10 / sizeof(void*), HookedLog, reinterpret_cast(&oLogNoRepeatFunction)); - - // Main loop - while (true) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - static int lastState = 0; - if (lastState != contraption->state) { - lastState = contraption->state; - // Send the state change packet - SendPacket(STATECHANGE, std::to_string(contraption->state)); - } - } + // Whenever the state changes, send a packet informing the launcher + contraption->OnStateChange([&](ContraptionState state) { + pipe->SendPacket(STATECHANGE, static_cast(state)); + }); + // Quit the thread, we never get here because the game closes the process + // but it's good practice to have a clean exit FreeLibraryAndExitThread(hModule, 0); return 0; } diff --git a/CarbonSupervisor/src/sender.cpp b/CarbonSupervisor/src/sender.cpp new file mode 100644 index 0000000..b41df35 --- /dev/null +++ b/CarbonSupervisor/src/sender.cpp @@ -0,0 +1,119 @@ +#include "utils.h" +#include "sm.h" + +using namespace Utils; + +void Pipe::ResetPipe(bool reInformLauncher) { + pipe = CreateFile( + "\\\\.\\pipe\\CarbonPipe", + GENERIC_WRITE, + 0, + nullptr, + OPEN_EXISTING, + 0, + nullptr + ); + + // Send the contraption state change so that the launcher knows the game didn't crash + if (reInformLauncher) { + SendPacket(STATECHANGE, std::to_string(SM::Contraption::GetInstance()->state)); + } +} + +void Pipe::ValidatePipe() { + if (pipe != nullptr && pipe != INVALID_HANDLE_VALUE) { + return; + } + + bool isPipeBroken = GetLastError() != ERROR_SUCCESS; + + // Continuously attempt to reconnect to the pipe + while (pipe == nullptr || pipe == INVALID_HANDLE_VALUE || isPipeBroken) { + ResetPipe(false); + isPipeBroken = GetLastError() != ERROR_SUCCESS; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + isPipeBroken = GetLastError() != ERROR_SUCCESS; + if (isPipeBroken) { + spdlog::error("Failed to reconnect to pipe"); + } +} + +void Pipe::SendPacket(PacketType packetType, int data) { + SendPacket(packetType, std::to_string(data)); +} + +void Pipe::SendPacket(PacketType packetType) { + SendPacket(packetType, ""); +} + +// TODO: Fix messages too large causing the pipe to break +void Pipe::SendPacket(PacketType packetType, const std::string& data) { + std::lock_guard lock(logMutex); + + ValidatePipe(); + + auto send = [&](const std::string& packet) { + DWORD bytesWritten = 0; + BOOL res = false; + res = WriteFile(pipe, packet.c_str(), (DWORD)packet.size(), &bytesWritten, nullptr); + if (!res) { + int error = GetLastError(); + char* errorStr = nullptr; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorStr, 0, nullptr); + // Trim the \n off the end + errorStr[strlen(errorStr) - 2] = '\0'; + spdlog::error("Failed to write to pipe: {} ({})", errorStr, error); + LocalFree((HLOCAL)errorStr); + ResetPipe(true); + return; + } + spdlog::info("Sent packet: {}", packet); + }; + + /* + // Check if there are new lines in the data + auto delimiter = data.find('\n'); + std::vector packets; + if (delimiter != std::string::npos) { + size_t start = 0; + while (delimiter != std::string::npos) { + packets.push_back(data.substr(start, delimiter - start)); + start = delimiter + 1; + delimiter = data.find('\n', start); + } + packets.push_back(data.substr(start)); + } + else { + packets.push_back(data); + } + + if (packets.size() == 0) { + return; + } + else if (packets.size() > 1) { + spdlog::warn("Splitting packet into {} parts", packets.size()); + }*/ + + //for (auto& packet : packets) { + std::string packetStr; + switch (packetType) { + case LOADED: + packetStr = "LOADED-:-"; + break; + case STATECHANGE: + packetStr = "STATECHANGE-:-" + data; + break; + case LOG: + packetStr = "LOG-:-" + data; + break; + default: + packetStr = "UNKNOWNTYPE-:-" + data; + break; + } + send(packetStr); + //} +} +