diff --git a/loader/include/Geode/loader/Event.hpp b/loader/include/Geode/loader/Event.hpp
index 7c7bf65f..1ccc3348 100644
--- a/loader/include/Geode/loader/Event.hpp
+++ b/loader/include/Geode/loader/Event.hpp
@@ -65,12 +65,20 @@ namespace geode {
             return ListenerResult::Propagate;
         }
 
-        EventListener(T filter = T()) {}
-        EventListener(std::function<Callback> fn, T filter = T()) : m_callback(fn), m_filter(filter) {}
-        EventListener(Callback* fnptr, T filter = T()) : m_callback(fnptr), m_filter(filter) {}
+        EventListener(T filter = T()) {
+            this->enable();
+        }
+        EventListener(std::function<Callback> fn, T filter = T()) : m_callback(fn), m_filter(filter) {
+            this->enable();
+        }
+        EventListener(Callback* fnptr, T filter = T()) : m_callback(fnptr), m_filter(filter) {
+            this->enable();
+        }
 
         template <class C>
-        EventListener(C* cls, MemberFn<C> fn, T filter = T()) : EventListener(std::bind(fn, cls, std::placeholders::_1), filter) {}
+        EventListener(C* cls, MemberFn<C> fn, T filter = T()) : EventListener(std::bind(fn, cls, std::placeholders::_1), filter) {
+            this->enable();
+        }
 
         void bind(std::function<Callback> fn) {
             m_callback = fn;
diff --git a/loader/include/Geode/loader/IPC.hpp b/loader/include/Geode/loader/IPC.hpp
new file mode 100644
index 00000000..5a8ab4ae
--- /dev/null
+++ b/loader/include/Geode/loader/IPC.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "Event.hpp"
+#include "Loader.hpp"
+
+namespace geode {
+    #ifdef GEODE_IS_WINDOWS
+    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\-_\.]+
+
+    class GEODE_DLL IPCEvent : public Event {
+    protected:
+        void* m_rawPipeHandle;
+        std::string m_targetModID;
+        std::string m_senderID;
+        std::string m_messageID;
+        std::string m_messageData;
+
+    public:
+        IPCEvent(
+            void* rawPipeHandle,
+            std::string targetModID,
+            std::string senderID,
+            std::string messageID,
+            std::string messageData
+        );
+
+        std::string getSenderID() const;
+        std::string getTargetModID() const;
+        std::string getMessageID() const;
+        std::string getMessageData() const;
+
+        /**
+         * Reply to the message. You can only reply once
+         */
+        void reply(std::string const& data);
+    };
+
+    class GEODE_DLL IPCFilter : public EventFilter<IPCEvent> {
+    public:
+        using Callback = void(IPCEvent*);
+
+    protected:
+        std::string m_modID;
+        std::string m_messageID;
+
+    public:
+        ListenerResult handle(std::function<Callback> fn, IPCEvent* event);
+		IPCFilter(
+            std::string const& modID,
+            std::string const& messageID
+        );
+    };
+
+    template<class = void>
+    std::monostate listenForIPC(std::string const& messageID, void(*callback)(IPCEvent*)) {
+        Loader::get()->scheduleOnModLoad(getMod(), [=]() {
+            new EventListener(
+                callback, IPCFilter(getMod()->getID(), messageID)
+            );
+        });
+        return std::monostate();
+    }
+}
diff --git a/loader/include/Geode/loader/SettingEvent.hpp b/loader/include/Geode/loader/SettingEvent.hpp
index c5d2dbc9..2d3f0553 100644
--- a/loader/include/Geode/loader/SettingEvent.hpp
+++ b/loader/include/Geode/loader/SettingEvent.hpp
@@ -57,7 +57,7 @@ namespace geode {
         std::string const& settingID, void (*callback)(std::shared_ptr<T>)
     ) {
         Loader::get()->scheduleOnModLoad(getMod(), [=]() {
-            static auto _ = EventListener(
+            new EventListener(
                 callback, SettingChangedFilter<T>(getMod()->getID(), settingID)
             );
         });
@@ -66,7 +66,7 @@ namespace geode {
 
     static std::monostate listenForAllSettingChanges(void (*callback)(std::shared_ptr<Setting>)) {
         Loader::get()->scheduleOnModLoad(getMod(), [=]() {
-            static auto _ = EventListener(
+            new EventListener(
                 callback, SettingChangedFilter(getMod()->getID())
             );
         });
diff --git a/loader/include/Geode/utils/ranges.hpp b/loader/include/Geode/utils/ranges.hpp
index 1cd6e431..561a6a3f 100644
--- a/loader/include/Geode/utils/ranges.hpp
+++ b/loader/include/Geode/utils/ranges.hpp
@@ -201,11 +201,11 @@ namespace geode::utils::ranges {
     }
 
     template <
-        ValidConstContainer From, ValidContainer Into,
+        ValidContainer Into, ValidConstContainer From,
         ValidIntoConverter<typename From::value_type, typename Into::value_type> Mapper>
     Into map(From const& from, Mapper mapper) {
         auto res = Into();
-        std::transform(from.begin(), from.end(), res.end(), mapper);
+        std::transform(from.begin(), from.end(), std::back_inserter(res), mapper);
         return res;
     }
 
diff --git a/loader/src/ids/LevelInfoLayer.cpp b/loader/src/ids/LevelInfoLayer.cpp
index 37d8c598..28e216b0 100644
--- a/loader/src/ids/LevelInfoLayer.cpp
+++ b/loader/src/ids/LevelInfoLayer.cpp
@@ -1,4 +1,5 @@
 #include "AddIDs.hpp"
+#include <Geode/binding/LevelInfoLayer.hpp>
 #include <Geode/modify/LevelInfoLayer.hpp>
 
 $register_ids(LevelInfoLayer) {
diff --git a/loader/src/internal/InternalLoader.cpp b/loader/src/internal/InternalLoader.cpp
index c9de6c10..abd34c46 100644
--- a/loader/src/internal/InternalLoader.cpp
+++ b/loader/src/internal/InternalLoader.cpp
@@ -39,6 +39,9 @@ bool InternalLoader::setup() {
 
     log::log(Severity::Debug, InternalMod::get(), "Loaded hooks");
 
+    log::log(Severity::Debug, InternalMod::get(), "Setting up IPC...");
+    this->setupIPC();
+
     return true;
 }
 
@@ -177,79 +180,3 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
 
     return true;
 }
-
-#if defined(GEODE_IS_WINDOWS)
-void InternalLoader::platformMessageBox(char const* title, std::string const& info) {
-    MessageBoxA(nullptr, info.c_str(), title, MB_ICONERROR);
-}
-
-void InternalLoader::openPlatformConsole() {
-    if (m_platformConsoleOpen) return;
-    if (AllocConsole() == 0) return;
-    SetConsoleCP(CP_UTF8);
-    // redirect console output
-    freopen_s(reinterpret_cast<FILE**>(stdout), "CONOUT$", "w", stdout);
-    freopen_s(reinterpret_cast<FILE**>(stdin), "CONIN$", "r", stdin);
-
-    m_platformConsoleOpen = true;
-
-    for (auto const& log : Loader::get()->getLogs()) {
-        std::cout << log->toString(true) << "\n";
-    }
-}
-
-void InternalLoader::closePlatformConsole() {
-    if (!m_platformConsoleOpen) return;
-
-    fclose(stdin);
-    fclose(stdout);
-    FreeConsole();
-
-    m_platformConsoleOpen = false;
-}
-
-#elif defined(GEODE_IS_MACOS)
-    #include <CoreFoundation/CoreFoundation.h>
-
-void InternalLoader::platformMessageBox(char const* title, std::string const& info) {
-    CFStringRef cfTitle = CFStringCreateWithCString(NULL, title, kCFStringEncodingUTF8);
-    CFStringRef cfMessage = CFStringCreateWithCString(NULL, info.c_str(), kCFStringEncodingUTF8);
-
-    CFUserNotificationDisplayNotice(
-        0, kCFUserNotificationNoteAlertLevel, NULL, NULL, NULL, cfTitle, cfMessage, NULL
-    );
-}
-
-void InternalLoader::openPlatformConsole() {
-    m_platformConsoleOpen = true;
-
-    for (auto const& log : Loader::get()->getLogs()) {
-        std::cout << log->toString(true) << "\n";
-    }
-}
-
-void InternalLoader::closePlatformConsole() {
-    m_platformConsoleOpen = false;
-}
-
-#elif defined(GEODE_IS_IOS)
-
-    #include <pwd.h>
-    #include <sys/types.h>
-    #include <unistd.h>
-
-void InternalLoader::platformMessageBox(char const* title, std::string const& info) {
-    std::cout << title << ": " << info << std::endl;
-}
-
-void InternalLoader::openPlatformConsole() {
-    ghc::filesystem::path(getpwuid(getuid())->pw_dir);
-    freopen(
-        ghc::filesystem::path(utils::file::geodeRoot() / "geode_log.txt").string().c_str(), "w",
-        stdout
-    );
-    InternalLoader::m_platformConsoleOpen = true;
-}
-
-void InternalLoader::closePlatformConsole() {}
-#endif
diff --git a/loader/src/internal/InternalLoader.hpp b/loader/src/internal/InternalLoader.hpp
index ff62f133..58965174 100644
--- a/loader/src/internal/InternalLoader.hpp
+++ b/loader/src/internal/InternalLoader.hpp
@@ -31,6 +31,9 @@ protected:
 
     void downloadLoaderResources(IndexUpdateCallback callback);
 
+    bool loadHooks();
+    void setupIPC();
+
     InternalLoader();
     ~InternalLoader();
 
@@ -41,7 +44,11 @@ public:
 
     bool setup();
 
-    bool loadHooks();
+    void postIPCMessage(
+        void* rawPipeHandle,
+        std::string const& senderID,
+        std::string const& data
+    );
 
     /**
      * Check if a one-time event has been shown to the user,
diff --git a/loader/src/internal/ios/InternalLoader.cpp b/loader/src/internal/ios/InternalLoader.cpp
new file mode 100644
index 00000000..6c026b15
--- /dev/null
+++ b/loader/src/internal/ios/InternalLoader.cpp
@@ -0,0 +1,38 @@
+#include "../InternalLoader.hpp"
+#include <Geode/loader/Log.hpp>
+#include <iostream>
+#include "../InternalMod.hpp"
+
+#ifdef GEODE_IS_IOS
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+void InternalLoader::platformMessageBox(char const* title, std::string const& info) {
+    std::cout << title << ": " << info << std::endl;
+}
+
+void InternalLoader::openPlatformConsole() {
+    ghc::filesystem::path(getpwuid(getuid())->pw_dir);
+    freopen(
+        ghc::filesystem::path(utils::file::geodeRoot() / "geode_log.txt").string().c_str(), "w",
+        stdout
+    );
+    InternalLoader::m_platformConsoleOpen = true;
+}
+
+void InternalLoader::closePlatformConsole() {}
+
+void InternalLoader::postIPCMessage(
+    void* rawPipeHandle,
+    std::string const& senderID,
+    std::string const& data
+) {}
+
+void InternalLoader::setupIPC() {
+    #warning "Set up pipes or smth for this platform"
+    log::log(Severity::Warning, InternalMod::get(), "IPC is not supported on this platform");
+}
+
+#endif
diff --git a/loader/src/internal/mac/InternalLoader.cpp b/loader/src/internal/mac/InternalLoader.cpp
new file mode 100644
index 00000000..5abc785c
--- /dev/null
+++ b/loader/src/internal/mac/InternalLoader.cpp
@@ -0,0 +1,42 @@
+#include "../InternalLoader.hpp"
+#include <Geode/loader/Log.hpp>
+#include <iostream>
+#include "../InternalMod.hpp"
+
+#ifdef GEODE_IS_MACOS
+
+#include <CoreFoundation/CoreFoundation.h>
+
+void InternalLoader::platformMessageBox(char const* title, std::string const& info) {
+    CFStringRef cfTitle = CFStringCreateWithCString(NULL, title, kCFStringEncodingUTF8);
+    CFStringRef cfMessage = CFStringCreateWithCString(NULL, info.c_str(), kCFStringEncodingUTF8);
+
+    CFUserNotificationDisplayNotice(
+        0, kCFUserNotificationNoteAlertLevel, NULL, NULL, NULL, cfTitle, cfMessage, NULL
+    );
+}
+
+void InternalLoader::openPlatformConsole() {
+    m_platformConsoleOpen = true;
+
+    for (auto const& log : Loader::get()->getLogs()) {
+        std::cout << log->toString(true) << "\n";
+    }
+}
+
+void InternalLoader::closePlatformConsole() {
+    m_platformConsoleOpen = false;
+}
+
+void InternalLoader::postIPCMessage(
+    void* rawPipeHandle,
+    std::string const& senderID,
+    std::string const& data
+) {}
+
+void InternalLoader::setupIPC() {
+    #warning "Set up pipes or smth for this platform"
+    log::log(Severity::Warning, InternalMod::get(), "IPC is not supported on this platform");
+}
+
+#endif
diff --git a/loader/src/internal/windows/InternalLoader.cpp b/loader/src/internal/windows/InternalLoader.cpp
new file mode 100644
index 00000000..ab709061
--- /dev/null
+++ b/loader/src/internal/windows/InternalLoader.cpp
@@ -0,0 +1,110 @@
+#include "../InternalLoader.hpp"
+#include <Geode/loader/Log.hpp>
+#include <Geode/loader/IPC.hpp>
+#include <iostream>
+#include "../InternalMod.hpp"
+
+USE_GEODE_NAMESPACE();
+
+#ifdef GEODE_IS_WINDOWS
+
+void InternalLoader::platformMessageBox(char const* title, std::string const& info) {
+    MessageBoxA(nullptr, info.c_str(), title, MB_ICONERROR);
+}
+
+void InternalLoader::openPlatformConsole() {
+    if (m_platformConsoleOpen) return;
+    if (AllocConsole() == 0) return;
+    SetConsoleCP(CP_UTF8);
+    // redirect console output
+    freopen_s(reinterpret_cast<FILE**>(stdout), "CONOUT$", "w", stdout);
+    freopen_s(reinterpret_cast<FILE**>(stdin), "CONIN$", "r", stdin);
+
+    m_platformConsoleOpen = true;
+
+    for (auto const& log : Loader::get()->getLogs()) {
+        std::cout << log->toString(true) << "\n";
+    }
+}
+
+void InternalLoader::closePlatformConsole() {
+    if (!m_platformConsoleOpen) return;
+
+    fclose(stdin);
+    fclose(stdout);
+    FreeConsole();
+
+    m_platformConsoleOpen = false;
+}
+
+void InternalLoader::postIPCMessage(
+    void* rawPipeHandle,
+    std::string const& senderID,
+    std::string const& data
+) {
+    std::string msg = senderID + "/" + data;
+    log::debug("Replying msg: {}", msg);
+    DWORD written;
+    WriteFile(rawPipeHandle, msg.c_str(), msg.size(), &written, nullptr);
+}
+
+void InternalLoader::setupIPC() {
+    auto pipe = CreateNamedPipeA(
+        IPC_PIPE_NAME,
+        PIPE_ACCESS_DUPLEX,
+        PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
+        1,
+        1024 * 16,
+        1024 * 16,
+        NMPWAIT_USE_DEFAULT_WAIT,
+        nullptr
+    );
+    std::thread([pipe]() {
+        while (pipe != INVALID_HANDLE_VALUE) {
+            log::debug("Waiting for connection");
+            if (ConnectNamedPipe(pipe, nullptr)) {
+                log::debug("Got connection");
+                char buffer[1024];
+                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;
+                            }
+                        }
+                    }
+                    if (modID.size() && senderID.size() && msgID.size()) {
+                        IPCEvent(pipe, modID, senderID, msgID, data).post();
+                    } else {
+                        log::warn("Received invalid IPC message: '{}'", buffer);
+                    }
+                }
+                log::debug("Connection done");
+            }
+            DisconnectNamedPipe(pipe);
+            log::debug("Disconnected pipe");
+        }
+        log::debug("IPC ended");
+    }).detach();
+    if (pipe != INVALID_HANDLE_VALUE) {
+        log::log(Severity::Debug, InternalMod::get(), "IPC set up");
+    } else {
+        log::log(Severity::Error, InternalMod::get(), "Unable to set up IPC");
+    }
+}
+
+#endif
diff --git a/loader/src/load/IPC.cpp b/loader/src/load/IPC.cpp
new file mode 100644
index 00000000..84d3081b
--- /dev/null
+++ b/loader/src/load/IPC.cpp
@@ -0,0 +1,55 @@
+#include <Geode/loader/IPC.hpp>
+#include <InternalLoader.hpp>
+
+USE_GEODE_NAMESPACE();
+
+IPCEvent::IPCEvent(
+    void* rawPipeHandle,
+    std::string targetModID,
+    std::string senderID,
+    std::string messageID,
+    std::string messageData
+) : m_rawPipeHandle(rawPipeHandle),
+    m_targetModID(targetModID),
+    m_senderID(senderID),
+    m_messageID(messageID),
+    m_messageData(messageData) {}
+
+std::string IPCEvent::getSenderID() const {
+    return m_senderID;
+}
+
+std::string IPCEvent::getTargetModID() const {
+    return m_targetModID;
+}
+
+std::string IPCEvent::getMessageID() const {
+    return m_messageID;
+}
+
+std::string IPCEvent::getMessageData() const {
+    return m_messageData;
+}
+
+void IPCEvent::reply(std::string const& data) {
+    if (m_rawPipeHandle) {
+        InternalLoader::get()->postIPCMessage(m_rawPipeHandle, m_senderID, data);
+    }
+}
+
+ListenerResult IPCFilter::handle(std::function<Callback> fn, IPCEvent* event) {
+    if (
+        event->getTargetModID() == m_modID &&
+        event->getMessageID() == m_messageID
+    ) {
+        fn(event);
+        return ListenerResult::Stop;
+    }
+    return ListenerResult::Propagate;
+}
+
+IPCFilter::IPCFilter(
+    std::string const& modID,
+    std::string const& messageID
+) : m_modID(modID),
+    m_messageID(messageID) {}
diff --git a/loader/src/main.cpp b/loader/src/main.cpp
index c77d9faa..e7211d1b 100644
--- a/loader/src/main.cpp
+++ b/loader/src/main.cpp
@@ -4,6 +4,7 @@
 #include <Geode/loader/Log.hpp>
 #include <Geode/loader/Mod.hpp>
 #include <Geode/loader/SettingEvent.hpp>
+#include <Geode/loader/IPC.hpp>
 #include <InternalLoader.hpp>
 #include <InternalMod.hpp>
 #include <array>
@@ -96,7 +97,9 @@ BOOL WINAPI DllMain(HINSTANCE lib, DWORD reason, LPVOID) {
 }
 #endif
 
-static auto _ = listenForSettingChanges(
+#define $_ GEODE_CONCAT(unnamedVar_, __LINE__)
+
+static auto $_ = listenForSettingChanges(
     "show-platform-console",
     +[](std::shared_ptr<BoolSetting> setting) {
         if (setting->getValue()) {
@@ -108,6 +111,23 @@ static auto _ = listenForSettingChanges(
     }
 );
 
+static auto $_ = listenForIPC("ipc-test", +[](IPCEvent* event) {
+    event->reply("Hello from Geode!");
+});
+
+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() + "\"";
+                }
+            ), ", "
+        ) + " ]"
+    );
+});
+
 int geodeEntry(void* platformData) {
     // setup internals