diff --git a/loader/include/Geode/loader/IPC.hpp b/loader/include/Geode/loader/IPC.hpp index 5a8ab4ae..46ab8ffc 100644 --- a/loader/include/Geode/loader/IPC.hpp +++ b/loader/include/Geode/loader/IPC.hpp @@ -8,35 +8,61 @@ namespace geode { constexpr const char* IPC_PIPE_NAME = "\\\\.\\pipe\\GeodeIPCPipe"; #endif - // message ID may be anything that doesn't contain /, but to be safe, you - // should only define message IDs that match the following: [a-z\-_\.]+ + // IPC (Inter-Process Communication) provides a way for Geode mods to talk + // to other programs on the user's computer. If you have, for example, a + // debugger, or an external modding UI, that application can open up the + // platform-specific pipe and start sending messages to mods. Mods can + // listen for messages using the listenForIPC function, and reply to + // messages the get by using the reply method on the event provided. For + // example, an external application can query what mods are loaded in Geode + // by sending the `list-mods` message to `geode.loader`. class GEODE_DLL IPCEvent : public Event { protected: void* m_rawPipeHandle; std::string m_targetModID; - std::string m_senderID; + std::optional<std::string> m_replyID; std::string m_messageID; - std::string m_messageData; + nlohmann::json m_messageData; + bool m_replied = false; public: IPCEvent( void* rawPipeHandle, - std::string targetModID, - std::string senderID, - std::string messageID, - std::string messageData + std::string const& targetModID, + std::string const& messageID, + std::optional<std::string> const& replyID, + nlohmann::json const& messageData ); + virtual ~IPCEvent(); - std::string getSenderID() const; + std::optional<std::string> getReplyID() const; std::string getTargetModID() const; std::string getMessageID() const; - std::string getMessageData() const; + nlohmann::json getMessageData() const; /** - * Reply to the message. You can only reply once + * Whether this message can be replied to, i.e. if it has a reply ID + * provided + * @returns True if the message can be replied to, false otherwise */ - void reply(std::string const& data); + bool canReply() const; + + /** + * Reply to the message. Will post a message back to the application + * the sent this message with the reply ID and provided data. + * You can only reply once; after the other application has received + * the reply, it can assume the reply ID can be freed and reused for + * other messages. Calling reply again on this message will not cause + * a new response to be sent. + * If reply is not explicitly called, a default response of null will + * be posted back. + * @param data The data to send back; will be the under the "data" key + * in the response JSON. The structure may be anything; however, you + * should document what kind of JSON structures applications may expect + * from your mod. + */ + void reply(nlohmann::json const& data); }; class GEODE_DLL IPCFilter : public EventFilter<IPCEvent> { diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index f07d9eb4..147c096b 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -172,7 +172,19 @@ namespace geode { */ static Result<ModInfo> create(ModJson const& json); + /** + * Convert to JSON. Essentially same as getRawJSON except dynamically + * adds runtime fields like path + */ + ModJson toJSON() const; + /** + * Get the raw JSON file + */ + ModJson getRawJSON() const; + private: + ModJson m_rawJSON; + /** * Version is passed for backwards * compatibility if we update the mod.json @@ -186,6 +198,9 @@ namespace geode { std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles(); }; + // For converting ModInfo back to JSON + void GEODE_DLL to_json(nlohmann::json& json, ModInfo const& info); + /** * @class DataStore * Internal class for notifying Mod diff --git a/loader/include/Geode/loader/SettingEvent.hpp b/loader/include/Geode/loader/SettingEvent.hpp index c92b5219..de30f4ed 100644 --- a/loader/include/Geode/loader/SettingEvent.hpp +++ b/loader/include/Geode/loader/SettingEvent.hpp @@ -52,8 +52,8 @@ namespace geode { }; template <class T> - - requires std::is_base_of_v<Setting, T> std::monostate listenForSettingChanges( + requires std::is_base_of_v<Setting, T> + std::monostate listenForSettingChanges( std::string const& settingID, void (*callback)(T*) ) { Loader::get()->scheduleOnModLoad(getMod(), [=]() { diff --git a/loader/include/Geode/utils/VersionInfo.hpp b/loader/include/Geode/utils/VersionInfo.hpp index 365e6d77..22119e4d 100644 --- a/loader/include/Geode/utils/VersionInfo.hpp +++ b/loader/include/Geode/utils/VersionInfo.hpp @@ -2,6 +2,7 @@ #include <Geode/DefaultInclude.hpp> #include <string_view> +#include "json.hpp" namespace geode { /** @@ -61,6 +62,8 @@ namespace geode { std::string toString() const; }; + void GEODE_DLL to_json(nlohmann::json& json, VersionInfo const& info); + inline std::ostream& operator<<(std::ostream& stream, VersionInfo const& version) { stream << version.toString(); return stream; diff --git a/loader/src/internal/InternalLoader.hpp b/loader/src/internal/InternalLoader.hpp index 58965174..144c172e 100644 --- a/loader/src/internal/InternalLoader.hpp +++ b/loader/src/internal/InternalLoader.hpp @@ -44,10 +44,10 @@ public: bool setup(); - void postIPCMessage( + void postIPCReply( void* rawPipeHandle, - std::string const& senderID, - std::string const& data + std::string const& replyID, + nlohmann::json const& data ); /** diff --git a/loader/src/internal/ios/InternalLoader.cpp b/loader/src/internal/ios/InternalLoader.cpp index 6c026b15..1ef12df6 100644 --- a/loader/src/internal/ios/InternalLoader.cpp +++ b/loader/src/internal/ios/InternalLoader.cpp @@ -24,7 +24,7 @@ void InternalLoader::openPlatformConsole() { void InternalLoader::closePlatformConsole() {} -void InternalLoader::postIPCMessage( +void InternalLoader::postIPCReply( void* rawPipeHandle, std::string const& senderID, std::string const& data diff --git a/loader/src/internal/mac/InternalLoader.cpp b/loader/src/internal/mac/InternalLoader.cpp index 5abc785c..d6f1e98d 100644 --- a/loader/src/internal/mac/InternalLoader.cpp +++ b/loader/src/internal/mac/InternalLoader.cpp @@ -28,7 +28,7 @@ void InternalLoader::closePlatformConsole() { m_platformConsoleOpen = false; } -void InternalLoader::postIPCMessage( +void InternalLoader::postIPCReply( void* rawPipeHandle, std::string const& senderID, std::string const& data diff --git a/loader/src/internal/windows/InternalLoader.cpp b/loader/src/internal/windows/InternalLoader.cpp index ab709061..c6e99206 100644 --- a/loader/src/internal/windows/InternalLoader.cpp +++ b/loader/src/internal/windows/InternalLoader.cpp @@ -37,17 +37,21 @@ void InternalLoader::closePlatformConsole() { m_platformConsoleOpen = false; } -void InternalLoader::postIPCMessage( +void InternalLoader::postIPCReply( void* rawPipeHandle, - std::string const& senderID, - std::string const& data + std::string const& replyID, + nlohmann::json const& data ) { - std::string msg = senderID + "/" + data; - log::debug("Replying msg: {}", msg); + auto msgJson = nlohmann::json::object(); + msgJson["reply"] = replyID; + msgJson["data"] = data; + auto msg = msgJson.dump(); + DWORD written; WriteFile(rawPipeHandle, msg.c_str(), msg.size(), &written, nullptr); } +// todo: multiple connections void InternalLoader::setupIPC() { auto pipe = CreateNamedPipeA( IPC_PIPE_NAME, @@ -68,29 +72,27 @@ void InternalLoader::setupIPC() { DWORD read; while (ReadFile(pipe, buffer, sizeof(buffer) - 1, &read, nullptr)) { buffer[read] = '\0'; - - // format of the message should be modID/senderID/msgID/data - std::string modID; - std::string senderID; - std::string msgID; - std::string data; - size_t collectPart = 0; - for (size_t i = 0; i < read; i++) { - if (buffer[i] == '/' && collectPart < 3) { - collectPart++; - } else { - switch (collectPart) { - case 0: modID += buffer[i]; break; - case 1: senderID += buffer[i]; break; - case 2: msgID += buffer[i]; break; - default: data += buffer[i]; break; - } + try { + auto json = nlohmann::json::parse(buffer); + if (!json.contains("mod") || !json["mod"].is_string()) { + log::warn("Received IPC message without 'mod' field"); + continue; } - } - if (modID.size() && senderID.size() && msgID.size()) { - IPCEvent(pipe, modID, senderID, msgID, data).post(); - } else { - log::warn("Received invalid IPC message: '{}'", buffer); + if (!json.contains("message") || !json["message"].is_string()) { + log::warn("Received IPC message without 'message' field"); + continue; + } + std::optional<std::string> reply = std::nullopt; + if (json.contains("reply") && json["reply"].is_string()) { + reply = json["reply"]; + } + nlohmann::json data; + if (json.contains("data")) { + data = json["data"]; + } + IPCEvent(pipe, json["mod"], json["message"], reply, data).post(); + } catch(...) { + log::warn("Received IPC message that isn't valid JSON"); } } log::debug("Connection done"); diff --git a/loader/src/load/IPC.cpp b/loader/src/load/IPC.cpp index 84d3081b..8974fe13 100644 --- a/loader/src/load/IPC.cpp +++ b/loader/src/load/IPC.cpp @@ -5,18 +5,22 @@ USE_GEODE_NAMESPACE(); IPCEvent::IPCEvent( void* rawPipeHandle, - std::string targetModID, - std::string senderID, - std::string messageID, - std::string messageData + std::string const& targetModID, + std::string const& messageID, + std::optional<std::string> const& replyID, + nlohmann::json const& messageData ) : m_rawPipeHandle(rawPipeHandle), m_targetModID(targetModID), - m_senderID(senderID), m_messageID(messageID), + m_replyID(replyID), m_messageData(messageData) {} -std::string IPCEvent::getSenderID() const { - return m_senderID; +IPCEvent::~IPCEvent() { + this->reply(nullptr); +} + +std::optional<std::string> IPCEvent::getReplyID() const { + return m_replyID; } std::string IPCEvent::getTargetModID() const { @@ -27,13 +31,20 @@ std::string IPCEvent::getMessageID() const { return m_messageID; } -std::string IPCEvent::getMessageData() const { +nlohmann::json IPCEvent::getMessageData() const { return m_messageData; } -void IPCEvent::reply(std::string const& data) { - if (m_rawPipeHandle) { - InternalLoader::get()->postIPCMessage(m_rawPipeHandle, m_senderID, data); +bool IPCEvent::canReply() const { + return m_replyID.has_value(); +} + +void IPCEvent::reply(nlohmann::json const& data) { + if (!m_replied && m_rawPipeHandle && m_replyID.has_value()) { + InternalLoader::get()->postIPCReply( + m_rawPipeHandle, m_replyID.value(), data + ); + m_replied = true; } } diff --git a/loader/src/load/ModInfo.cpp b/loader/src/load/ModInfo.cpp index 926e1fad..4ac43838 100644 --- a/loader/src/load/ModInfo.cpp +++ b/loader/src/load/ModInfo.cpp @@ -15,6 +15,7 @@ Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) { ModInfo info; auto json = rawJson; + info.m_rawJSON = rawJson; #define PROPAGATE(err) \ { \ @@ -290,3 +291,18 @@ std::vector<std::pair<std::string, std::optional<std::string>*>> ModInfo::getSpe { "support.md", &m_supportInfo }, }; } + +ModJson ModInfo::toJSON() const { + auto json = m_rawJSON; + json["path"] = m_path; + json["binary"] = m_binaryName; + return json; +} + +ModJson ModInfo::getRawJSON() const { + return m_rawJSON; +} + +void to_json(nlohmann::json& json, ModInfo const& info) { + json = info.toJSON(); +} diff --git a/loader/src/main.cpp b/loader/src/main.cpp index 3bea32c8..a0cc0b68 100644 --- a/loader/src/main.cpp +++ b/loader/src/main.cpp @@ -102,7 +102,7 @@ BOOL WINAPI DllMain(HINSTANCE lib, DWORD reason, LPVOID) { #define $_ GEODE_CONCAT(unnamedVar_, __LINE__) -static auto $_ = listenForSettingChanges( +static auto $_ = listenForSettingChanges<BoolSetting>( "show-platform-console", [](BoolSetting* setting) { if (setting->getValue()) { @@ -119,16 +119,11 @@ static auto $_ = listenForIPC("ipc-test", +[](IPCEvent* event) { }); static auto $_ = listenForIPC("list-mods", +[](IPCEvent* event) { - event->reply( - "[ " + ranges::join( - ranges::map<std::vector<std::string>>( - Loader::get()->getAllMods(), - [](Mod* mod) { - return "\"" + mod->getID() + "\""; - } - ), ", " - ) + " ]" - ); + event->reply(ranges::map<std::vector<nlohmann::json>>( + Loader::get()->getAllMods(), [](Mod* mod) { + return mod->getModInfo().toJSON(); + } + )); }); int geodeEntry(void* platformData) { diff --git a/loader/src/utils/version.cpp b/loader/src/utils/version.cpp index cd72a00a..55f9b6d9 100644 --- a/loader/src/utils/version.cpp +++ b/loader/src/utils/version.cpp @@ -105,6 +105,10 @@ bool VersionInfo::match( return false; } +void to_json(nlohmann::json& json, VersionInfo const& info) { + json = info.toString(); +} + std::string VersionInfo::toString() const { return "v" + std::to_string(this->m_major) + "." + std::to_string(this->m_minor) + "." + std::to_string(this->m_patch);