diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp
index 54dab08d..0156cf3c 100644
--- a/loader/include/Geode/utils/JsonValidation.hpp
+++ b/loader/include/Geode/utils/JsonValidation.hpp
@@ -140,12 +140,10 @@ namespace geode {
             return *this;
         }
 
-        template <matjson::Type T>
-        JsonMaybeValue& is() {
-            if (this->isError()) return *this;
-            self().m_hasValue = jsonConvertibleTo(self().m_json.type(), T);
-            m_inferType = false;
-            return *this;
+        template <class T>
+        bool is() {
+            if (this->isError()) return false;
+            return self().m_json.template is<T>();
         }
 
         template <class T>
diff --git a/loader/include/Geode/utils/cocos.hpp b/loader/include/Geode/utils/cocos.hpp
index 2b1dcdeb..48e15999 100644
--- a/loader/include/Geode/utils/cocos.hpp
+++ b/loader/include/Geode/utils/cocos.hpp
@@ -816,8 +816,30 @@ namespace geode::cocos {
                 static_cast<GLubyte>(hexValue >> 0 & 0xff)};
     }
 
-    GEODE_DLL Result<cocos2d::ccColor3B> cc3bFromHexString(std::string const& hexValue);
-    GEODE_DLL Result<cocos2d::ccColor4B> cc4bFromHexString(std::string const& hexValue);
+    /**
+     * Parse a ccColor3B from a hexadecimal string. The string may not contain 
+     * a leading '#'
+     * @param hexValue The string to parse into a color
+     * @param permissive If true, strings like "f" are considered valid 
+     * representations of the color white. Useful for UIs that allow entering 
+     * a hex color. Empty strings evaluate to pure white
+     * @returns A ccColor3B if it could be succesfully parsed, or an error 
+     * indicating the failure reason
+     */
+    GEODE_DLL Result<cocos2d::ccColor3B> cc3bFromHexString(std::string const& hexValue, bool permissive = false);
+    /**
+     * Parse a ccColor4B from a hexadecimal string. The string may not contain 
+     * a leading '#'
+     * @param hexValue The string to parse into a color
+     * @param requireAlpha Require the alpha component to be passed. If false, 
+     * alpha defaults to 255
+     * @param permissive If true, strings like "f" are considered valid 
+     * representations of the color white. Useful for UIs that allow entering 
+     * a hex color. Empty strings evaluate to pure white
+     * @returns A ccColor4B if it could be succesfully parsed, or an error 
+     * indicating the failure reason
+     */
+    GEODE_DLL Result<cocos2d::ccColor4B> cc4bFromHexString(std::string const& hexValue, bool requireAlpha = false, bool permissive = false);
     GEODE_DLL std::string cc3bToHexString(cocos2d::ccColor3B const& color);
     GEODE_DLL std::string cc4bToHexString(cocos2d::ccColor4B const& color);
 
diff --git a/loader/src/loader/Setting.cpp b/loader/src/loader/Setting.cpp
index e202944b..531a358f 100644
--- a/loader/src/loader/Setting.cpp
+++ b/loader/src/loader/Setting.cpp
@@ -15,7 +15,21 @@ template<class T>
 static void parseCommon(T& sett, JsonMaybeObject& obj) {
     obj.has("name").into(sett.name);
     obj.has("description").into(sett.description);
-    obj.has("default").into(sett.defaultValue);
+    if (auto defValue = obj.needs("default")) {
+        // Platform-specific default value
+        if (defValue.template is<matjson::Object>()) {
+            auto def = defValue.obj();
+            if (auto plat = def.has(PlatformID::toShortString(GEODE_PLATFORM_TARGET, true))) {
+                plat.into(sett.defaultValue);
+            }
+            else {
+                defValue.into(sett.defaultValue);
+            }
+        }
+        else {
+            defValue.into(sett.defaultValue);
+        }
+    }
 }
 
 Result<BoolSetting> BoolSetting::parse(JsonMaybeObject& obj) {
diff --git a/loader/src/ui/nodes/ColorPickPopup.cpp b/loader/src/ui/nodes/ColorPickPopup.cpp
index 2238387e..afdb9e0b 100644
--- a/loader/src/ui/nodes/ColorPickPopup.cpp
+++ b/loader/src/ui/nodes/ColorPickPopup.cpp
@@ -213,7 +213,7 @@ void ColorPickPopup::textChanged(CCTextInputNode* input) {
         switch (input->getTag()) {
             case TAG_HEX_INPUT:
                 {
-                    if (auto color = cc3bFromHexString(input->getString())) {
+                    if (auto color = cc3bFromHexString(input->getString(), true)) {
                         m_color.r = color.unwrap().r;
                         m_color.g = color.unwrap().g;
                         m_color.b = color.unwrap().b;
diff --git a/loader/src/utils/cocos.cpp b/loader/src/utils/cocos.cpp
index 7ebc56d3..0db65f29 100644
--- a/loader/src/utils/cocos.cpp
+++ b/loader/src/utils/cocos.cpp
@@ -114,8 +114,8 @@ ccColor4B matjson::Serialize<ccColor4B>::from_json(matjson::Value const& json) {
     return color;
 }
 
-Result<ccColor3B> geode::cocos::cc3bFromHexString(std::string const& hexValue) {
-    if (hexValue.empty()) {
+Result<ccColor3B> geode::cocos::cc3bFromHexString(std::string const& hexValue, bool permissive) {
+    if (permissive && hexValue.empty()) {
         return Ok(ccc3(255, 255, 255));
     }
     if (hexValue.size() > 6) {
@@ -142,21 +142,34 @@ Result<ccColor3B> geode::cocos::cc3bFromHexString(std::string const& hexValue) {
         } break;
 
         case 2: {
+            if (!permissive) {
+                return Err("Invalid hex pattern, expected RGB or RRGGBB");
+            }
             auto num = static_cast<uint8_t>(numValue);
             return Ok(ccc3(num, num, num));
         } break;
 
         case 1: {
+            if (!permissive) {
+                return Err("Invalid hex pattern, expected RGB or RRGGBB");
+            }
             auto num = static_cast<uint8_t>(numValue) * 17;
             return Ok(ccc3(num, num, num));
         } break;
 
-        default: return Err("Invalid hex size, expected 1, 2, 3, or 6");
+        default: {
+            if (permissive) {
+                return Err("Invalid hex pattern, expected R, RR, RGB, or RRGGBB");
+            }
+            else {
+                return Err("Invalid hex pattern, expected RGB or RRGGBB");
+            }
+        }
     }
 }
 
-Result<ccColor4B> geode::cocos::cc4bFromHexString(std::string const& hexValue) {
-    if (hexValue.empty()) {
+Result<ccColor4B> geode::cocos::cc4bFromHexString(std::string const& hexValue, bool requireAlpha, bool permissive) {
+    if (permissive && hexValue.empty()) {
         return Ok(ccc4(255, 255, 255, 255));
     }
     if (hexValue.size() > 8) {
@@ -177,6 +190,9 @@ Result<ccColor4B> geode::cocos::cc4bFromHexString(std::string const& hexValue) {
         } break;
 
         case 6: {
+            if (requireAlpha) {
+                return Err("Alpha component is required, got only RRGGBB");
+            }
             auto r = static_cast<uint8_t>((numValue & 0xFF0000) >> 16);
             auto g = static_cast<uint8_t>((numValue & 0x00FF00) >> 8);
             auto b = static_cast<uint8_t>((numValue & 0x0000FF));
@@ -192,6 +208,9 @@ Result<ccColor4B> geode::cocos::cc4bFromHexString(std::string const& hexValue) {
         } break;
 
         case 3: {
+            if (requireAlpha) {
+                return Err("Alpha component is required, got only RGB");
+            }
             auto r = static_cast<uint8_t>(((numValue & 0xF00) >> 8) * 17);
             auto g = static_cast<uint8_t>(((numValue & 0x0F0) >> 4) * 17);
             auto b = static_cast<uint8_t>(((numValue & 0x00F)) * 17);
@@ -199,16 +218,38 @@ Result<ccColor4B> geode::cocos::cc4bFromHexString(std::string const& hexValue) {
         } break;
 
         case 2: {
+            if (!permissive) {
+                return Err("Invalid hex pattern, expected RGBA or RRGGBBAA");
+            }
+            if (requireAlpha) {
+                return Err("Alpha component is required, specify full RRGGBBAA");
+            }
             auto num = static_cast<uint8_t>(numValue);
             return Ok(ccc4(num, num, num, 255));
         } break;
 
         case 1: {
+            if (!permissive) {
+                return Err("Invalid hex pattern, expected RGBA or RRGGBBAA");
+            }
+            if (requireAlpha) {
+                return Err("Alpha component is required, specify full RGBA");
+            }
             auto num = static_cast<uint8_t>(numValue) * 17;
             return Ok(ccc4(num, num, num, 255));
         } break;
 
-        default: return Err("Invalid hex size, expected 1, 2, 3, 4, 6, or 8");
+        default: {
+            if (requireAlpha) {
+                return Err("Invalid hex pattern, expected RGBA or RRGGBBAA");
+            }
+            else if (permissive) {
+                return Err("Invalid hex pattern, expected R, RR, RGB, RGBA, RRGGBB, or RRGGBBAA");
+            }
+            else {
+                return Err("Invalid hex pattern, expected RGB, RGBA, RRGGBB, or RRGGBBAA");
+            }
+        }
     }
 }