diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9243fc1a..e559a5f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Geode Changelog
 
+## v2.0.0-beta.26
+ * Bring in several UI helpers from the `new-index-but-better` branch: `ListBorders`, `addSideArt`, `AxisLayout` improvements, ... (26729c3, 7ff257c)
+ * Make it possible to compile mods in Debug mode (517ad45)
+ * Add `GJDifficultyName` and `GJFeatureState` (#706)
+ * Add `geode::cocos::isSpriteName` and `geode::cocos::getChildBySpriteName` (#725)
+ * Add some Android keycodes (4fa8098)
+ * Update FMOD on Mac (c4a682b)
+ * Bump JSON version (5cc23e7)
+ * Fixes to `InputNode` touches (29b4732)
+ * Fix `file::readFromJson` (77e0f2e)
+ * Fix issues with TulipHook (f2ce7d0)
+
 ## v2.0.0-beta.25
  * Fix updater sometimes skipping releases (18dd0b7)
  * Fix resources getting downloaded every time (5f571d9)
diff --git a/VERSION b/VERSION
index d093a126..e555e41e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.0.0-beta.25
\ No newline at end of file
+2.0.0-beta.26
\ No newline at end of file
diff --git a/loader/include/Geode/cocos/base_nodes/CCNode.h b/loader/include/Geode/cocos/base_nodes/CCNode.h
index b5c9a176..3b01f37f 100644
--- a/loader/include/Geode/cocos/base_nodes/CCNode.h
+++ b/loader/include/Geode/cocos/base_nodes/CCNode.h
@@ -913,6 +913,22 @@ public:
      */
     GEODE_DLL CCNode* getChildByIDRecursive(std::string const& id);
 
+    /**
+     * Get a child based on a query. Searches the child tree for a matching 
+     * child. The query currently only supports the following features:
+     *  - `node-id`: Match a node with a specific ID
+     *  - `node-id-1 node-id-2`: Match a descendant (possibly not immediate) 
+     *    child of a node with a specific ID
+     *  - `node-id-1 > node-id-2`: Match the immediate child of a node with a 
+     *    specific ID 
+     * For example, the query "my-layer button-menu > mod.id/epic-button" is 
+     * equivalent to `getChildByIDRecursive("my-layer")
+     * ->getChildByIDRecursive("button-menu")
+     * ->getChildByID("mod.id/epic-button")`
+     * @returns The first matching node, or nullptr if none was found
+     */
+    GEODE_DLL CCNode* querySelector(std::string const& query);
+
     /** 
      * Removes a child from the container by its ID.
      * @param id The ID of the node
diff --git a/loader/include/Geode/cocos/base_nodes/Layout.hpp b/loader/include/Geode/cocos/base_nodes/Layout.hpp
index 8a31c25e..592d3fd3 100644
--- a/loader/include/Geode/cocos/base_nodes/Layout.hpp
+++ b/loader/include/Geode/cocos/base_nodes/Layout.hpp
@@ -5,7 +5,7 @@
 #include "../cocoa/CCArray.h"
 #include <Geode/platform/platform.hpp>
 #include <optional>
-#include <unordered_map>
+#include <memory>
 
 NS_CC_BEGIN
 
diff --git a/loader/include/Geode/cocos/particle_nodes/CCParticleSystem.h b/loader/include/Geode/cocos/particle_nodes/CCParticleSystem.h
index cdaac739..5b2676e6 100644
--- a/loader/include/Geode/cocos/particle_nodes/CCParticleSystem.h
+++ b/loader/include/Geode/cocos/particle_nodes/CCParticleSystem.h
@@ -313,7 +313,31 @@ public:
     virtual bool isBlendAdditive();
     virtual void setBlendAdditive(bool value);
 //////////////////////////////////////////////////////////////////////////
-    
+
+    RT_ADD(
+        float m_fFadeInTime;
+        float m_fFadeInTimeVar;
+        float m_fFadeOutTime;
+        float m_fFadeOutTimeVar;
+        float m_fFrictionPos;
+        float m_fFrictionPosVar;
+        float m_fFrictionSize;
+        float m_fFrictionSizeVar;
+        float m_fFrictionRot;
+        float m_fFrictionRotVar;
+        float m_fRespawn;
+        float m_fRespawnVar;
+        bool m_bStartSpinEqualToEnd;
+        bool m_bStartSizeEqualToEnd;
+        bool m_bStartRadiusEqualToEnd;
+        bool m_bDynamicRotationIsDir;
+        bool m_bOrderSensitive;
+        bool m_bStartRGBVarSync;
+        bool m_bEndRGBVarSync;
+        bool m_bWasRemoved;
+        bool m_bUsingSchedule;
+    )
+
     /** start size in pixels of each particle */
     CC_PROPERTY(float, m_fStartSize, StartSize)
     /** size variance in pixels of each particle */
diff --git a/loader/include/Geode/cocos/platform/win32/CCApplication.h b/loader/include/Geode/cocos/platform/win32/CCApplication.h
index 60455572..36b1ba50 100644
--- a/loader/include/Geode/cocos/platform/win32/CCApplication.h
+++ b/loader/include/Geode/cocos/platform/win32/CCApplication.h
@@ -5,7 +5,7 @@
 #include "CCStdC.h"
 #include "../CCCommon.h"
 #include "../CCApplicationProtocol.h"
-#include "CCControllerHandler.h"
+#include "CXBOXController.h"
 #include <string>
 
 NS_CC_BEGIN
@@ -51,7 +51,7 @@ public:
     RT_ADD(
         void setupVerticalSync();
         void updateVerticalSync();
-        void updateControllerKeys();
+        void updateControllerKeys(CXBOXController* controller, int userIndex);
 
         int getTimeElapsed();
 	    void resetForceTimer();
@@ -96,8 +96,8 @@ public:
     LARGE_INTEGER       m_nVsyncInterval;
     gd::string          m_resourceRootPath;
     gd::string          m_startupScriptFilename;
-    CCControllerHandler* m_pControllerHandler;
-    void*               m_unk; //might be swapped with m_pControllerHandler
+    CXBOXController* m_pControllerHandler;
+    CXBOXController* m_pController2Handler; //might be swapped with m_pControllerHandler
     bool m_bUpdateController;
     CC_SYNTHESIZE_NV(bool, m_bShutdownCalled, ShutdownCalled);
     INPUT m_iInput;
@@ -114,6 +114,7 @@ public:
     CC_SYNTHESIZE_NV(bool, m_bFullscreen, Fullscreen);
     CC_SYNTHESIZE_NV(bool, m_bBorderless, Borderless);
 
+protected:
     static CCApplication * sm_pSharedApplication;
 };
 
diff --git a/loader/include/Geode/cocos/platform/win32/CCEGLView.h b/loader/include/Geode/cocos/platform/win32/CCEGLView.h
index f3e73378..32a6795c 100644
--- a/loader/include/Geode/cocos/platform/win32/CCEGLView.h
+++ b/loader/include/Geode/cocos/platform/win32/CCEGLView.h
@@ -208,7 +208,9 @@ public:
         float m_fMouseX;
         float m_fMouseY;
         bool m_bIsFullscreen;
+        bool m_bIsBorderless;
         bool m_bShouldHideCursor;
+        bool m_bCursorLocked;
         bool m_bShouldCallGLFinish;
     )
 
diff --git a/loader/include/Geode/cocos/platform/win32/CCControllerHandler.h b/loader/include/Geode/cocos/platform/win32/CXBOXController.h
similarity index 79%
rename from loader/include/Geode/cocos/platform/win32/CCControllerHandler.h
rename to loader/include/Geode/cocos/platform/win32/CXBOXController.h
index 83e8108d..56cee344 100644
--- a/loader/include/Geode/cocos/platform/win32/CCControllerHandler.h
+++ b/loader/include/Geode/cocos/platform/win32/CXBOXController.h
@@ -1,14 +1,12 @@
-#ifndef __CC_CONTROLLER_HANDLER_WIN32_H__
-#define __CC_CONTROLLER_HANDLER_WIN32_H__
+#ifndef __CXBOXCONTROLLER_WIN32_H__
+#define __CXBOXCONTROLLER_WIN32_H__
 
 #include "../../include/ccMacros.h"
 #include "CCStdC.h"
 #include "CCControllerState.h"
 #include <Xinput.h>
 
-NS_CC_BEGIN
-
-class CC_DLL CCControllerHandler
+class CC_DLL CXBOXController
 {
     GEODE_FRIEND_MODIFY
 public:
@@ -35,6 +33,4 @@ public:
     bool m_buttonY;
 };
 
-NS_CC_END
-
-#endif
\ No newline at end of file
+#endif
diff --git a/loader/include/Geode/ui/General.hpp b/loader/include/Geode/ui/General.hpp
index e8b6acbc..1578521b 100644
--- a/loader/include/Geode/ui/General.hpp
+++ b/loader/include/Geode/ui/General.hpp
@@ -27,6 +27,13 @@ namespace geode {
         return static_cast<bool>(static_cast<int>(a) & static_cast<int>(b));
     }
 
+    enum class SideArtStyle {
+        Layer,
+        LayerGray,
+        PopupBlue,
+        PopupGold,
+    };
+
     /**
      * Add side art (corner pieces) for a layer
      * @param to Layer to add corner pieces to
@@ -34,7 +41,25 @@ namespace geode {
      * @param useAnchorLayout If true, `to` is given an `AnchorLayout` and the 
      * corners' positions are dynamically updated
      */
-    GEODE_DLL void addSideArt(cocos2d::CCNode* to, SideArt sides = SideArt::All, bool useAnchorLayout = false);
+    GEODE_DLL void addSideArt(
+        cocos2d::CCNode* to,
+        SideArt sides = SideArt::All,
+        bool useAnchorLayout = false
+    );
+    /**
+     * Add side art (corner pieces) for a layer
+     * @param to Layer to add corner pieces to
+     * @param sides Which corners to populate; by default, populates all
+     * @param style Which side art sprites to use
+     * @param useAnchorLayout If true, `to` is given an `AnchorLayout` and the 
+     * corners' positions are dynamically updated
+     */
+    GEODE_DLL void addSideArt(
+        cocos2d::CCNode* to,
+        SideArt sides,
+        SideArtStyle style,
+        bool useAnchorLayout = false
+    );
 
     /**
      * Add the rounded comment borders to a node
diff --git a/loader/include/Geode/utils/Result.hpp b/loader/include/Geode/utils/Result.hpp
index 4d58a15c..dff90491 100644
--- a/loader/include/Geode/utils/Result.hpp
+++ b/loader/include/Geode/utils/Result.hpp
@@ -196,6 +196,13 @@ namespace geode {
             return this->Base::value_or(std::forward<U>(val));
         }
 
+        [[nodiscard]] constexpr decltype(auto) unwrapOrDefault() && requires std::is_default_constructible_v<T> {
+            return this->Base::value_or(T());
+        }
+        [[nodiscard]] constexpr decltype(auto) unwrapOrDefault() const& requires std::is_default_constructible_v<T> {
+            return this->Base::value_or(T());
+        }
+
         template <class U>
         [[nodiscard]] constexpr decltype(auto) errorOr(U&& val) && {
             return this->Base::error_or(std::forward<U>(val));
diff --git a/loader/include/Geode/utils/general.hpp b/loader/include/Geode/utils/general.hpp
index 9fcfa284..cfd0b55d 100644
--- a/loader/include/Geode/utils/general.hpp
+++ b/loader/include/Geode/utils/general.hpp
@@ -153,6 +153,13 @@ namespace geode {
         }
 
         GEODE_DLL std::string timePointAsString(std::chrono::system_clock::time_point const& tp);
+
+        /**
+         * Gets the display pixel factor for the current screen,
+         * i.e. the ratio between physical pixels and logical pixels on one axis.
+         * On most platforms this is 1.0, but on retina displays for example this returns 2.0.
+        */
+        GEODE_DLL float getDisplayFactor();
     }
 }
 
diff --git a/loader/src/hooks/GeodeNodeMetadata.cpp b/loader/src/hooks/GeodeNodeMetadata.cpp
index 612e9bef..a36241ff 100644
--- a/loader/src/hooks/GeodeNodeMetadata.cpp
+++ b/loader/src/hooks/GeodeNodeMetadata.cpp
@@ -3,6 +3,7 @@
 #include <Geode/modify/Field.hpp>
 #include <Geode/modify/CCNode.hpp>
 #include <cocos2d.h>
+#include <queue>
 
 using namespace geode::prelude;
 using namespace geode::modifier;
@@ -143,6 +144,162 @@ CCNode* CCNode::getChildByIDRecursive(std::string const& id) {
     return nullptr;
 }
 
+class BFSNodeTreeCrawler final {
+private:
+    std::queue<CCNode*> m_queue;
+    std::unordered_set<CCNode*> m_explored;
+
+public:
+    BFSNodeTreeCrawler(CCNode* target) {
+        if (auto first = getChild(target, 0)) {
+            m_explored.insert(first);
+            m_queue.push(first);
+        }
+    }
+
+    CCNode* next() {
+        if (m_queue.empty()) {
+            return nullptr;
+        }
+        auto node = m_queue.front();
+        m_queue.pop();
+        for (auto sibling : CCArrayExt<CCNode*>(node->getParent()->getChildren())) {
+            if (!m_explored.contains(sibling)) {
+                m_explored.insert(sibling);
+                m_queue.push(sibling);
+            }
+        }
+        for (auto child : CCArrayExt<CCNode*>(node->getChildren())) {
+            if (!m_explored.contains(child)) {
+                m_explored.insert(child);
+                m_queue.push(child);
+            }
+        }
+        return node;
+    }
+};
+
+class NodeQuery final {
+private:
+    enum class Op {
+        ImmediateChild,
+        DescendantChild,
+    };
+
+    std::string m_targetID;
+    Op m_nextOp;
+    std::unique_ptr<NodeQuery> m_next = nullptr;
+
+public:
+    static Result<std::unique_ptr<NodeQuery>> parse(std::string const& query) {
+        if (query.empty()) {
+            return Err("Query may not be empty");
+        }
+
+        auto result = std::make_unique<NodeQuery>();
+        NodeQuery* current = result.get();
+
+        size_t i = 0;
+        std::string collectedID;
+        std::optional<Op> nextOp = Op::DescendantChild;
+        while (i < query.size()) {
+            auto c = query.at(i);
+            if (c == ' ') {
+                if (!nextOp) {
+                    nextOp.emplace(Op::DescendantChild);
+                }
+            }
+            else if (c == '>') {
+                if (!nextOp || *nextOp == Op::DescendantChild) {
+                    nextOp.emplace(Op::ImmediateChild);
+                }
+                // Double >> is syntax error
+                else {
+                    return Err("Can't have multiple child operators at once (index {})", i);
+                }
+            }
+            // ID-valid characters
+            else if (std::isalnum(c) || c == '-' || c == '_' || c == '/' || c == '.') {
+                if (nextOp) {
+                    current->m_next = std::make_unique<NodeQuery>();
+                    current->m_nextOp = *nextOp;
+                    current->m_targetID = collectedID;
+                    current = current->m_next.get();
+
+                    collectedID = "";
+                    nextOp = std::nullopt;
+                }
+                collectedID.push_back(c);
+            }
+            // Any other character is syntax error due to needing to reserve 
+            // stuff for possible future features
+            else {
+                return Err("Unexpected character '{}' at index {}", c, i);
+            }
+            i += 1;
+        }
+        if (nextOp || collectedID.empty()) {
+            return Err("Expected node ID but got end of query");
+        }
+        current->m_targetID = collectedID;
+
+        return Ok(std::move(result));
+    }
+
+    CCNode* match(CCNode* node) const {
+        // Make sure this matches the ID being looked for
+        if (!m_targetID.empty() && node->getID() != m_targetID) {
+            return nullptr;
+        }
+        // If this is the last thing to match, return the result
+        if (!m_next) {
+            return node;
+        }
+        switch (m_nextOp) {
+            case Op::ImmediateChild: {
+                for (auto c : CCArrayExt<CCNode*>(node->getChildren())) {
+                    if (auto r = m_next->match(c)) {
+                        return r;
+                    }
+                }
+            } break;
+
+            case Op::DescendantChild: {
+                auto crawler = BFSNodeTreeCrawler(node);
+                while (auto c = crawler.next()) {
+                    if (auto r = m_next->match(c)) {
+                        return r;
+                    }
+                }
+            } break;
+        }
+        return nullptr;
+    }
+
+    std::string toString() const {
+        auto str = m_targetID.empty() ? "&" : m_targetID;
+        if (m_next) {
+            switch (m_nextOp) {
+                case Op::ImmediateChild: str += " > "; break;
+                case Op::DescendantChild: str += " "; break;
+            }
+            str += m_next->toString();
+        }
+        return str;
+    }
+};
+
+CCNode* CCNode::querySelector(std::string const& queryStr) {
+    auto res = NodeQuery::parse(queryStr);
+    if (!res) {
+        log::error("Invalid CCNode::querySelector query '{}': {}", queryStr, res.unwrapErr());
+        return nullptr;
+    }
+    auto query = std::move(res.unwrap());
+    log::info("parsed query: {}", query->toString());
+    return query->match(this);
+}
+
 void CCNode::removeChildByID(std::string const& id) {
     if (auto child = this->getChildByID(id)) {
         this->removeChild(child);
diff --git a/loader/src/platform/mac/util.mm b/loader/src/platform/mac/util.mm
index d49409ae..16c66a0a 100644
--- a/loader/src/platform/mac/util.mm
+++ b/loader/src/platform/mac/util.mm
@@ -9,6 +9,10 @@ using namespace geode::prelude;
 #include <objc/runtime.h>
 #include <Geode/utils/web.hpp>
 
+#define CommentType CommentTypeDummy
+#import <Cocoa/Cocoa.h>
+#undef CommentType
+
 bool utils::clipboard::write(std::string const& data) {
     [[NSPasteboard generalPasteboard] clearContents];
     [[NSPasteboard generalPasteboard] setString:[NSString stringWithUTF8String:data.c_str()]
@@ -332,3 +336,16 @@ std::string geode::utils::thread::getDefaultName() {
 void geode::utils::thread::platformSetName(std::string const& name) {
     pthread_setname_np(name.c_str());
 }
+
+float geode::utils::getDisplayFactor() {
+    float displayScale = 1.f;
+    if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)]) {
+        NSArray* screens = [NSScreen screens];
+        for (int i = 0; i < screens.count; i++) {
+            float s = [screens[i] backingScaleFactor];
+            if (s > displayScale)
+                displayScale = s;
+        }
+    }
+    return displayScale;
+}
\ No newline at end of file
diff --git a/loader/src/ui/nodes/General.cpp b/loader/src/ui/nodes/General.cpp
index fc8f6722..9f927828 100644
--- a/loader/src/ui/nodes/General.cpp
+++ b/loader/src/ui/nodes/General.cpp
@@ -18,28 +18,40 @@ CCSprite* geode::createLayerBG() {
     return bg;
 }
 
-void geode::addSideArt(CCNode* to, SideArt sides, bool useAnchorLayout) {
+void geode::addSideArt(CCNode* to, SideArt sides, SideArtStyle style, bool useAnchorLayout) {
+    const char* sprite;
+    float offset;
+    switch (style) {
+        default:
+        case SideArtStyle::Layer:     sprite = "GJ_sideArt_001.png";       offset = 35; break;
+        case SideArtStyle::LayerGray: sprite = "gauntletCorner_001.png";   offset = 35; break;
+        case SideArtStyle::PopupBlue: sprite = "rewardCorner_001.png";     offset = 24.75f; break;
+        case SideArtStyle::PopupGold: sprite = "dailyLevelCorner_001.png"; offset = 24.75f; break;
+    }
     if (sides & SideArt::BottomLeft) {
-        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
-        to->addChildAtPosition(spr, Anchor::BottomLeft, ccp(35, 35), useAnchorLayout);
+        auto spr = CCSprite::createWithSpriteFrameName(sprite);
+        to->addChildAtPosition(spr, Anchor::BottomLeft, ccp(offset, offset), useAnchorLayout);
     }
     if (sides & SideArt::BottomRight) {
-        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        auto spr = CCSprite::createWithSpriteFrameName(sprite);
         spr->setFlipX(true);
-        to->addChildAtPosition(spr, Anchor::BottomRight, ccp(-35, 35), useAnchorLayout);
+        to->addChildAtPosition(spr, Anchor::BottomRight, ccp(-offset, offset), useAnchorLayout);
     }
     if (sides & SideArt::TopLeft) {
-        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        auto spr = CCSprite::createWithSpriteFrameName(sprite);
         spr->setFlipY(true);
-        to->addChildAtPosition(spr, Anchor::TopLeft, ccp(35, -35), useAnchorLayout);
+        to->addChildAtPosition(spr, Anchor::TopLeft, ccp(offset, -offset), useAnchorLayout);
     }
     if (sides & SideArt::TopRight) {
-        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        auto spr = CCSprite::createWithSpriteFrameName(sprite);
         spr->setFlipX(true);
         spr->setFlipY(true);
-        to->addChildAtPosition(spr, Anchor::TopRight, ccp(-35, -35), useAnchorLayout);
+        to->addChildAtPosition(spr, Anchor::TopRight, ccp(-offset, -offset), useAnchorLayout);
     }
 }
+void geode::addSideArt(CCNode* to, SideArt sides, bool useAnchorLayout) {
+    return addSideArt(to, sides, SideArtStyle::Layer, useAnchorLayout);
+}
 
 void geode::addListBorders(CCNode* to, CCPoint const& center, CCSize const& size) {
     // if the size is 346.f, the top aligns perfectly by default :3
diff --git a/loader/src/utils/general.cpp b/loader/src/utils/general.cpp
new file mode 100644
index 00000000..c9ba8b0d
--- /dev/null
+++ b/loader/src/utils/general.cpp
@@ -0,0 +1,8 @@
+#include <Geode/utils/general.hpp>
+
+#ifndef GEODE_IS_MACOS
+// feel free to properly implement this for other platforms
+float geode::utils::getDisplayFactor() {
+    return 1.0f;
+}
+#endif
\ No newline at end of file