├── .gitignore ├── screenshot.png ├── CarbonLauncher ├── src │ ├── state.cpp │ ├── repo.cpp │ ├── utils.cpp │ ├── main.cpp │ ├── discordmanager.cpp │ ├── pipemanager.cpp │ ├── processmanager.cpp │ ├── modmanager.cpp │ └── guimanager.cpp ├── CarbonLauncher.aps ├── CarbonLauncher.ico ├── CarbonLauncher.rc ├── vendor │ ├── src │ │ ├── dllmain.cpp │ │ ├── connection.h │ │ ├── backoff.h │ │ ├── msg_queue.h │ │ ├── rpc_connection.h │ │ ├── connection_win.cpp │ │ ├── rpc_connection.cpp │ │ ├── CMakeLists.txt │ │ ├── discord_register_win.cpp │ │ ├── serialization.h │ │ ├── serialization.cpp │ │ └── discord_rpc.cpp │ └── include │ │ ├── discord_register.h │ │ └── discord_rpc.h ├── vcpkg.json ├── vcpkg-configuration.json ├── resource.h ├── include │ ├── utils.h │ ├── managers │ │ └── repo.h │ ├── discordmanager.h │ ├── guimanager.h │ ├── modmanager.h │ ├── pipemanager.h │ ├── state.h │ └── processmanager.h └── CarbonLauncher.vcxproj ├── DummyGame ├── vcpkg.json ├── vcpkg-configuration.json ├── src │ └── main.cpp └── DummyGame.vcxproj ├── CarbonSupervisor ├── vcpkg.json ├── vcpkg-configuration.json ├── include │ ├── globals.h │ ├── utils.h │ └── sm.h ├── src │ ├── main.cpp │ ├── console.cpp │ └── pipe.cpp └── CarbonSupervisor.vcxproj ├── schema.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── LICENSE ├── CarbonLauncher.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | vcpkg_installed 3 | x64 4 | *.vcxproj.* 5 | imgui.ini 6 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScrappySM/CarbonLauncher/HEAD/screenshot.png -------------------------------------------------------------------------------- /CarbonLauncher/src/state.cpp: -------------------------------------------------------------------------------- 1 | #include "state.h" 2 | using namespace Carbon; 3 | CarbonState_t C; -------------------------------------------------------------------------------- /DummyGame/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "minhook", 4 | "spdlog" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CarbonSupervisor/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "spdlog", 4 | "minhook" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CarbonLauncher/CarbonLauncher.aps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScrappySM/CarbonLauncher/HEAD/CarbonLauncher/CarbonLauncher.aps -------------------------------------------------------------------------------- /CarbonLauncher/CarbonLauncher.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScrappySM/CarbonLauncher/HEAD/CarbonLauncher/CarbonLauncher.ico -------------------------------------------------------------------------------- /CarbonLauncher/CarbonLauncher.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScrappySM/CarbonLauncher/HEAD/CarbonLauncher/CarbonLauncher.rc -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // outsmart GCC's missing-declarations warning 4 | BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID); 5 | BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID) 6 | { 7 | return TRUE; 8 | } 9 | -------------------------------------------------------------------------------- /CarbonLauncher/src/repo.cpp: -------------------------------------------------------------------------------- 1 | #include "managers/repo.h" 2 | 3 | namespace Managers { 4 | Repo::Repo() { 5 | ZoneScoped; 6 | 7 | this->FetchRepo("https://api.github.com/repos/CarbonMod/Carbon/releases/latest"); 8 | } 9 | 10 | void Repo::FetchRepo(const std::string_view& url) { 11 | ZoneScoped; 12 | 13 | // Firstly, download the repos JSON file 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CarbonLauncher/vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "cpr", 4 | { 5 | "name": "glad", 6 | "features": [ 7 | "gl-api-46" 8 | ] 9 | }, 10 | "glfw3", 11 | { 12 | "name": "imgui", 13 | "features": [ 14 | "glfw-binding", 15 | "opengl3-binding" 16 | ] 17 | }, 18 | "spdlog", 19 | "nlohmann-json", 20 | "rapidjson" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /DummyGame/vcpkg-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "default-registry": { 3 | "kind": "git", 4 | "baseline": "2960d7d80e8d09c84ae8abf15c12196c2ca7d39a", 5 | "repository": "https://github.com/microsoft/vcpkg" 6 | }, 7 | "registries": [ 8 | { 9 | "kind": "artifact", 10 | "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", 11 | "name": "microsoft" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CarbonLauncher/vcpkg-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "default-registry": { 3 | "kind": "git", 4 | "baseline": "b2a47d316de1f3625ea43a7ca3e42dd28c52ece7", 5 | "repository": "https://github.com/microsoft/vcpkg" 6 | }, 7 | "registries": [ 8 | { 9 | "kind": "artifact", 10 | "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", 11 | "name": "microsoft" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CarbonSupervisor/vcpkg-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "default-registry": { 3 | "kind": "git", 4 | "baseline": "b2a47d316de1f3625ea43a7ca3e42dd28c52ece7", 5 | "repository": "https://github.com/microsoft/vcpkg" 6 | }, 7 | "registries": [ 8 | { 9 | "kind": "artifact", 10 | "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", 11 | "name": "microsoft" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CarbonSupervisor/include/globals.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | struct LogMessage { 10 | WORD colour; 11 | std::string message; 12 | }; 13 | 14 | class Globals_t { 15 | public: 16 | std::queue logMessages = {}; 17 | 18 | static Globals_t* GetInstance() { 19 | static Globals_t instance; 20 | return &instance; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /CarbonLauncher/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by CarbonLauncher.rc 4 | // 5 | #define IDI_ICON1 102 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 103 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "name": { 6 | "type": "string" 7 | }, 8 | "authors": { 9 | "type": "array", 10 | "items": [ 11 | { 12 | "type": "string" 13 | } 14 | ] 15 | }, 16 | "description": { 17 | "type": "string" 18 | }, 19 | "url": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": [ 24 | "name", 25 | "description" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This is to wrap the platform specific kinds of connect/read/write. 4 | 5 | #include 6 | #include 7 | 8 | // not really connectiony, but need per-platform 9 | int GetProcessId(); 10 | 11 | struct BaseConnection { 12 | static BaseConnection* Create(); 13 | static void Destroy(BaseConnection*&); 14 | bool isOpen{false}; 15 | bool Open(); 16 | bool Close(); 17 | bool Write(const void* data, size_t length); 18 | bool Read(void* data, size_t length); 19 | }; 20 | -------------------------------------------------------------------------------- /CarbonLauncher/include/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #pragma once 3 | 4 | #define WIN32_LEAN_AND_MEAN 5 | #include 6 | 7 | #include 8 | 9 | namespace Carbon::Utils { 10 | // Get the current module path 11 | // @return The current module path 12 | std::string GetCurrentModulePath(); 13 | 14 | // Get the current module directory 15 | // @return The current module directory 16 | std::string GetCurrentModuleDir(); 17 | 18 | // Get the data directory for Carbon Launcher 19 | // @return The data directory for Carbon Launcher 20 | std::string GetDataDir(); 21 | }; // namespace Carbon::Utils 22 | -------------------------------------------------------------------------------- /CarbonLauncher/include/managers/repo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pch.h" 4 | 5 | class Mod { 6 | public: 7 | Mod() = default; 8 | ~Mod() = default; 9 | 10 | struct GHAttachment { 11 | std::string author; 12 | std::string repo; 13 | std::vector verifiedHash; // TODO 14 | } ghAttachment; 15 | }; 16 | 17 | namespace Managers { 18 | class Repo { 19 | public: 20 | Repo() = default; 21 | ~Repo() = default; 22 | 23 | Repo(const Repo&) = delete; 24 | Repo& operator=(const Repo&) = delete; 25 | 26 | void FetchRepo(const std::string_view& url); 27 | 28 | private: 29 | std::vector mods; 30 | }; 31 | } // namespace Managers 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve on a bug 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: BenMcAvoy 7 | 8 | --- 9 | 10 | ## Bug description 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps to reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ## Additional context 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/include/discord_register.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(DISCORD_DYNAMIC_LIB) 4 | #if defined(_WIN32) 5 | #if defined(DISCORD_BUILDING_SDK) 6 | #define DISCORD_EXPORT __declspec(dllexport) 7 | #else 8 | #define DISCORD_EXPORT __declspec(dllimport) 9 | #endif 10 | #else 11 | #define DISCORD_EXPORT __attribute__((visibility("default"))) 12 | #endif 13 | #else 14 | #define DISCORD_EXPORT 15 | #endif 16 | 17 | #ifdef __cplusplus 18 | extern "C" { 19 | #endif 20 | 21 | DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command); 22 | DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); 23 | 24 | #ifdef __cplusplus 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: BenMcAvoy 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /CarbonLauncher/src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include "state.h" 3 | 4 | #define WIN32_LEAN_AND_MEAN 5 | #include 6 | 7 | std::string Carbon::Utils::GetCurrentModulePath() { 8 | char path[MAX_PATH]; 9 | GetModuleFileNameA(NULL, path, MAX_PATH); 10 | return std::string(path); 11 | } 12 | 13 | std::string Carbon::Utils::GetCurrentModuleDir() { 14 | std::string path = GetCurrentModulePath(); 15 | return path.substr(0, path.find_last_of('\\') + 1); 16 | } 17 | 18 | std::string Carbon::Utils::GetDataDir() { 19 | if (!std::filesystem::exists(C.settings.dataDir)) { 20 | if (!std::filesystem::create_directory(C.settings.dataDir)) { 21 | spdlog::error("Failed to create data directory"); 22 | return GetCurrentModuleDir(); 23 | } 24 | } 25 | 26 | return C.settings.dataDir; 27 | } 28 | -------------------------------------------------------------------------------- /CarbonLauncher/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "state.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | using namespace Carbon; 9 | 10 | /* 11 | * Main entry point for the Carbon Launcher 12 | * 13 | * @param hInstance The instance of the application 14 | * @param hPrevInstance The previous instance of the application 15 | * @param lpCmdLine The command line arguments 16 | * @param nCmdShow The command show 17 | * @return The exit code of the application 18 | */ 19 | int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { 20 | // Tell windows this program isn't important, so other processes (i.e. like the game) can have more resources 21 | SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS); 22 | 23 | C.guiManager.RenderCallback(_GUI); 24 | C.guiManager.Run(); 25 | 26 | FreeConsole(); 27 | 28 | return 0; 29 | } -------------------------------------------------------------------------------- /CarbonSupervisor/include/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | enum PacketType { 7 | LOADED, 8 | STATECHANGE, 9 | LOG, 10 | UNKNOWNTYPE 11 | }; 12 | 13 | namespace Utils { 14 | void InitLogging(); 15 | 16 | class Pipe { 17 | public: 18 | Pipe() = default; 19 | 20 | static Pipe* GetInstance() { 21 | static Pipe instance; 22 | return &instance; 23 | } 24 | 25 | // Delete copy constructor and assignment operator 26 | Pipe(Pipe const&) = delete; 27 | void operator=(Pipe const&) = delete; 28 | 29 | void ResetPipe(bool reInformLauncher = false); 30 | 31 | void SendPacket(PacketType packetType, const std::string& data); 32 | void SendPacket(PacketType packetType, int data); 33 | void SendPacket(PacketType packetType); 34 | 35 | void ValidatePipe(); 36 | 37 | private: 38 | std::mutex logMutex{}; 39 | HANDLE pipe = INVALID_HANDLE_VALUE; 40 | }; 41 | } // namespace Utils 42 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/backoff.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct Backoff { 9 | int64_t minAmount; 10 | int64_t maxAmount; 11 | int64_t current; 12 | int fails; 13 | std::mt19937_64 randGenerator; 14 | std::uniform_real_distribution<> randDistribution; 15 | 16 | double rand01() { return randDistribution(randGenerator); } 17 | 18 | Backoff(int64_t min, int64_t max) 19 | : minAmount(min) 20 | , maxAmount(max) 21 | , current(min) 22 | , fails(0) 23 | , randGenerator((uint64_t)time(0)) 24 | { 25 | } 26 | 27 | void reset() 28 | { 29 | fails = 0; 30 | current = minAmount; 31 | } 32 | 33 | int64_t nextDelay() 34 | { 35 | ++fails; 36 | int64_t delay = (int64_t)((double)current * 2.0 * rand01()); 37 | current = std::min(current + delay, maxAmount); 38 | return current; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /CarbonLauncher/include/discordmanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace Carbon { 9 | // Manages the Discord RPC 10 | class DiscordManager { 11 | public: 12 | // Initializes the Discord RPC 13 | DiscordManager(); 14 | ~DiscordManager(); 15 | 16 | // Updates the state of the Discord RPC 17 | // @param state The new state 18 | void UpdateState(const std::string& state); 19 | 20 | // Updates the details of the Discord RPC 21 | // @param details The new details 22 | void UpdateDetails(const std::string& details); 23 | 24 | // Gets the current activity 25 | DiscordRichPresence& GetPresence(); 26 | 27 | // Updates the Discord RPC and listens for game state changes 28 | void Update(); 29 | 30 | private: 31 | DiscordEventHandlers discordHandlers = { 0 }; 32 | DiscordRichPresence discordPresence = { 0 }; 33 | 34 | /*discord::User currentUser = discord::User{}; 35 | discord::Activity currentActivity = discord::Activity{}; 36 | 37 | std::unique_ptr core = nullptr;*/ 38 | }; 39 | }; // namespace Carbon 40 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/msg_queue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // A simple queue. No locks, but only works with a single thread as producer and a single thread as 6 | // a consumer. Mutex up as needed. 7 | 8 | template 9 | class MsgQueue { 10 | ElementType queue_[QueueSize]; 11 | std::atomic_uint nextAdd_{0}; 12 | std::atomic_uint nextSend_{0}; 13 | std::atomic_uint pendingSends_{0}; 14 | 15 | public: 16 | MsgQueue() {} 17 | 18 | ElementType* GetNextAddMessage() 19 | { 20 | // if we are falling behind, bail 21 | if (pendingSends_.load() >= QueueSize) { 22 | return nullptr; 23 | } 24 | auto index = (nextAdd_++) % QueueSize; 25 | return &queue_[index]; 26 | } 27 | void CommitAdd() { ++pendingSends_; } 28 | 29 | bool HavePendingSends() const { return pendingSends_.load() != 0; } 30 | ElementType* GetNextSendMessage() 31 | { 32 | auto index = (nextSend_++) % QueueSize; 33 | return &queue_[index]; 34 | } 35 | void CommitSend() { --pendingSends_; } 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ScrappySM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CarbonLauncher/include/guimanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define GLFW_INCLUDE_NONE 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "modmanager.h" 13 | 14 | namespace Carbon { 15 | enum LogColour : WORD { 16 | DARKGREEN = 2, 17 | BLUE = 3, 18 | PURPLE = 5, 19 | GOLD = 6, 20 | WHITE = 7, 21 | DARKGRAY = 8, 22 | DARKBLUE = 9, 23 | GREEN = 10, 24 | CYAN = 11, 25 | RED = 12, 26 | PINK = 13, 27 | YELLOW = 14, 28 | }; 29 | 30 | class GUIManager { 31 | public: 32 | // Initializes the GUI manager 33 | GUIManager(); 34 | ~GUIManager(); 35 | 36 | void RenderCallback(std::function callback) { 37 | this->renderCallback = callback; 38 | } 39 | 40 | // Runs the GUI manager 41 | // @note This function will block until the window is closed 42 | void Run() const; 43 | 44 | GLFWwindow* window; 45 | std::function renderCallback; 46 | 47 | ModTarget target = ModTarget::Game; 48 | 49 | enum class Tab { 50 | MyMods, 51 | Discover, 52 | Console, 53 | Settings, 54 | } tab{ Tab::MyMods }; 55 | }; 56 | }; // namespace Carbon 57 | 58 | void _GUI(); 59 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/rpc_connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "connection.h" 4 | #include "serialization.h" 5 | 6 | // I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much 7 | // smaller. 8 | constexpr size_t MaxRpcFrameSize = 64 * 1024; 9 | 10 | struct RpcConnection { 11 | enum class ErrorCode : int { 12 | Success = 0, 13 | PipeClosed = 1, 14 | ReadCorrupt = 2, 15 | }; 16 | 17 | enum class Opcode : uint32_t { 18 | Handshake = 0, 19 | Frame = 1, 20 | Close = 2, 21 | Ping = 3, 22 | Pong = 4, 23 | }; 24 | 25 | struct MessageFrameHeader { 26 | Opcode opcode; 27 | uint32_t length; 28 | }; 29 | 30 | struct MessageFrame : public MessageFrameHeader { 31 | char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)]; 32 | }; 33 | 34 | enum class State : uint32_t { 35 | Disconnected, 36 | SentHandshake, 37 | AwaitingResponse, 38 | Connected, 39 | }; 40 | 41 | BaseConnection* connection{nullptr}; 42 | State state{State::Disconnected}; 43 | void (*onConnect)(JsonDocument& message){nullptr}; 44 | void (*onDisconnect)(int errorCode, const char* message){nullptr}; 45 | char appId[64]{}; 46 | int lastErrorCode{0}; 47 | char lastErrorMessage[256]{}; 48 | RpcConnection::MessageFrame sendFrame; 49 | 50 | static RpcConnection* Create(const char* applicationId); 51 | static void Destroy(RpcConnection*&); 52 | 53 | inline bool IsOpen() const { return state == State::Connected; } 54 | 55 | void Open(); 56 | void Close(); 57 | bool Write(const void* data, size_t length); 58 | bool Read(JsonDocument& message); 59 | }; 60 | -------------------------------------------------------------------------------- /CarbonSupervisor/src/main.cpp: -------------------------------------------------------------------------------- 1 | #define WINDOWS_LEAN_AND_MEAN 2 | #include 3 | 4 | #include "sm.h" 5 | #include "utils.h" 6 | #include "globals.h" 7 | 8 | /* 9 | * DLL main thread 10 | * 11 | * @param lpParam The parameter, in this case the module handle 12 | */ 13 | DWORD WINAPI DllMainThread(LPVOID lpParam) { 14 | HMODULE hModule = static_cast(lpParam); // Get the module handle to our DLL 15 | Utils::InitLogging(); // Setup our custom logging 16 | 17 | auto contraption = SM::Contraption::GetInstance(); // Contraption is the game's main singleton 18 | auto pipe = Utils::Pipe::GetInstance(); // Connection to launcher 19 | 20 | contraption->console->Hook(); // Hook the console, sending log messages to the launcher 21 | contraption->WaitForStateEgress(LOADING); // Wait for the game to finish loading 22 | 23 | pipe->SendPacket(LOADED); // Inform the launcher we have loaded 24 | 25 | // Whenever the state changes, send a packet informing the launcher 26 | contraption->OnStateChange([&pipe](ContraptionState state) { 27 | pipe->SendPacket(STATECHANGE, static_cast(state)); 28 | }); 29 | 30 | // Quit the thread, we never get here because the game closes the process 31 | // but it's good practice to have a clean exit 32 | FreeLibraryAndExitThread(hModule, 0); 33 | return 0; 34 | } 35 | 36 | /* 37 | * DLL entry point 38 | * 39 | * @param hModule The module handle 40 | * @param ulReason The reason for calling this function 41 | * @param lpReserved Reserved 42 | * @return The exit code of the application 43 | */ 44 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReason, LPVOID lpReserved) { 45 | if (ulReason == DLL_PROCESS_ATTACH) { 46 | DisableThreadLibraryCalls(hModule); 47 | CreateThread(nullptr, 0, DllMainThread, hModule, 0, nullptr); 48 | } 49 | 50 | return TRUE; 51 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Artifacts 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | env: 11 | SOLUTION_FILE_PATH: . 12 | BUILD_CONFIGURATION: Release 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | build: 19 | runs-on: windows-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Setup VCPKG 25 | uses: lukka/run-vcpkg@v11 26 | with: 27 | vcpkgGitCommitId: aafb8b71554a4590aa5108bdb5005d81d72db6c1 28 | 29 | - name: Integrate vcpkg 30 | run: vcpkg integrate install 31 | 32 | - name: Add MSBuild to PATH 33 | uses: microsoft/setup-msbuild@v1.0.2 34 | 35 | - name: Build 36 | working-directory: ${{env.GITHUB_WORKSPACE}} 37 | run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} 38 | 39 | - name: Upload build artifacts 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: CarbonLauncher_${{env.BUILD_CONFIGURATION}} 43 | path: | 44 | x64\${{env.BUILD_CONFIGURATION}}\ 45 | 46 | - name: Create ZIP of Artifacts 47 | run: | 48 | $zipPath = "release\artifacts.zip" 49 | $files = Get-ChildItem -Path "x64\${{env.BUILD_CONFIGURATION}}" -Filter *.dll -Recurse 50 | $files += "x64\${{env.BUILD_CONFIGURATION}}\CarbonLauncher.exe" 51 | if (!(Test-Path -Path "release")) { New-Item -ItemType Directory -Path "release" } 52 | Compress-Archive -Path $files -DestinationPath $zipPath 53 | 54 | - name: Create Release 55 | uses: ncipollo/release-action@v1.14.0 56 | with: 57 | artifacts: release\artifacts.zip 58 | draft: true 59 | makeLatest: true 60 | tag: dev 61 | -------------------------------------------------------------------------------- /CarbonLauncher/include/modmanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | constexpr const char* REPOS_URL = "https://github.com/ScrappySM/CarbonRepo/raw/refs/heads/main/repos.json"; 10 | 11 | namespace Carbon { 12 | struct Mod { 13 | // Name of the mode 14 | std::string name; 15 | 16 | // A list of all the authors of the mod 17 | std::vector authors; 18 | 19 | // A short description of the mod 20 | std::string description; 21 | 22 | // The link to the mods GitHub page 23 | std::string ghUser; 24 | std::string ghRepo; 25 | 26 | bool installed = false; 27 | bool wantsUpdate = false; 28 | 29 | void Install(); 30 | void Uninstall(); 31 | void Update(); 32 | }; 33 | 34 | enum ModTarget { 35 | Game, // Scrap Mechanic 36 | ModTool, // Scrap Mechanic Mod Tool 37 | }; 38 | 39 | class ModManager { 40 | public: 41 | // Initializes the RepoManager and downloads the repos.json file 42 | ModManager(); 43 | ~ModManager(); 44 | 45 | // Converts a JSON object to a Repo object 46 | // @param json The JSON object to convert 47 | // @return The converted Repo object (`json` -> `Repo`) 48 | std::pair, std::vector> URLToMods(const std::string& url); 49 | 50 | // Gets all the repos 51 | // @return A vector of all the repos 52 | std::vector& GetMods(ModTarget target) { 53 | std::lock_guard lock(this->repoMutex); 54 | return target == ModTarget::Game ? gameMods : modToolMods; 55 | } 56 | 57 | bool hasLoaded = false; 58 | 59 | private: 60 | std::vector gameMods; 61 | std::vector modToolMods; 62 | 63 | std::optional JSONToMod(const nlohmann::json& jMod); 64 | std::mutex repoMutex; 65 | }; 66 | }; // namespace Carbon 67 | -------------------------------------------------------------------------------- /CarbonLauncher/include/pipemanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace Carbon { 14 | // The type of packet 15 | enum class PacketType { 16 | LOADED, // Sent when the game is loaded 17 | STATECHANGE, // Sent when the game state changes (e.g. menu -> game) 18 | LOG, // (e.g.game log [will be implemented later]) 19 | UNKNOWNTYPE // Unknown packet type 20 | }; 21 | 22 | // A packet sent over the pipe 23 | class Packet { 24 | public: 25 | PacketType type = PacketType::UNKNOWNTYPE; 26 | std::optional data; 27 | }; 28 | 29 | // Manages the pipe connection to the game 30 | class PipeManager { 31 | public: 32 | // Initializes the pipe manager and starts a thread 33 | // listening for packets from the game 34 | PipeManager(); 35 | ~PipeManager(); 36 | 37 | // Gets all the packets received from the game 38 | // @return A vector of all the packets received from the game 39 | // @note This function is thread-safe 40 | std::vector& GetPackets() { 41 | std::lock_guard lock(this->pipeMutex); 42 | return this->packets; 43 | } 44 | 45 | bool IsConnected() const { return this->connected; } 46 | void SetConnected(bool connected) { this->connected = connected; } 47 | 48 | // Gets all the packets of a specific type, removing them from the vector 49 | // @param packet The type of packet to get 50 | // @return A queue of all the packets of the specified type 51 | std::queue GetPacketsByType(PacketType packet); 52 | 53 | private: 54 | // Used to read packets from the pipe 55 | std::mutex pipeMutex; 56 | std::thread pipeReader; 57 | std::vector packets = {}; 58 | 59 | bool connected = false; 60 | }; 61 | }; // namespace Carbon 62 | -------------------------------------------------------------------------------- /CarbonLauncher/include/state.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "guimanager.h" 7 | #include "discordmanager.h" 8 | #include "processmanager.h" 9 | #include "pipemanager.h" 10 | #include "modmanager.h" 11 | 12 | namespace Carbon { 13 | struct LogMessage { 14 | int colour = 0; 15 | std::string message; 16 | std::string time; 17 | }; 18 | 19 | struct Settings { 20 | //std::string dataDir = "C:\\Program Files\\CarbonLauncher"; // Needs admin permissions 21 | std::string dataDir = "C:\\Users\\" + std::string(getenv("USERNAME")) + "\\AppData\\Local\\CarbonLauncher\\"; // Doesn't need admin permissions 22 | }; 23 | 24 | // The main state of the Carbon Launcher 25 | // Contains all the managers and the process target 26 | class CarbonState_t { 27 | public: 28 | CarbonState_t() { 29 | //#ifndef NDEBUG 30 | AllocConsole(); 31 | FILE* file; 32 | freopen_s(&file, "CONOUT$", "w", stdout); 33 | 34 | auto console = spdlog::stdout_color_mt("carbon"); // Create a new logger with color support 35 | spdlog::set_default_logger(console); // Set the default logger to the console logger 36 | console->set_level(spdlog::level::trace); // Set the log level to info 37 | spdlog::set_pattern("%^[ %H:%M:%S | %-8l] %n: %v%$"); // Nice log format 38 | //#endif 39 | } 40 | 41 | Carbon::GUIManager guiManager; 42 | Carbon::DiscordManager discordManager; 43 | Carbon::ProcessManager processManager; 44 | Carbon::PipeManager pipeManager; 45 | Carbon::ModManager modManager; 46 | 47 | // The settings for the Carbon Launcher 48 | Carbon::Settings settings; 49 | 50 | std::vector logMessages; 51 | 52 | // The target process to manage (e.g. ScrapMechanic.exe) 53 | // This should never be a process that does not have a Contraption 54 | // located in the same place as ScrapMechanic.exe 55 | // const char* processTarget = "ScrapMechanic.exe"; 56 | // const char* processTarget = "DummyGame.exe"; 57 | }; 58 | }; // namespace Carbon 59 | 60 | 61 | extern Carbon::CarbonState_t C; 62 | -------------------------------------------------------------------------------- /CarbonSupervisor/include/sm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | enum ContraptionState { 11 | LOADING = 1, 12 | IN_GAME = 2, 13 | MAIN_MENU = 3 14 | }; 15 | 16 | namespace SM { 17 | const uintptr_t ContraptionOffset = 0x1267538; 18 | 19 | struct LogMessage { 20 | int colour; 21 | std::string message; 22 | }; 23 | 24 | class Console { 25 | private: 26 | virtual ~Console() {} 27 | 28 | public: 29 | virtual void Log(const std::string&, WORD colour, WORD LogType) = 0; 30 | virtual void LogNoRepeat(const std::string&, WORD colour, WORD LogType) = 0; 31 | 32 | void Hook(); 33 | }; 34 | 35 | class Contraption { 36 | private: 37 | /* 0x0000 */ char pad_0x0000[0x58]; 38 | public: 39 | /* 0x0058 */ Console* console; 40 | private: 41 | /* 0x0060 */ char pad_0x0060[0x11C]; 42 | public: 43 | /* 0x017C */ int state; 44 | 45 | public: 46 | static Contraption* GetInstance() { 47 | // TODO sig scan for this (90 48 89 05 ? ? ? ? + 0x4 @ Contraption) 48 | auto contraption = *reinterpret_cast((uintptr_t)GetModuleHandle(nullptr) + ContraptionOffset); 49 | while (contraption == nullptr || contraption->state < LOADING || contraption->state > 3) { 50 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 51 | contraption = SM::Contraption::GetInstance(); 52 | 53 | static int i = 0; 54 | if (i++ % 30 == 0) { // Every 3 seconds 55 | spdlog::warn("Waiting for Contraption..."); 56 | } 57 | } 58 | 59 | return contraption; 60 | } 61 | 62 | void WaitForStateEgress(ContraptionState state) const { 63 | while (this->state == (int)state) { 64 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 65 | } 66 | } 67 | 68 | void OnStateChange(std::function callback) const { 69 | while (true) { 70 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 71 | static int lastState = 0; 72 | if (lastState != this->state) { 73 | lastState = this->state; 74 | callback((ContraptionState)lastState); 75 | } 76 | } 77 | } 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /CarbonLauncher/include/processmanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace Carbon { 14 | // Manages the game process including starting and stopping it 15 | // along with monitoring the game's executable and loaded modules 16 | class ProcessManager { 17 | public: 18 | // Initializes the game manager and starts a thread 19 | // listening for the game process 20 | ProcessManager(); 21 | ~ProcessManager(); 22 | 23 | // Checks if the game is running 24 | // @return True if the game is running, false otherwise 25 | bool IsGameRunning(); 26 | 27 | // Injects a module into the game process 28 | // @param modulePath The path to the module to inject 29 | void InjectModule(const std::string& modulePath); 30 | 31 | // Starts the game and waits for it to be running 32 | // This function will block until the game is running 33 | // This function spawns a new thread to actually start the game process 34 | void LaunchProcess(const std::string& name); 35 | 36 | // Stops the game forcefully 37 | void KillGame(); 38 | 39 | // Checks if a module is loaded in the game process 40 | // @param moduleName The name of the module to check 41 | // @return True if the module is loaded, false otherwise 42 | bool IsModuleLoaded(const std::string& moduleName) const; 43 | 44 | // Gets all the loaded custom modules 45 | // @return A vector of all the loaded custom modules (mods injected via CarbonLauncher) 46 | int GetLoadedCustomModules() const; 47 | 48 | private: 49 | // Checks every 1s if the game is running 50 | std::thread gameStatusThread; 51 | 52 | // Mutex to protect the game status 53 | std::mutex gameStatusMutex; 54 | 55 | // Checks every 1s for game injected modules 56 | std::thread moduleHandlerThread; 57 | 58 | // Tracks when the game was first seen as running by the GameManager 59 | std::optional> gameStartedTime; 60 | 61 | // A list of all the loaded modules in the game process 62 | std::vector modules; 63 | bool gameRunning = false; 64 | 65 | // The PID of the game process 66 | DWORD pid = 0; 67 | 68 | // Amount of custom modules loaded (excluding the supervisor) 69 | std::optional loadedCustomModules = std::nullopt; 70 | }; 71 | }; // namespace Carbon 72 | -------------------------------------------------------------------------------- /CarbonSupervisor/src/console.cpp: -------------------------------------------------------------------------------- 1 | #include "sm.h" 2 | #include "utils.h" 3 | #include "globals.h" 4 | 5 | using LogFunction = void(SM::Console::*)(const std::string&, WORD, WORD); 6 | LogFunction oLogFunction; 7 | LogFunction oLogNoRepeatFunction; 8 | 9 | static void HookedLog(SM::Console* console, const std::string& message, WORD colour, WORD type) { 10 | static Globals_t* globals = Globals_t::GetInstance(); 11 | globals->logMessages.emplace(LogMessage{ colour, message }); 12 | (console->*oLogFunction)(message, colour, type); 13 | } 14 | 15 | static void HookedLogNoRepeat(SM::Console* console, const std::string& message, WORD colour, WORD type) { 16 | static Globals_t* globals = Globals_t::GetInstance(); 17 | globals->logMessages.emplace(LogMessage{ colour, message }); 18 | (console->*oLogNoRepeatFunction)(message, colour, type); 19 | } 20 | 21 | static void HookVTableFunc(void** vtable, int index, void* newFunction, void** originalFunction = nullptr) { 22 | DWORD oldProtect; 23 | VirtualProtect(&vtable[index], sizeof(void*), PAGE_EXECUTE_READWRITE, &oldProtect); 24 | if (originalFunction) *originalFunction = vtable[index]; 25 | vtable[index] = newFunction; 26 | VirtualProtect(&vtable[index], sizeof(void*), oldProtect, &oldProtect); 27 | } 28 | 29 | void SM::Console::Hook() { 30 | spdlog::info("Hooking console functions"); 31 | 32 | // Hook the consoles `Log` and `LogNoRepeat` functions (to the same thing) 33 | // Make them send a packet to the pipe with LOG:-: 34 | void** vtable = *reinterpret_cast(this); 35 | HookVTableFunc(vtable, 1, HookedLog, reinterpret_cast(&oLogFunction)); 36 | HookVTableFunc(vtable, 2, HookedLogNoRepeat, reinterpret_cast(&oLogNoRepeatFunction)); 37 | 38 | // Start a thread to send log messages to the launcher 39 | std::thread([]() { 40 | auto pipe = Utils::Pipe::GetInstance(); 41 | static Globals_t* globals = Globals_t::GetInstance(); 42 | auto& logMessages = globals->logMessages; 43 | 44 | while (true) { 45 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 46 | if (!logMessages.empty()) { 47 | auto& message = logMessages.front(); 48 | pipe->SendPacket(LOG, fmt::format("{}-|-{}", message.colour, message.message)); 49 | logMessages.pop(); 50 | } 51 | } 52 | }).detach(); 53 | } 54 | 55 | void Utils::InitLogging() { 56 | AllocConsole(); 57 | freopen_s(reinterpret_cast(stdout), "CONOUT$", "w", stdout); 58 | 59 | auto console = spdlog::stdout_color_mt("carbon"); 60 | spdlog::set_default_logger(console); 61 | spdlog::set_level(spdlog::level::trace); 62 | spdlog::set_pattern("%^[ %H:%M:%S | %-8l] %n: %v%$"); 63 | } 64 | -------------------------------------------------------------------------------- /CarbonLauncher.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.13.35507.96 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CarbonLauncher", "CarbonLauncher\CarbonLauncher.vcxproj", "{F55718C8-63D5-4CCA-8499-07500910C9BC}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CarbonSupervisor", "CarbonSupervisor\CarbonSupervisor.vcxproj", "{97DA7043-249B-4EEA-9116-78A77D28DD84}" 9 | EndProject 10 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DummyGame", "DummyGame\DummyGame.vcxproj", "{8E68D329-FE3C-4D80-A0A9-5968F3020754}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|x64 = Debug|x64 15 | Debug|x86 = Debug|x86 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Debug|x64.ActiveCfg = Debug|x64 21 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Debug|x64.Build.0 = Debug|x64 22 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Debug|x86.ActiveCfg = Debug|Win32 23 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Debug|x86.Build.0 = Debug|Win32 24 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Release|x64.ActiveCfg = Release|x64 25 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Release|x64.Build.0 = Release|x64 26 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Release|x86.ActiveCfg = Release|Win32 27 | {F55718C8-63D5-4CCA-8499-07500910C9BC}.Release|x86.Build.0 = Release|Win32 28 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Debug|x64.ActiveCfg = Debug|x64 29 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Debug|x64.Build.0 = Debug|x64 30 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Debug|x86.ActiveCfg = Debug|Win32 31 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Debug|x86.Build.0 = Debug|Win32 32 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Release|x64.ActiveCfg = Release|x64 33 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Release|x64.Build.0 = Release|x64 34 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Release|x86.ActiveCfg = Release|Win32 35 | {97DA7043-249B-4EEA-9116-78A77D28DD84}.Release|x86.Build.0 = Release|Win32 36 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Debug|x64.ActiveCfg = Debug|x64 37 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Debug|x64.Build.0 = Debug|x64 38 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Debug|x86.ActiveCfg = Debug|Win32 39 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Debug|x86.Build.0 = Debug|Win32 40 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Release|x64.ActiveCfg = Release|x64 41 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Release|x64.Build.0 = Release|x64 42 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Release|x86.ActiveCfg = Release|Win32 43 | {8E68D329-FE3C-4D80-A0A9-5968F3020754}.Release|x86.Build.0 = Release|Win32 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {5C1163F1-0D28-415C-BAC3-80138FA2F668} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /CarbonSupervisor/src/pipe.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include "sm.h" 3 | 4 | using namespace Utils; 5 | 6 | void Pipe::ResetPipe(bool reInformLauncher) { 7 | pipe = CreateFile( 8 | "\\\\.\\pipe\\CarbonPipe", 9 | GENERIC_WRITE, 10 | 0, 11 | nullptr, 12 | OPEN_EXISTING, 13 | 0, 14 | nullptr 15 | ); 16 | 17 | // Send the contraption state change so that the launcher knows the game didn't crash 18 | if (reInformLauncher) { 19 | SendPacket(STATECHANGE, std::to_string(SM::Contraption::GetInstance()->state)); 20 | } 21 | } 22 | 23 | void Pipe::ValidatePipe() { 24 | if (pipe != nullptr && pipe != INVALID_HANDLE_VALUE) { 25 | return; 26 | } 27 | 28 | bool isPipeBroken = GetLastError() != ERROR_SUCCESS; 29 | 30 | // Continuously attempt to reconnect to the pipe 31 | while (pipe == nullptr || pipe == INVALID_HANDLE_VALUE || isPipeBroken) { 32 | ResetPipe(false); 33 | isPipeBroken = GetLastError() != ERROR_SUCCESS; 34 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 35 | } 36 | 37 | isPipeBroken = GetLastError() != ERROR_SUCCESS; 38 | if (isPipeBroken) { 39 | spdlog::error("Failed to reconnect to pipe"); 40 | } 41 | } 42 | 43 | void Pipe::SendPacket(PacketType packetType, int data) { 44 | SendPacket(packetType, std::to_string(data)); 45 | } 46 | 47 | void Pipe::SendPacket(PacketType packetType) { 48 | SendPacket(packetType, ""); 49 | } 50 | 51 | // TODO: Fix messages too large causing the pipe to break 52 | void Pipe::SendPacket(PacketType packetType, const std::string& data) { 53 | if (data.size() > static_cast(1024) * 9) { // anything over 9kb is ridiculous 54 | spdlog::warn("Packet data too large, size: {}", data.size()); 55 | SendPacket(packetType, fmt::format("{}... (truncated)", data.substr(0, 1024 * 8))); 56 | return; 57 | } 58 | 59 | std::lock_guard lock(logMutex); 60 | 61 | ValidatePipe(); 62 | 63 | auto send = [&](const std::string& packet) { 64 | DWORD bytesWritten = 0; 65 | BOOL res = false; 66 | res = WriteFile(pipe, packet.c_str(), (DWORD)packet.size(), &bytesWritten, nullptr); 67 | if (!res) { 68 | int error = GetLastError(); 69 | char* errorStr = nullptr; 70 | FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 71 | nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorStr, 0, nullptr); 72 | // Trim the \n off the end 73 | errorStr[strlen(errorStr) - 2] = '\0'; 74 | spdlog::error("Failed to write to pipe: {} ({})", errorStr, error); 75 | LocalFree((HLOCAL)errorStr); 76 | ResetPipe(true); 77 | return; 78 | } 79 | spdlog::info("Sent packet: {}", packet); 80 | }; 81 | 82 | std::string packetStr; 83 | switch (packetType) { 84 | case LOADED: 85 | packetStr = "LOADED-:-"; 86 | break; 87 | case STATECHANGE: 88 | packetStr = "STATECHANGE-:-" + data; 89 | break; 90 | case LOG: 91 | packetStr = "LOG-:-" + data; 92 | break; 93 | default: 94 | packetStr = "UNKNOWNTYPE-:-" + data; 95 | break; 96 | } 97 | send(packetStr); 98 | } 99 | 100 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/include/discord_rpc.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // clang-format off 5 | 6 | #if defined(DISCORD_DYNAMIC_LIB) 7 | # if defined(_WIN32) 8 | # if defined(DISCORD_BUILDING_SDK) 9 | # define DISCORD_EXPORT __declspec(dllexport) 10 | # else 11 | # define DISCORD_EXPORT __declspec(dllimport) 12 | # endif 13 | # else 14 | # define DISCORD_EXPORT __attribute__((visibility("default"))) 15 | # endif 16 | #else 17 | # define DISCORD_EXPORT 18 | #endif 19 | 20 | // clang-format on 21 | 22 | #ifdef __cplusplus 23 | extern "C" { 24 | #endif 25 | 26 | typedef struct DiscordRichPresence { 27 | const char* details; /* max 128 bytes */ 28 | const char* state; /* max 128 bytes */ 29 | int64_t startTimestamp; 30 | int64_t endTimestamp; 31 | const char* largeImageKey; /* max 32 bytes */ 32 | const char* largeImageText; /* max 128 bytes */ 33 | const char* smallImageKey; /* max 32 bytes */ 34 | const char* smallImageText; /* max 128 bytes */ 35 | const char* partyId; /* max 128 bytes */ 36 | int partySize; 37 | int partyMax; 38 | int partyPrivacy; 39 | const char* matchSecret; /* max 128 bytes */ 40 | const char* joinSecret; /* max 128 bytes */ 41 | const char* spectateSecret; /* max 128 bytes */ 42 | const char* button1Label; /* max 32 bytes */ 43 | const char* button1Url; /* max 512 bytes */ 44 | const char* button2Label; /* max 32 bytes */ 45 | const char* button2Url; /* max 512 bytes */ 46 | int8_t instance; 47 | } DiscordRichPresence; 48 | 49 | typedef struct DiscordUser { 50 | const char* userId; 51 | const char* username; 52 | const char* discriminator; 53 | const char* avatar; 54 | } DiscordUser; 55 | 56 | typedef struct DiscordEventHandlers { 57 | void (*ready)(const DiscordUser* request); 58 | void (*disconnected)(int errorCode, const char* message); 59 | void (*errored)(int errorCode, const char* message); 60 | void (*joinGame)(const char* joinSecret); 61 | void (*spectateGame)(const char* spectateSecret); 62 | void (*joinRequest)(const DiscordUser* request); 63 | } DiscordEventHandlers; 64 | 65 | #define DISCORD_REPLY_NO 0 66 | #define DISCORD_REPLY_YES 1 67 | #define DISCORD_REPLY_IGNORE 2 68 | #define DISCORD_PARTY_PRIVATE 0 69 | #define DISCORD_PARTY_PUBLIC 1 70 | 71 | DISCORD_EXPORT void Discord_Initialize(const char* applicationId, 72 | DiscordEventHandlers* handlers, 73 | int autoRegister, 74 | const char* optionalSteamId); 75 | DISCORD_EXPORT void Discord_Shutdown(void); 76 | 77 | /* checks for incoming messages, dispatches callbacks */ 78 | DISCORD_EXPORT void Discord_RunCallbacks(void); 79 | 80 | /* If you disable the lib starting its own io thread, you'll need to call this from your own */ 81 | #ifdef DISCORD_DISABLE_IO_THREAD 82 | DISCORD_EXPORT void Discord_UpdateConnection(void); 83 | #endif 84 | 85 | DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence); 86 | DISCORD_EXPORT void Discord_ClearPresence(void); 87 | 88 | DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); 89 | 90 | DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers); 91 | 92 | #ifdef __cplusplus 93 | } /* extern "C" */ 94 | #endif 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carbon Launcher 2 | 3 | > [!WARNING] 4 | > This is v1, it is being deprecated as the [v2 branch](https://github.com/ScrappySM/CarbonLauncher/tree/tauri) is being worked on so please do not expect updates here (unless they are major issues) 5 | 6 | ![Carbon Launcher](screenshot.png) 7 | 8 | **Carbon Launcher** is a powerful and user-friendly modding launcher for [Scrap Mechanic](https://www.scrapmechanic.com/), designed to support DLL mods and provide cloud synchronization for an enhanced modding experience. With Carbon Launcher, players can seamlessly download and manage DLL mods from a variety of online repositories, creating a vast library of modding content directly accessible from the launcher. 9 | 10 | Whether you're a veteran modder or a newcomer, Carbon Launcher simplifies the process of discovering, installing, and managing mods with automatic updates and a streamlined user interface. 11 | 12 | ## Features 13 | 14 | - **Seamless Installation & Uninstallation:** Installing mods is as simple as a few clicks, and removing them is just as easy. 15 | - **Auto-Updates:** Mods are kept up to date with minimal effort. Any new versions or updates from the repository are automatically fetched and installed. 16 | - **Community Support:** Participate in the growing modding community by submitting your own mods, or use the launcher to browse and contribute to shared content. 17 | - **Game console forwarding:** Launch the game and watch it's output flow through the launcher allowing you to spot any issues with mods even if the game closes immediately. 18 | 19 | ## Installation 20 | 21 | ### Prerequisites 22 | 23 | - Visual C++ Redistrutables 24 | - OpenGL support 25 | 26 | ### Steps to Install: 27 | 28 | 1. **Download** the latest release of Carbon Launcher from [here](https://github.com/ScrappySM/CarbonLauncher/releases/latest). 29 | 2. **Extract** the downloaded zip file to a desired location on your system. 30 | 3. Run **CarbonLauncher.exe** to start the application. 31 | 32 | ## Contributing 33 | 34 | **Carbon Launcher** is an open-source project, and contributions are welcome! 35 | 36 | If you’d like to contribute, here’s how: 37 | 38 | ### Reporting Issues 39 | If you encounter any bugs or have suggestions, feel free to open an issue in the issue tracker on GitHub. 40 | 41 | ### Getting your mod added 42 | Open an issue [here](https://github.com/ScrappySM/CarbonRepo/issues) with the following information: 43 | - **Mod Name:** The name of your mod. 44 | - **Mod Description:** A brief description of your mod. 45 | 46 | Some requirements for your mod: 47 | - It **must** have it's DLLs as a release on GitHub and it must be published by a GitHub action. (this is to make malware less likely) 48 | - By extension of the above, it **must** be open source. 49 | 50 | Some recommendations for your mod: 51 | - Your mod has a `manifest.json` file, an example one can be found [here](https://github.com/ScrappySM/ModTemplate/blob/main/manifest.json). 52 | - Provide information on how to update your mod if applicable, an example can be found [here](https://github.com/ScrappySM/DevCheckBypass/blob/main/dllmain.cpp). 53 | 54 | ### Ideas & Improvements 55 | We encourage feedback and suggestions. If you have any ideas for new features or improvements, please open an issue and share your thoughts! 56 | 57 | ## Known Issues 58 | 59 | - **Compatibility:** Some mods may not be compatible with certain game versions. Most mods will simply tell you this when you start the game, just be patient and wait for the modders to update their mods, thanks! 60 | 61 | ## License 62 | 63 | Carbon Launcher is open source and distributed under the [MIT License](LICENSE). 64 | 65 | ## Acknowledgements 66 | 67 | - Thanks to all contributors for their hard work and dedication. 68 | - Special thanks to the Scrap Mechanic modding community for their continued support and creativity. 69 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/connection_win.cpp: -------------------------------------------------------------------------------- 1 | #include "connection.h" 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define NOMCX 5 | #define NOSERVICE 6 | #define NOIME 7 | #include 8 | #include 9 | 10 | int GetProcessId() 11 | { 12 | return (int)::GetCurrentProcessId(); 13 | } 14 | 15 | struct BaseConnectionWin : public BaseConnection { 16 | HANDLE pipe{INVALID_HANDLE_VALUE}; 17 | }; 18 | 19 | static BaseConnectionWin Connection; 20 | 21 | /*static*/ BaseConnection* BaseConnection::Create() 22 | { 23 | return &Connection; 24 | } 25 | 26 | /*static*/ void BaseConnection::Destroy(BaseConnection*& c) 27 | { 28 | auto self = reinterpret_cast(c); 29 | self->Close(); 30 | c = nullptr; 31 | } 32 | 33 | bool BaseConnection::Open() 34 | { 35 | wchar_t pipeName[]{L"\\\\?\\pipe\\discord-ipc-0"}; 36 | const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2; 37 | pipeName[pipeDigit] = L'0'; 38 | auto self = reinterpret_cast(this); 39 | for (;;) { 40 | self->pipe = ::CreateFileW( 41 | pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); 42 | if (self->pipe != INVALID_HANDLE_VALUE) { 43 | self->isOpen = true; 44 | return true; 45 | } 46 | 47 | auto lastError = GetLastError(); 48 | if (lastError == ERROR_FILE_NOT_FOUND) { 49 | if (pipeName[pipeDigit] < L'9') { 50 | pipeName[pipeDigit]++; 51 | continue; 52 | } 53 | } 54 | else if (lastError == ERROR_PIPE_BUSY) { 55 | if (!WaitNamedPipeW(pipeName, 10000)) { 56 | return false; 57 | } 58 | continue; 59 | } 60 | return false; 61 | } 62 | } 63 | 64 | bool BaseConnection::Close() 65 | { 66 | auto self = reinterpret_cast(this); 67 | ::CloseHandle(self->pipe); 68 | self->pipe = INVALID_HANDLE_VALUE; 69 | self->isOpen = false; 70 | return true; 71 | } 72 | 73 | bool BaseConnection::Write(const void* data, size_t length) 74 | { 75 | if (length == 0) { 76 | return true; 77 | } 78 | auto self = reinterpret_cast(this); 79 | assert(self); 80 | if (!self) { 81 | return false; 82 | } 83 | if (self->pipe == INVALID_HANDLE_VALUE) { 84 | return false; 85 | } 86 | assert(data); 87 | if (!data) { 88 | return false; 89 | } 90 | const DWORD bytesLength = (DWORD)length; 91 | DWORD bytesWritten = 0; 92 | return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && 93 | bytesWritten == bytesLength; 94 | } 95 | 96 | bool BaseConnection::Read(void* data, size_t length) 97 | { 98 | assert(data); 99 | if (!data) { 100 | return false; 101 | } 102 | auto self = reinterpret_cast(this); 103 | assert(self); 104 | if (!self) { 105 | return false; 106 | } 107 | if (self->pipe == INVALID_HANDLE_VALUE) { 108 | return false; 109 | } 110 | DWORD bytesAvailable = 0; 111 | if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) { 112 | if (bytesAvailable >= length) { 113 | DWORD bytesToRead = (DWORD)length; 114 | DWORD bytesRead = 0; 115 | if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) { 116 | assert(bytesToRead == bytesRead); 117 | return true; 118 | } 119 | else { 120 | Close(); 121 | } 122 | } 123 | } 124 | else { 125 | Close(); 126 | } 127 | return false; 128 | } 129 | -------------------------------------------------------------------------------- /CarbonLauncher/src/discordmanager.cpp: -------------------------------------------------------------------------------- 1 | #include "discordmanager.h" 2 | #include "guimanager.h" 3 | #include "state.h" 4 | 5 | #include 6 | #include 7 | 8 | constexpr auto discordClientId = "1315436867545595904"; 9 | 10 | using namespace Carbon; 11 | 12 | DiscordManager::DiscordManager() { 13 | DiscordEventHandlers handlers; 14 | memset(&handlers, 0, sizeof(handlers)); 15 | handlers.ready = [](const DiscordUser* connectedUser) { 16 | spdlog::info("Discord connected to user {}", connectedUser->username); 17 | }; 18 | handlers.disconnected = [](int errorCode, const char* message) { 19 | spdlog::warn("Discord disconnected with error code {}: {}", errorCode, message); 20 | }; 21 | handlers.errored = [](int errorCode, const char* message) { 22 | spdlog::error("Discord error with code {}: {}", errorCode, message); 23 | }; 24 | handlers.joinGame = [](const char* secret) { 25 | spdlog::info("Joining game with secret {}", secret); 26 | }; 27 | handlers.spectateGame = [](const char* secret) { 28 | spdlog::info("Spectating game with secret {}", secret); 29 | }; 30 | handlers.joinRequest = [](const DiscordUser* request) { 31 | spdlog::info("Join request from {}#{}", request->username, request->discriminator); 32 | }; 33 | Discord_Initialize(discordClientId, &handlers, 1, NULL); 34 | this->discordHandlers = handlers; 35 | 36 | spdlog::info("Initialized Discord instance"); 37 | 38 | this->discordPresence = DiscordRichPresence{ 39 | .details = "The most advanced mod loader for Scrap Mechanic", 40 | .state = "In the launcher", 41 | .largeImageKey = "icon", 42 | .largeImageText = "Carbon Launcher", 43 | .button1Label = "GitHub", 44 | .button1Url = "https://github.com/ScrappySM/CarbonLauncher", 45 | .button2Label = "Download", 46 | .button2Url = "https://github.com/ScrappySM/CarbonLauncher/releases/latest", 47 | .instance = 0, 48 | }; 49 | 50 | Discord_UpdatePresence(&this->discordPresence); 51 | 52 | spdlog::info("Updated Discord presence"); 53 | 54 | Discord_UpdateHandlers(&handlers); 55 | } 56 | 57 | void DiscordManager::UpdateState(const std::string& state) { 58 | this->discordPresence.state = state.c_str(); 59 | Discord_UpdatePresence(&this->discordPresence); 60 | } 61 | 62 | void DiscordManager::UpdateDetails(const std::string& details) { 63 | this->discordPresence.details = details.c_str(); 64 | Discord_UpdatePresence(&this->discordPresence); 65 | } 66 | 67 | DiscordManager::~DiscordManager() { 68 | spdlog::info("Destroying Discord instance"); 69 | } 70 | 71 | DiscordRichPresence& DiscordManager::GetPresence() { 72 | return this->discordPresence; 73 | } 74 | 75 | void DiscordManager::Update() { 76 | auto statePackets = C.pipeManager.GetPacketsByType(PacketType::STATECHANGE); 77 | if (!statePackets.empty()) { 78 | auto& packet = statePackets.front(); 79 | 80 | if (!packet.data.has_value()) { 81 | spdlog::warn("Received state change packet with no data"); 82 | statePackets.pop(); 83 | return; 84 | } 85 | 86 | int state = 0; 87 | try { 88 | state = std::stoi(packet.data.value()); 89 | } 90 | catch (std::invalid_argument& e) { 91 | spdlog::error("Failed to parse state change packet: {}", e.what()); 92 | statePackets.pop(); 93 | return; 94 | } 95 | 96 | switch (state) { 97 | case 1: 98 | spdlog::trace("In a loading screen"); 99 | this->UpdateState("In a loading screen"); 100 | break; 101 | case 2: 102 | spdlog::trace("In a game!"); 103 | 104 | this->UpdateState(fmt::format("In a game! ({} mods loaded)", C.processManager.GetLoadedCustomModules())); 105 | break; 106 | case 3: 107 | spdlog::trace("In the main menu"); 108 | this->UpdateState(fmt::format("In the main menu with {} mods loaded", C.processManager.GetLoadedCustomModules())); 109 | break; 110 | }; 111 | 112 | statePackets.pop(); 113 | } 114 | 115 | Discord_RunCallbacks(); 116 | Discord_UpdateHandlers(&this->discordHandlers); 117 | } -------------------------------------------------------------------------------- /DummyGame/src/main.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | typedef unsigned int uint4_t; 16 | 17 | // LoadLibraryA function pointer 18 | // typedef HMODULE(WINAPI* LoadLibraryA_t)(LPCSTR lpLibFileName); 19 | 20 | // LoadLibraryW function pointer 21 | typedef HMODULE(WINAPI* LoadLibraryW_t)(_In_ LPCWSTR lpLibFileName); 22 | static LoadLibraryW_t oLoadLibraryW = nullptr; 23 | 24 | struct Contraption_t { 25 | /* 0x000 */ char pad_056[0x17C]; // this is genuinely random in the game 26 | /* 0x17C */ uint4_t gameStateType = 0; // This is random at first but managed 27 | /* 0x180 */ char pad_004[0x20]; // this is genuinely random in the game (we are coa 28 | /* 0x1A0 */ HWND hWnd; // this is random in the game at first but managed 29 | }; 30 | 31 | #ifdef NDEBUG 32 | static char pad[0x1267538 - 0x1DC30] = { 0 }; // offset it to replicate the game's memory layout 33 | #else 34 | static_assert(false, "This is only for the release build"); 35 | #endif 36 | 37 | static Contraption_t* Contraption = new Contraption_t(); 38 | 39 | int main() { 40 | spdlog::set_level(spdlog::level::trace); 41 | 42 | // Stop the compiler optimizing the pad array 43 | if (pad[0] == 1) { 44 | spdlog::critical("This should never be printed"); 45 | } 46 | 47 | // Get a pointer to the pointer and print it so we can adjust it to be the same as the game 48 | Contraption_t** ContraptionPtr = &Contraption; 49 | 50 | // Hook LoadLibraryW so we can detect and block DLL injection (we need to block it 51 | // because since we aren't the real game so mods could crash, this is only for testing, 52 | // so seeing the mods injecting isn't important) 53 | MH_Initialize(); 54 | MH_CreateHook(&LoadLibraryW, (LPVOID)(LoadLibraryW_t)[](LPCWSTR lpLibFileName)->HMODULE { 55 | std::wstring libFileName(lpLibFileName); 56 | std::string libFileNameStr(libFileName.begin(), libFileName.end()); 57 | 58 | // Check if `Carbon` is in the DLL name 59 | if (libFileNameStr.find("Carbon") != std::string::npos) { 60 | spdlog::info("Allowing DLL injection of {}", libFileNameStr); 61 | return oLoadLibraryW(lpLibFileName); 62 | } 63 | 64 | spdlog::trace("Blocking DLL injection of {}", libFileNameStr); 65 | return nullptr; 66 | }, (LPVOID*)&oLoadLibraryW); 67 | 68 | MH_EnableHook(&LoadLibraryW); 69 | spdlog::info("Allowing one DLL injection..."); 70 | 71 | spdlog::info("Contraption: {}", reinterpret_cast((uintptr_t)ContraptionPtr - (uintptr_t)GetModuleHandle(nullptr))); 72 | 73 | // Wait 5s 74 | std::this_thread::sleep_for(std::chrono::seconds(5)); 75 | 76 | HINSTANCE hInst = GetModuleHandle(nullptr); 77 | 78 | HWND newHwnd = CreateWindowExA(0, "STATIC", "Dummy Game", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1280, 720, NULL, NULL, hInst, NULL); 79 | 80 | // Save hwnd to the Contraption struct and set the game state type to 1 (loading screen) 81 | Contraption->hWnd = (HWND)newHwnd; 82 | Contraption->gameStateType = 1; 83 | 84 | // Wait 2s 85 | std::this_thread::sleep_for(std::chrono::seconds(2)); 86 | 87 | // Set the game state type to 2 (in-game) 88 | Contraption->gameStateType = 2; 89 | 90 | spdlog::info("Game state type: {}", Contraption->gameStateType); 91 | 92 | while (!(GetAsyncKeyState(VK_HOME) & 1 && GetAsyncKeyState(VK_END)) & 1) { 93 | // Fx sets contraption game state type to x 94 | if (GetAsyncKeyState(VK_F1) & 1) { 95 | Contraption->gameStateType = 1; 96 | spdlog::trace("Game state type is now 1"); 97 | } 98 | 99 | if (GetAsyncKeyState(VK_F2) & 1) { 100 | Contraption->gameStateType = 2; 101 | spdlog::trace("Game state type is now 2"); 102 | } 103 | 104 | if (GetAsyncKeyState(VK_F3) & 1) { 105 | Contraption->gameStateType = 3; 106 | spdlog::trace("Game state type is now 3"); 107 | } 108 | 109 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 110 | } 111 | 112 | MH_DisableHook(&LoadLibraryW); 113 | MH_Uninitialize(); 114 | 115 | return 0; 116 | } -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/rpc_connection.cpp: -------------------------------------------------------------------------------- 1 | #include "rpc_connection.h" 2 | #include "serialization.h" 3 | 4 | #include 5 | 6 | static const int RpcVersion = 1; 7 | static RpcConnection Instance; 8 | 9 | /*static*/ RpcConnection* RpcConnection::Create(const char* applicationId) 10 | { 11 | Instance.connection = BaseConnection::Create(); 12 | StringCopy(Instance.appId, applicationId); 13 | return &Instance; 14 | } 15 | 16 | /*static*/ void RpcConnection::Destroy(RpcConnection*& c) 17 | { 18 | c->Close(); 19 | BaseConnection::Destroy(c->connection); 20 | c = nullptr; 21 | } 22 | 23 | void RpcConnection::Open() 24 | { 25 | if (state == State::Connected) { 26 | return; 27 | } 28 | 29 | if (state == State::Disconnected && !connection->Open()) { 30 | return; 31 | } 32 | 33 | if (state == State::SentHandshake) { 34 | JsonDocument message; 35 | if (Read(message)) { 36 | auto cmd = GetStrMember(&message, "cmd"); 37 | auto evt = GetStrMember(&message, "evt"); 38 | if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) { 39 | state = State::Connected; 40 | if (onConnect) { 41 | onConnect(message); 42 | } 43 | } 44 | } 45 | } 46 | else { 47 | sendFrame.opcode = Opcode::Handshake; 48 | sendFrame.length = (uint32_t)JsonWriteHandshakeObj( 49 | sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId); 50 | 51 | if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) { 52 | state = State::SentHandshake; 53 | } 54 | else { 55 | Close(); 56 | } 57 | } 58 | } 59 | 60 | void RpcConnection::Close() 61 | { 62 | if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) { 63 | onDisconnect(lastErrorCode, lastErrorMessage); 64 | } 65 | connection->Close(); 66 | state = State::Disconnected; 67 | } 68 | 69 | bool RpcConnection::Write(const void* data, size_t length) 70 | { 71 | sendFrame.opcode = Opcode::Frame; 72 | memcpy(sendFrame.message, data, length); 73 | sendFrame.length = (uint32_t)length; 74 | if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) { 75 | Close(); 76 | return false; 77 | } 78 | return true; 79 | } 80 | 81 | bool RpcConnection::Read(JsonDocument& message) 82 | { 83 | if (state != State::Connected && state != State::SentHandshake) { 84 | return false; 85 | } 86 | MessageFrame readFrame; 87 | for (;;) { 88 | bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader)); 89 | if (!didRead) { 90 | if (!connection->isOpen) { 91 | lastErrorCode = (int)ErrorCode::PipeClosed; 92 | StringCopy(lastErrorMessage, "Pipe closed"); 93 | Close(); 94 | } 95 | return false; 96 | } 97 | 98 | if (readFrame.length > 0) { 99 | didRead = connection->Read(readFrame.message, readFrame.length); 100 | if (!didRead) { 101 | lastErrorCode = (int)ErrorCode::ReadCorrupt; 102 | StringCopy(lastErrorMessage, "Partial data in frame"); 103 | Close(); 104 | return false; 105 | } 106 | readFrame.message[readFrame.length] = 0; 107 | } 108 | 109 | switch (readFrame.opcode) { 110 | case Opcode::Close: { 111 | message.ParseInsitu(readFrame.message); 112 | lastErrorCode = GetIntMember(&message, "code"); 113 | StringCopy(lastErrorMessage, GetStrMember(&message, "message", "")); 114 | Close(); 115 | return false; 116 | } 117 | case Opcode::Frame: 118 | message.ParseInsitu(readFrame.message); 119 | return true; 120 | case Opcode::Ping: 121 | readFrame.opcode = Opcode::Pong; 122 | if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) { 123 | Close(); 124 | } 125 | break; 126 | case Opcode::Pong: 127 | break; 128 | case Opcode::Handshake: 129 | default: 130 | // something bad happened 131 | lastErrorCode = (int)ErrorCode::ReadCorrupt; 132 | StringCopy(lastErrorMessage, "Bad ipc frame"); 133 | Close(); 134 | return false; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${PROJECT_SOURCE_DIR}/include) 2 | 3 | option(ENABLE_IO_THREAD "Start up a separate I/O thread, otherwise I'd need to call an update function" ON) 4 | option(USE_STATIC_CRT "Use /MT[d] for dynamic library" OFF) 5 | option(WARNINGS_AS_ERRORS "When enabled, compiles with `-Werror` (on *nix platforms)." OFF) 6 | 7 | set(CMAKE_CXX_STANDARD 14) 8 | 9 | set(BASE_RPC_SRC 10 | ${PROJECT_SOURCE_DIR}/include/discord_rpc.h 11 | discord_rpc.cpp 12 | ${PROJECT_SOURCE_DIR}/include/discord_register.h 13 | rpc_connection.h 14 | rpc_connection.cpp 15 | serialization.h 16 | serialization.cpp 17 | connection.h 18 | backoff.h 19 | msg_queue.h 20 | ) 21 | 22 | if (${BUILD_SHARED_LIBS}) 23 | if(WIN32) 24 | set(BASE_RPC_SRC ${BASE_RPC_SRC} dllmain.cpp) 25 | endif(WIN32) 26 | endif(${BUILD_SHARED_LIBS}) 27 | 28 | if(WIN32) 29 | add_definitions(-DDISCORD_WINDOWS) 30 | set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_win.cpp discord_register_win.cpp) 31 | add_library(discord-rpc ${BASE_RPC_SRC}) 32 | if (MSVC) 33 | if(USE_STATIC_CRT) 34 | foreach(CompilerFlag 35 | CMAKE_CXX_FLAGS 36 | CMAKE_CXX_FLAGS_DEBUG 37 | CMAKE_CXX_FLAGS_RELEASE 38 | CMAKE_C_FLAGS 39 | CMAKE_C_FLAGS_DEBUG 40 | CMAKE_C_FLAGS_RELEASE) 41 | string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") 42 | endforeach() 43 | endif(USE_STATIC_CRT) 44 | target_compile_options(discord-rpc PRIVATE /EHsc 45 | /Wall 46 | /wd4100 # unreferenced formal parameter 47 | /wd4514 # unreferenced inline 48 | /wd4625 # copy constructor deleted 49 | /wd5026 # move constructor deleted 50 | /wd4626 # move assignment operator deleted 51 | /wd4668 # not defined preprocessor macro 52 | /wd4710 # function not inlined 53 | /wd4711 # function was inlined 54 | /wd4820 # structure padding 55 | /wd4946 # reinterpret_cast used between related classes 56 | /wd5027 # move assignment operator was implicitly defined as deleted 57 | ) 58 | endif(MSVC) 59 | target_link_libraries(discord-rpc PRIVATE psapi advapi32) 60 | endif(WIN32) 61 | 62 | if(UNIX) 63 | set(BASE_RPC_SRC ${BASE_RPC_SRC} connection_unix.cpp) 64 | 65 | if (APPLE) 66 | add_definitions(-DDISCORD_OSX) 67 | set(BASE_RPC_SRC ${BASE_RPC_SRC} discord_register_osx.m) 68 | else (APPLE) 69 | add_definitions(-DDISCORD_LINUX) 70 | set(BASE_RPC_SRC ${BASE_RPC_SRC} discord_register_linux.cpp) 71 | endif(APPLE) 72 | 73 | add_library(discord-rpc ${BASE_RPC_SRC}) 74 | target_link_libraries(discord-rpc PUBLIC pthread) 75 | 76 | if (APPLE) 77 | target_link_libraries(discord-rpc PRIVATE "-framework AppKit, -mmacosx-version-min=10.10") 78 | endif (APPLE) 79 | 80 | target_compile_options(discord-rpc PRIVATE 81 | -g 82 | -Wall 83 | -Wextra 84 | -Wpedantic 85 | ) 86 | 87 | if (${WARNINGS_AS_ERRORS}) 88 | target_compile_options(discord-rpc PRIVATE -Werror) 89 | endif (${WARNINGS_AS_ERRORS}) 90 | 91 | target_compile_options(discord-rpc PRIVATE 92 | -Wno-unknown-pragmas # pragma push thing doesn't work on clang 93 | -Wno-old-style-cast # it's fine 94 | -Wno-c++98-compat # that was almost 2 decades ago 95 | -Wno-c++98-compat-pedantic 96 | -Wno-missing-noreturn 97 | -Wno-padded # structure padding 98 | -Wno-covered-switch-default 99 | -Wno-exit-time-destructors # not sure about these 100 | -Wno-global-constructors 101 | ) 102 | 103 | if (${BUILD_SHARED_LIBS}) 104 | target_compile_options(discord-rpc PRIVATE -fPIC) 105 | endif (${BUILD_SHARED_LIBS}) 106 | 107 | if (APPLE) 108 | target_link_libraries(discord-rpc PRIVATE "-framework AppKit") 109 | endif (APPLE) 110 | endif(UNIX) 111 | 112 | target_include_directories(discord-rpc PRIVATE ${RAPIDJSON}/include) 113 | 114 | if (NOT ${ENABLE_IO_THREAD}) 115 | target_compile_definitions(discord-rpc PUBLIC -DDISCORD_DISABLE_IO_THREAD) 116 | endif (NOT ${ENABLE_IO_THREAD}) 117 | 118 | if (${BUILD_SHARED_LIBS}) 119 | target_compile_definitions(discord-rpc PUBLIC -DDISCORD_DYNAMIC_LIB) 120 | target_compile_definitions(discord-rpc PRIVATE -DDISCORD_BUILDING_SDK) 121 | endif(${BUILD_SHARED_LIBS}) 122 | 123 | if (CLANG_FORMAT_CMD) 124 | add_dependencies(discord-rpc clangformat) 125 | endif(CLANG_FORMAT_CMD) 126 | 127 | # install 128 | 129 | install( 130 | TARGETS discord-rpc 131 | EXPORT "discord-rpc" 132 | RUNTIME 133 | DESTINATION "${CMAKE_INSTALL_BINDIR}" 134 | LIBRARY 135 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 136 | ARCHIVE 137 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 138 | INCLUDES 139 | DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" 140 | ) 141 | 142 | install( 143 | FILES 144 | "../include/discord_rpc.h" 145 | "../include/discord_register.h" 146 | DESTINATION "include" 147 | ) 148 | -------------------------------------------------------------------------------- /CarbonLauncher/src/pipemanager.cpp: -------------------------------------------------------------------------------- 1 | #include "pipemanager.h" 2 | #include "state.h" 3 | 4 | #include 5 | 6 | #include 7 | 8 | using namespace Carbon; 9 | 10 | static constexpr int BUFFER_SIZE = 1024 * 10; 11 | 12 | PipeManager::PipeManager() { 13 | this->pipeReader = std::thread([this]() { 14 | // Create a pipe that *other* processes can connect to 15 | auto pipe = CreateNamedPipe( 16 | L"\\\\.\\pipe\\CarbonPipe", 17 | PIPE_ACCESS_INBOUND, 18 | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 19 | PIPE_UNLIMITED_INSTANCES, 20 | BUFFER_SIZE, 21 | BUFFER_SIZE, 22 | 0, 23 | NULL 24 | ); 25 | 26 | if (pipe == INVALID_HANDLE_VALUE) { 27 | spdlog::error("Failed to create pipe"); 28 | return; 29 | } 30 | 31 | if (!ConnectNamedPipe(pipe, NULL)) { 32 | spdlog::error("Failed to connect to pipe"); 33 | CloseHandle(pipe); 34 | return; 35 | } 36 | 37 | spdlog::info("Connected to pipe"); 38 | SetConnected(true); 39 | 40 | // Infinitely read and parse packets and add them to the queue 41 | while (true) { 42 | static char* buffer = new char[BUFFER_SIZE]; 43 | DWORD bytesRead = 0; 44 | auto res = ReadFile(pipe, buffer, BUFFER_SIZE, &bytesRead, NULL); 45 | if (!res) { 46 | SetConnected(false); 47 | 48 | //spdlog::error("Failed to read from pipe"); 49 | int code = GetLastError(); 50 | const char* error = nullptr; 51 | FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 52 | nullptr, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&error, 0, nullptr); 53 | spdlog::error("Failed to read from pipe: {}", error); 54 | LocalFree((HLOCAL)error); 55 | 56 | C.discordManager.UpdateState("In the launcher!"); 57 | 58 | // In this case, it is most likely the supervisor process has closed the pipe 59 | // We should wait for the supervisor to reconnect 60 | 61 | // Close the pipe 62 | CloseHandle(pipe); 63 | 64 | // Wait for the supervisor to reconnect 65 | pipe = CreateNamedPipe( 66 | L"\\\\.\\pipe\\CarbonPipe", 67 | PIPE_ACCESS_INBOUND, 68 | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 69 | PIPE_UNLIMITED_INSTANCES, 70 | 1024 * 10, 71 | 1024 * 10, 72 | PIPE_WAIT, 73 | NULL 74 | ); 75 | 76 | if (pipe == INVALID_HANDLE_VALUE) { 77 | spdlog::error("Failed to create pipe"); 78 | SetConnected(false); 79 | return; 80 | } 81 | 82 | if (!ConnectNamedPipe(pipe, NULL)) { 83 | spdlog::error("Failed to connect to pipe"); 84 | SetConnected(false); 85 | CloseHandle(pipe); 86 | return; 87 | } 88 | 89 | spdlog::info("Reconnected to pipe"); 90 | C.pipeManager.SetConnected(true); 91 | continue; 92 | } 93 | 94 | // Parse the packet 95 | std::string packet(buffer, bytesRead); 96 | auto delimiter = packet.find("-:-"); 97 | if (delimiter == std::string::npos) { 98 | spdlog::error("Invalid packet received, data: `{}`", packet); 99 | continue; 100 | } 101 | auto type = packet.substr(0, delimiter); 102 | auto data = packet.substr(delimiter + 3); 103 | PacketType packetType = PacketType::UNKNOWNTYPE; 104 | if (type == "LOADED") { 105 | packetType = PacketType::LOADED; 106 | } 107 | else if (type == "STATECHANGE") { 108 | packetType = PacketType::STATECHANGE; 109 | } 110 | else if (type == "LOG") { 111 | packetType = PacketType::LOG; 112 | 113 | std::time_t currentTime = std::time(nullptr); 114 | std::tm timeInfo; 115 | 116 | localtime_s(&timeInfo, ¤tTime); 117 | 118 | // HH::MM::SS 119 | std::string time = fmt::format("{:02}:{:02}:{:02}", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec); 120 | 121 | // Split on -|- to get the log colour 122 | auto colourDelimiter = data.find("-|-"); 123 | auto colour = data.substr(0, colourDelimiter); 124 | data = data.substr(colourDelimiter + 3); 125 | 126 | LogMessage logMessage; 127 | logMessage.colour = std::stoi(colour); 128 | logMessage.message = data; 129 | logMessage.time = time; 130 | C.logMessages.emplace_back(logMessage); 131 | 132 | // If we have more than 200 messages, remove the oldest one 133 | if (C.logMessages.size() > 200) { 134 | C.logMessages.erase(C.logMessages.begin()); 135 | } 136 | 137 | // Early return here, we don't want to add the log packet to the queue 138 | continue; 139 | } 140 | else { 141 | spdlog::error("Unknown packet type received, data: `{}`", packet); 142 | spdlog::trace("G-L : `{}` @ {}", !data.empty() ? data : "null", type); 143 | continue; 144 | } 145 | 146 | spdlog::trace("G-L : `{}` @ {}", !data.empty() ? data : "null", type); 147 | 148 | Packet parsedPacket; 149 | parsedPacket.type = packetType; 150 | parsedPacket.data = data; 151 | 152 | if (data.empty()) { 153 | parsedPacket.data = std::nullopt; 154 | } 155 | 156 | { 157 | std::lock_guard lock(this->pipeMutex); 158 | this->packets.push_back(parsedPacket); 159 | } 160 | 161 | // Allow the thread some breathing room 162 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 163 | } 164 | }); 165 | this->pipeReader.detach(); 166 | } 167 | 168 | PipeManager::~PipeManager() {} 169 | 170 | std::queue PipeManager::GetPacketsByType(PacketType packet) { 171 | std::queue filteredPackets; 172 | std::lock_guard lock(this->pipeMutex); 173 | 174 | for (auto& currentPacket : this->packets) { 175 | if (currentPacket.type == packet) { 176 | filteredPackets.push(currentPacket); 177 | 178 | // Delete the packet from the vector 179 | for (auto it = this->packets.begin(); it != this->packets.end(); ++it) { 180 | if (it->type == packet) { 181 | this->packets.erase(it); 182 | break; 183 | } 184 | } 185 | } 186 | } 187 | 188 | return filteredPackets; 189 | } -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/discord_register_win.cpp: -------------------------------------------------------------------------------- 1 | #include "discord_rpc.h" 2 | #include "discord_register.h" 3 | 4 | #define WIN32_LEAN_AND_MEAN 5 | #define NOMCX 6 | #define NOSERVICE 7 | #define NOIME 8 | #include 9 | #include 10 | #include 11 | 12 | /** 13 | * Updated fixes for MinGW and WinXP 14 | * This block is written the way it does not involve changing the rest of the code 15 | * Checked to be compiling 16 | * 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW 17 | * #include guarded, functions redirected to substitutes 18 | * 2) RegSetKeyValueW and LSTATUS are not declared in 19 | * The entire function is rewritten 20 | */ 21 | #ifdef __MINGW32__ 22 | #include 23 | /// strsafe.h fixes 24 | static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) 25 | { 26 | HRESULT ret; 27 | va_list va; 28 | va_start(va, pszFormat); 29 | cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault 30 | // othervise 31 | ret = vsnwprintf(pszDest, cbDest, pszFormat, va); 32 | pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned 33 | va_end(va); 34 | return ret; 35 | } 36 | #else 37 | #include 38 | #include 39 | #endif // __MINGW32__ 40 | 41 | /// winreg.h fixes 42 | #ifndef LSTATUS 43 | #define LSTATUS LONG 44 | #endif 45 | #ifdef RegSetKeyValueW 46 | #undefine RegSetKeyValueW 47 | #endif 48 | #define RegSetKeyValueW regset 49 | static LSTATUS regset(HKEY hkey, 50 | LPCWSTR subkey, 51 | LPCWSTR name, 52 | DWORD type, 53 | const void* data, 54 | DWORD len) 55 | { 56 | HKEY htkey = hkey, hsubkey = nullptr; 57 | LSTATUS ret; 58 | if (subkey && subkey[0]) { 59 | if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) != 60 | ERROR_SUCCESS) 61 | return ret; 62 | htkey = hsubkey; 63 | } 64 | ret = RegSetValueExW(htkey, name, 0, type, (const BYTE*)data, len); 65 | if (hsubkey && hsubkey != hkey) 66 | RegCloseKey(hsubkey); 67 | return ret; 68 | } 69 | 70 | static void Discord_RegisterW(const wchar_t* applicationId, const wchar_t* command) 71 | { 72 | // https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx 73 | // we want to register games so we can run them as discord-:// 74 | // Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions. 75 | 76 | wchar_t exeFilePath[MAX_PATH]; 77 | DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH); 78 | wchar_t openCommand[1024]; 79 | 80 | if (command && command[0]) { 81 | StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command); 82 | } 83 | else { 84 | // StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath); 85 | StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath); 86 | } 87 | 88 | wchar_t protocolName[64]; 89 | StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId); 90 | wchar_t protocolDescription[128]; 91 | StringCbPrintfW( 92 | protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId); 93 | wchar_t urlProtocol = 0; 94 | 95 | wchar_t keyName[256]; 96 | StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName); 97 | HKEY key; 98 | auto status = 99 | RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr); 100 | if (status != ERROR_SUCCESS) { 101 | fprintf(stderr, "Error creating key\n"); 102 | return; 103 | } 104 | DWORD len; 105 | LSTATUS result; 106 | len = (DWORD)lstrlenW(protocolDescription) + 1; 107 | result = 108 | RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t)); 109 | if (FAILED(result)) { 110 | fprintf(stderr, "Error writing description\n"); 111 | } 112 | 113 | len = (DWORD)lstrlenW(protocolDescription) + 1; 114 | result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t)); 115 | if (FAILED(result)) { 116 | fprintf(stderr, "Error writing description\n"); 117 | } 118 | 119 | result = RegSetKeyValueW( 120 | key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t)); 121 | if (FAILED(result)) { 122 | fprintf(stderr, "Error writing icon\n"); 123 | } 124 | 125 | len = (DWORD)lstrlenW(openCommand) + 1; 126 | result = RegSetKeyValueW( 127 | key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t)); 128 | if (FAILED(result)) { 129 | fprintf(stderr, "Error writing command\n"); 130 | } 131 | RegCloseKey(key); 132 | } 133 | 134 | extern "C" DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command) 135 | { 136 | wchar_t appId[32]; 137 | MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); 138 | 139 | wchar_t openCommand[1024]; 140 | const wchar_t* wcommand = nullptr; 141 | if (command && command[0]) { 142 | const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand); 143 | MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen); 144 | wcommand = openCommand; 145 | } 146 | 147 | Discord_RegisterW(appId, wcommand); 148 | } 149 | 150 | extern "C" DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, 151 | const char* steamId) 152 | { 153 | wchar_t appId[32]; 154 | MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); 155 | 156 | wchar_t wSteamId[32]; 157 | MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32); 158 | 159 | HKEY key; 160 | auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key); 161 | if (status != ERROR_SUCCESS) { 162 | fprintf(stderr, "Error opening Steam key\n"); 163 | return; 164 | } 165 | 166 | wchar_t steamPath[MAX_PATH]; 167 | DWORD pathBytes = sizeof(steamPath); 168 | status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE*)steamPath, &pathBytes); 169 | RegCloseKey(key); 170 | if (status != ERROR_SUCCESS || pathBytes < 1) { 171 | fprintf(stderr, "Error reading SteamExe key\n"); 172 | return; 173 | } 174 | 175 | DWORD pathChars = pathBytes / sizeof(wchar_t); 176 | for (DWORD i = 0; i < pathChars; ++i) { 177 | if (steamPath[i] == L'/') { 178 | steamPath[i] = L'\\'; 179 | } 180 | } 181 | 182 | wchar_t command[1024]; 183 | StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId); 184 | 185 | Discord_RegisterW(appId, command); 186 | } 187 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/serialization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifndef __MINGW32__ 6 | #pragma warning(push) 7 | 8 | #pragma warning(disable : 4061) // enum is not explicitly handled by a case label 9 | #pragma warning(disable : 4365) // signed/unsigned mismatch 10 | #pragma warning(disable : 4464) // relative include path contains 11 | #pragma warning(disable : 4668) // is not defined as a preprocessor macro 12 | #pragma warning(disable : 6313) // Incorrect operator 13 | #endif // __MINGW32__ 14 | 15 | #include "rapidjson/document.h" 16 | #include "rapidjson/stringbuffer.h" 17 | #include "rapidjson/writer.h" 18 | 19 | #ifndef __MINGW32__ 20 | #pragma warning(pop) 21 | #endif // __MINGW32__ 22 | 23 | // if only there was a standard library function for this 24 | template 25 | inline size_t StringCopy(char (&dest)[Len], const char* src) 26 | { 27 | if (!src || !Len) { 28 | return 0; 29 | } 30 | size_t copied; 31 | char* out = dest; 32 | for (copied = 1; *src && copied < Len; ++copied) { 33 | *out++ = *src++; 34 | } 35 | *out = 0; 36 | return copied - 1; 37 | } 38 | 39 | size_t JsonWriteHandshakeObj(char* dest, size_t maxLen, int version, const char* applicationId); 40 | 41 | // Commands 42 | struct DiscordRichPresence; 43 | size_t JsonWriteRichPresenceObj(char* dest, 44 | size_t maxLen, 45 | int nonce, 46 | int pid, 47 | const DiscordRichPresence* presence); 48 | size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName); 49 | 50 | size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName); 51 | 52 | size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce); 53 | 54 | // I want to use as few allocations as I can get away with, and to do that with RapidJson, you need 55 | // to supply some of your own allocators for stuff rather than use the defaults 56 | 57 | class LinearAllocator { 58 | public: 59 | char* buffer_; 60 | char* end_; 61 | LinearAllocator() 62 | { 63 | assert(0); // needed for some default case in rapidjson, should not use 64 | } 65 | LinearAllocator(char* buffer, size_t size) 66 | : buffer_(buffer) 67 | , end_(buffer + size) 68 | { 69 | } 70 | static const bool kNeedFree = false; 71 | void* Malloc(size_t size) 72 | { 73 | char* res = buffer_; 74 | buffer_ += size; 75 | if (buffer_ > end_) { 76 | buffer_ = res; 77 | return nullptr; 78 | } 79 | return res; 80 | } 81 | void* Realloc(void* originalPtr, size_t originalSize, size_t newSize) 82 | { 83 | if (newSize == 0) { 84 | return nullptr; 85 | } 86 | // allocate how much you need in the first place 87 | assert(!originalPtr && !originalSize); 88 | // unused parameter warning 89 | (void)(originalPtr); 90 | (void)(originalSize); 91 | return Malloc(newSize); 92 | } 93 | static void Free(void* ptr) 94 | { 95 | /* shrug */ 96 | (void)ptr; 97 | } 98 | }; 99 | 100 | template 101 | class FixedLinearAllocator : public LinearAllocator { 102 | public: 103 | char fixedBuffer_[Size]; 104 | FixedLinearAllocator() 105 | : LinearAllocator(fixedBuffer_, Size) 106 | { 107 | } 108 | static const bool kNeedFree = false; 109 | }; 110 | 111 | // wonder why this isn't a thing already, maybe I missed it 112 | class DirectStringBuffer { 113 | public: 114 | using Ch = char; 115 | char* buffer_; 116 | char* end_; 117 | char* current_; 118 | 119 | DirectStringBuffer(char* buffer, size_t maxLen) 120 | : buffer_(buffer) 121 | , end_(buffer + maxLen) 122 | , current_(buffer) 123 | { 124 | } 125 | 126 | void Put(char c) 127 | { 128 | if (current_ < end_) { 129 | *current_++ = c; 130 | } 131 | } 132 | void Flush() {} 133 | size_t GetSize() const { return (size_t)(current_ - buffer_); } 134 | }; 135 | 136 | using MallocAllocator = rapidjson::CrtAllocator; 137 | using PoolAllocator = rapidjson::MemoryPoolAllocator; 138 | using UTF8 = rapidjson::UTF8; 139 | // Writer appears to need about 16 bytes per nested object level (with 64bit size_t) 140 | using StackAllocator = FixedLinearAllocator<2048>; 141 | constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t)); 142 | using JsonWriterBase = 143 | rapidjson::Writer; 144 | class JsonWriter : public JsonWriterBase { 145 | public: 146 | DirectStringBuffer stringBuffer_; 147 | StackAllocator stackAlloc_; 148 | 149 | JsonWriter(char* dest, size_t maxLen) 150 | : JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels) 151 | , stringBuffer_(dest, maxLen) 152 | , stackAlloc_() 153 | { 154 | } 155 | 156 | size_t Size() const { return stringBuffer_.GetSize(); } 157 | }; 158 | 159 | using JsonDocumentBase = rapidjson::GenericDocument; 160 | class JsonDocument : public JsonDocumentBase { 161 | public: 162 | static const int kDefaultChunkCapacity = 32 * 1024; 163 | // json parser will use this buffer first, then allocate more if needed; I seriously doubt we 164 | // send any messages that would use all of this, though. 165 | char parseBuffer_[32 * 1024]; 166 | MallocAllocator mallocAllocator_; 167 | PoolAllocator poolAllocator_; 168 | StackAllocator stackAllocator_; 169 | JsonDocument() 170 | : JsonDocumentBase(rapidjson::kObjectType, 171 | &poolAllocator_, 172 | sizeof(stackAllocator_.fixedBuffer_), 173 | &stackAllocator_) 174 | , poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_) 175 | , stackAllocator_() 176 | { 177 | } 178 | }; 179 | 180 | using JsonValue = rapidjson::GenericValue; 181 | 182 | inline JsonValue* GetObjMember(JsonValue* obj, const char* name) 183 | { 184 | if (obj) { 185 | auto member = obj->FindMember(name); 186 | if (member != obj->MemberEnd() && member->value.IsObject()) { 187 | return &member->value; 188 | } 189 | } 190 | return nullptr; 191 | } 192 | 193 | inline int GetIntMember(JsonValue* obj, const char* name, int notFoundDefault = 0) 194 | { 195 | if (obj) { 196 | auto member = obj->FindMember(name); 197 | if (member != obj->MemberEnd() && member->value.IsInt()) { 198 | return member->value.GetInt(); 199 | } 200 | } 201 | return notFoundDefault; 202 | } 203 | 204 | inline const char* GetStrMember(JsonValue* obj, 205 | const char* name, 206 | const char* notFoundDefault = nullptr) 207 | { 208 | if (obj) { 209 | auto member = obj->FindMember(name); 210 | if (member != obj->MemberEnd() && member->value.IsString()) { 211 | return member->value.GetString(); 212 | } 213 | } 214 | return notFoundDefault; 215 | } 216 | -------------------------------------------------------------------------------- /DummyGame/DummyGame.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {8e68d329-fe3c-4d80-a0a9-5968f3020754} 25 | DummyGame 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | false 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | false 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | 78 | Level3 79 | true 80 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 81 | true 82 | stdcpp20 83 | include 84 | /utf-8 %(AdditionalOptions) 85 | true 86 | 87 | 88 | Console 89 | true 90 | 91 | 92 | 93 | 94 | Level3 95 | true 96 | true 97 | true 98 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 99 | true 100 | stdcpp20 101 | include 102 | /utf-8 %(AdditionalOptions) 103 | 104 | 105 | Console 106 | true 107 | true 108 | true 109 | 110 | 111 | 112 | 113 | Level3 114 | true 115 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 116 | true 117 | stdcpp20 118 | include 119 | /utf-8 %(AdditionalOptions) 120 | true 121 | true 122 | 123 | 124 | Console 125 | true 126 | 127 | 128 | 129 | 130 | Level3 131 | true 132 | true 133 | true 134 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 135 | true 136 | stdcpp20 137 | include 138 | /utf-8 %(AdditionalOptions) 139 | 140 | 141 | Console 142 | true 143 | true 144 | true 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /CarbonSupervisor/CarbonSupervisor.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {97da7043-249b-4eea-9116-78a77d28dd84} 25 | CarbonSupervisor 26 | 10.0 27 | 28 | 29 | 30 | DynamicLibrary 31 | true 32 | v143 33 | MultiByte 34 | 35 | 36 | DynamicLibrary 37 | false 38 | v143 39 | true 40 | MultiByte 41 | 42 | 43 | DynamicLibrary 44 | true 45 | v143 46 | MultiByte 47 | 48 | 49 | DynamicLibrary 50 | false 51 | v143 52 | true 53 | MultiByte 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | true 78 | 79 | 80 | true 81 | 82 | 83 | true 84 | 85 | 86 | true 87 | 88 | 89 | 90 | Level3 91 | true 92 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 93 | true 94 | stdcpp20 95 | include 96 | /utf-8 %(AdditionalOptions) 97 | MultiThreadedDebug 98 | 99 | 100 | Windows 101 | true 102 | 103 | 104 | 105 | 106 | Level3 107 | true 108 | true 109 | true 110 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 111 | true 112 | stdcpp20 113 | include 114 | /utf-8 %(AdditionalOptions) 115 | MultiThreaded 116 | 117 | 118 | Windows 119 | true 120 | true 121 | true 122 | 123 | 124 | 125 | 126 | Level3 127 | true 128 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 129 | true 130 | stdcpp20 131 | include 132 | /utf-8 %(AdditionalOptions) 133 | MultiThreadedDebug 134 | 135 | 136 | Windows 137 | true 138 | 139 | 140 | 141 | 142 | Level3 143 | true 144 | true 145 | true 146 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 147 | true 148 | stdcpp20 149 | include 150 | /utf-8 %(AdditionalOptions) 151 | MultiThreaded 152 | 153 | 154 | Windows 155 | true 156 | true 157 | true 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/serialization.cpp: -------------------------------------------------------------------------------- 1 | #include "serialization.h" 2 | #include "connection.h" 3 | #include "discord_rpc.h" 4 | 5 | template 6 | void NumberToString(char* dest, T number) 7 | { 8 | if (!number) { 9 | *dest++ = '0'; 10 | *dest++ = 0; 11 | return; 12 | } 13 | if (number < 0) { 14 | *dest++ = '-'; 15 | number = -number; 16 | } 17 | char temp[32]; 18 | int place = 0; 19 | while (number) { 20 | auto digit = number % 10; 21 | number = number / 10; 22 | temp[place++] = '0' + (char)digit; 23 | } 24 | for (--place; place >= 0; --place) { 25 | *dest++ = temp[place]; 26 | } 27 | *dest = 0; 28 | } 29 | 30 | // it's ever so slightly faster to not have to strlen the key 31 | template 32 | void WriteKey(JsonWriter& w, T& k) 33 | { 34 | w.Key(k, sizeof(T) - 1); 35 | } 36 | 37 | struct WriteObject { 38 | JsonWriter& writer; 39 | WriteObject(JsonWriter& w) 40 | : writer(w) 41 | { 42 | writer.StartObject(); 43 | } 44 | template 45 | WriteObject(JsonWriter& w, T& name) 46 | : writer(w) 47 | { 48 | WriteKey(writer, name); 49 | writer.StartObject(); 50 | } 51 | ~WriteObject() { writer.EndObject(); } 52 | }; 53 | 54 | struct WriteArray { 55 | JsonWriter& writer; 56 | template 57 | WriteArray(JsonWriter& w, T& name) 58 | : writer(w) 59 | { 60 | WriteKey(writer, name); 61 | writer.StartArray(); 62 | } 63 | ~WriteArray() { writer.EndArray(); } 64 | }; 65 | 66 | template 67 | void WriteOptionalString(JsonWriter& w, T& k, const char* value) 68 | { 69 | if (value && value[0]) { 70 | w.Key(k, sizeof(T) - 1); 71 | w.String(value); 72 | } 73 | } 74 | 75 | static void JsonWriteNonce(JsonWriter& writer, int nonce) 76 | { 77 | WriteKey(writer, "nonce"); 78 | char nonceBuffer[32]; 79 | NumberToString(nonceBuffer, nonce); 80 | writer.String(nonceBuffer); 81 | } 82 | 83 | size_t JsonWriteRichPresenceObj(char* dest, 84 | size_t maxLen, 85 | int nonce, 86 | int pid, 87 | const DiscordRichPresence* presence) 88 | { 89 | JsonWriter writer(dest, maxLen); 90 | 91 | { 92 | WriteObject top(writer); 93 | 94 | JsonWriteNonce(writer, nonce); 95 | 96 | WriteKey(writer, "cmd"); 97 | writer.String("SET_ACTIVITY"); 98 | 99 | { 100 | WriteObject args(writer, "args"); 101 | 102 | WriteKey(writer, "pid"); 103 | writer.Int(pid); 104 | 105 | if (presence != nullptr) { 106 | WriteObject activity(writer, "activity"); 107 | 108 | WriteOptionalString(writer, "state", presence->state); 109 | WriteOptionalString(writer, "details", presence->details); 110 | 111 | if (presence->startTimestamp || presence->endTimestamp) { 112 | WriteObject timestamps(writer, "timestamps"); 113 | 114 | if (presence->startTimestamp) { 115 | WriteKey(writer, "start"); 116 | writer.Int64(presence->startTimestamp); 117 | } 118 | 119 | if (presence->endTimestamp) { 120 | WriteKey(writer, "end"); 121 | writer.Int64(presence->endTimestamp); 122 | } 123 | } 124 | 125 | if ((presence->largeImageKey && presence->largeImageKey[0]) || 126 | (presence->largeImageText && presence->largeImageText[0]) || 127 | (presence->smallImageKey && presence->smallImageKey[0]) || 128 | (presence->smallImageText && presence->smallImageText[0])) { 129 | WriteObject assets(writer, "assets"); 130 | WriteOptionalString(writer, "large_image", presence->largeImageKey); 131 | WriteOptionalString(writer, "large_text", presence->largeImageText); 132 | WriteOptionalString(writer, "small_image", presence->smallImageKey); 133 | WriteOptionalString(writer, "small_text", presence->smallImageText); 134 | } 135 | 136 | if ((presence->partyId && presence->partyId[0]) || presence->partySize || 137 | presence->partyMax || presence->partyPrivacy) { 138 | WriteObject party(writer, "party"); 139 | WriteOptionalString(writer, "id", presence->partyId); 140 | if (presence->partySize && presence->partyMax) { 141 | WriteArray size(writer, "size"); 142 | writer.Int(presence->partySize); 143 | writer.Int(presence->partyMax); 144 | } 145 | 146 | if (presence->partyPrivacy) { 147 | WriteKey(writer, "privacy"); 148 | writer.Int(presence->partyPrivacy); 149 | } 150 | } 151 | 152 | if ((presence->matchSecret && presence->matchSecret[0]) || 153 | (presence->joinSecret && presence->joinSecret[0]) || 154 | (presence->spectateSecret && presence->spectateSecret[0])) { 155 | WriteObject secrets(writer, "secrets"); 156 | WriteOptionalString(writer, "match", presence->matchSecret); 157 | WriteOptionalString(writer, "join", presence->joinSecret); 158 | WriteOptionalString(writer, "spectate", presence->spectateSecret); 159 | } 160 | 161 | if ((presence->button1Label && presence->button1Label[0]) && 162 | (presence->button1Url && presence->button1Url[0]) || 163 | (presence->button2Label && presence->button2Label[0]) && 164 | (presence->button2Url && presence->button2Url[0])) { 165 | WriteArray buttons(writer, "buttons"); 166 | 167 | if (presence->button1Label && presence->button1Label[0] && 168 | presence->button1Url && presence->button1Url[0]) { 169 | WriteObject button1(writer); 170 | WriteKey(writer, "label"); 171 | writer.String(presence->button1Label); 172 | WriteKey(writer, "url"); 173 | writer.String(presence->button1Url); 174 | } 175 | 176 | if (presence->button2Label && presence->button2Label[0] && 177 | presence->button2Url && presence->button2Url[0]) { 178 | WriteObject button2(writer); 179 | WriteKey(writer, "label"); 180 | writer.String(presence->button2Label); 181 | WriteKey(writer, "url"); 182 | writer.String(presence->button2Url); 183 | } 184 | } 185 | 186 | writer.Key("instance"); 187 | writer.Bool(presence->instance != 0); 188 | } 189 | } 190 | } 191 | 192 | return writer.Size(); 193 | } 194 | 195 | size_t JsonWriteHandshakeObj(char* dest, size_t maxLen, int version, const char* applicationId) 196 | { 197 | JsonWriter writer(dest, maxLen); 198 | 199 | { 200 | WriteObject obj(writer); 201 | WriteKey(writer, "v"); 202 | writer.Int(version); 203 | WriteKey(writer, "client_id"); 204 | writer.String(applicationId); 205 | } 206 | 207 | return writer.Size(); 208 | } 209 | 210 | size_t JsonWriteSubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName) 211 | { 212 | JsonWriter writer(dest, maxLen); 213 | 214 | { 215 | WriteObject obj(writer); 216 | 217 | JsonWriteNonce(writer, nonce); 218 | 219 | WriteKey(writer, "cmd"); 220 | writer.String("SUBSCRIBE"); 221 | 222 | WriteKey(writer, "evt"); 223 | writer.String(evtName); 224 | } 225 | 226 | return writer.Size(); 227 | } 228 | 229 | size_t JsonWriteUnsubscribeCommand(char* dest, size_t maxLen, int nonce, const char* evtName) 230 | { 231 | JsonWriter writer(dest, maxLen); 232 | 233 | { 234 | WriteObject obj(writer); 235 | 236 | JsonWriteNonce(writer, nonce); 237 | 238 | WriteKey(writer, "cmd"); 239 | writer.String("UNSUBSCRIBE"); 240 | 241 | WriteKey(writer, "evt"); 242 | writer.String(evtName); 243 | } 244 | 245 | return writer.Size(); 246 | } 247 | 248 | size_t JsonWriteJoinReply(char* dest, size_t maxLen, const char* userId, int reply, int nonce) 249 | { 250 | JsonWriter writer(dest, maxLen); 251 | 252 | { 253 | WriteObject obj(writer); 254 | 255 | WriteKey(writer, "cmd"); 256 | if (reply == DISCORD_REPLY_YES) { 257 | writer.String("SEND_ACTIVITY_JOIN_INVITE"); 258 | } 259 | else { 260 | writer.String("CLOSE_ACTIVITY_JOIN_REQUEST"); 261 | } 262 | 263 | WriteKey(writer, "args"); 264 | { 265 | WriteObject args(writer); 266 | 267 | WriteKey(writer, "user_id"); 268 | writer.String(userId); 269 | } 270 | 271 | JsonWriteNonce(writer, nonce); 272 | } 273 | 274 | return writer.Size(); 275 | } 276 | -------------------------------------------------------------------------------- /CarbonLauncher/CarbonLauncher.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {f55718c8-63d5-4cca-8499-07500910c9bc} 25 | CarbonLauncher 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | false 48 | 49 | 50 | Application 51 | false 52 | v143 53 | true 54 | Unicode 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | true 76 | 77 | 78 | 79 | Level3 80 | true 81 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 82 | true 83 | stdcpp20 84 | include;. 85 | /utf-8 %(AdditionalOptions) 86 | 87 | 88 | Windows 89 | true 90 | 91 | 92 | 93 | 94 | Level3 95 | true 96 | true 97 | true 98 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 99 | true 100 | stdcpp20 101 | include;. 102 | /utf-8 %(AdditionalOptions) 103 | 104 | 105 | Windows 106 | true 107 | true 108 | true 109 | 110 | 111 | 112 | 113 | Level3 114 | true 115 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 116 | true 117 | stdcpp20 118 | include;vendor\include;. 119 | /utf-8 %(AdditionalOptions) 120 | 121 | 122 | Windows 123 | true 124 | 125 | 126 | 127 | 128 | Level3 129 | true 130 | true 131 | true 132 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 133 | true 134 | stdcpp20 135 | include;vendor\include;. 136 | /utf-8 %(AdditionalOptions) 137 | 138 | 139 | Windows 140 | true 141 | true 142 | true 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /CarbonLauncher/src/processmanager.cpp: -------------------------------------------------------------------------------- 1 | #include "processmanager.h" 2 | #include "state.h" 3 | #include "utils.h" 4 | 5 | #define WIN32_LEAN_AND_MEAN 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | using namespace Carbon; 16 | 17 | ProcessManager::ProcessManager() { 18 | this->gameStatusThread = std::thread([this]() { 19 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 20 | 21 | while (true) { 22 | std::this_thread::sleep_for(std::chrono::seconds(1)); 23 | 24 | auto hProcesses = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 25 | if (hProcesses == INVALID_HANDLE_VALUE) { 26 | continue; 27 | } 28 | 29 | PROCESSENTRY32 entry{}; 30 | entry.dwSize = sizeof(entry); 31 | 32 | if (!Process32First(hProcesses, &entry)) { 33 | CloseHandle(hProcesses); 34 | continue; 35 | } 36 | 37 | bool found = false; 38 | do { 39 | std::string target(C.guiManager.target == ModTarget::Game ? "ScrapMechanic.exe" : "ModTool.exe"); 40 | if (std::wstring(entry.szExeFile) == std::wstring(target.begin(), target.end())) { 41 | found = true; 42 | break; 43 | } 44 | } while (Process32Next(hProcesses, &entry)); 45 | 46 | CloseHandle(hProcesses); 47 | 48 | if (found) { 49 | // This may occur if the game was started before Carbon Launcher 50 | // was opened. 51 | if (!this->gameStartedTime.has_value()) { 52 | this->gameStartedTime = std::chrono::system_clock::now(); 53 | } 54 | 55 | std::lock_guard lock(this->gameStatusMutex); 56 | 57 | if (found != this->gameRunning) { 58 | spdlog::info("Game is {}", found ? "running" : "not running"); 59 | } 60 | 61 | this->gameRunning = true; 62 | this->pid = entry.th32ProcessID; 63 | 64 | if (!this->gameStartedTime.has_value()) { 65 | this->gameStartedTime = std::chrono::system_clock::now(); 66 | } 67 | } 68 | else { 69 | std::lock_guard lock(this->gameStatusMutex); 70 | this->gameRunning = false; 71 | this->modules.clear(); 72 | this->gameStartedTime = std::nullopt; 73 | this->pid = 0; 74 | } 75 | 76 | // Allow the thread some breathing room 77 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 78 | } 79 | }); 80 | 81 | this->gameStatusThread.detach(); 82 | 83 | this->moduleHandlerThread = std::thread([this]() { 84 | while (true) { 85 | if (this->IsGameRunning()) { 86 | if (!C.pipeManager.GetPacketsByType(PacketType::LOADED).empty()) { 87 | // Get module list 88 | auto hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); 89 | if (!hProcess) { 90 | spdlog::error("Failed to open process with PID {}", pid); 91 | continue; 92 | } 93 | 94 | MODULEENTRY32 moduleEntry{}; 95 | moduleEntry.dwSize = sizeof(moduleEntry); 96 | 97 | auto hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid); 98 | if (hModuleSnap == INVALID_HANDLE_VALUE) { 99 | CloseHandle(hProcess); 100 | continue; 101 | } 102 | 103 | if (!Module32First(hModuleSnap, &moduleEntry)) { 104 | CloseHandle(hModuleSnap); 105 | CloseHandle(hProcess); 106 | continue; 107 | } 108 | 109 | do { 110 | this->modules.emplace_back(moduleEntry); 111 | } while (Module32Next(hModuleSnap, &moduleEntry)); 112 | 113 | CloseHandle(hModuleSnap); 114 | CloseHandle(hProcess); 115 | 116 | this->modules = modules; 117 | 118 | spdlog::info("Game loaded, injecting modules"); 119 | 120 | std::string modulesDir = Utils::GetDataDir() + "mods"; 121 | std::filesystem::create_directory(modulesDir); 122 | 123 | int loadedCustomModules = 0; 124 | for (auto& module : std::filesystem::recursive_directory_iterator(modulesDir)) { 125 | if (!module.is_regular_file() || module.path().extension() != ".dll") 126 | continue; // Skip directories and non-DLL files 127 | 128 | bool found = false; 129 | for (auto& foundModule : this->modules) { 130 | if (std::wstring(module.path().filename().wstring()) == foundModule.szModule) { 131 | found = true; 132 | break; 133 | } 134 | } 135 | 136 | if (!found) { 137 | this->InjectModule(module.path().string()); 138 | loadedCustomModules++; 139 | } 140 | else { 141 | spdlog::warn("Module `{}` is already loaded", module.path().string()); 142 | } 143 | } 144 | 145 | this->loadedCustomModules = loadedCustomModules; 146 | } 147 | } 148 | std::this_thread::sleep_for(std::chrono::seconds(1)); 149 | } 150 | }); 151 | 152 | this->moduleHandlerThread.detach(); 153 | } 154 | 155 | ProcessManager::~ProcessManager() {} 156 | 157 | bool ProcessManager::IsGameRunning() { 158 | std::lock_guard lock(this->gameStatusMutex); 159 | return this->gameRunning; 160 | } 161 | 162 | void ProcessManager::InjectModule(const std::string& modulePath) { 163 | if (!this->IsGameRunning() || this->pid == 0) { 164 | spdlog::warn("Game manager was requested to inject `{}`, but the game is not running", modulePath); 165 | return; 166 | } 167 | 168 | if (!std::filesystem::exists(modulePath)) { 169 | spdlog::error("Module `{}` does not exist", modulePath); 170 | return; 171 | } 172 | 173 | auto hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); 174 | if (!hProcess) { 175 | spdlog::error("Failed to open process with PID {}", pid); 176 | return; 177 | } 178 | 179 | auto modulePathW = std::wstring(modulePath.begin(), modulePath.end()); 180 | auto modulePathSize = (modulePath.size() + 1) * sizeof(wchar_t); 181 | auto modulePathRemote = VirtualAllocEx(hProcess, NULL, modulePathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); 182 | WriteProcessMemory(hProcess, modulePathRemote, modulePathW.c_str(), modulePathSize, NULL); 183 | auto hKernel32 = GetModuleHandle(L"Kernel32.dll"); 184 | auto hLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryW"); 185 | auto hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)hLoadLibrary, modulePathRemote, 0, NULL); 186 | WaitForSingleObject(hThread, INFINITE); 187 | CloseHandle(hThread); 188 | CloseHandle(hProcess); 189 | 190 | spdlog::info("Injected module `{}`", modulePath); 191 | } 192 | 193 | void ProcessManager::LaunchProcess(const std::string& name) { 194 | this->gameStartedTime = std::nullopt; 195 | this->gameRunning = false; 196 | this->pid = 0; 197 | this->modules.clear(); 198 | 199 | std::thread([&]() { 200 | if (name == "ScrapMechanic.exe") { 201 | ShellExecute(NULL, L"open", L"steam://rungameid/387990", NULL, NULL, SW_SHOWNORMAL); 202 | spdlog::info("Launching Scrap Mechanic via Steam"); 203 | } 204 | else if (name == "ModTool.exe") { 205 | ShellExecute(NULL, L"open", L"steam://rungameid/588870", NULL, NULL, SW_SHOWNORMAL); 206 | spdlog::info("Launching Scrap Mechanic Mod Tool via Steam"); 207 | } 208 | else { 209 | ShellExecute(NULL, L"open", std::wstring(name.begin(), name.end()).c_str(), NULL, NULL, SW_SHOWNORMAL); 210 | spdlog::info("Launching game via ShellExecute"); 211 | } 212 | 213 | while (!this->IsGameRunning()) { 214 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 215 | } 216 | 217 | this->gameStartedTime = std::chrono::system_clock::now(); 218 | std::this_thread::sleep_for(std::chrono::seconds(1)); 219 | 220 | if (C.guiManager.target == ModTarget::Game) { 221 | this->InjectModule(Utils::GetCurrentModuleDir() + "CarbonSupervisor.dll"); 222 | } 223 | else { 224 | // Inject every module in the mods directory 225 | for (auto& module : std::filesystem::recursive_directory_iterator(Utils::GetDataDir() + "mods")) { 226 | if (!module.is_regular_file() || module.path().extension() != ".dll") 227 | continue; // Skip directories and non-DLL files 228 | this->InjectModule(module.path().string()); 229 | } 230 | } 231 | }).detach(); 232 | 233 | spdlog::info("Started game in detached thread"); 234 | } 235 | 236 | void ProcessManager::KillGame() { 237 | auto snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 238 | if (snapshot == INVALID_HANDLE_VALUE) { 239 | return; 240 | } 241 | PROCESSENTRY32 entry{}; 242 | entry.dwSize = sizeof(entry); 243 | if (!Process32First(snapshot, &entry)) { 244 | CloseHandle(snapshot); 245 | return; 246 | } 247 | do { 248 | std::string target(C.guiManager.target == ModTarget::Game ? "ScrapMechanic.exe" : "ModTool.exe"); 249 | if (std::wstring(entry.szExeFile) == std::wstring(target.begin(), target.end())) { 250 | HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, entry.th32ProcessID); 251 | if (process) { 252 | TerminateProcess(process, 0); 253 | CloseHandle(process); 254 | } 255 | } 256 | } while (Process32Next(snapshot, &entry)); 257 | CloseHandle(snapshot); 258 | 259 | while (this->IsGameRunning()) { 260 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 261 | } 262 | } 263 | 264 | bool ProcessManager::IsModuleLoaded(const std::string& moduleName) const { 265 | std::vector modules; 266 | auto hProcesses = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, this->pid); 267 | if (hProcesses == INVALID_HANDLE_VALUE) { 268 | return false; 269 | } 270 | 271 | MODULEENTRY32 entry{ sizeof(entry) }; 272 | 273 | if (!Module32First(hProcesses, &entry)) { 274 | CloseHandle(hProcesses); 275 | return false; 276 | } 277 | 278 | do { 279 | modules.emplace_back(entry); 280 | } while (Module32Next(hProcesses, &entry)); 281 | 282 | CloseHandle(hProcesses); 283 | 284 | for (auto& module : modules) { 285 | std::wstring moduleNameW(module.szModule); 286 | int bufferSize = WideCharToMultiByte(CP_UTF8, 0, moduleNameW.c_str(), -1, NULL, 0, NULL, NULL); 287 | std::string moduleNameA(bufferSize, 0); 288 | WideCharToMultiByte(CP_UTF8, 0, moduleNameW.c_str(), -1, moduleNameA.data(), bufferSize, NULL, NULL); 289 | 290 | if (moduleNameA == moduleName) { 291 | return true; 292 | } 293 | } 294 | 295 | return false; 296 | } 297 | 298 | int ProcessManager::GetLoadedCustomModules() const { 299 | while (!this->loadedCustomModules.has_value()) { 300 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 301 | } 302 | 303 | return this->loadedCustomModules.value(); 304 | } 305 | -------------------------------------------------------------------------------- /CarbonLauncher/src/modmanager.cpp: -------------------------------------------------------------------------------- 1 | #include "modmanager.h" 2 | #include "state.h" 3 | #include "utils.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace Carbon; 10 | 11 | // This is here to allow people to not be limited to 60 requests per hour, please don't abuse this 12 | // It has no permissions anyway, so it can't do anything malicious 13 | // It's like this to stop: 14 | // - GitHub from revoking the token because it thinks it's put here by accident 15 | // - Bots scraping the token and using it for malicious purposes 16 | // 17 | // Please, do not abuse this! In the Scrap Mechanic community we trust, right? 18 | constexpr int tok[] = { 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5f, 0x70, 0x61, 0x74, 0x5f, 0x31, 0x31, 0x42, 0x4f, 0x43, 0x54, 0x58, 0x45, 0x59, 0x30, 0x67, 0x44, 0x4e, 0x38, 0x53, 19 | 0x49, 0x74, 0x32, 0x6b, 0x4c, 0x69, 0x50, 0x5f, 0x58, 0x39, 0x6a, 0x68, 0x34, 0x75, 0x4b, 0x66, 0x4f, 0x5a, 0x66, 0x67, 0x6e, 0x59, 0x43, 0x4e, 0x66, 0x39, 0x67, 0x64, 0x6c, 0x77, 20 | 0x79, 0x36, 0x4e, 0x44, 0x50, 0x75, 0x68, 0x36, 0x63, 0x6a, 0x38, 0x31, 0x39, 0x34, 0x48, 0x49, 0x72, 0x62, 0x67, 0x79, 0x68, 0x49, 0x33, 0x42, 0x45, 0x42, 0x4d, 0x42, 0x4e, 0x4e, 21 | 0x42, 0x73, 0x65, 0x37, 0x73, 0x65, 0x59 }; 22 | std::string token = std::string(tok, tok + sizeof(tok) / sizeof(tok[0])); 23 | 24 | cpr::Header authHeader = cpr::Header{ { "Authorization", "token " + token } }; 25 | 26 | std::string getDefaultBranch(std::string ghUser, std::string ghRepo) { 27 | std::string repoDataURL = fmt::format("https://api.github.com/repos/{}/{}", ghUser, ghRepo); 28 | auto response = cpr::Get(cpr::Url{ repoDataURL }, authHeader); 29 | 30 | try { 31 | auto json = nlohmann::json::parse(response.text); 32 | return json["default_branch"]; 33 | } 34 | catch (nlohmann::json::parse_error& e) { 35 | spdlog::error("Failed to parse JSON: {}", e.what()); 36 | return "main"; 37 | } 38 | } 39 | 40 | std::optional ModManager::JSONToMod(const nlohmann::json& jMod) { 41 | std::string branch = ""; 42 | 43 | std::string user = ""; 44 | std::string repo = ""; 45 | 46 | if (jMod.is_string()) { 47 | std::string modRepo = jMod.get(); 48 | user = modRepo.substr(0, modRepo.find('/')); 49 | repo = modRepo.substr(modRepo.find('/') + 1); 50 | } 51 | else { 52 | user = jMod["ghUser"]; 53 | repo = jMod["ghRepo"]; 54 | } 55 | 56 | branch = getDefaultBranch(user, repo); 57 | 58 | std::string manifestURL = fmt::format("https://raw.githubusercontent.com/{}/{}/{}/manifest.json", user, repo, branch); 59 | cpr::Response manifest = cpr::Get(cpr::Url{ manifestURL }); 60 | 61 | bool hasManifest = manifest.status_code == 200; 62 | 63 | Mod mod; 64 | 65 | std::string ghUser = ""; 66 | std::string ghRepo = ""; 67 | if (jMod.is_string()) { 68 | std::string modRepo = jMod.get(); 69 | ghUser = modRepo.substr(0, modRepo.find('/')); 70 | ghRepo = modRepo.substr(modRepo.find('/') + 1); 71 | 72 | mod.ghUser = ghUser; 73 | mod.ghRepo = ghRepo; 74 | } 75 | else { 76 | mod.ghUser = jMod["ghUser"]; 77 | mod.ghRepo = jMod["ghRepo"]; 78 | ghUser = jMod["ghUser"]; 79 | ghRepo = jMod["ghRepo"]; 80 | } 81 | 82 | if (hasManifest) { 83 | spdlog::trace("Mod {} has a manifest.json!", ghUser + "/" + ghRepo); 84 | //auto jManifest = nlohmann::json::parse(manifest.text); 85 | nlohmann::json jManifest; 86 | try { 87 | jManifest = nlohmann::json::parse(manifest.text); 88 | } 89 | catch (nlohmann::json::parse_error& e) { 90 | spdlog::error("Failed to parse JSON: {}", e.what()); 91 | return std::nullopt; 92 | } 93 | 94 | mod.name = jManifest["name"]; 95 | mod.authors = jManifest["authors"].get>(); 96 | mod.description = jManifest["description"]; 97 | mod.installed = false; 98 | } 99 | else { 100 | spdlog::warn("Mod {} ({}) does not have a manifest.json!", jMod["name"].get(), jMod["ghRepo"].get()); 101 | 102 | mod.name = jMod["name"]; 103 | mod.authors = jMod["authors"].get>(); 104 | mod.description = jMod["description"]; 105 | mod.installed = false; 106 | } 107 | 108 | // Check if the mod is installed 109 | if (std::filesystem::exists(fmt::format("{}/mods/game/{}", Utils::GetDataDir(), mod.ghRepo)) || std::filesystem::exists(fmt::format("{}/mods/modtool/{}", Utils::GetDataDir(), mod.ghRepo))) { 110 | mod.installed = true; 111 | 112 | // Check if the mod wants an update 113 | //std::string tagFile = Utils::GetDataDir() + "/mods/" + (mod.ghRepo == "CarbonLauncher" ? "modtool" : "game") + "/" + mod.ghRepo + ".tag"; 114 | std::string tagFile = fmt::format("{}mods/game/{}/tag", Utils::GetDataDir(), mod.ghRepo); 115 | if (!std::filesystem::exists(tagFile)) { 116 | tagFile = fmt::format("{}mods/modtool/{}/tag", Utils::GetDataDir(), mod.ghRepo); 117 | } 118 | spdlog::info("Checking tag file: {}", tagFile); 119 | std::ifstream file(tagFile); 120 | std::string tag; 121 | file >> tag; 122 | file.close(); 123 | 124 | auto latestURL = fmt::format("https://api.github.com/repos/{}/{}/releases/latest", mod.ghUser, mod.ghRepo); 125 | auto latest = cpr::Get(cpr::Url{ latestURL }, authHeader); 126 | 127 | auto jContents = nlohmann::json(); 128 | auto jLatest = nlohmann::json(); 129 | 130 | try { 131 | jLatest = nlohmann::json::parse(latest.text); 132 | } 133 | catch (nlohmann::json::parse_error& e) { 134 | spdlog::error("Failed to parse JSON: {}", e.what()); 135 | return std::nullopt; 136 | } 137 | 138 | // Find the latest tag 139 | std::string currentTag = jLatest["tag_name"]; 140 | if (tag != currentTag) { 141 | mod.wantsUpdate = true; 142 | } 143 | 144 | spdlog::info("Mod {} is installed and {} want an update ({} {} {})!", mod.name, mod.wantsUpdate ? "does" : "does not", tag, mod.wantsUpdate ? "!=" : "==", currentTag); 145 | } 146 | 147 | return mod; 148 | } 149 | 150 | std::pair, std::vector> ModManager::URLToMods(const std::string& url) { 151 | std::chrono::time_point start = std::chrono::system_clock::now(); 152 | cpr::Response response = cpr::Get(cpr::Url{ url }); 153 | 154 | nlohmann::json json; 155 | try { 156 | json = nlohmann::json::parse(response.text); 157 | } 158 | catch (nlohmann::json::parse_error& e) { 159 | spdlog::error("Failed to parse JSON: {}", e.what()); 160 | return {}; 161 | } 162 | 163 | std::vector threads; 164 | std::mutex mtx; 165 | 166 | std::vector gameMods; 167 | for (auto& jMod : json["mods"]["game"]) { 168 | threads.push_back(std::thread([&]() { 169 | auto mod = this->JSONToMod(jMod); 170 | if (mod.has_value()) 171 | std::lock_guard lock(mtx); 172 | gameMods.push_back(mod.value()); 173 | })); 174 | } 175 | 176 | std::vector modToolMods; 177 | for (auto& jMod : json["mods"]["modtool"]) { 178 | threads.push_back(std::thread([&, this]() { 179 | auto mod = this->JSONToMod(jMod); 180 | if (mod.has_value()) 181 | std::lock_guard lock(mtx); 182 | modToolMods.push_back(mod.value()); 183 | })); 184 | } 185 | 186 | for (auto& thread : threads) { 187 | thread.join(); 188 | } 189 | 190 | std::chrono::time_point end = std::chrono::system_clock::now(); 191 | std::chrono::duration elapsed_seconds = end - start; 192 | spdlog::info("Loaded {} mods in {} seconds", gameMods.size() + modToolMods.size(), elapsed_seconds.count()); 193 | 194 | return { gameMods, modToolMods }; 195 | } 196 | 197 | ModManager::ModManager() { 198 | std::thread([this]() { 199 | // Allow some time for the console to initialize 200 | std::this_thread::sleep_for(std::chrono::milliseconds(200)); 201 | 202 | std::lock_guard lock(this->repoMutex); 203 | //this->mods = URLToMods(REPOS_URL); 204 | auto mods = URLToMods(REPOS_URL); 205 | this->gameMods = mods.first; 206 | this->modToolMods = mods.second; 207 | this->hasLoaded = true; 208 | }).detach(); 209 | } 210 | 211 | ModManager::~ModManager() { 212 | this->gameMods.clear(); 213 | this->modToolMods.clear(); 214 | } 215 | 216 | void Mod::Install() { 217 | std::thread([&]() { 218 | // Display the mod as already installed to 219 | // make the user aware that the mod is being installed 220 | this->installed = true; 221 | 222 | auto contentsURL = fmt::format("https://api.github.com/repos/{}/{}/contents/", this->ghUser, this->ghRepo); 223 | auto latestURL = fmt::format("https://api.github.com/repos/{}/{}/releases/latest", this->ghUser, this->ghRepo); 224 | auto contents = cpr::Get(cpr::Url{ contentsURL }, authHeader); 225 | auto latest = cpr::Get(cpr::Url{ latestURL }, authHeader); 226 | 227 | auto jContents = nlohmann::json(); 228 | auto jLatest = nlohmann::json(); 229 | 230 | try { 231 | jContents = nlohmann::json::parse(contents.text); 232 | jLatest = nlohmann::json::parse(latest.text); 233 | } 234 | catch (nlohmann::json::parse_error& e) { 235 | spdlog::error("Failed to parse JSON: {}", e.what()); 236 | this->installed = false; 237 | return; 238 | } 239 | 240 | // Find the latest tag 241 | std::string tag = jLatest["tag_name"]; 242 | 243 | std::unordered_map downloadURLs; 244 | for (const auto& asset : jLatest["assets"]) { 245 | try { 246 | std::string url = asset["browser_download_url"]; 247 | std::string name = asset["name"]; 248 | downloadURLs.insert({ name, url }); 249 | } 250 | catch (nlohmann::json::exception& e) { 251 | spdlog::error("Failed to get browser_download_url: {}", e.what()); 252 | continue; 253 | } 254 | } 255 | 256 | // Download the mod 257 | for (const auto& [name, url] : downloadURLs) { 258 | auto download = cpr::Get(cpr::Url{ url }, authHeader); 259 | 260 | if (download.status_code != 200) { 261 | spdlog::error("Failed to download mod: {}", download.status_code); 262 | this->installed = false; 263 | return; 264 | } 265 | 266 | // mod dir 267 | //std::string modDir = Utils::GetDataDir() + "/mods/" + this->ghRepo + "/"; 268 | //std::string modDir = Utils::GetDataDir() + "/mods/" + C.guiManager.target == ModTarget::Game ? "game" : "modtool" + "/" + this->ghRepo + "/"; 269 | std::string modDir = fmt::format("{}mods/{}/{}", Utils::GetDataDir(), C.guiManager.target == ModTarget::Game ? "game" : "modtool", this->ghRepo); 270 | spdlog::info("modDir: {}", modDir); 271 | std::filesystem::create_directories(modDir); 272 | 273 | // mod file 274 | std::string modFile = modDir + "\\" + name; 275 | std::ofstream file(modFile, std::ios::binary); 276 | file << download.text; 277 | file.close(); 278 | 279 | spdlog::info("Downloaded: {}", modFile); 280 | } 281 | 282 | // Save `tag` file with the tag name 283 | std::string tagFile = fmt::format("{}mods/{}/{}", Utils::GetDataDir(), C.guiManager.target == ModTarget::Game ? "game" : "modtool", this->ghRepo) + "/tag"; 284 | std::ofstream file(tagFile); 285 | file << tag; 286 | file.close(); 287 | 288 | this->installed = true; 289 | 290 | spdlog::info("Installed: {}", this->name); 291 | }).detach(); 292 | } 293 | 294 | void Mod::Uninstall() { 295 | if (C.processManager.IsGameRunning()) { 296 | spdlog::error("Game is running, cannot uninstall mod"); 297 | return; 298 | } 299 | 300 | //std::string modDir = Utils::GetDataDir() + "/mods/" + this->ghRepo + "/"; 301 | std::string modDir = fmt::format("{}mods/{}/{}", Utils::GetDataDir(), C.guiManager.target == ModTarget::Game ? "game" : "modtool", this->ghRepo); 302 | std::filesystem::remove_all(modDir); 303 | this->installed = false; 304 | spdlog::info("Uninstalled: {}", this->name); 305 | } 306 | 307 | void Mod::Update() { 308 | if (C.processManager.IsGameRunning()) { 309 | spdlog::error("Game is running, cannot update mod"); 310 | return; 311 | } 312 | 313 | if (!this->wantsUpdate) { 314 | spdlog::warn("Mod {} does not want an update but was requested to update", this->name); 315 | return; 316 | } 317 | 318 | auto latestURL = fmt::format("https://api.github.com/repos/{}/{}/releases/latest", this->ghUser, this->ghRepo); 319 | auto latest = cpr::Get(cpr::Url{ latestURL }, authHeader); 320 | 321 | auto jLatest = nlohmann::json(); 322 | try { 323 | jLatest = nlohmann::json::parse(latest.text); 324 | } 325 | catch (nlohmann::json::parse_error& e) { 326 | spdlog::error("Failed to parse JSON: {}", e.what()); 327 | return; 328 | } 329 | 330 | // Find the latest tag 331 | std::string tag = jLatest["tag_name"]; 332 | 333 | std::unordered_map downloadURLs; 334 | for (const auto& asset : jLatest["assets"]) { 335 | try { 336 | std::string url = asset["browser_download_url"]; 337 | std::string name = asset["name"]; 338 | downloadURLs.insert({ name, url }); 339 | } 340 | catch (nlohmann::json::exception& e) { 341 | spdlog::error("Failed to get browser_download_url: {}", e.what()); 342 | continue; 343 | } 344 | } 345 | 346 | // Download the mod 347 | for (const auto& [name, url] : downloadURLs) { 348 | auto download = cpr::Get(cpr::Url{ url }, authHeader); 349 | // mod dir 350 | std::string modDir = fmt::format("{}mods/{}/{}", Utils::GetDataDir(), C.guiManager.target == ModTarget::Game ? "game" : "modtool", this->ghRepo); 351 | std::filesystem::create_directories(modDir); 352 | // mod file 353 | std::string modFile = modDir + name; 354 | std::ofstream file(modFile, std::ios::binary); 355 | file << download.text; 356 | file.close(); 357 | spdlog::info("Downloaded: {}", modFile); 358 | } 359 | 360 | // Save `tag` file with the tag name 361 | std::string tagFile = fmt::format("{}mods/{}/{}", Utils::GetDataDir(), C.guiManager.target == ModTarget::Game ? "game" : "modtool", this->ghRepo) + "/tag"; 362 | std::ofstream file(tagFile); 363 | file << tag; 364 | file.close(); 365 | 366 | spdlog::info("Updated: {}", this->name); 367 | 368 | this->wantsUpdate = false; 369 | } 370 | -------------------------------------------------------------------------------- /CarbonLauncher/src/guimanager.cpp: -------------------------------------------------------------------------------- 1 | #pragma comment(lib, "dwmapi.lib") 2 | #define GLFW_INCLUDE_NONE 3 | #define GLFW_EXPOSE_NATIVE_WIN32 4 | #define WIN32_LEAN_AND_MEAN 5 | 6 | #include "resource.h" 7 | 8 | #include "guimanager.h" 9 | #include "utils.h" 10 | #include "state.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | 34 | using namespace Carbon; 35 | 36 | GUIManager::GUIManager() : renderCallback(nullptr), window(nullptr) { 37 | // Initialize GLFW 38 | if (!glfwInit()) { 39 | MessageBox(NULL, L"GLFW Initialization Failed!", L"Error!", MB_ICONEXCLAMATION | MB_OK); 40 | return; 41 | } 42 | 43 | glfwSetErrorCallback([](int error, const char* description) { 44 | spdlog::error("GLFW Error {}: {}", error, description); 45 | }); 46 | 47 | // Create the OpenGL context 48 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); 49 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); 50 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 51 | 52 | // Create the GLFW window 53 | this->window = glfwCreateWindow(1280, 720, "Carbon Launcher", NULL, NULL); 54 | if (!this->window) { 55 | spdlog::error("GLFW Window Creation Failed!"); 56 | glfwTerminate(); 57 | return; 58 | } 59 | 60 | glfwMakeContextCurrent(this->window); 61 | 62 | // Initialize GLAD 63 | if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { 64 | spdlog::error("GLAD Initialization Failed!"); 65 | glfwTerminate(); 66 | return; 67 | } 68 | 69 | // Enable dark mode (Windows only) 70 | BOOL darkMode = TRUE; 71 | HWND hWnd = glfwGetWin32Window(this->window); 72 | DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &darkMode, sizeof(darkMode)); 73 | 74 | // Initialize ImGui 75 | IMGUI_CHECKVERSION(); 76 | ImGui::CreateContext(); 77 | ImGuiIO& io = ImGui::GetIO(); 78 | 79 | ImGui::StyleColorsDark(); 80 | 81 | // Round everything, disable some frames 82 | ImGuiStyle& style = ImGui::GetStyle(); 83 | style.WindowRounding = 4.0f; 84 | style.FramePadding = ImVec2(8, 4); 85 | style.TabBarBorderSize = 2; 86 | style.ScrollbarSize = 10; 87 | style.FrameRounding = 4.0f; 88 | style.GrabRounding = 4.0f; 89 | style.TabRounding = 12.0f; 90 | style.TabBarBorderSize = 0.0f; 91 | style.ChildRounding = 6.0f; 92 | style.PopupRounding = 4.0f; 93 | style.ScrollbarRounding = 12.0f; 94 | style.FrameBorderSize = 0.0f; 95 | style.WindowBorderSize = 0.0f; 96 | style.PopupBorderSize = 0.0f; 97 | style.TabBorderSize = 0.0f; 98 | 99 | constexpr float fontSize = 18.0f; 100 | io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisb.ttf", fontSize); 101 | 102 | constexpr float iconFontSize = fontSize * 2.0f / 3.0f; 103 | void* data = (void*)s_fa_solid_900_ttf; 104 | int size = sizeof(s_fa_solid_900_ttf); 105 | 106 | static const ImWchar iconsRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 }; 107 | ImFontConfig iconsConfig; 108 | iconsConfig.MergeMode = true; 109 | iconsConfig.PixelSnapH = true; 110 | iconsConfig.GlyphMinAdvanceX = iconFontSize; 111 | iconsConfig.FontDataOwnedByAtlas = false; 112 | io.Fonts->AddFontFromMemoryTTF(data, size, iconFontSize, &iconsConfig, iconsRanges); 113 | io.Fonts->Build(); 114 | 115 | // Go through every colour and get hsv values, if it's 151 then change it to 0 and then set the colour 116 | for (int i = 0; i < ImGuiCol_COUNT; i++) { 117 | ImVec4* col = &style.Colors[i]; 118 | float h, s, v; 119 | ImGui::ColorConvertRGBtoHSV(col->x, col->y, col->z, h, s, v); 120 | if (h > 0.59 && h < 0.61) { 121 | h = 0.0f; 122 | s = 0.5f; 123 | spdlog::info("Modified colour {}", i); 124 | } 125 | else { 126 | spdlog::info("Colour {} is {}", i, h); 127 | } 128 | ImGui::ColorConvertHSVtoRGB(h, s, v, col->x, col->y, col->z); 129 | } 130 | 131 | ImGui_ImplGlfw_InitForOpenGL(this->window, true); 132 | ImGui_ImplOpenGL3_Init("#version 460"); 133 | glfwSwapInterval(1); 134 | } 135 | 136 | GUIManager::~GUIManager() { 137 | // Terminate GLFW 138 | glfwTerminate(); 139 | 140 | // Terminate ImGui 141 | ImGui_ImplOpenGL3_Shutdown(); 142 | ImGui_ImplGlfw_Shutdown(); 143 | ImGui::DestroyContext(); 144 | } 145 | 146 | void GUIManager::Run() const { 147 | while (!glfwWindowShouldClose(window)) { 148 | glfwPollEvents(); 149 | 150 | glClearColor(0.1f, 0.1f, 0.1f, 1.0f); 151 | glClear(GL_COLOR_BUFFER_BIT); 152 | 153 | ImGui_ImplOpenGL3_NewFrame(); 154 | ImGui_ImplGlfw_NewFrame(); 155 | ImGui::NewFrame(); 156 | 157 | if (this->renderCallback) { 158 | this->renderCallback(); 159 | } 160 | 161 | ImGui::Render(); 162 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 163 | 164 | glfwSwapBuffers(window); 165 | } 166 | } 167 | 168 | void _GUI() { 169 | using namespace Carbon; 170 | 171 | C.discordManager.Update(); 172 | 173 | bool shouldOpenPopup = false; 174 | // Begin main menu bar 175 | if (ImGui::BeginMainMenuBar()) { 176 | if (ImGui::BeginMenu("File")) { 177 | if (ImGui::MenuItem("Exit")) { 178 | glfwSetWindowShouldClose(C.guiManager.window, GLFW_TRUE); 179 | } 180 | ImGui::EndMenu(); 181 | } 182 | 183 | if (ImGui::BeginMenu("Help")) { 184 | if (ImGui::MenuItem("About")) { 185 | //ImGui::OpenPopup("About Carbon Launcher"); 186 | shouldOpenPopup = true; 187 | } 188 | 189 | ImGui::EndMenu(); 190 | } 191 | 192 | ImGui::EndMainMenuBar(); 193 | } 194 | 195 | if (shouldOpenPopup) { 196 | ImGui::OpenPopup("About Carbon Launcher"); 197 | } 198 | 199 | if (ImGui::BeginPopupModal("About Carbon Launcher", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { 200 | ImGui::Text("Carbon Launcher"); 201 | ImGui::Text("Version 1.0.0"); 202 | ImGui::Text("Developed by @BenMcAvoy"); 203 | if (ImGui::Button("View their GitHub")) { 204 | ShellExecute(NULL, L"open", L"https://github.com/BenMcAvoy", NULL, NULL, SW_SHOWNORMAL); 205 | } 206 | 207 | ImGui::SameLine(); 208 | 209 | if (ImGui::Button("Close")) { 210 | ImGui::CloseCurrentPopup(); 211 | } 212 | 213 | ImGui::EndPopup(); 214 | } 215 | 216 | int w, h; 217 | glfwGetWindowSize(C.guiManager.window, &w, &h); 218 | 219 | ImGui::SetNextWindowPos(ImVec2(0, 0)); 220 | ImGui::SetNextWindowSize(ImVec2((float)w, (float)h)); 221 | ImGui::Begin("Carbon Launcher", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_MenuBar); 222 | 223 | ImGui::BeginChild("Control", ImVec2(0, 64), true); 224 | 225 | if (C.processManager.IsGameRunning()) { 226 | if (ImGui::Button(ICON_FA_STOP " Kill Game", ImVec2(128, 48))) { 227 | C.processManager.KillGame(); 228 | } 229 | 230 | if (ImGui::IsItemHovered()) { 231 | ImGui::BeginTooltip(); 232 | ImGui::Text("Kill the game and all its processes, should be used as a last resort!"); 233 | ImGui::EndTooltip(); 234 | } 235 | } 236 | else { 237 | if (ImGui::Button(ICON_FA_PLAY " Launch Game", ImVec2(128, 48))) { 238 | C.processManager.LaunchProcess(C.guiManager.target == ModTarget::Game ? "ScrapMechanic.exe" : "ModTool.exe"); 239 | } 240 | } 241 | 242 | ImGui::SameLine(); 243 | 244 | // Combo box for selecting the mod target 245 | static const char* items[] = { "Game", "Mod Tool" }; 246 | static int item_current = 0; 247 | ImGui::SetNextItemWidth(128); 248 | ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 12); 249 | if (ImGui::Combo("Target", &item_current, items, IM_ARRAYSIZE(items))) { 250 | C.guiManager.target = item_current == 0 ? ModTarget::Game : ModTarget::ModTool; 251 | } 252 | 253 | ImGui::SameLine(); 254 | 255 | // Display if the pipe is connected or not if the game is running 256 | if (C.processManager.IsGameRunning()) { 257 | if (!C.pipeManager.IsConnected()) { 258 | static ImVec4 colours[3] = {}; 259 | if (colours[0].x == 0) { 260 | colours[0] = ImGui::GetStyleColorVec4(ImGuiCol_Button); 261 | colours[1] = ImGui::GetStyleColorVec4(ImGuiCol_Button); 262 | colours[2] = ImGui::GetStyleColorVec4(ImGuiCol_Button); 263 | 264 | for (int i = 0; i < 3; i++) { 265 | colours[i].w += 0.5f; 266 | 267 | float h, s, v; 268 | ImGui::ColorConvertRGBtoHSV(colours[i].x, colours[i].y, colours[i].z, h, s, v); 269 | h = 0.0f; 270 | s = 0.9f; 271 | ImGui::ColorConvertHSVtoRGB(h, s, v, colours[i].x, colours[i].y, colours[i].z); 272 | } 273 | } 274 | 275 | ImGui::PushStyleColor(ImGuiCol_Button, colours[0]); 276 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colours[1]); 277 | ImGui::PushStyleColor(ImGuiCol_ButtonActive, colours[2]); 278 | 279 | ImGui::Button(ICON_FA_X " Pipe Disconnected! " ICON_FA_FACE_SAD_TEAR, ImVec2(256, 48)); 280 | 281 | if (ImGui::IsItemHovered()) { 282 | ImGui::BeginTooltip(); 283 | ImGui::Text("The pipe is disconnected, the game may not be running correctly and Carbon Launcher cannot communicate with it, did you launch the game from the launcher?"); 284 | ImGui::EndTooltip(); 285 | } 286 | 287 | ImGui::PopStyleColor(3); 288 | } 289 | } 290 | 291 | ImGui::EndChild(); 292 | 293 | ImGui::BeginChild("Tabs", ImVec2(64, 0), true); 294 | 295 | auto highlight = [&](GUIManager::Tab tab) -> bool { 296 | static ImVec4 colours[3] = {}; 297 | if (colours[0].x == 0) { 298 | colours[0] = ImGui::GetStyleColorVec4(ImGuiCol_Button); 299 | colours[1] = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); 300 | colours[2] = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); 301 | 302 | colours[0].w += 0.5f; 303 | colours[1].w += 0.5f; 304 | colours[2].w += 0.5f; 305 | } 306 | if (C.guiManager.tab == tab) { 307 | ImGui::PushStyleColor(ImGuiCol_Button, colours[0]); 308 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colours[1]); 309 | ImGui::PushStyleColor(ImGuiCol_ButtonActive, colours[1]); 310 | } 311 | 312 | return C.guiManager.tab == tab; 313 | }; 314 | 315 | auto renderTag = [&](GUIManager::Tab tag, const char* icon, const char* tooltip) -> void { 316 | bool highlighted = highlight(tag); 317 | 318 | if (ImGui::Button(icon, ImVec2(48, 48))) { 319 | C.guiManager.tab = tag; 320 | } 321 | 322 | if (ImGui::IsItemHovered()) { 323 | ImGui::BeginTooltip(); 324 | ImGui::Text(tooltip); 325 | ImGui::EndTooltip(); 326 | } 327 | 328 | if (highlighted) 329 | ImGui::PopStyleColor(3); 330 | }; 331 | 332 | renderTag(GUIManager::Tab::MyMods, ICON_FA_PUZZLE_PIECE, "My Mods, where you can manage the mods you have installed"); 333 | renderTag(GUIManager::Tab::Discover, ICON_FA_SHOP, "Discover, where you can discover new mods"); 334 | renderTag(GUIManager::Tab::Console, ICON_FA_TERMINAL, "Console, where you can see the output of the game"); 335 | renderTag(GUIManager::Tab::Settings, ICON_FA_GEAR, "Settings, where you can configure the launcher"); 336 | 337 | ImGui::EndChild(); 338 | 339 | ImGui::SameLine(); 340 | 341 | ImGui::BeginChild("Content", ImVec2(0, 0), true); 342 | 343 | auto renderMod = [&](Mod& mod) -> void { 344 | ImGui::BeginChild(mod.ghRepo.c_str(), ImVec2(0, 72), false); 345 | 346 | float buttons = mod.wantsUpdate ? 2.75f : 2.0f; 347 | ImGui::BeginChild("Details", ImVec2(ImGui::GetContentRegionAvail().x - (64 * buttons), 0), false); 348 | 349 | ImGui::SetWindowFontScale(1.2f); 350 | ImGui::TextWrapped(mod.name.c_str()); 351 | ImGui::SetWindowFontScale(1.0f); 352 | 353 | ImGui::TextWrapped(mod.description.c_str()); 354 | 355 | ImGui::TextWrapped("Authors:"); 356 | ImGui::SameLine(); 357 | auto authBegin = mod.authors.begin(); 358 | auto authEnd = mod.authors.end(); 359 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 2)); 360 | for (auto it = authBegin; it != authEnd; it++) { 361 | auto& author = *it; 362 | if (it != authBegin) { 363 | ImGui::SameLine(); 364 | ImGui::TextWrapped(", "); 365 | } 366 | ImGui::SameLine(); 367 | if (ImGui::SmallButton(author.c_str())) { 368 | std::string url = fmt::format("https://github.com/{}", author); 369 | ShellExecute(NULL, L"open", std::wstring(url.begin(), url.end()).c_str(), NULL, NULL, SW_SHOWNORMAL); 370 | } 371 | } 372 | ImGui::PopStyleVar(); 373 | 374 | ImGui::EndChild(); 375 | 376 | ImGui::SameLine(); 377 | 378 | ImGui::BeginChild("Management", ImVec2(0, 0), false); 379 | 380 | if (mod.wantsUpdate) { 381 | if (ImGui::Button(ICON_FA_ARROWS_SPIN, ImVec2(48, 48))) { 382 | mod.Update(); 383 | } 384 | 385 | if (ImGui::IsItemHovered()) { 386 | ImGui::BeginTooltip(); 387 | ImGui::Text("Update the mod"); 388 | ImGui::EndTooltip(); 389 | } 390 | } 391 | 392 | ImGui::SameLine(); 393 | 394 | if (ImGui::Button(ICON_FA_GLOBE, ImVec2(48, 48))) { 395 | std::string url = fmt::format("https://github.com/{}/{}", mod.ghUser, mod.ghRepo); 396 | ShellExecute(NULL, L"open", std::wstring(url.begin(), url.end()).c_str(), NULL, NULL, SW_SHOWNORMAL); 397 | } 398 | 399 | if (ImGui::IsItemHovered()) { 400 | ImGui::BeginTooltip(); 401 | ImGui::Text("Open the mods GitHub page"); 402 | ImGui::EndTooltip(); 403 | } 404 | 405 | ImGui::SameLine(); 406 | 407 | if (mod.installed) { 408 | if (ImGui::Button(ICON_FA_TRASH, ImVec2(48, 48))) { 409 | mod.Uninstall(); 410 | } 411 | } 412 | else { 413 | if (ImGui::Button(ICON_FA_DOWNLOAD, ImVec2(48, 48))) { 414 | mod.Install(); 415 | } 416 | } 417 | 418 | if (ImGui::IsItemHovered()) { 419 | ImGui::BeginTooltip(); 420 | ImGui::Text(mod.installed ? "Uninstall the mod" : "Install the mod"); 421 | ImGui::EndTooltip(); 422 | } 423 | 424 | ImGui::EndChild(); 425 | ImGui::EndChild(); 426 | }; 427 | 428 | auto renderMods = [&](bool mustBeInstalled) -> void { 429 | for (auto& mod : C.modManager.GetMods(C.guiManager.target)) { 430 | if (mod.installed == mustBeInstalled) { 431 | renderMod(mod); 432 | } 433 | } 434 | 435 | if (std::all_of(C.modManager.GetMods(C.guiManager.target).begin(), C.modManager.GetMods(C.guiManager.target).end(), [&](Mod& mod) -> bool { 436 | return mod.installed != mustBeInstalled; 437 | })) { 438 | if (mustBeInstalled) { 439 | ImGui::TextWrapped("No mods installed! Get some from the discover tab " ICON_FA_FACE_SMILE); 440 | if (ImGui::Button("Take me there!")) { 441 | C.guiManager.tab = GUIManager::Tab::Discover; 442 | } 443 | } 444 | else { 445 | ImGui::TextWrapped("No more mods to discover! Check back later " ICON_FA_FACE_SMILE); 446 | } 447 | } 448 | }; 449 | 450 | auto renderConsole = [&]() -> void { 451 | ImGui::BeginChild("Console", ImVec2(0, 0), true); 452 | auto begin = C.logMessages.begin(); 453 | auto end = C.logMessages.end(); 454 | for (auto it = begin; it != end; it++) { 455 | auto& message = *it; 456 | ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), message.time.c_str()); 457 | ImGui::SameLine(); 458 | 459 | static auto typeToColour = std::map{ 460 | { LogColour::DARKGREEN, ImVec4(0.2f, 0.6f, 0.2f, 1.0f) }, 461 | { LogColour::BLUE, ImVec4(0.2f, 0.2f, 0.6f, 1.0f) }, 462 | { LogColour::PURPLE, ImVec4(0.5f, 0.2f, 0.5f, 1.0f) }, 463 | { LogColour::GOLD, ImVec4(0.7f, 0.5f, 0.2f, 1.0f) }, 464 | { LogColour::WHITE, ImVec4(0.8f, 0.8f, 0.8f, 1.0f) }, 465 | { LogColour::DARKGRAY, ImVec4(0.3f, 0.3f, 0.3f, 1.0f) }, 466 | { LogColour::DARKBLUE, ImVec4(0.1f, 0.1f, 0.4f, 1.0f) }, 467 | { LogColour::GREEN, ImVec4(0.2f, 0.7f, 0.2f, 1.0f) }, 468 | { LogColour::CYAN, ImVec4(0.2f, 0.7f, 0.7f, 1.0f) }, 469 | { LogColour::RED, ImVec4(0.7f, 0.2f, 0.2f, 1.0f) }, 470 | { LogColour::PINK, ImVec4(0.7f, 0.2f, 0.7f, 1.0f) }, 471 | { LogColour::YELLOW, ImVec4(0.7f, 0.7f, 0.2f, 1.0f) }, 472 | }; 473 | 474 | ImGui::PushStyleColor(ImGuiCol_Text, typeToColour[static_cast(message.colour)]); 475 | ImGui::TextWrapped(message.message.c_str()); 476 | ImGui::PopStyleColor(); 477 | } 478 | 479 | // If we were scrolled to the bottom, scroll down 480 | if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { 481 | ImGui::SetScrollHereY(1.0f); 482 | } 483 | 484 | ImGui::EndChild(); 485 | }; 486 | 487 | switch (C.guiManager.tab) { 488 | case GUIManager::Tab::MyMods: 489 | ImGui::SeparatorText("My Mods"); 490 | renderMods(true); 491 | break; 492 | case GUIManager::Tab::Discover: 493 | ImGui::SeparatorText("Discover"); 494 | renderMods(false); 495 | break; 496 | case GUIManager::Tab::Console: 497 | ImGui::SeparatorText("Console"); 498 | renderConsole(); 499 | 500 | break; 501 | case GUIManager::Tab::Settings: 502 | ImGui::SeparatorText("Settings"); 503 | 504 | if (ImGui::Button("Open Mods Directory")) { 505 | std::string path = Utils::GetDataDir() + "mods"; 506 | ShellExecute(NULL, L"open", std::wstring(path.begin(), path.end()).c_str(), NULL, NULL, SW_SHOWNORMAL); 507 | } 508 | 509 | break; 510 | }; 511 | 512 | ImGui::EndChild(); 513 | 514 | ImGui::End(); 515 | } 516 | -------------------------------------------------------------------------------- /CarbonLauncher/vendor/src/discord_rpc.cpp: -------------------------------------------------------------------------------- 1 | #include "discord_rpc.h" 2 | 3 | #include "backoff.h" 4 | #include "discord_register.h" 5 | #include "msg_queue.h" 6 | #include "rpc_connection.h" 7 | #include "serialization.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #ifndef DISCORD_DISABLE_IO_THREAD 14 | #include 15 | #include 16 | #endif 17 | 18 | constexpr size_t MaxMessageSize{16 * 1024}; 19 | constexpr size_t MessageQueueSize{8}; 20 | constexpr size_t JoinQueueSize{8}; 21 | 22 | struct QueuedMessage { 23 | size_t length; 24 | char buffer[MaxMessageSize]; 25 | 26 | void Copy(const QueuedMessage& other) 27 | { 28 | length = other.length; 29 | if (length) { 30 | memcpy(buffer, other.buffer, length); 31 | } 32 | } 33 | }; 34 | 35 | struct User { 36 | // snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null 37 | // terminator = 21 38 | char userId[32]; 39 | // 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null 40 | // terminator = 129 41 | char username[344]; 42 | // 4 decimal digits + 1 null terminator = 5 43 | char discriminator[8]; 44 | // optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35 45 | char avatar[128]; 46 | // Rounded way up because I'm paranoid about games breaking from future changes in these sizes 47 | }; 48 | 49 | static RpcConnection* Connection{nullptr}; 50 | static DiscordEventHandlers QueuedHandlers{}; 51 | static DiscordEventHandlers Handlers{}; 52 | static std::atomic_bool WasJustConnected{false}; 53 | static std::atomic_bool WasJustDisconnected{false}; 54 | static std::atomic_bool GotErrorMessage{false}; 55 | static std::atomic_bool WasJoinGame{false}; 56 | static std::atomic_bool WasSpectateGame{false}; 57 | static std::atomic_bool UpdatePresence{false}; 58 | static char JoinGameSecret[256]; 59 | static char SpectateGameSecret[256]; 60 | static int LastErrorCode{0}; 61 | static char LastErrorMessage[256]; 62 | static int LastDisconnectErrorCode{0}; 63 | static char LastDisconnectErrorMessage[256]; 64 | static std::mutex PresenceMutex; 65 | static std::mutex HandlerMutex; 66 | static QueuedMessage QueuedPresence{}; 67 | static MsgQueue SendQueue; 68 | static MsgQueue JoinAskQueue; 69 | static User connectedUser; 70 | 71 | // We want to auto connect, and retry on failure, but not as fast as possible. This does expoential 72 | // backoff from 0.5 seconds to 1 minute 73 | static Backoff ReconnectTimeMs(500, 60 * 1000); 74 | static auto NextConnect = std::chrono::system_clock::now(); 75 | static int Pid{0}; 76 | static int Nonce{1}; 77 | 78 | #ifndef DISCORD_DISABLE_IO_THREAD 79 | static void Discord_UpdateConnection(void); 80 | class IoThreadHolder { 81 | private: 82 | std::atomic_bool keepRunning{true}; 83 | std::mutex waitForIOMutex; 84 | std::condition_variable waitForIOActivity; 85 | std::thread ioThread; 86 | 87 | public: 88 | void Start() 89 | { 90 | keepRunning.store(true); 91 | ioThread = std::thread([&]() { 92 | const std::chrono::duration maxWait{500LL}; 93 | Discord_UpdateConnection(); 94 | while (keepRunning.load()) { 95 | std::unique_lock lock(waitForIOMutex); 96 | waitForIOActivity.wait_for(lock, maxWait); 97 | Discord_UpdateConnection(); 98 | } 99 | }); 100 | } 101 | 102 | void Notify() { waitForIOActivity.notify_all(); } 103 | 104 | void Stop() 105 | { 106 | keepRunning.exchange(false); 107 | Notify(); 108 | if (ioThread.joinable()) { 109 | ioThread.join(); 110 | } 111 | } 112 | 113 | ~IoThreadHolder() { Stop(); } 114 | }; 115 | #else 116 | class IoThreadHolder { 117 | public: 118 | void Start() {} 119 | void Stop() {} 120 | void Notify() {} 121 | }; 122 | #endif // DISCORD_DISABLE_IO_THREAD 123 | static IoThreadHolder* IoThread{nullptr}; 124 | 125 | static void UpdateReconnectTime() 126 | { 127 | NextConnect = std::chrono::system_clock::now() + 128 | std::chrono::duration{ReconnectTimeMs.nextDelay()}; 129 | } 130 | 131 | #ifdef DISCORD_DISABLE_IO_THREAD 132 | extern "C" DISCORD_EXPORT void Discord_UpdateConnection(void) 133 | #else 134 | static void Discord_UpdateConnection(void) 135 | #endif 136 | { 137 | if (!Connection) { 138 | return; 139 | } 140 | 141 | if (!Connection->IsOpen()) { 142 | if (std::chrono::system_clock::now() >= NextConnect) { 143 | UpdateReconnectTime(); 144 | Connection->Open(); 145 | } 146 | } 147 | else { 148 | // reads 149 | 150 | for (;;) { 151 | JsonDocument message; 152 | 153 | if (!Connection->Read(message)) { 154 | break; 155 | } 156 | 157 | const char* evtName = GetStrMember(&message, "evt"); 158 | const char* nonce = GetStrMember(&message, "nonce"); 159 | 160 | if (nonce) { 161 | // in responses only -- should use to match up response when needed. 162 | 163 | if (evtName && strcmp(evtName, "ERROR") == 0) { 164 | auto data = GetObjMember(&message, "data"); 165 | LastErrorCode = GetIntMember(data, "code"); 166 | StringCopy(LastErrorMessage, GetStrMember(data, "message", "")); 167 | GotErrorMessage.store(true); 168 | } 169 | } 170 | else { 171 | // should have evt == name of event, optional data 172 | if (evtName == nullptr) { 173 | continue; 174 | } 175 | 176 | auto data = GetObjMember(&message, "data"); 177 | 178 | if (strcmp(evtName, "ACTIVITY_JOIN") == 0) { 179 | auto secret = GetStrMember(data, "secret"); 180 | if (secret) { 181 | StringCopy(JoinGameSecret, secret); 182 | WasJoinGame.store(true); 183 | } 184 | } 185 | else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) { 186 | auto secret = GetStrMember(data, "secret"); 187 | if (secret) { 188 | StringCopy(SpectateGameSecret, secret); 189 | WasSpectateGame.store(true); 190 | } 191 | } 192 | else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) { 193 | auto user = GetObjMember(data, "user"); 194 | auto userId = GetStrMember(user, "id"); 195 | auto username = GetStrMember(user, "username"); 196 | auto avatar = GetStrMember(user, "avatar"); 197 | auto joinReq = JoinAskQueue.GetNextAddMessage(); 198 | if (userId && username && joinReq) { 199 | StringCopy(joinReq->userId, userId); 200 | StringCopy(joinReq->username, username); 201 | auto discriminator = GetStrMember(user, "discriminator"); 202 | if (discriminator) { 203 | StringCopy(joinReq->discriminator, discriminator); 204 | } 205 | if (avatar) { 206 | StringCopy(joinReq->avatar, avatar); 207 | } 208 | else { 209 | joinReq->avatar[0] = 0; 210 | } 211 | JoinAskQueue.CommitAdd(); 212 | } 213 | } 214 | } 215 | } 216 | 217 | // writes 218 | if (UpdatePresence.exchange(false) && QueuedPresence.length) { 219 | QueuedMessage local; 220 | { 221 | std::lock_guard guard(PresenceMutex); 222 | local.Copy(QueuedPresence); 223 | } 224 | if (!Connection->Write(local.buffer, local.length)) { 225 | // if we fail to send, requeue 226 | std::lock_guard guard(PresenceMutex); 227 | QueuedPresence.Copy(local); 228 | UpdatePresence.exchange(true); 229 | } 230 | } 231 | 232 | while (SendQueue.HavePendingSends()) { 233 | auto qmessage = SendQueue.GetNextSendMessage(); 234 | Connection->Write(qmessage->buffer, qmessage->length); 235 | SendQueue.CommitSend(); 236 | } 237 | } 238 | } 239 | 240 | static void SignalIOActivity() 241 | { 242 | if (IoThread != nullptr) { 243 | IoThread->Notify(); 244 | } 245 | } 246 | 247 | static bool RegisterForEvent(const char* evtName) 248 | { 249 | auto qmessage = SendQueue.GetNextAddMessage(); 250 | if (qmessage) { 251 | qmessage->length = 252 | JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName); 253 | SendQueue.CommitAdd(); 254 | SignalIOActivity(); 255 | return true; 256 | } 257 | return false; 258 | } 259 | 260 | static bool DeregisterForEvent(const char* evtName) 261 | { 262 | auto qmessage = SendQueue.GetNextAddMessage(); 263 | if (qmessage) { 264 | qmessage->length = 265 | JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName); 266 | SendQueue.CommitAdd(); 267 | SignalIOActivity(); 268 | return true; 269 | } 270 | return false; 271 | } 272 | 273 | extern "C" DISCORD_EXPORT void Discord_Initialize(const char* applicationId, 274 | DiscordEventHandlers* handlers, 275 | int autoRegister, 276 | const char* optionalSteamId) 277 | { 278 | IoThread = new (std::nothrow) IoThreadHolder(); 279 | if (IoThread == nullptr) { 280 | return; 281 | } 282 | 283 | if (autoRegister) { 284 | if (optionalSteamId && optionalSteamId[0]) { 285 | Discord_RegisterSteamGame(applicationId, optionalSteamId); 286 | } 287 | else { 288 | Discord_Register(applicationId, nullptr); 289 | } 290 | } 291 | 292 | Pid = GetProcessId(); 293 | 294 | { 295 | std::lock_guard guard(HandlerMutex); 296 | 297 | if (handlers) { 298 | QueuedHandlers = *handlers; 299 | } 300 | else { 301 | QueuedHandlers = {}; 302 | } 303 | 304 | Handlers = {}; 305 | } 306 | 307 | if (Connection) { 308 | return; 309 | } 310 | 311 | Connection = RpcConnection::Create(applicationId); 312 | Connection->onConnect = [](JsonDocument& readyMessage) { 313 | Discord_UpdateHandlers(&QueuedHandlers); 314 | if (QueuedPresence.length > 0) { 315 | UpdatePresence.exchange(true); 316 | SignalIOActivity(); 317 | } 318 | auto data = GetObjMember(&readyMessage, "data"); 319 | auto user = GetObjMember(data, "user"); 320 | auto userId = GetStrMember(user, "id"); 321 | auto username = GetStrMember(user, "username"); 322 | auto avatar = GetStrMember(user, "avatar"); 323 | if (userId && username) { 324 | StringCopy(connectedUser.userId, userId); 325 | StringCopy(connectedUser.username, username); 326 | auto discriminator = GetStrMember(user, "discriminator"); 327 | if (discriminator) { 328 | StringCopy(connectedUser.discriminator, discriminator); 329 | } 330 | if (avatar) { 331 | StringCopy(connectedUser.avatar, avatar); 332 | } 333 | else { 334 | connectedUser.avatar[0] = 0; 335 | } 336 | } 337 | WasJustConnected.exchange(true); 338 | ReconnectTimeMs.reset(); 339 | }; 340 | Connection->onDisconnect = [](int err, const char* message) { 341 | LastDisconnectErrorCode = err; 342 | StringCopy(LastDisconnectErrorMessage, message); 343 | WasJustDisconnected.exchange(true); 344 | UpdateReconnectTime(); 345 | }; 346 | 347 | IoThread->Start(); 348 | } 349 | 350 | extern "C" DISCORD_EXPORT void Discord_Shutdown(void) 351 | { 352 | if (!Connection) { 353 | return; 354 | } 355 | Connection->onConnect = nullptr; 356 | Connection->onDisconnect = nullptr; 357 | Handlers = {}; 358 | QueuedPresence.length = 0; 359 | UpdatePresence.exchange(false); 360 | if (IoThread != nullptr) { 361 | IoThread->Stop(); 362 | delete IoThread; 363 | IoThread = nullptr; 364 | } 365 | 366 | RpcConnection::Destroy(Connection); 367 | } 368 | 369 | extern "C" DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence) 370 | { 371 | { 372 | std::lock_guard guard(PresenceMutex); 373 | QueuedPresence.length = JsonWriteRichPresenceObj( 374 | QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence); 375 | UpdatePresence.exchange(true); 376 | } 377 | SignalIOActivity(); 378 | } 379 | 380 | extern "C" DISCORD_EXPORT void Discord_ClearPresence(void) 381 | { 382 | Discord_UpdatePresence(nullptr); 383 | } 384 | 385 | extern "C" DISCORD_EXPORT void Discord_Respond(const char* userId, /* DISCORD_REPLY_ */ int reply) 386 | { 387 | // if we are not connected, let's not batch up stale messages for later 388 | if (!Connection || !Connection->IsOpen()) { 389 | return; 390 | } 391 | auto qmessage = SendQueue.GetNextAddMessage(); 392 | if (qmessage) { 393 | qmessage->length = 394 | JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++); 395 | SendQueue.CommitAdd(); 396 | SignalIOActivity(); 397 | } 398 | } 399 | 400 | extern "C" DISCORD_EXPORT void Discord_RunCallbacks(void) 401 | { 402 | // Note on some weirdness: internally we might connect, get other signals, disconnect any number 403 | // of times inbetween calls here. Externally, we want the sequence to seem sane, so any other 404 | // signals are book-ended by calls to ready and disconnect. 405 | 406 | if (!Connection) { 407 | return; 408 | } 409 | 410 | bool wasDisconnected = WasJustDisconnected.exchange(false); 411 | bool isConnected = Connection->IsOpen(); 412 | 413 | if (isConnected) { 414 | // if we are connected, disconnect cb first 415 | std::lock_guard guard(HandlerMutex); 416 | if (wasDisconnected && Handlers.disconnected) { 417 | Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); 418 | } 419 | } 420 | 421 | if (WasJustConnected.exchange(false)) { 422 | std::lock_guard guard(HandlerMutex); 423 | if (Handlers.ready) { 424 | DiscordUser du{connectedUser.userId, 425 | connectedUser.username, 426 | connectedUser.discriminator, 427 | connectedUser.avatar}; 428 | Handlers.ready(&du); 429 | } 430 | } 431 | 432 | if (GotErrorMessage.exchange(false)) { 433 | std::lock_guard guard(HandlerMutex); 434 | if (Handlers.errored) { 435 | Handlers.errored(LastErrorCode, LastErrorMessage); 436 | } 437 | } 438 | 439 | if (WasJoinGame.exchange(false)) { 440 | std::lock_guard guard(HandlerMutex); 441 | if (Handlers.joinGame) { 442 | Handlers.joinGame(JoinGameSecret); 443 | } 444 | } 445 | 446 | if (WasSpectateGame.exchange(false)) { 447 | std::lock_guard guard(HandlerMutex); 448 | if (Handlers.spectateGame) { 449 | Handlers.spectateGame(SpectateGameSecret); 450 | } 451 | } 452 | 453 | // Right now this batches up any requests and sends them all in a burst; I could imagine a world 454 | // where the implementer would rather sequentially accept/reject each one before the next invite 455 | // is sent. I left it this way because I could also imagine wanting to process these all and 456 | // maybe show them in one common dialog and/or start fetching the avatars in parallel, and if 457 | // not it should be trivial for the implementer to make a queue themselves. 458 | while (JoinAskQueue.HavePendingSends()) { 459 | auto req = JoinAskQueue.GetNextSendMessage(); 460 | { 461 | std::lock_guard guard(HandlerMutex); 462 | if (Handlers.joinRequest) { 463 | DiscordUser du{req->userId, req->username, req->discriminator, req->avatar}; 464 | Handlers.joinRequest(&du); 465 | } 466 | } 467 | JoinAskQueue.CommitSend(); 468 | } 469 | 470 | if (!isConnected) { 471 | // if we are not connected, disconnect message last 472 | std::lock_guard guard(HandlerMutex); 473 | if (wasDisconnected && Handlers.disconnected) { 474 | Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); 475 | } 476 | } 477 | } 478 | 479 | extern "C" DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* newHandlers) 480 | { 481 | if (newHandlers) { 482 | #define HANDLE_EVENT_REGISTRATION(handler_name, event) \ 483 | if (!Handlers.handler_name && newHandlers->handler_name) { \ 484 | RegisterForEvent(event); \ 485 | } \ 486 | else if (Handlers.handler_name && !newHandlers->handler_name) { \ 487 | DeregisterForEvent(event); \ 488 | } 489 | 490 | std::lock_guard guard(HandlerMutex); 491 | HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN") 492 | HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE") 493 | HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST") 494 | 495 | #undef HANDLE_EVENT_REGISTRATION 496 | 497 | Handlers = *newHandlers; 498 | } 499 | else { 500 | std::lock_guard guard(HandlerMutex); 501 | Handlers = {}; 502 | } 503 | return; 504 | } 505 | --------------------------------------------------------------------------------