diff --git a/loader/include/Geode/Loader.hpp b/loader/include/Geode/Loader.hpp
index bb4e1789..fbb5e04f 100644
--- a/loader/include/Geode/Loader.hpp
+++ b/loader/include/Geode/Loader.hpp
@@ -7,3 +7,4 @@
 #include "loader/Loader.hpp"
 #include "loader/Interface.hpp"
 #include "loader/Setting.hpp"
+#include "loader/SettingEvent.hpp"
diff --git a/loader/include/Geode/loader/Event.hpp b/loader/include/Geode/loader/Event.hpp
index 3362f15b..8e0ec567 100644
--- a/loader/include/Geode/loader/Event.hpp
+++ b/loader/include/Geode/loader/Event.hpp
@@ -4,33 +4,40 @@
 #include <type_traits>
 #include "Mod.hpp"
 #include "Interface.hpp"
+#include <unordered_set>
 
 namespace geode {
 	class Mod;
 	class Event;
 
+	enum class PassThrough : bool {
+		Propagate,
+		Stop,
+	};
+
 	struct GEODE_DLL BasicEventHandler {
-		virtual bool onEvent(Event*) = 0;
+		virtual PassThrough passThrough(Event*) = 0;
 
 		void listen();
 		void unlisten();
 	};
 
 	class GEODE_DLL Event {
-		static std::vector<BasicEventHandler*> handlers;
+		static std::unordered_set<BasicEventHandler*> s_handlers;
+
 	 	friend BasicEventHandler;
 
 	 	Mod* m_sender;
 
 	public:
-	 	static std::vector<BasicEventHandler*> const& getHandlers();
+	 	static std::unordered_set<BasicEventHandler*> const& getHandlers();
 
 	 	void postFrom(Mod* sender);
 	 	inline void post() {
 	 		postFrom(Mod::get());
 	 	}
 
-	 	Mod* sender();
+	 	Mod* getSender();
 
 	 	virtual ~Event();
 	};
@@ -38,12 +45,12 @@ namespace geode {
 	template <typename T>
 	class EventHandler : public BasicEventHandler {
 	public:
-		virtual bool handle(T*) = 0;
-		bool onEvent(Event* ev) override {
+		virtual PassThrough handle(T*) = 0;
+		PassThrough passThrough(Event* ev) override {
 			if (auto myev = dynamic_cast<T*>(ev)) {
 				return handle(myev);
 			}
-			return true;
+			return PassThrough::Propagate;
 		}
 
 		EventHandler() {
diff --git a/loader/include/Geode/loader/Setting.hpp b/loader/include/Geode/loader/Setting.hpp
index 338506f1..fec606f9 100644
--- a/loader/include/Geode/loader/Setting.hpp
+++ b/loader/include/Geode/loader/Setting.hpp
@@ -17,12 +17,15 @@
 namespace geode {
     using ModJson = nlohmann::ordered_json;
 
+    class Setting;
     class SettingNode;
     class BoolSetting;
     class IntSetting;
     class FloatSetting;
     class StringSetting;
 
+    struct ModInfo;
+
     enum class SettingType {
         Bool,
         Int,
@@ -34,9 +37,21 @@ namespace geode {
         User,
     };
 
-    class GEODE_DLL Setting {
+    /**
+     * Base class for all settings in Geode mods. Note that for most purposes 
+     * you should use the built-in setting types. If you need a custom setting 
+     * type however, inherit from this class. Do note that you are responsible 
+     * for things like storing the default value, broadcasting value change 
+     * events, making the setting node etc.
+     */
+    class GEODE_DLL Setting :
+        public std::enable_shared_from_this<Setting>
+    {
     protected:
         std::string m_key;
+        std::string m_modID;
+
+        friend struct ModInfo;
 
         static Result<std::shared_ptr<Setting>> parse(
             std::string const& type,
@@ -59,6 +74,8 @@ namespace geode {
 
         virtual SettingNode* createNode(float width) = 0;
 
+        void valueChanged();
+
         std::string getKey() const;
         virtual SettingType getType() const = 0;
     };
@@ -161,6 +178,7 @@ namespace geode {
                 if constexpr (std::is_base_of_v<IMatch<Class, ValueType>, Class>) {
                     static_cast<Class*>(this)->constrainMatch(m_value);
                 }
+                this->valueChanged();
             }
 
             Result<> isValidValue(ValueType value) {
@@ -392,8 +410,7 @@ namespace geode {
     }
 
     class GEODE_DLL BoolSetting :
-        public GeodeSetting<BoolSetting, bool, SettingType::Bool>,
-        public std::enable_shared_from_this<BoolSetting>
+        public GeodeSetting<BoolSetting, bool, SettingType::Bool>
     {
     public:
         SettingNode* createNode(float width) override;
@@ -403,7 +420,6 @@ namespace geode {
         public GeodeSetting<IntSetting, int64_t, SettingType::Int>,
         public IOneOf<IntSetting, int64_t>,
         public IMinMax<int64_t>,
-        public std::enable_shared_from_this<IntSetting>,
         public ICArrows, public ICSlider<int64_t>, public ICInput
     {
     public:
@@ -414,7 +430,6 @@ namespace geode {
         public GeodeSetting<FloatSetting, double, SettingType::Float>,
         public IOneOf<FloatSetting, double>,
         public IMinMax<double>,
-        public std::enable_shared_from_this<FloatSetting>,
         public ICArrows, public ICSlider<double>, public ICInput
     {
     public:
@@ -424,8 +439,7 @@ namespace geode {
     class GEODE_DLL StringSetting : 
         public GeodeSetting<StringSetting, std::string, SettingType::String>,
         public IOneOf<StringSetting, std::string>,
-        public IMatch<StringSetting, std::string>,
-        public std::enable_shared_from_this<StringSetting>
+        public IMatch<StringSetting, std::string>
     {
     public:
         SettingNode* createNode(float width) override;
@@ -433,24 +447,21 @@ namespace geode {
     
     class GEODE_DLL FileSetting :
         public GeodeSetting<FileSetting, ghc::filesystem::path, SettingType::File>,
-        public ICFileFilters,
-        public std::enable_shared_from_this<FileSetting>
+        public ICFileFilters
     {
     public:
         SettingNode* createNode(float width) override;
     };
 
     class GEODE_DLL ColorSetting : 
-        public GeodeSetting<ColorSetting, cocos2d::ccColor3B, SettingType::Color>,
-        public std::enable_shared_from_this<ColorSetting>
+        public GeodeSetting<ColorSetting, cocos2d::ccColor3B, SettingType::Color>
     {
     public:
         SettingNode* createNode(float width) override;
     };
     
     class GEODE_DLL ColorAlphaSetting : 
-        public GeodeSetting<ColorAlphaSetting, cocos2d::ccColor4B, SettingType::ColorAlpha>,
-        public std::enable_shared_from_this<ColorAlphaSetting>
+        public GeodeSetting<ColorAlphaSetting, cocos2d::ccColor4B, SettingType::ColorAlpha>
     {
     public:
         SettingNode* createNode(float width) override;
diff --git a/loader/include/Geode/loader/SettingEvent.hpp b/loader/include/Geode/loader/SettingEvent.hpp
new file mode 100644
index 00000000..18572664
--- /dev/null
+++ b/loader/include/Geode/loader/SettingEvent.hpp
@@ -0,0 +1,71 @@
+#pragma once
+
+#include "Event.hpp"
+#include <optional>
+#include "Setting.hpp"
+
+namespace geode {
+    class GEODE_DLL SettingChangedEvent : public Event {
+    protected:
+        std::string m_modID;
+        std::shared_ptr<Setting> m_setting;
+    
+    public:
+        SettingChangedEvent(
+            std::string const& modID,
+            std::shared_ptr<Setting> setting
+        );
+        std::string getModID() const;
+        std::shared_ptr<Setting> getSetting() const;
+    };
+
+    template<class T>
+	class SettingChangedEventHandler : public EventHandler<SettingChangedEvent> {
+	public:
+		using Consumer = void(*)(std::shared_ptr<T>);
+        
+        static_assert(
+            std::is_base_of_v<Setting, T>,
+            "Setting must inherit from the Setting class"
+        );
+
+	protected:
+		Consumer m_consumer;
+		std::string m_modID;
+		std::optional<std::string> m_targetKey;
+	
+	public:
+        PassThrough handle(SettingChangedEvent* event) override {
+            if (
+                m_modID == event->getModID() && (
+                !m_targetKey ||
+                m_targetKey.value() == event->getSetting()->getKey()
+            )) {
+                m_consumer(std::static_pointer_cast<T>(event->getSetting()));
+            }
+			return PassThrough::Propagate;
+		}
+
+        /**
+         * Listen to changes on a specific setting
+         */
+		SettingChangedEventHandler(
+			std::string const& modID,
+			std::string const& settingID,
+			Consumer handler
+		) : m_modID(modID),
+            m_targetKey(settingID),
+            m_consumer(handler) {}
+
+        /**
+         * Listen to changes on all of a mods' settings
+         */
+		SettingChangedEventHandler(
+			std::string const& modID,
+			Consumer handler
+		) : m_modID(modID),
+            m_targetKey(std::nullopt),
+            m_consumer(handler) {}
+	};
+}
+
diff --git a/loader/src/internal/InternalLoader.cpp b/loader/src/internal/InternalLoader.cpp
index a8b23a7b..ec279877 100644
--- a/loader/src/internal/InternalLoader.cpp
+++ b/loader/src/internal/InternalLoader.cpp
@@ -56,12 +56,14 @@ void InternalLoader::executeGDThreadQueue() {
     m_gdThreadMutex.unlock();
 }
 
-void InternalLoader::queueConsoleMessage(LogPtr* msg) {
-    this->m_logQueue.push_back(msg);
+void InternalLoader::logConsoleMessage(LogPtr* msg) {
+    if (m_platformConsoleOpen) {
+        std::cout << msg->toString(true);
+    }
 }
 
-bool InternalLoader::platformConsoleReady() const {
-    return m_platformConsoleReady;
+bool InternalLoader::platformConsoleOpen() const {
+    return m_platformConsoleOpen;
 }
 
 bool InternalLoader::shownInfoAlert(std::string const& key) {
@@ -85,42 +87,28 @@ void InternalLoader::platformMessageBox(const char* title, std::string const& in
     MessageBoxA(nullptr, info.c_str(), title, MB_ICONERROR);
 }
 
-void InternalLoader::setupPlatformConsole() {
-    if (m_platformConsoleReady) return;
+void InternalLoader::openPlatformConsole() {
+    if (m_platformConsoleOpen) return;
     if (AllocConsole() == 0)    return;
     // redirect console output
     freopen_s(reinterpret_cast<FILE**>(stdout), "CONOUT$", "w", stdout);
     freopen_s(reinterpret_cast<FILE**>(stdin), "CONIN$", "r", stdin);
 
-    m_platformConsoleReady = true;
-}
+    m_platformConsoleOpen = true;
 
-void InternalLoader::awaitPlatformConsole() {
-    if (!m_platformConsoleReady) return;
-
-    for (auto const& log : m_logQueue) {
+    for (auto const& log : Loader::get()->getLogs()) {
         std::cout << log->toString(true) << "\n";
-        m_logQueue.clear();
     }
-
-    std::string inp;
-    getline(std::cin, inp);
-    std::string inpa;
-    std::stringstream ss(inp);
-    std::vector<std::string> args;
-
-    while (ss >> inpa) args.push_back(inpa);
-    ss.clear();
-    
-    this->awaitPlatformConsole();
 }
 
 void InternalLoader::closePlatformConsole() {
-    if (!m_platformConsoleReady) return;
+    if (!m_platformConsoleOpen) return;
 
     fclose(stdin);
     fclose(stdout);
     FreeConsole();
+
+    m_platformConsoleOpen = false;
 }
 
 #elif defined(GEODE_IS_MACOS)
@@ -130,8 +118,8 @@ void InternalLoader::platformMessageBox(const char* title, std::string const& in
 	std::cout << title << ": " << info << std::endl;
 }
 
-void InternalLoader::setupPlatformConsole() {
-    m_platformConsoleReady = true;
+void InternalLoader::openPlatformConsole() {
+    m_platformConsoleOpen = true;
 }
 
 void InternalLoader::awaitPlatformConsole() {
@@ -150,13 +138,13 @@ void InternalLoader::platformMessageBox(const char* title, std::string const& in
     std::cout << title << ": " << info << std::endl;
 }
 
-void InternalLoader::setupPlatformConsole() {
+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_platformConsoleReady = true;
+    m_platformConsoleOpen = true;
 }
 
 void InternalLoader::awaitPlatformConsole() {
diff --git a/loader/src/internal/InternalLoader.hpp b/loader/src/internal/InternalLoader.hpp
index c817df7c..099707a0 100644
--- a/loader/src/internal/InternalLoader.hpp
+++ b/loader/src/internal/InternalLoader.hpp
@@ -17,10 +17,9 @@ USE_GEODE_NAMESPACE();
  */
 class InternalLoader : public Loader {
 protected:
-	std::vector<LogPtr*> m_logQueue;
 	std::vector<std::function<void(void)>> m_gdThreadQueue;
 	mutable std::mutex m_gdThreadMutex;
-	bool m_platformConsoleReady = false;
+	bool m_platformConsoleOpen = false;
 	std::unordered_set<std::string> m_shownInfoAlerts;
 
 	void saveInfoAlerts(nlohmann::json& json);
@@ -48,10 +47,9 @@ public:
 	void queueInGDThread(std::function<void GEODE_CALL(void)> func);
 	void executeGDThreadQueue();
 
-	void queueConsoleMessage(LogPtr*);
-	bool platformConsoleReady() const;
-	void setupPlatformConsole();
-	void awaitPlatformConsole();
+	void logConsoleMessage(LogPtr*);
+	bool platformConsoleOpen() const;
+	void openPlatformConsole();
 	void closePlatformConsole();
 	static void platformMessageBox(const char* title, std::string const& info);
 
diff --git a/loader/src/load/Event.cpp b/loader/src/load/Event.cpp
index 410dfa3a..73a78f3c 100644
--- a/loader/src/load/Event.cpp
+++ b/loader/src/load/Event.cpp
@@ -3,15 +3,14 @@
 
 USE_GEODE_NAMESPACE();
 
-std::vector<BasicEventHandler*> Event::handlers = {};
+std::unordered_set<BasicEventHandler*> Event::s_handlers = {};
 
 void BasicEventHandler::listen() {
-	if (!utils::vector::contains(Event::handlers, this))
-		Event::handlers.push_back(this);
+	Event::s_handlers.insert(this);
 }
 
 void BasicEventHandler::unlisten() {
-	utils::vector::erase(Event::handlers, this);
+	Event::s_handlers.erase(this);
 }
 
 Event::~Event() {}
@@ -20,12 +19,17 @@ void Event::postFrom(Mod* m) {
 	if (m)
 		m_sender = m;
 
-	for (auto h : Event::handlers) {
-		if (!h->onEvent(this))
+	for (auto h : Event::s_handlers) {
+		if (h->passThrough(this) == PassThrough::Stop) {
 			break;
+		}
 	}
 }
 
-std::vector<BasicEventHandler*> const& Event::getHandlers() {
-	return Event::handlers;
+Mod* Event::getSender() {
+	return m_sender;
+}
+
+std::unordered_set<BasicEventHandler*> const& Event::getHandlers() {
+	return Event::s_handlers;
 }
diff --git a/loader/src/load/Loader.cpp b/loader/src/load/Loader.cpp
index ea871000..b830eb68 100644
--- a/loader/src/load/Loader.cpp
+++ b/loader/src/load/Loader.cpp
@@ -376,11 +376,7 @@ Loader::~Loader() {
 void Loader::pushLog(LogPtr* logptr) {
     m_logs.push_back(logptr);
 
-    if (InternalLoader::get()->platformConsoleReady()) {
-        std::cout << logptr->toString(true);
-    } else {
-        InternalLoader::get()->queueConsoleMessage(logptr);
-    }
+    InternalLoader::get()->logConsoleMessage(logptr);
 
     m_logStream << logptr->toString(true) << std::endl;
 }
@@ -458,12 +454,7 @@ bool Loader::supportedModVersion(VersionInfo const& version) {
 }
 
 void Loader::openPlatformConsole() {
-    if (!InternalLoader::get()->platformConsoleReady()) {
-        InternalLoader::get()->setupPlatformConsole();
-        std::thread([]() {
-            InternalLoader::get()->awaitPlatformConsole();
-        }).detach();
-    }
+    InternalLoader::get()->openPlatformConsole();
 }
 
 void Loader::closePlatfromConsole() {
diff --git a/loader/src/load/Log.cpp b/loader/src/load/Log.cpp
index 73a6e140..1d0bab95 100644
--- a/loader/src/load/Log.cpp
+++ b/loader/src/load/Log.cpp
@@ -136,7 +136,7 @@ void Log::flush() {
 
 Log::~Log() {
     this->flush();
-    if (InternalLoader::get()->platformConsoleReady()) {
+    if (InternalLoader::get()->platformConsoleOpen()) {
         std::cout << std::endl;
     }
 }
diff --git a/loader/src/load/ModInfo.cpp b/loader/src/load/ModInfo.cpp
index 1d528560..bf9b5579 100644
--- a/loader/src/load/ModInfo.cpp
+++ b/loader/src/load/ModInfo.cpp
@@ -64,9 +64,11 @@ Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) {
     }
 
     for (auto& [key, value] : root.has("settings").items()) {
-        auto sett = Setting::parse(key, value.json());
-        PROPAGATE(sett);
-        info.m_settings.push_back({ key, sett.value() });
+        auto settRes = Setting::parse(key, value.json());
+        PROPAGATE(settRes);
+        auto sett = settRes.value();
+        sett->m_modID = info.m_id;
+        info.m_settings.push_back({ key, sett });
     }
 
     if (auto resources = root.has("resources").obj()) {
diff --git a/loader/src/load/Setting.cpp b/loader/src/load/Setting.cpp
index b4956b3a..eb094ab2 100644
--- a/loader/src/load/Setting.cpp
+++ b/loader/src/load/Setting.cpp
@@ -1,4 +1,5 @@
 #include <Geode/loader/Setting.hpp>
+#include <Geode/loader/SettingEvent.hpp>
 #include <Geode/utils/general.hpp>
 #include <Geode/loader/SettingNode.hpp>
 #include "../ui/internal/settings/GeodeSettingNode.hpp"
@@ -54,30 +55,68 @@ Result<std::shared_ptr<Setting>> Setting::parse(
     return Err("Setting value is not an object");
 }
 
+void Setting::valueChanged() {
+    SettingChangedEvent(m_modID, shared_from_this()).post();
+}
+
 SettingNode* BoolSetting::createNode(float width) {
-    return BoolSettingNode::create(shared_from_this(), width);
+    return BoolSettingNode::create(
+        std::static_pointer_cast<BoolSetting>(shared_from_this()),
+        width
+    );
 }
 
 SettingNode* IntSetting::createNode(float width) {
-    return IntSettingNode::create(shared_from_this(), width);
+    return IntSettingNode::create(
+        std::static_pointer_cast<IntSetting>(shared_from_this()),
+        width
+    );
 }
 
 SettingNode* FloatSetting::createNode(float width) {
-    return FloatSettingNode::create(shared_from_this(), width);
+    return FloatSettingNode::create(
+        std::static_pointer_cast<FloatSetting>(shared_from_this()),
+        width
+    );
 }
 
 SettingNode* StringSetting::createNode(float width) {
-    return StringSettingNode::create(shared_from_this(), width);
+    return StringSettingNode::create(
+        std::static_pointer_cast<StringSetting>(shared_from_this()),
+        width
+    );
 }
 
 SettingNode* FileSetting::createNode(float width) {
-    return FileSettingNode::create(shared_from_this(), width);
+    return FileSettingNode::create(
+        std::static_pointer_cast<FileSetting>(shared_from_this()),
+        width
+    );
 }
 
 SettingNode* ColorSetting::createNode(float width) {
-    return ColorSettingNode::create(shared_from_this(), width);
+    return ColorSettingNode::create(
+        std::static_pointer_cast<ColorSetting>(shared_from_this()),
+        width
+    );
 }
 
 SettingNode* ColorAlphaSetting::createNode(float width) {
-    return ColorAlphaSettingNode::create(shared_from_this(), width);
+    return ColorAlphaSettingNode::create(
+        std::static_pointer_cast<ColorAlphaSetting>(shared_from_this()),
+        width
+    );
+}
+
+SettingChangedEvent::SettingChangedEvent(
+    std::string const& modID,
+    std::shared_ptr<Setting> setting
+) : m_modID(modID), m_setting(setting) {}
+
+std::string SettingChangedEvent::getModID() const {
+    return m_modID;
+}
+
+std::shared_ptr<Setting> SettingChangedEvent::getSetting() const {
+    return m_setting;
 }
diff --git a/loader/src/main.cpp b/loader/src/main.cpp
index d48f4c9f..62b49a93 100644
--- a/loader/src/main.cpp
+++ b/loader/src/main.cpp
@@ -1,5 +1,6 @@
 #include <Geode/loader/Mod.hpp>
 #include <Geode/loader/Loader.hpp>
+#include <Geode/loader/SettingEvent.hpp>
 #include <InternalLoader.hpp>
 #include <InternalMod.hpp>
 #include <Geode/loader/Log.hpp>
@@ -50,6 +51,16 @@ BOOL WINAPI DllMain(HINSTANCE lib, DWORD reason, LPVOID) {
 }
 #endif
 
+static SettingChangedEventHandler<BoolSetting> _(
+    "geode.loader", "show-platform-console",
+    [](std::shared_ptr<BoolSetting> setting) {
+        if (setting->getValue()) {
+            Loader::get()->openPlatformConsole();
+        } else {
+            Loader::get()->closePlatfromConsole();
+        }
+    }
+);
 
 int geodeEntry(void* platformData) {
     // setup internals
@@ -108,8 +119,7 @@ int geodeEntry(void* platformData) {
         << "Set up loader";
     
     if (InternalMod::get()->getSettingValue<bool>("show-platform-console")) {
-        InternalLoader::get()->setupPlatformConsole();
-        InternalLoader::get()->awaitPlatformConsole();
+        Loader::get()->openPlatformConsole();
     }
 
     InternalMod::get()->log()