From b1776d1d26effbbfc78f313126461e32a0a8cf2a Mon Sep 17 00:00:00 2001
From: HJfod <60038575+HJfod@users.noreply.github.com>
Date: Wed, 5 Oct 2022 15:41:05 +0300
Subject: [PATCH] move fetch from Index to exported utils + add close button as
 a member to Popup and move setup to be last in init + add
 GEODE_PLATFORM_SHORT_IDENTIFIER macro for the platform's identifier in GitHub
 release zips + add GEODE_VERSION to cmake + move unzipTo from Index to be an
 exported util in file namespace + add mod resources directories in accordance
 with new CLI

---
 CMakeLists.txt                                |  4 +-
 loader/CMakeLists.txt                         |  3 +-
 loader/include/Geode/loader/Loader.hpp        | 33 +++------
 loader/include/Geode/loader/Setting.hpp       |  2 +-
 loader/include/Geode/meta/preproc.hpp         |  4 ++
 loader/include/Geode/platform/cplatform.h     |  4 ++
 loader/include/Geode/platform/platform.hpp    |  4 ++
 loader/include/Geode/ui/Popup.hpp             | 15 ++--
 loader/include/Geode/utils/fetch.hpp          | 50 ++++++++++++++
 loader/include/Geode/utils/file.hpp           | 13 +++-
 .../lilac/include/geode/core/meta/preproc.hpp |  4 ++
 loader/resources/mod.json.in                  |  8 ++-
 loader/src/hooks/MenuLayer.cpp                |  1 +
 loader/src/index/Index.cpp                    | 63 ++---------------
 loader/src/index/InstallTicket.cpp            |  8 +--
 loader/src/index/fetch.hpp                    | 25 -------
 loader/src/internal/InternalLoader.cpp        |  1 +
 loader/src/internal/InternalLoader.hpp        |  4 +-
 loader/src/load/Loader.cpp                    | 35 +++++++---
 loader/src/load/ModInfo.cpp                   | 10 +--
 loader/src/main.cpp                           |  2 +-
 loader/src/{index => utils}/fetch.cpp         | 15 ++--
 loader/src/utils/file.cpp                     | 68 +++++++++++++++++++
 23 files changed, 232 insertions(+), 144 deletions(-)
 create mode 100644 loader/include/Geode/utils/fetch.hpp
 delete mode 100644 loader/src/index/fetch.hpp
 rename loader/src/{index => utils}/fetch.cpp (85%)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6fb980e7..15ddde33 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,8 @@
 cmake_minimum_required(VERSION 3.21 FATAL_ERROR)
 
-project(geode-sdk VERSION 0.3.0 LANGUAGES CXX C)
+set(GEODE_VERSION 0.3.0)
+
+project(geode-sdk VERSION ${GEODE_VERSION} LANGUAGES CXX C)
 
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt
index 27eabade..00a13695 100644
--- a/loader/CMakeLists.txt
+++ b/loader/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.21 FATAL_ERROR)
 
-project(geode-loader VERSION 0.2.1 LANGUAGES C CXX)
+project(geode-loader VERSION ${GEODE_VERSION} LANGUAGES C CXX)
 set(PROJECT_VERSION_TYPE Alpha)
 
 # Package info file for internal representation
@@ -28,6 +28,7 @@ file(GLOB CORE_SOURCES
 	src/utils/zip/*.cpp
 	src/index/*.cpp
 	src/ui/nodes/*.cpp
+	src/ui/internal/*.cpp
 	src/ui/internal/credits/*.cpp
 	src/ui/internal/dev/*.cpp
 	src/ui/internal/info/*.cpp
diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp
index 183d2182..6f148a03 100644
--- a/loader/include/Geode/loader/Loader.hpp
+++ b/loader/include/Geode/loader/Loader.hpp
@@ -60,25 +60,6 @@ namespace geode {
         bool m_isSetup = false;
         static bool s_unloading;
 
-        /**
-         * Lowest supported mod version.
-         * Any mod targeting a geode version 
-         * lower than this will not be loaded, 
-         * as they will be considered out-of-date.
-         */
-        static constexpr VersionInfo s_supportedVersionMin { 0, 1, 0 };
-        /**
-         * Highest support mod version.
-         * Any mod targeting a geode version 
-         * higher than this will not be loaded, 
-         * as a higher version means that 
-         * the user's geode is out-of-date, 
-         * or that the user is a time traveller 
-         * and has downloaded a mod from the 
-         * future.
-         */
-        static constexpr VersionInfo s_supportedVersionMax { 0, 2, 1 };
-
         Result<std::string> createTempDirectoryForMod(ModInfo const& info);
         Result<Mod*> loadModFromFile(std::string const& file);
         size_t loadModsFromDirectory(
@@ -107,8 +88,8 @@ namespace geode {
          */
         static Loader* get();
         
-        VersionInfo getVersion() const;
-        std::string getVersionType() const;
+        static VersionInfo getVersion();
+        static std::string getVersionType();
 
         Result<> saveSettings();
         Result<> loadSettings();
@@ -133,10 +114,18 @@ namespace geode {
          */
         ghc::filesystem::path getGeodeSaveDirectory() const;
 
+        /**
+         * Minimum supported mod version
+         */
+        static VersionInfo minModVersion();
+        /**
+         * Maximum supported mod version
+         */
+        static VersionInfo maxModVersion();
         /**
          * Check if a mod's version is within the supported range
          */
-        bool supportedModVersion(VersionInfo const& version);
+        static bool supportedModVersion(VersionInfo const& version);
 
         /**
          * Whether mod specified with ID is enabled 
diff --git a/loader/include/Geode/loader/Setting.hpp b/loader/include/Geode/loader/Setting.hpp
index fec606f9..77a93546 100644
--- a/loader/include/Geode/loader/Setting.hpp
+++ b/loader/include/Geode/loader/Setting.hpp
@@ -332,7 +332,7 @@ namespace geode {
             bool m_hasArrows = true;
             bool m_hasBigArrows = false;
             size_t m_arrowStep = 1;
-            size_t m_bigArrowStep = 1;
+            size_t m_bigArrowStep = 5;
         
         public:
             Result<> parseArrows(JsonMaybeObject<ModJson>& obj) {
diff --git a/loader/include/Geode/meta/preproc.hpp b/loader/include/Geode/meta/preproc.hpp
index 80dcc9a7..6a0beabe 100644
--- a/loader/include/Geode/meta/preproc.hpp
+++ b/loader/include/Geode/meta/preproc.hpp
@@ -11,6 +11,7 @@
         #define GEODE_PLATFORM_TARGET PlatformID::Windows
         #define GEODE_CALL __stdcall
         #define GEODE_PLATFORM_EXTENSION ".dll"
+        #define GEODE_PLATFORM_SHORT_IDENTIFIER "win"
     #else
         #define GEODE_WINDOWS(...)
     #endif
@@ -26,6 +27,7 @@
 			#define GEODE_PLATFORM_NAME "iOS"
 			#define GEODE_PLATFORM_TARGET PlatformID::iOS
 			#define GEODE_PLATFORM_EXTENSION ".ios.dylib"
+            #define GEODE_PLATFORM_SHORT_IDENTIFIER "ios"
 		#else
 			#define GEODE_IOS(...)
 			#define GEODE_MACOS(...) __VA_ARGS__
@@ -34,6 +36,7 @@
 			#define GEODE_PLATFORM_NAME "MacOS"
 			#define GEODE_PLATFORM_TARGET PlatformID::MacOS
 			#define GEODE_PLATFORM_EXTENSION ".dylib"
+            #define GEODE_PLATFORM_SHORT_IDENTIFIER "mac"
 		#endif
 		#define GEODE_CALL
 	#else
@@ -50,6 +53,7 @@
         #define GEODE_PLATFORM_TARGET PlatformID::Android
         #define GEODE_CALL
         #define GEODE_PLATFORM_EXTENSION ".so"
+        #define GEODE_PLATFORM_SHORT_IDENTIFIER "android"
     #else
         #define GEODE_ANDROID(...)
     #endif
diff --git a/loader/include/Geode/platform/cplatform.h b/loader/include/Geode/platform/cplatform.h
index 297f8aea..5efbee26 100644
--- a/loader/include/Geode/platform/cplatform.h
+++ b/loader/include/Geode/platform/cplatform.h
@@ -20,6 +20,7 @@
 	#define GEODE_PLATFORM_NAME "Windows"
 	#define GEODE_CALL __stdcall
 	#define GEODE_PLATFORM_EXTENSION ".dll"
+	#define GEODE_PLATFORM_SHORT_IDENTIFIER "win"
 	
 	#ifdef GEODE_EXPORTING
 		#undef GEODE_C_DLL
@@ -43,6 +44,7 @@
 		#define GEODE_IS_MOBILE
 		#define GEODE_PLATFORM_NAME "iOS"
 		#define GEODE_PLATFORM_EXTENSION ".ios.dylib"
+        #define GEODE_PLATFORM_SHORT_IDENTIFIER "ios"
 	#else
 		#define GEODE_IOS(...)
 		#define GEODE_MACOS(...) __VA_ARGS__
@@ -50,6 +52,7 @@
 		#define GEODE_IS_DESKTOP
 		#define GEODE_PLATFORM_NAME "MacOS"
 		#define GEODE_PLATFORM_EXTENSION ".dylib"
+        #define GEODE_PLATFORM_SHORT_IDENTIFIER "mac"
 	#endif
 	#define GEODE_CALL
 #else
@@ -65,6 +68,7 @@
 	#define GEODE_PLATFORM_NAME "Android"
 	#define GEODE_CALL
 	#define GEODE_PLATFORM_EXTENSION ".so"
+	#define GEODE_PLATFORM_SHORT_IDENTIFIER "android"
 #else
 	#define GEODE_ANDROID(...)
 #endif
diff --git a/loader/include/Geode/platform/platform.hpp b/loader/include/Geode/platform/platform.hpp
index 4b4ee893..bb34ca2f 100644
--- a/loader/include/Geode/platform/platform.hpp
+++ b/loader/include/Geode/platform/platform.hpp
@@ -77,6 +77,7 @@ namespace std {
 	#define GEODE_VIRTUAL_CONSTEXPR 
 	#define GEODE_NOINLINE __declspec(noinline)
 	#define GEODE_PLATFORM_EXTENSION ".dll"
+	#define GEODE_PLATFORM_SHORT_IDENTIFIER "win"
 
 	#ifdef GEODE_EXPORTING
 	    #define GEODE_DLL    __declspec(dllexport)
@@ -111,6 +112,7 @@ namespace std {
 	#define GEODE_VIRTUAL_CONSTEXPR constexpr
 	#define GEODE_NOINLINE __attribute__((noinline))
 	#define GEODE_PLATFORM_EXTENSION ".ios.dylib"
+	#define GEODE_PLATFORM_SHORT_IDENTIFIER "ios"
 
 	#ifdef GEODE_EXPORTING
 	    #define GEODE_DLL    __attribute__((visibility("default")))
@@ -137,6 +139,7 @@ namespace std {
 	#define GEODE_VIRTUAL_CONSTEXPR constexpr
 	#define GEODE_NOINLINE __attribute__((noinline))
 	#define GEODE_PLATFORM_EXTENSION ".dylib"
+	#define GEODE_PLATFORM_SHORT_IDENTIFIER "mac"
 
 	#ifdef GEODE_EXPORTING
 	    #define GEODE_DLL    __attribute__((visibility("default")))
@@ -169,6 +172,7 @@ namespace std {
 	#define GEODE_VIRTUAL_CONSTEXPR constexpr
 	#define GEODE_NOINLINE __attribute__((noinline))
 	#define GEODE_PLATFORM_EXTENSION ".so"
+	#define GEODE_PLATFORM_SHORT_IDENTIFIER "android"
 
 	#ifdef GEODE_EXPORTING
 	    #define GEODE_DLL    __attribute__((visibility("default")))
diff --git a/loader/include/Geode/ui/Popup.hpp b/loader/include/Geode/ui/Popup.hpp
index c24cf9fc..5077fd2b 100644
--- a/loader/include/Geode/ui/Popup.hpp
+++ b/loader/include/Geode/ui/Popup.hpp
@@ -9,6 +9,7 @@ namespace geode {
         cocos2d::CCSize m_size;
         cocos2d::extension::CCScale9Sprite* m_bgSprite;
         cocos2d::CCLabelBMFont* m_title = nullptr;
+        CCMenuItemSpriteExtra* m_closeBtn;
 
         bool init(
             float width,
@@ -36,21 +37,21 @@ namespace geode {
             cocos2d::CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
             this->registerWithTouchDispatcher();
 
-            if (!setup(std::forward<InitArgs>(args)...)) {
-                return false;
-            }
-
             auto closeSpr = cocos2d::CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png");
             closeSpr->setScale(.8f);
 
-            auto closeBtn = CCMenuItemSpriteExtra::create(
+            m_closeBtn = CCMenuItemSpriteExtra::create(
                 closeSpr, this, (cocos2d::SEL_MenuHandler)(&Popup::onClose)
             );
-            closeBtn->setPosition(
+            m_closeBtn->setPosition(
                 -m_size.width / 2 + 3.f,
                 m_size.height / 2 - 3.f
             );
-            m_buttonMenu->addChild(closeBtn);
+            m_buttonMenu->addChild(m_closeBtn);
+
+            if (!setup(std::forward<InitArgs>(args)...)) {
+                return false;
+            }
 
             this->setKeypadEnabled(true);
             this->setTouchEnabled(true);
diff --git a/loader/include/Geode/utils/fetch.hpp b/loader/include/Geode/utils/fetch.hpp
new file mode 100644
index 00000000..0686fefe
--- /dev/null
+++ b/loader/include/Geode/utils/fetch.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "../DefaultInclude.hpp"
+#include <fs/filesystem.hpp>
+#include "Result.hpp"
+#include "json.hpp"
+
+namespace geode::utils::web {
+    using FileProgressCallback = std::function<bool(double, double)>;
+
+    /**
+     * Synchronously fetch data from the internet
+     * @param url URL to fetch
+     * @returns Returned data as string, or error on error
+     */
+    GEODE_DLL Result<std::string> fetch(std::string const& url);
+
+    /**
+     * Syncronously download a file from the internet
+     * @param url URL to fetch
+     * @param into Path to download file into
+     * @param prog Progress function; first parameter is bytes downloaded so 
+     * far, and second is total bytes to download. Return true to continue 
+     * downloading, and false to interrupt. Note that interrupting does not 
+     * automatically remove the file that was being downloaded
+     * @returns Returned data as JSON, or error on error
+     */
+    GEODE_DLL Result<> fetchFile(
+        std::string const& url,
+        ghc::filesystem::path const& into,
+        FileProgressCallback prog = nullptr
+    );
+
+    /**
+     * Synchronously fetch data from the internet and parse it as JSON
+     * @param url URL to fetch
+     * @returns Returned data as JSON, or error on error
+     */
+    template<class Json = nlohmann::json>
+    Result<Json> fetchJSON(std::string const& url) {
+        auto res = fetch(url);
+        if (!res) return Err(res.error());
+        try {
+            return Ok(Json::parse(res.value()));
+        } catch(std::exception& e) {
+            return Err(e.what());
+        }
+    }
+}
+
diff --git a/loader/include/Geode/utils/file.hpp b/loader/include/Geode/utils/file.hpp
index 999bbd85..059f466f 100644
--- a/loader/include/Geode/utils/file.hpp
+++ b/loader/include/Geode/utils/file.hpp
@@ -4,7 +4,7 @@
 #include "Result.hpp"
 #include <string>
 #include "types.hpp"
-#include "fs/filesystem.hpp"
+#include <fs/filesystem.hpp>
 
 namespace geode::utils::file {
     GEODE_DLL Result<std::string> readString(std::string            const& path);
@@ -25,4 +25,15 @@ namespace geode::utils::file {
     GEODE_DLL Result<> createDirectoryAll(std::string const& path);
     GEODE_DLL Result<std::vector<std::string>> listFiles(std::string const& path);
     GEODE_DLL Result<std::vector<std::string>> listFilesRecursively(std::string const& path);
+
+    /**
+     * Unzip file to directory
+     * @param from File to unzip
+     * @param to Directory to unzip to
+     * @returns Ok on success, Error on error
+     */
+    GEODE_DLL Result<> unzipTo(
+        ghc::filesystem::path const& from,
+        ghc::filesystem::path const& to
+    );
 }
diff --git a/loader/lilac/include/geode/core/meta/preproc.hpp b/loader/lilac/include/geode/core/meta/preproc.hpp
index bd584ea0..2cb8f24f 100644
--- a/loader/lilac/include/geode/core/meta/preproc.hpp
+++ b/loader/lilac/include/geode/core/meta/preproc.hpp
@@ -11,6 +11,7 @@
         #define GEODE_PLATFORM_TARGET PlatformID::Windows
         #define GEODE_CALL __stdcall
         #define GEODE_PLATFORM_EXTENSION ".dll"
+        #define GEODE_PLATFORM_SHORT_IDENTIFIER "win"
     #else
         #define GEODE_WINDOWS(...)
     #endif
@@ -26,6 +27,7 @@
 			#define GEODE_PLATFORM_NAME "iOS"
 			#define GEODE_PLATFORM_TARGET PlatformID::iOS
 			#define GEODE_PLATFORM_EXTENSION ".ios.dylib"
+            #define GEODE_PLATFORM_SHORT_IDENTIFIER "ios"
 		#else
 			#define GEODE_IOS(...)
 			#define GEODE_MACOS(...) __VA_ARGS__
@@ -34,6 +36,7 @@
 			#define GEODE_PLATFORM_NAME "MacOS"
 			#define GEODE_PLATFORM_TARGET PlatformID::MacOS
 			#define GEODE_PLATFORM_EXTENSION ".dylib"
+            #define GEODE_PLATFORM_SHORT_IDENTIFIER "mac"
 		#endif
 		#define GEODE_CALL
 	#else
@@ -50,6 +53,7 @@
         #define GEODE_PLATFORM_TARGET PlatformID::Android
         #define GEODE_CALL
         #define GEODE_PLATFORM_EXTENSION ".so"
+        #define GEODE_PLATFORM_SHORT_IDENTIFIER "android"
     #else
         #define GEODE_ANDROID(...)
     #endif
diff --git a/loader/resources/mod.json.in b/loader/resources/mod.json.in
index 0debeb34..9646fbdc 100644
--- a/loader/resources/mod.json.in
+++ b/loader/resources/mod.json.in
@@ -52,7 +52,13 @@
             "type": "bool",
             "default": false,
             "name": "Show Platform Console",
-            "description": "Show the native console (if one exists). <cr>This setting is meant for developers</c>."
+            "description": "Show the native console (if one exists). <cr>This setting is meant for developers</c>"
+        },
+        "auto-check-updates": {
+            "type": "bool",
+            "default": true,
+            "name": "Check For Updates",
+            "description": "Automatically check for <cy>updates</c> to Geode on startup"
         }
     },
     "issues": {
diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp
index 1754a534..b948da56 100644
--- a/loader/src/hooks/MenuLayer.cpp
+++ b/loader/src/hooks/MenuLayer.cpp
@@ -7,6 +7,7 @@
 #include <Geode/ui/MDPopup.hpp>
 #include <InternalMod.hpp>
 #include "../ui/internal/info/ModInfoLayer.hpp"
+#include <InternalLoader.hpp>
 
 USE_GEODE_NAMESPACE();
 
diff --git a/loader/src/index/Index.cpp b/loader/src/index/Index.cpp
index 7a18fb71..36dd368a 100644
--- a/loader/src/index/Index.cpp
+++ b/loader/src/index/Index.cpp
@@ -2,7 +2,7 @@
 #include <thread>
 #include <Geode/utils/json.hpp>
 #include <Geode/utils/JsonValidation.hpp>
-#include "fetch.hpp"
+#include <Geode/utils/fetch.hpp>
 
 #define GITHUB_DONT_RATE_LIMIT_ME_PLS 0
 
@@ -19,61 +19,6 @@ static Result<Json> readJSON(ghc::filesystem::path const& path) {
     }
 }
 
-static Result<> unzipTo(
-    ghc::filesystem::path const& from,
-    ghc::filesystem::path const& to
-) {
-    // unzip downloaded
-    auto unzip = ZipFile(from.string());
-    if (!unzip.isLoaded()) {
-        return Err("Unable to unzip index.zip");
-    }
-
-    for (auto file : unzip.getAllFiles()) {
-        // this is a very bad check for seeing 
-        // if file is a directory. it seems to 
-        // work on windows at least. idk why 
-        // getAllFiles returns the directories 
-        // aswell now
-        if (
-            utils::string::endsWith(file, "\\") ||
-            utils::string::endsWith(file, "/")
-        ) continue;
-
-        auto zipPath = file;
-
-        // dont include the github repo folder
-        file = file.substr(file.find_first_of("/") + 1);
-
-        auto path = ghc::filesystem::path(file);
-        if (path.has_parent_path()) {
-            if (
-                !ghc::filesystem::exists(to / path.parent_path()) &&
-                !ghc::filesystem::create_directories(to / path.parent_path())
-            ) {
-                return Err(
-                    "Unable to create directories \"" + 
-                    path.parent_path().string() + "\""
-                );
-            }
-        }
-        unsigned long size;
-        auto data = unzip.getFileData(zipPath, &size);
-        if (!data || !size) {
-            return Err("Unable to read \"" + std::string(zipPath) + "\"");
-        }
-        auto wrt = utils::file::writeBinary(
-            to / file,
-            byte_array(data, data + size)
-        );
-        if (!wrt) {
-            return Err("Unable to write \"" + file + "\": " + wrt.error());
-        }
-    }
-
-    return Ok();
-}
-
 static PlatformID platformFromString(std::string const& str) {
     switch (hash(utils::string::trim(utils::string::toLower(str)).c_str())) {
         default:
@@ -126,7 +71,7 @@ void Index::updateIndexThread(bool force) {
     );
 
     // get all commits in index repo
-    auto commit = fetchJSON(
+    auto commit = web::fetchJSON(
         "https://api.github.com/repos/geode-sdk/mods/commits"
     );
     if (!commit) {
@@ -202,7 +147,7 @@ void Index::updateIndexThread(bool force) {
             "Downloading index",
             50
         );
-        auto gotZip = fetchFile(
+        auto gotZip = web::fetchFile(
             "https://github.com/geode-sdk/mods/zipball/main",
             indexDir / "index.zip"
         );
@@ -218,7 +163,7 @@ void Index::updateIndexThread(bool force) {
             ghc::filesystem::remove_all(indexDir / "index");
         }
 
-        auto unzip = unzipTo(indexDir / "index.zip", indexDir);
+        auto unzip = file::unzipTo(indexDir / "index.zip", indexDir);
         if (!unzip) {
             return indexUpdateProgress(
                 UpdateStatus::Failed, unzip.error()
diff --git a/loader/src/index/InstallTicket.cpp b/loader/src/index/InstallTicket.cpp
index 4fc82e18..1833d022 100644
--- a/loader/src/index/InstallTicket.cpp
+++ b/loader/src/index/InstallTicket.cpp
@@ -2,7 +2,7 @@
 #include <thread>
 #include <Geode/utils/json.hpp>
 #include <hash.hpp>
-#include "fetch.hpp"
+#include <Geode/utils/fetch.hpp>
 
 void InstallTicket::postProgress(
     UpdateStatus status,
@@ -46,7 +46,7 @@ void InstallTicket::install(std::string const& id) {
     auto tempFile = indexDir / item.m_download.m_filename;
 
     this->postProgress(UpdateStatus::Progress, "Fetching binary", 0);
-    auto res = fetchFile(
+    auto res = web::fetchFile(
         item.m_download.m_url,
         tempFile,
         [this, tempFile](double now, double total) -> int {
@@ -54,7 +54,7 @@ void InstallTicket::install(std::string const& id) {
             std::lock_guard cancelLock(m_cancelMutex);
             if (m_cancelling) {
                 try { ghc::filesystem::remove(tempFile); } catch(...) {}
-                return 1;
+                return false;
             }
 
             // no need to scope the lock guard more as this 
@@ -65,7 +65,7 @@ void InstallTicket::install(std::string const& id) {
                 "Downloading binary",
                 static_cast<uint8_t>(now / total * 100.0)
             );
-            return 0;
+            return true;
         }
     );
     if (!res) {
diff --git a/loader/src/index/fetch.hpp b/loader/src/index/fetch.hpp
deleted file mode 100644
index b2f590cd..00000000
--- a/loader/src/index/fetch.hpp
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma once
-
-#include <Geode/Geode.hpp>
-
-USE_GEODE_NAMESPACE();
-
-Result<std::string> fetch(std::string const& url);
-Result<> fetchFile(
-    std::string const& url,
-    ghc::filesystem::path const& into,
-    std::function<int(double, double)> prog = nullptr
-);
-
-template<class Json = nlohmann::json>
-Result<Json> fetchJSON(std::string const& url) {
-    auto res = fetch(url);
-    if (!res) return Err(res.error());
-    try {
-        return Ok(Json::parse(res.value()));
-    } catch(std::exception& e) {
-        return Err(e.what());
-    }
-}
-
-
diff --git a/loader/src/internal/InternalLoader.cpp b/loader/src/internal/InternalLoader.cpp
index c396ac5d..dc541631 100644
--- a/loader/src/internal/InternalLoader.cpp
+++ b/loader/src/internal/InternalLoader.cpp
@@ -7,6 +7,7 @@
 #include <Geode/loader/Log.hpp>
 #include <Geode/loader/Loader.hpp>
 #include <Geode/Geode.hpp>
+#include <Geode/utils/fetch.hpp>
 #include <thread>
 
 InternalLoader::InternalLoader() : Loader() {}
diff --git a/loader/src/internal/InternalLoader.hpp b/loader/src/internal/InternalLoader.hpp
index 099707a0..0492143c 100644
--- a/loader/src/internal/InternalLoader.hpp
+++ b/loader/src/internal/InternalLoader.hpp
@@ -8,6 +8,8 @@
 #include <Geode/loader/Log.hpp>
 #include <Geode/utils/Result.hpp>
 #include <unordered_set>
+#include <Geode/utils/json.hpp>
+#include <optional>
 
 USE_GEODE_NAMESPACE();
 
@@ -52,6 +54,6 @@ public:
 	void openPlatformConsole();
 	void closePlatformConsole();
 	static void platformMessageBox(const char* title, std::string const& info);
-
+	
 	friend int geodeEntry(void* platformData);
 };
diff --git a/loader/src/load/Loader.cpp b/loader/src/load/Loader.cpp
index b830eb68..8471fade 100644
--- a/loader/src/load/Loader.cpp
+++ b/loader/src/load/Loader.cpp
@@ -19,11 +19,11 @@ USE_GEODE_NAMESPACE();
 bool Loader::s_unloading = false;
 std::mutex g_unloadMutex;
 
-VersionInfo Loader::getVersion() const {
+VersionInfo Loader::getVersion() {
     return LOADER_VERSION;
 }
 
-std::string Loader::getVersionType() const {
+std::string Loader::getVersionType() {
     return LOADER_VERSION_TYPE;
 }
 
@@ -51,14 +51,23 @@ void Loader::createDirectories() {
 }
 
 void Loader::updateResourcePaths() {
-    // add own resources directory
+    // add own geode/resources directory
     CCFileUtils::sharedFileUtils()->addSearchPath(
         (this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY).string().c_str()
     );
-    // add mods directory
-    CCFileUtils::sharedFileUtils()->addSearchPath(
-        (this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY).string().c_str()
-    );
+
+    // add geode/temp for accessing root resources in mods
+    auto tempDir = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
+    CCFileUtils::sharedFileUtils()->addSearchPath(tempDir.string().c_str());
+
+    // add geode/temp/mod.id/resources for accessing additional resources in mods
+    for (auto& [_, mod] : m_mods) {
+        if (mod->m_addResourcesToSearchPath) {
+            CCFileUtils::sharedFileUtils()->addSearchPath(
+                (tempDir / mod->getID() / "resources").string().c_str()
+            );
+        }
+    }
 }
 
 void Loader::updateModResources(Mod* mod) {
@@ -447,10 +456,18 @@ size_t Loader::getFieldIndexForClass(size_t hash) {
 	return nextIndex[hash]++;
 }
 
+VersionInfo Loader::minModVersion() {
+    return { 0, 1, 0 };
+}
+
+VersionInfo Loader::maxModVersion() {
+    return Loader::getVersion();
+}
+
 bool Loader::supportedModVersion(VersionInfo const& version) {
     return 
-        version >= s_supportedVersionMin &&
-        version <= s_supportedVersionMax;
+        version >= Loader::minModVersion() &&
+        version <= Loader::maxModVersion();
 }
 
 void Loader::openPlatformConsole() {
diff --git a/loader/src/load/ModInfo.cpp b/loader/src/load/ModInfo.cpp
index af3b322e..b9b600f3 100644
--- a/loader/src/load/ModInfo.cpp
+++ b/loader/src/load/ModInfo.cpp
@@ -148,28 +148,28 @@ Result<ModInfo> ModInfo::create(ModJson const& json) {
             "specified, or it is invalidally formatted (required: \"[v]X.X.X\")!"
         );
     }
-    if (schema < Loader::s_supportedVersionMin) {
+    if (schema < Loader::minModVersion()) {
         return Err(
             "[mod.json] is built for an older version (" + 
             schema.toString() + ") of Geode (current: " + 
-            Loader::s_supportedVersionMin.toString() +
+            Loader::minModVersion().toString() +
             "). Please update the mod to the latest version, "
             "and if the problem persists, contact the developer "
             "to update it."
         );
     }
-    if (schema > Loader::s_supportedVersionMax) {
+    if (schema > Loader::maxModVersion()) {
         return Err(
             "[mod.json] is built for a newer version (" + 
             schema.toString() + ") of Geode (current: " +
-            Loader::s_supportedVersionMax.toString() +
+            Loader::maxModVersion().toString() +
             "). You need to update Geode in order to use "
             "this mod."
         );
     }
 
     // Handle mod.json data based on target
-    if (schema <= VersionInfo(0, 2, 1)) {
+    if (schema >= VersionInfo(0, 1, 0)) {
         return ModInfo::createFromSchemaV010(json);
     }
 
diff --git a/loader/src/main.cpp b/loader/src/main.cpp
index 62b9fb6d..6bdebdc4 100644
--- a/loader/src/main.cpp
+++ b/loader/src/main.cpp
@@ -121,7 +121,7 @@ int geodeEntry(void* platformData) {
     if (InternalMod::get()->getSettingValue<bool>("show-platform-console")) {
         Loader::get()->openPlatformConsole();
     }
-
+    
     InternalMod::get()->log()
         << Severity::Debug
         << "Entry done.";
diff --git a/loader/src/index/fetch.cpp b/loader/src/utils/fetch.cpp
similarity index 85%
rename from loader/src/index/fetch.cpp
rename to loader/src/utils/fetch.cpp
index 3a88d486..299ec3ad 100644
--- a/loader/src/index/fetch.cpp
+++ b/loader/src/utils/fetch.cpp
@@ -1,5 +1,8 @@
-#include "fetch.hpp"
+#include <Geode/utils/fetch.hpp>
 #include <curl/curl.h>
+#include <Geode/utils/casts.hpp>
+
+USE_GEODE_NAMESPACE();
 
 namespace geode::utils::fetch {
     static size_t writeData(char* data, size_t size, size_t nmemb, void* str) {
@@ -13,14 +16,14 @@ namespace geode::utils::fetch {
     }
 
     static int progress(void* ptr, double total, double now, double, double) {
-        return (*as<std::function<int(double, double)>*>(ptr))(now, total);
+        return (*as<web::FileProgressCallback*>(ptr))(now, total) != true;
     }
 }
 
-Result<> fetchFile(
+Result<> web::fetchFile(
     std::string const& url,
     ghc::filesystem::path const& into,
-    std::function<int(double, double)> prog
+    FileProgressCallback prog
 ) {
     auto curl = curl_easy_init();
     
@@ -46,7 +49,7 @@ Result<> fetchFile(
     auto res = curl_easy_perform(curl);
     if (res != CURLE_OK) {
         curl_easy_cleanup(curl);
-        return Err("Fetch failed");
+        return Err("Fetch failed: " + std::string(curl_easy_strerror(res)));
     }
 
     char* ct;
@@ -59,7 +62,7 @@ Result<> fetchFile(
     return Err("Error getting info: " + std::string(curl_easy_strerror(res)));
 }
 
-Result<std::string> fetch(std::string const& url) {
+Result<std::string> web::fetch(std::string const& url) {
     auto curl = curl_easy_init();
     
     if (!curl) return Err("Curl not initialized!");
diff --git a/loader/src/utils/file.cpp b/loader/src/utils/file.cpp
index 5307d6f6..e6175b38 100644
--- a/loader/src/utils/file.cpp
+++ b/loader/src/utils/file.cpp
@@ -1,5 +1,7 @@
 #include <Geode/utils/file.hpp>
+#include <Geode/utils/string.hpp>
 #include <fstream>
+#include <Geode/Bindings.hpp>
 
 USE_GEODE_NAMESPACE();
 
@@ -188,3 +190,69 @@ Result<std::vector<std::string>> utils::file::listFilesRecursively(std::string c
     }
     return Ok<>(res);
 }
+
+Result<> utils::file::unzipTo(
+    ghc::filesystem::path const& from,
+    ghc::filesystem::path const& to
+) {
+    // unzip downloaded
+    auto unzip = ZipFile(from.string());
+    if (!unzip.isLoaded()) {
+        return Err("Unable to unzip index.zip");
+    }
+
+    if (
+        !ghc::filesystem::exists(to) &&
+        !ghc::filesystem::create_directories(to)
+    ) {
+        return Err(
+            "Unable to create directories \"" + 
+            to.string() + "\""
+        );
+    }
+
+    for (auto file : unzip.getAllFiles()) {
+        // this is a very bad check for seeing 
+        // if file is a directory. it seems to 
+        // work on windows at least. idk why 
+        // getAllFiles returns the directories 
+        // aswell now
+        if (
+            utils::string::endsWith(file, "\\") ||
+            utils::string::endsWith(file, "/")
+        ) continue;
+
+        auto zipPath = file;
+
+        // dont include the github repo folder
+        file = file.substr(file.find_first_of("/") + 1);
+
+        auto path = ghc::filesystem::path(file);
+        if (path.has_parent_path()) {
+            auto dir = to / path.parent_path();
+            if (
+                !ghc::filesystem::exists(dir) &&
+                !ghc::filesystem::create_directories(dir)
+            ) {
+                return Err(
+                    "Unable to create directories \"" + 
+                    dir.string() + "\""
+                );
+            }
+        }
+        unsigned long size;
+        auto data = unzip.getFileData(zipPath, &size);
+        if (!data || !size) {
+            return Err("Unable to read \"" + std::string(zipPath) + "\"");
+        }
+        auto wrt = utils::file::writeBinary(
+            to / file,
+            byte_array(data, data + size)
+        );
+        if (!wrt) {
+            return Err("Unable to write \"" + (to / file).string() + "\": " + wrt.error());
+        }
+    }
+
+    return Ok();
+}