diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt
index b02159cb..98f35b75 100644
--- a/loader/CMakeLists.txt
+++ b/loader/CMakeLists.txt
@@ -23,6 +23,7 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
 	src/loader/*.cpp
 	src/main.cpp
 	src/utils/*.cpp
+	src/ui/*.cpp
 	src/ui/nodes/*.cpp
 	src/ui/internal/*.cpp
 	src/ui/internal/credits/*.cpp
diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp
index 770f6f78..412bada5 100644
--- a/loader/src/hooks/MenuLayer.cpp
+++ b/loader/src/hooks/MenuLayer.cpp
@@ -9,6 +9,7 @@
 #include <Geode/ui/GeodeUI.hpp>
 #include <Geode/ui/Notification.hpp>
 #include <Geode/ui/Popup.hpp>
+#include <Geode/ui/MDPopup.hpp>
 #include <Geode/utils/cocos.hpp>
 #include <loader/ModImpl.hpp>
 #include <loader/LoaderImpl.hpp>
@@ -68,14 +69,17 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
 			CircleBaseColor::Green,
 			CircleBaseSize::Medium2
 		);
+		auto geodeBtnSelector = &CustomMenuLayer::onGeode;
 		if (!m_fields->m_geodeButton) {
+			geodeBtnSelector = &CustomMenuLayer::onMissingTextures;
 			m_fields->m_geodeButton = ButtonSprite::create("!!");
 		}
 
         auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu"));
 
 		auto btn = CCMenuItemSpriteExtra::create(
-			m_fields->m_geodeButton, this, menu_selector(CustomMenuLayer::onGeode)
+			m_fields->m_geodeButton, this,
+			static_cast<SEL_MenuHandler>(geodeBtnSelector)
 		);
 		btn->setID("geode-button"_spr);
 		bottomMenu->addChild(btn);
@@ -193,6 +197,64 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
 		}
 	}
 
+	void onMissingTextures(CCObject*) {
+		static bool shownInfoPopup = false;
+		if (shownInfoPopup) {
+			return this->onGeode(nullptr);
+		}
+		shownInfoPopup = true;
+
+	#ifdef GEODE_IS_DESKTOP
+
+		try {
+			ghc::filesystem::create_directories(dirs::getGeodeDir() / "update" / "resources");
+		} catch(...) {}
+
+		createQuickPopup(
+			"Missing Textures",
+			"You appear to be missing textures, and the automatic texture fixer "
+			"hasn't fixed the issue.\n"
+			"Download <cy>resources.zip</c> from the latest release on GitHub, "
+			"and <cy>unzip its contents</c> into <cb>geode/update/resources</c>.\n"
+			"Afterwards, <cg>restart the game</c>.\n"
+			"You may also continue without installing resources, but be aware that "
+			"the game <cr>will crash</c>.",
+			"Dismiss", "Open Github",
+			[](auto, bool btn2) {
+				if (btn2) {
+					web::openLinkInBrowser("https://github.com/geode-sdk/geode/releases/latest");
+					file::openFolder(dirs::getGeodeDir() / "update" / "resources");
+					FLAlertLayer::create(
+						"Info",
+						"Opened GitHub in your browser and the destination in "
+						"your file browser.\n"
+						"Download <cy>resources.zip</c>, "
+						"and <cy>unzip its contents</c> into the destination "
+						"folder.\n"
+						"<cb>Don't add any new folders to the destination!</c>",
+						"OK"
+					)->show();
+				}
+			}
+		);
+
+	#else
+
+		// dunno if we can auto-create target directory on mobile, nor if the 
+		// user has access to moving stuff there
+
+		FLAlertLayer::create(
+			"Missing Textures",
+			"You appear to be missing textures, and the automatic texture fixer "
+			"hasn't fixed the issue.\n"
+			"**<cy>Report this bug to the Geode developers</c>**. It is very likely "
+			"that your game <cr>will crash</c> until the issue is resolved.",
+			"OK"
+		)->show();
+
+	#endif
+	}
+
     void onGeode(CCObject*) {
         ModListLayer::scene();
     }