diff --git a/CMakeLists.txt b/CMakeLists.txt
index a5798580..218f26d9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,14 +15,32 @@ string(STRIP "${GEODE_VERSION}" GEODE_VERSION)
 
 # Check if version has a tag like v1.0.0-alpha
 string(FIND ${GEODE_VERSION} "-" GEODE_VERSION_HAS_TAG)
-if (GEODE_VERSION_HAS_TAG)
-	string(REGEX MATCH "[a-z]+[0-9]?$" GEODE_VERSION_TAG ${GEODE_VERSION})
+if (NOT ${GEODE_VERSION_HAS_TAG} EQUAL "-1")
+	string(REGEX MATCH "[a-z]+(\.[0-9]+)?$" GEODE_VERSION_TAG ${GEODE_VERSION})
 	string(SUBSTRING "${GEODE_VERSION}" 0 ${GEODE_VERSION_HAS_TAG} GEODE_VERSION)
+	string(FIND ${GEODE_VERSION_TAG} "." GEODE_VERSION_TAG_HAS_NUMBER)
+
+	# Extract tag type and number from tag
+	if (NOT ${GEODE_VERSION_TAG_HAS_NUMBER} EQUAL "-1")
+		string(SUBSTRING "${GEODE_VERSION_TAG}" 0 ${GEODE_VERSION_TAG_HAS_NUMBER} GEODE_VERSION_TAG_TYPE)
+		math(EXPR GEODE_VERSION_TAG_HAS_NUMBER "${GEODE_VERSION_TAG_HAS_NUMBER} + 1")
+		string(SUBSTRING "${GEODE_VERSION_TAG}" ${GEODE_VERSION_TAG_HAS_NUMBER} -1 GEODE_VERSION_TAG_NUMBER)
+	else()
+		set(GEODE_VERSION_TAG_TYPE "${GEODE_VERSION_TAG}")
+		set(GEODE_VERSION_TAG_NUMBER "")
+	endif()
+
+	# Capitalize first letter of tag type
+	string(SUBSTRING ${GEODE_VERSION_TAG_TYPE} 0 1 FIRST_LETTER)
+	string(TOUPPER ${FIRST_LETTER} FIRST_LETTER)
+	string(REGEX REPLACE "^.(.*)" "${FIRST_LETTER}\\1" GEODE_VERSION_TAG_TYPE "${GEODE_VERSION_TAG_TYPE}")
 else()
 	set(GEODE_VERSION_TAG "")
+	set(GEODE_VERSION_TAG_TYPE "")
+	set(GEODE_VERSION_TAG_NUMBER "")
 endif()
 
-message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG}")
+message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG} (type: ${GEODE_VERSION_TAG_TYPE}, number: ${GEODE_VERSION_TAG_NUMBER})")
 
 project(geode-sdk VERSION ${GEODE_VERSION} LANGUAGES CXX C)
 
@@ -60,7 +78,7 @@ if (GEODE_DISABLE_FMT_CONSTEVAL)
 	target_compile_definitions(${PROJECT_NAME} INTERFACE -DFMT_CONSTEVAL=)
 endif()
 
-CPMAddPackage("gh:geode-sdk/json#2b76460")
+CPMAddPackage("gh:geode-sdk/json#cef9c64")
 CPMAddPackage("gh:fmtlib/fmt#9.1.0")
 CPMAddPackage("gh:gulrak/filesystem#3e5b930")
 
diff --git a/VERSION b/VERSION
index 537aabf7..844dc4b5 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.0-beta
\ No newline at end of file
+1.0.0-beta.6
\ No newline at end of file
diff --git a/bindings/GeometryDash.bro b/bindings/GeometryDash.bro
index 91741b45..349db2b5 100644
--- a/bindings/GeometryDash.bro
+++ b/bindings/GeometryDash.bro
@@ -296,6 +296,7 @@ class CCCircleWave : cocos2d::CCNode {
     PAD = win 0x4;
     float m_currentRadius;
     float m_currentOpacity;
+    cocos2d::ccColor3B m_color;
     cocos2d::CCPoint m_circleCenter;
     int m_filled;
     int m_lineWidth;
@@ -1120,7 +1121,7 @@ class EditorPauseLayer : CCBlockLayer, FLAlertLayerProtocol {
         if (!EditorUI::get()) return nullptr;
 
         auto editor = LevelEditorLayer::get();
-        for (auto i = 0; i < editor->getChildrenCount(); ++i) {
+        for (auto i = 0u; i < editor->getChildrenCount(); ++i) {
             if (auto layer = cast::safe_cast<EditorPauseLayer*>(editor->getChildren()->objectAtIndex(i))) {
                 return layer;
             }
@@ -1154,11 +1155,13 @@ class EditorPauseLayer : CCBlockLayer, FLAlertLayerProtocol {
     void uncheckAllPortals(cocos2d::CCObject* sender) = win 0x74760;
     void onResetUnusedColors(cocos2d::CCObject* sender) = win 0x74810;
     void doResetUnused() = win 0x165070;
+    void updateSongButton() = win 0x74f10, mac 0x13e530;
+    void onSong(cocos2d::CCObject*) = win 0x74e70, mac 0x13e470;
 
     bool m_saved;
     PAD = mac 0x8, win 0x4;
-    CCMenuItemSpriteExtra* m_button0;
-    CCMenuItemSpriteExtra* m_button1;
+    CCMenuItemSpriteExtra* m_guidelinesOffButton;
+    CCMenuItemSpriteExtra* m_guidelinesOnButton;
     LevelEditorLayer* m_editorLayer;
 }
 
@@ -2615,6 +2618,9 @@ class GameLevelManager : cocos2d::CCNode {
     void storeUserNames(gd::string) = win 0xa1840;
     gd::string userNameForUserID(int id) = win 0xa1c20;
     void updateUserScore() = win 0xada60;
+    void downloadLevel(int id, bool downloadData) = win 0xaa730;
+    bool hasDownloadedLevel(int id) = win 0xab830;
+    GJGameLevel* getSavedLevel(int id) = win 0xa2ee0;
 
     inline static GameLevelManager* get() {
         return GameLevelManager::sharedState();
@@ -2820,7 +2826,7 @@ class GameManager : GManager {
     void getGTexture(int) = mac 0x1cca40, win 0xc9a50;
     virtual bool init() = mac 0x1c2ec0, win 0xc4ad0;
     void reportAchievementWithID(char const*, int, bool) = mac 0x1c6460, win 0xc64c0;
-    cocos2d::CCSize* resolutionForKey(cocos2d::CCSize*, int) = mac 0x1d0b40, win 0xceca0;
+    cocos2d::CCSize resolutionForKey(int) = mac 0x1d0b40, win 0xceca0;
     virtual void update(float) = mac 0x1d0270, win 0xce440;
     bool isColorUnlocked(int _id, bool _type) = mac 0x1c3b90, win 0xc53f0;
     bool isIconUnlocked(int _id, IconType _type) = mac 0x1c35b0, win 0xc4fc0;
@@ -3271,6 +3277,9 @@ class GameSoundManager : cocos2d::CCNode {
     void asynchronousSetup() = win 0x25520;
     ~GameSoundManager() = mac 0x362c00, win 0x25640;
     static GameSoundManager* sharedManager() = mac 0x3610f0, win 0x24800;
+    inline static GameSoundManager* get() {
+        return GameSoundManager::sharedManager();
+    }
 
     cocos2d::CCDictionary* m_dictionary1;
     cocos2d::CCDictionary* m_dictionary2;
@@ -3962,6 +3971,9 @@ class MenuLayer : cocos2d::CCLayer, FLAlertLayerProtocol, GooglePlayDelegate {
     void onYouTube(cocos2d::CCObject*) = win 0x1919A0;
     static cocos2d::CCScene* scene(bool) = mac 0x1d12d0, win 0x190720, ios 0x19e57c;
     static MenuLayer* node() = win 0x190550;
+    inline static MenuLayer* create() {
+        return MenuLayer::node();
+    }
 
     cocos2d::CCSprite* m_googlePlaySprite;
     cocos2d::CCSprite* m_viewProfileInfoText;
@@ -4562,7 +4574,7 @@ class PlayerObject : GameObject, AnimatedSpriteDelegate {
     void modeDidChange() = mac 0x22bfd0;
     void placeStreakPoint() = mac 0x21af90, win 0x1f95e0;
     void playBurstEffect() = mac 0x21c780, win 0x1f6790;
-    void playDeathEffect() = mac 0x225930, win 0x2efbe0;
+    void playDeathEffect() = mac 0x225930, win 0x1efbe0;
     void playDynamicSpiderRun() = mac 0x222ec0, win 0x1f9d80;
     void playerDestroyed(bool) = mac 0x2256d0, win 0x1efaa0;
     bool playerIsFalling() = mac 0x21c730, win 0x1f5d60;
diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt
index cced6f1a..8470206f 100644
--- a/loader/CMakeLists.txt
+++ b/loader/CMakeLists.txt
@@ -1,8 +1,21 @@
 cmake_minimum_required(VERSION 3.21 FATAL_ERROR)
 
 project(geode-loader VERSION ${GEODE_VERSION} LANGUAGES C CXX)
-set(PROJECT_VERSION_TYPE geode::VersionTag::Beta)
-set(PROJECT_VERSION_SUFFIX -beta)
+if (GEODE_VERSION_TAG_TYPE)
+	if (GEODE_VERSION_TAG_NUMBER)
+		set(PROJECT_VERSION_TAG_CONSTR "geode::VersionTag(geode::VersionTag::${GEODE_VERSION_TAG_TYPE}, ${GEODE_VERSION_TAG_NUMBER})")
+	else()
+		set(PROJECT_VERSION_TAG_CONSTR "geode::VersionTag::${GEODE_VERSION_TAG_TYPE}")
+	endif()
+else()
+	set(PROJECT_VERSION_TAG_CONSTR "std::nullopt")
+endif()
+
+if (GEODE_VERSION_TAG)
+	set(PROJECT_VERSION_SUFFIX "-${GEODE_VERSION_TAG}")
+else()
+	set(PROJECT_VERSION_SUFFIX "")
+endif()
 
 # Package info file for internal representation
 configure_file(resources/mod.json.in ${CMAKE_CURRENT_SOURCE_DIR}/resources/mod.json)
diff --git a/loader/include/Geode/cocos/base_nodes/CCNode.h b/loader/include/Geode/cocos/base_nodes/CCNode.h
index 62916885..809f4e97 100644
--- a/loader/include/Geode/cocos/base_nodes/CCNode.h
+++ b/loader/include/Geode/cocos/base_nodes/CCNode.h
@@ -616,17 +616,15 @@ public:
      * Return an array of children
      *
      * Composing a "tree" structure is a very important feature of CCNode
-     * Here's a sample code of traversing children array:
-     * @code
+     * @example
+     * // Here's a sample code of traversing children array:
      * CCNode* node = NULL;
      * CCARRAY_FOREACH(parent->getChildren(), node)
      * {
      *     node->setPosition(0,0);
      * }
-     * @endcode
-     * This sample code traverses all children nodes, and set theie position to (0,0)
-     *
-     * @return An array of children
+     * // This sample code traverses all children nodes, and set theie position to (0,0)
+     * @returns An array of children
      */
     virtual CCArray* getChildren();
     
@@ -761,7 +759,7 @@ public:
      * Returns a tag that is used to identify the node easily.
      *
      * You can set tags to node then identify them easily.
-     * @code
+     * @example
      * #define TAG_PLAYER  1
      * #define TAG_MONSTER 2
      * #define TAG_BOSS    3
@@ -786,9 +784,7 @@ public:
      *             break;
      *     }
      * }
-     * @endcode
-     *
-     * @return A interger that identifies the node.
+     * @returns A interger that identifies the node.
      */
     RT_REMOVE(  virtual int getTag() const; )
     /**
@@ -883,6 +879,26 @@ public:
      */
     GEODE_DLL CCNode* getChildByIDRecursive(std::string const& id);
 
+    /**
+     * Add a child before a specified existing child
+     * @param child The node to add. The node may not be a child of another  
+     * node already
+     * @param before The child the node is added before of. If this is null or 
+     * not a child of this node, the new child will be placed at the start of the 
+     * child list
+     */
+    GEODE_DLL void insertBefore(CCNode* child, CCNode* before);
+
+    /**
+     * Add a child after an specified existing child
+     * @param child The node to add. The node may not be a child of another  
+     * node already
+     * @param after The child the node is added after of. If this is null or 
+     * not a child of this node, the new child will be placed at the end of the 
+     * child list
+     */
+    GEODE_DLL void insertAfter(CCNode* child, CCNode* after);
+
     /**
      * Set an attribute on a node. Attributes are a system added by Geode, 
      * where a node may have any sort of extra data associated with it. Used 
@@ -921,9 +937,14 @@ public:
      * has been added, call updateLayout
      * @param layout Layout to set to this node
      * @param apply Whether to call updateLayout now or not
+     * @param respectAnchor If true, if the target node is 
+     * isIgnoreAnchorPointForPosition, then it is set to false and the children 
+     * are automatically moved to match where they should be positioned. 
+     * Visually, this should result in no difference; however, when dealing with 
+     * CCLayers / CCMenus, this will change where the children are located
      * @note Geode addition
      */
-    GEODE_DLL void setLayout(Layout* layout, bool apply = true);
+    GEODE_DLL void setLayout(Layout* layout, bool apply = true, bool respectAnchor = true);
     /**
      * Get the Layout for this node
      * @returns The current layout, or nullptr if no layout is set
@@ -935,23 +956,22 @@ public:
      * set, nothing happens
      * @note Geode addition
      */
-    GEODE_DLL void updateLayout();
-
+    GEODE_DLL void updateLayout(bool updateChildOrder = true);
     /**
-     * Give a hint to the current Layout about where this node should be 
-     * positioned in it. Allows detaching the node from the current 
-     * layout by setting position to absolute
-     * @param hint The hint to set
-     * @note The layout definitely should, but might not respect the hint 
-     * given
+     * Set the layout options for this node. Layout options can be used to 
+     * control how this node is positioned in its parent's Layout, for example 
+     * setting the grow size for a flex layout
+     * @param options The layout options
+     * @param apply Whether to update the layout of the parent node
      * @note Geode addition
      */
-    GEODE_DLL void setPositionHint(PositionHint hint);
+    GEODE_DLL void setLayoutOptions(LayoutOptions* options, bool apply = true);
     /**
-     * Get the current position hint for this node
+     * Get the layout options for this node
+     * @returns The current layout options, or nullptr if no options are set
      * @note Geode addition
      */
-    GEODE_DLL PositionHint getPositionHint();
+    GEODE_DLL LayoutOptions* getLayoutOptions();
 
     /**
      * Swap two children
diff --git a/loader/include/Geode/cocos/base_nodes/Layout.hpp b/loader/include/Geode/cocos/base_nodes/Layout.hpp
index 1367986c..afc1d19b 100644
--- a/loader/include/Geode/cocos/base_nodes/Layout.hpp
+++ b/loader/include/Geode/cocos/base_nodes/Layout.hpp
@@ -18,136 +18,334 @@ class CCNode;
  * RowLayout, ColumnLayout, and GridLayout, but if you need a different kind 
  * of layout you can inherit from the Layout class.
  */
-class Layout {
+class GEODE_DLL Layout {
+protected:
+    static CCArray* getNodesToPosition(CCNode* forNode);
+
 public:
     /**
      * Automatically apply the layout's positioning on a set of nodes
-     * @param nodes Nodes to position 
-     * @param availableSize Give hints to the layout about how much space is 
-     * available. Note that the layout may still overflow
+     * @param on Node to apply the layout on. Position's the node's children 
+     * according to the layout. The content size of the node should be 
+     * respected as a boundary the layout shouldn't overflow. The node may be 
+     * rescaled to better fit its contents
      */
-    virtual void apply(CCArray* nodes, CCSize const& availableSize) = 0;
+    virtual void apply(CCNode* on) = 0;
+
+    virtual ~Layout() = default;
+};
+
+class GEODE_DLL LayoutOptions {
+public:
+    virtual ~LayoutOptions() = default;
 };
 
 /**
- * Determines how a node should be positioned within its parent, if that 
- * parent has an automatically positioning layout
+ * The direction of an AxisLayout
  */
-enum class PositionHint {
-    // The container can determine the best position 
-    // for this node
-    Default,
-    // The container's layout should not affect the 
-    // position of this node
-    Absolute,
+enum class Axis {
+    Row,
+    Column,
 };
 
 /**
- * Specifies the alignment of something
+ * Specifies the alignment of something in an AxisLayout
  */
-enum class Alignment {
-    Begin,
+enum class AxisAlignment {
+    // Align items to the start
+    // |ooo......|
+    Start,
+    // All items are centered
+    // |...ooo...|
     Center,
+    // Align items to the end
+    // |......ooo|
     End,
+    // Each item gets the same portion from the layout (disregards gap)
+    // |.o..o..o.|
+    Even,
+};
+
+constexpr float AXISLAYOUT_DEFAULT_MIN_SCALE = 0.65f;
+constexpr int AXISLAYOUT_DEFAULT_PRIORITY = 0;
+
+/**
+ * Options for controlling the behaviour of individual nodes in an AxisLayout
+ * @example
+ * auto node = CCNode::create();
+ * // this node will have 10 units of spacing between it and the next one
+ * node->setLayoutOptions(
+ *     AxisLayoutOptions::create()
+ *         ->setNextGap(10.f)
+ * );
+ * someNodeWithALayout->addChild(node);
+ */
+class GEODE_DLL AxisLayoutOptions : public LayoutOptions {
+protected:
+    std::optional<bool> m_autoScale = std::nullopt;
+    float m_maxScale = 1.f;
+    float m_minScale = AXISLAYOUT_DEFAULT_MIN_SCALE;
+    float m_relativeScale = 1.f;
+    std::optional<float> m_length = std::nullopt;
+    std::optional<float> m_nextGap = std::nullopt;
+    std::optional<float> m_prevGap = std::nullopt;
+    bool m_breakLine = false;
+    bool m_sameLine = false;
+    int m_scalePriority = AXISLAYOUT_DEFAULT_PRIORITY;
+
+public:
+    static AxisLayoutOptions* create();
+
+    std::optional<bool> getAutoScale() const;
+    float getMaxScale() const;
+    float getMinScale() const;
+    float getRelativeScale() const;
+    std::optional<float> getLength() const;
+    std::optional<float> getPrevGap() const;
+    std::optional<float> getNextGap() const;
+    bool getBreakLine() const;
+    bool getSameLine() const;
+    int getScalePriority() const;
+
+    /**
+     * Set the maximum scale this node can be if it's contained in an 
+     * auto-scaled layout. Default is 1
+     */
+    AxisLayoutOptions* setMaxScale(float scale);
+
+    /**
+     * Set the minimum scale this node can be if it's contained in an 
+     * auto-scaled layout. Default is AXISLAYOUT_DEFAULT_MIN_SCALE
+     */
+    AxisLayoutOptions* setMinScale(float scale);
+
+    /**
+     * Set the relative scale of this node compared to other nodes if it's 
+     * contained in an auto-scaled layout. Default is 1
+     */
+    AxisLayoutOptions* setRelativeScale(float scale);
+
+    /**
+     * Set auto-scaling for this node, overriding the layout's auto-scale 
+     * setting. If nullopt, the layout's auto-scale options will be used
+    */
+    AxisLayoutOptions* setAutoScale(std::optional<bool> enabled);
+
+    /**
+     * Set an absolute length for this node. If nullopt, the length will be 
+     * dynamically calculated based on content size
+     */
+    AxisLayoutOptions* setLength(std::optional<float> length);
+
+    /**
+     * Override the default gap in the layout between this node and the 
+     * previous one. If nullopt, the default gap of the layout will be used
+     */
+    AxisLayoutOptions* setPrevGap(std::optional<float> gap);
+
+    /**
+     * Override the default gap in the layout between this node and the next 
+     * one. If nullopt, the default gap of the layout will be used
+     */
+    AxisLayoutOptions* setNextGap(std::optional<float> gap);
+
+    /**
+     * If enabled, the node will always cause a growable axis layout to break 
+     * into a new line even if the current line could've fit the next node
+     */
+    AxisLayoutOptions* setBreakLine(bool enable);
+
+    /**
+     * If enabled, the node will be forced to be on the same line as the 
+     * previous node even if doing this would overflow
+     */
+    AxisLayoutOptions* setSameLine(bool enable);
+
+    /**
+     * Set the scale priority of this node. Nodes with higher priority will be 
+     * scaled down first before nodes with lower priority when an auto-scaled 
+     * layout attempts to fit its contents. Default is 
+     * AXISLAYOUT_DEFAULT_PRIORITY
+     * @note For optimal performance, the priorities should all be close to 
+     * each other with no gaps
+     */
+    AxisLayoutOptions* setScalePriority(int priority);
+};
+
+/**
+ * A multi-purpose dynamic layout for arranging nodes along an axis. Can be 
+ * used to arrange nodes in a single line, a grid, or a flex layout. The 
+ * RowLayout and ColumnLayout classes function as simple thin wrappers over 
+ * AxisLayout. The positioning of individual nodes in the layout can be 
+ * further controlled using AxisLayoutOptions
+ * @warning Calculating layouts can get increasingly expensive for large 
+ * amounts of child nodes being fit into a small space - while this should 
+ * never prove a real performance concern as most layouts only have a few 
+ * hundred children at the very most, be aware that you probably shouldn't 
+ * call CCNode::updateLayout every frame for a menu with thousands of children
+ * @example
+ * auto menu = CCMenu::create();
+ * // The menu's children will be arranged horizontally, unless they overflow 
+ * // the content size width in which case a new line will be inserted and 
+ * // aligned to the left. The menu automatically will automatically grow in 
+ * // height to fit all the rows
+ * menu->setLayout(
+ *     RowLayout::create()
+ *         ->setGap(10.f)
+ *         ->setGrowCrossAxis(true)
+ *         ->setAxisAlignment(AxisAlignment::Start)
+ * );
+ * menu->setContentSize({ 200.f, 0.f });
+ * menu->addChild(...);
+ * menu->updateLayout();
+ */
+class GEODE_DLL AxisLayout : public Layout {
+protected:
+    Axis m_axis;
+    AxisAlignment m_axisAlignment = AxisAlignment::Center;
+    AxisAlignment m_crossAlignment = AxisAlignment::Center;
+    AxisAlignment m_crossLineAlignment = AxisAlignment::Center;
+    float m_gap = 5.f;
+    bool m_autoScale = true;
+    bool m_axisReverse = false;
+    bool m_crossReverse = false;
+    bool m_allowCrossAxisOverflow = true;
+    bool m_growCrossAxis = false;
+
+    struct Row;
+    
+    float minScaleForPrio(CCArray* nodes, int prio) const;
+    float maxScaleForPrio(CCArray* nodes, int prio) const;
+    bool shouldAutoScale(AxisLayoutOptions const* opts) const;
+    bool canTryScalingDown(
+        CCArray* nodes,
+        int& prio, float& scale,
+        float crossScaleDownFactor,
+        std::pair<int, int> const& minMaxPrios
+    ) const;
+    float nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const;
+    Row* fitInRow(
+        CCNode* on, CCArray* nodes,
+        std::pair<int, int> const& minMaxPrios,
+        bool doAutoScale,
+        float scale, float squish, int prio
+    ) const;
+    void tryFitLayout(
+        CCNode* on, CCArray* nodes,
+        std::pair<int, int> const& minMaxPrios,
+        bool doAutoScale,
+        float scale, float squish, int prio
+    ) const;
+
+    AxisLayout(Axis);
+
+public:
+    /**
+     * Create a new AxisLayout. Note that this class is not automatically 
+     * managed by default, so you must assign it to a CCNode or manually 
+     * manage the memory yourself. See the chainable setters on AxisLayout for 
+     * what options you can customize for the layout
+     * @param axis The direction of the layout
+     * @note For convenience, you can use the RowLayout and ColumnLayout 
+     * classes, which are just thin wrappers over AxisLayout
+     * @returns Created AxisLayout
+     */
+    static AxisLayout* create(Axis axis = Axis::Row);
+
+    void apply(CCNode* on) override;
+
+    Axis getAxis() const;
+    AxisAlignment getAxisAlignment() const;
+    AxisAlignment getCrossAxisAlignment() const;
+    AxisAlignment getCrossAxisLineAlignment() const;
+    float getGap() const;
+    bool getAxisReverse() const;
+    bool getCrossAxisReverse() const;
+    bool getAutoScale() const;
+    bool getGrowCrossAxis() const;
+    bool getCrossAxisOverflow() const;
+
+    AxisLayout* setAxis(Axis axis);
+    /**
+     * Sets where to align the target node's children on the main axis (X-axis 
+     * for Row, Y-axis for Column)
+     */
+    AxisLayout* setAxisAlignment(AxisAlignment align);
+    /**
+     * Sets where to align the target node's children on the cross-axis (Y-axis 
+     * for Row, X-axis for Column)
+     */
+    AxisLayout* setCrossAxisAlignment(AxisAlignment align);
+    /**
+     * Sets where to align the target node's children on the cross-axis for 
+     * each row (Y-axis for Row, X-axis for Column)
+     */
+    AxisLayout* setCrossAxisLineAlignment(AxisAlignment align);
+    /**
+     * The spacing between the children of the node this layout applies to. 
+     * Measured as the space between their edges, not centres. Does not apply 
+     * on the main / cross axis if their alignment is AxisAlignment::Even
+     */
+    AxisLayout* setGap(float gap);
+    /**
+     * Whether to reverse the direction of the children in this layout or not
+     */
+    AxisLayout* setAxisReverse(bool reverse);
+    /**
+     * Whether to reverse the direction of the rows on the cross-axis or not
+     */
+    AxisLayout* setCrossAxisReverse(bool reverse);
+    /**
+     * If enabled, then the layout may scale the target's children if they are 
+     * about to overflow. Assumes that all the childrens' intended scale is 1
+     */
+    AxisLayout* setAutoScale(bool enable);
+    /**
+     * If true, if the main axis overflows extra nodes will be placed on new 
+     * rows/columns on the cross-axis
+     */
+    AxisLayout* setGrowCrossAxis(bool expand);
+    /**
+     * If true, the cross-axis content size of the target node will be 
+     * automatically adjusted to fit the children
+     */
+    AxisLayout* setCrossAxisOverflow(bool allow);
 };
 
 /**
  * Simple layout for arranging nodes in a row (horizontal line)
  */
-class GEODE_DLL RowLayout : public Layout {
+class GEODE_DLL RowLayout : public AxisLayout {
 protected:
-    Alignment m_alignment = Alignment::Center;
-    std::optional<float> m_alignVertically;
-    float m_gap;
+    RowLayout();
 
 public:
-    void apply(CCArray* nodes, CCSize const& availableSize) override;
-
     /**
      * Create a new RowLayout. Note that this class is not automatically 
      * managed by default, so you must assign it to a CCNode or manually 
-     * manage the memory yourself.
-     * @param gap Space between nodes
-     * @param alignVertically Whether to align the nodes vertically, and if so, 
-     * what Y position to align them at
+     * manage the memory yourself. See the chainable setters on RowLayout for 
+     * what options you can customize for the layout
      * @returns Created RowLayout
      */
-    static RowLayout* create(
-        float gap = 5.f,
-        std::optional<float> alignVertically = std::nullopt
-    );
-
-    RowLayout* setAlignment(Alignment align);
-    RowLayout* setGap(float gap);
-    RowLayout* setAlignVertically(std::optional<float> align);
+    static RowLayout* create();
 };
 
 /**
  * Simple layout for arranging nodes in a column (vertical line)
  */
-class GEODE_DLL ColumnLayout : public Layout {
+class GEODE_DLL ColumnLayout : public AxisLayout {
 protected:
-    Alignment m_alignment = Alignment::Center;
-    std::optional<float> m_alignHorizontally;
-    float m_gap;
+    ColumnLayout();
 
 public:
-    void apply(CCArray* nodes, CCSize const& availableSize) override;
-
-    static ColumnLayout* create(
-        float gap = 5.f,
-        std::optional<float> alignHorizontally = std::nullopt
-    );
-
-    ColumnLayout* setAlignment(Alignment align);
-    ColumnLayout* setGap(float gap);
-    ColumnLayout* setAlignHorizontally(std::optional<float> align);
-};
-
-/**
- * Grid direction; which direction the grid should add its next row to if the 
- * current row is full
- */
-enum class GridDirection {
-    // Downward
-    Column,
-    // Upward
-    ReverseColumn,
-    // Right
-    Row,
-    // Left
-    ReverseRow,
-};
-
-/**
- * Grid alignment; same as normal Alignment but also features the "Stretch" 
- * option which will stretch the row out to be the same size as the others
-*/
-enum class GridAlignment {
-    Begin,
-    Center,
-    Stretch,
-    End,
-};
-
-class GEODE_DLL GridLayout : public Layout {
-protected:
-    GridDirection m_direction = GridDirection::Column;
-    GridAlignment m_alignment = GridAlignment::Center;
-    std::optional<size_t> m_rowSize;
-
-public:
-    void apply(CCArray* nodes, CCSize const& availableSize) override;
-
-    static GridLayout* create(
-        std::optional<size_t> rowSize,
-        GridAlignment alignment = GridAlignment::Center,
-        GridDirection direction = GridDirection::Column
-    );
-
-    GridLayout* setDirection(GridDirection direction);
-    GridLayout* setAlignment(GridAlignment alignment);
-    GridLayout* setRowSize(std::optional<size_t> rowSize);
+    /**
+     * Create a new ColumnLayout. Note that this class is not automatically 
+     * managed by default, so you must assign it to a CCNode or manually 
+     * manage the memory yourself. See the chainable setters on RowLayout for 
+     * what options you can customize for the layout
+     * @returns Created ColumnLayout
+     */
+    static ColumnLayout* create();
 };
 
 NS_CC_END
diff --git a/loader/include/Geode/cocos/cocoa/CCArray.h b/loader/include/Geode/cocos/cocoa/CCArray.h
index 8ce4f710..f6d53a8b 100644
--- a/loader/include/Geode/cocos/cocoa/CCArray.h
+++ b/loader/include/Geode/cocos/cocoa/CCArray.h
@@ -234,13 +234,11 @@ public:
     void fastRemoveObject(CCObject* object);
     /** Fast way to remove an element with a certain index */
     void fastRemoveObjectAtIndex(unsigned int index);
-
-    RT_ADD(
-        /** 
-         * Rob modification
-         * Fast way to remove an element with a certain index */
-        void fastRemoveObjectAtIndexNew(unsigned int index);
-    );
+    /** 
+     * Fast way to remove an element with a certain index
+     * @note RobTop addition
+     */
+    void fastRemoveObjectAtIndexNew(unsigned int index);
 
     // Rearranging Content
 
@@ -263,6 +261,13 @@ public:
      */
     virtual CCObject* copyWithZone(CCZone* pZone);
 
+    /**
+     * Creates a shallow copy of this array, aka only clones the pointers to 
+     * the array members and not the members themselves
+     * @returns New array with same members
+     */
+    GEODE_DLL CCArray* shallowCopy();
+
     /* override functions */
     virtual void acceptVisitor(CCDataVisitor &visitor);
 
diff --git a/loader/include/Geode/cocos/cocoa/CCGeometry.h b/loader/include/Geode/cocos/cocoa/CCGeometry.h
index 2e31ccf3..3783b140 100644
--- a/loader/include/Geode/cocos/cocoa/CCGeometry.h
+++ b/loader/include/Geode/cocos/cocoa/CCGeometry.h
@@ -360,6 +360,13 @@ public:
     inline bool equals(const CCSize& target) const {
         return (fabs(this->width  - target.width)  < FLT_EPSILON) && (fabs(this->height - target.height) < FLT_EPSILON);
     }
+    /**
+     * Get the aspect ratio of this CCSize
+     * @note Geode addition
+     */
+    inline float aspect() const {
+        return this->width / this->height;
+    }
 };
 
 // alk cont
diff --git a/loader/include/Geode/cocos/platform/win32/CCEGLView.h b/loader/include/Geode/cocos/platform/win32/CCEGLView.h
index 59c12c73..a8a7b838 100644
--- a/loader/include/Geode/cocos/platform/win32/CCEGLView.h
+++ b/loader/include/Geode/cocos/platform/win32/CCEGLView.h
@@ -96,18 +96,35 @@ public:
     */
     static CCEGLView* sharedOpenGLView();
 
+    /**
+     * @note Geode addition
+     */
     static GEODE_DLL CCEGLView* get();
 
-    RT_ADD( static CCEGLView* create(const gd::string&);   )
+    /**
+     * @note RobTop addition
+     */
+    static CCEGLView* create(const gd::string&);
 
-    RT_ADD(
-        //actually this is my function but i dont wanna make a new macro for it
-        inline CCPoint getMousePosition() { return { m_fMouseX, m_fMouseY }; }
+    /**
+     * @note Geode addition
+     */
+    inline CCPoint getMousePosition() { return { m_fMouseX, m_fMouseY }; }
 
-        void toggleFullScreen(bool fullscreen);
+    /**
+     * @note RobTop addition
+     */
+    void toggleFullScreen(bool fullscreen);
 
-        GLFWwindow* getWindow(void) const;
-    )
+    /**
+     * @note RobTop addition
+     */
+    GLFWwindow* getWindow(void) const;
+
+    /**
+     * @note RobTop addition
+     */
+    CCSize getDisplaySize();
 
 protected:
 	static CCEGLView* s_pEglView;
diff --git a/loader/include/Geode/loader/Log.hpp b/loader/include/Geode/loader/Log.hpp
index 8681efaf..38c88627 100644
--- a/loader/include/Geode/loader/Log.hpp
+++ b/loader/include/Geode/loader/Log.hpp
@@ -58,6 +58,44 @@ namespace geode {
             return buf.str();
         }
 
+        // todo: maybe add a debugParse function for these?
+
+        template <class T>
+            requires requires(T t) {
+                parse(t);
+            }
+        std::string parse(std::optional<T> const& thing) {
+            if (thing.has_value()) {
+                return "opt(" + parse(thing.value()) + ")";
+            }
+            return "nullopt";
+        }
+
+        template <class A, class B>
+            requires requires(A a, B b) {
+                parse(a);
+                parse(b);
+            }
+        std::string parse(std::pair<A, B> const& thing) {
+            return "(" + parse(thing.first) + ", " + parse(thing.second) + ")";
+        }
+
+        template <class... T, std::size_t... Is>
+        std::string parseTupleImpl(std::tuple<T...> const& tuple, std::index_sequence<Is...>) {
+            std::string ret = "(";
+            ((ret += (Is == 0 ? "" : ", ") + parse(std::get<Is>(tuple))), ...);
+            ret += ")";
+            return ret;
+        }
+
+        template <class... T>
+            requires requires(T... t) {
+                (parse(t), ...);
+            }
+        std::string parse(std::tuple<T...> const& tuple) {
+            return parseTupleImpl(tuple, std::index_sequence_for<T...> {});
+        }
+
         // Log component system
 
         struct GEODE_DLL ComponentTrait {
diff --git a/loader/include/Geode/platform/windows.hpp b/loader/include/Geode/platform/windows.hpp
index 5af71b81..33dbe263 100644
--- a/loader/include/Geode/platform/windows.hpp
+++ b/loader/include/Geode/platform/windows.hpp
@@ -75,6 +75,10 @@ namespace geode::cast {
             std::is_polymorphic_v<std::remove_pointer_t<Before>>, "Input is not a polymorphic type"
         );
 
+        if (!ptr) {
+            return After();
+        }
+
         auto basePtr = dynamic_cast<void*>(ptr);
         auto vftable = *reinterpret_cast<VftableType**>(basePtr);
 
diff --git a/loader/include/Geode/ui/BasedButtonSprite.hpp b/loader/include/Geode/ui/BasedButtonSprite.hpp
index 4aba6218..3704b600 100644
--- a/loader/include/Geode/ui/BasedButtonSprite.hpp
+++ b/loader/include/Geode/ui/BasedButtonSprite.hpp
@@ -2,6 +2,8 @@
 
 #include <cocos2d.h>
 
+#pragma warning(disable: 4275)
+
 namespace geode {
     enum class CircleBaseSize {
         Tiny = 0, // Equivalent to the tiny delete button
diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp
index eafc44df..188631b9 100644
--- a/loader/include/Geode/utils/JsonValidation.hpp
+++ b/loader/include/Geode/utils/JsonValidation.hpp
@@ -77,7 +77,7 @@ namespace geode {
     struct JsonMaybeObject;
     struct JsonMaybeValue;
 
-    struct JsonMaybeSomething {
+    struct GEODE_DLL JsonMaybeSomething {
     protected:
         JsonChecker& m_checker;
         json::Value& m_json;
@@ -87,29 +87,29 @@ namespace geode {
         friend struct JsonMaybeObject;
         friend struct JsonMaybeValue;
 
-        GEODE_DLL void setError(std::string const& error);
+        void setError(std::string const& error);
 
     public:
-        GEODE_DLL json::Value& json();
+        json::Value& json();
 
-        GEODE_DLL JsonMaybeSomething(
+        JsonMaybeSomething(
             JsonChecker& checker, json::Value& json, std::string const& hierarchy, bool hasValue
         );
 
-        GEODE_DLL bool isError() const;
-        GEODE_DLL std::string getError() const;
+        bool isError() const;
+        std::string getError() const;
 
-        GEODE_DLL operator bool() const;
+        operator bool() const;
     };
 
-    struct JsonMaybeValue : public JsonMaybeSomething {
+    struct GEODE_DLL JsonMaybeValue : public JsonMaybeSomething {
         bool m_inferType = true;
 
-        GEODE_DLL JsonMaybeValue(
+        JsonMaybeValue(
             JsonChecker& checker, json::Value& json, std::string const& hierarchy, bool hasValue
         );
 
-        GEODE_DLL JsonMaybeSomething& self();
+        JsonMaybeSomething& self();
 
         template <json::Type T>
         JsonMaybeValue& as() {
@@ -124,7 +124,7 @@ namespace geode {
             return *this;
         }
 
-        GEODE_DLL JsonMaybeValue& array();
+        JsonMaybeValue& array();
 
         template <json::Type... T>
         JsonMaybeValue& asOneOf() {
@@ -231,7 +231,7 @@ namespace geode {
             return T();
         }
 
-        GEODE_DLL JsonMaybeObject obj();
+        JsonMaybeObject obj();
 
         template <class T>
         struct Iterator {
@@ -257,46 +257,46 @@ namespace geode {
             }
         };
 
-        GEODE_DLL JsonMaybeValue at(size_t i);
+        JsonMaybeValue at(size_t i);
 
-        GEODE_DLL Iterator<JsonMaybeValue> iterate();
+        Iterator<JsonMaybeValue> iterate();
 
-        GEODE_DLL Iterator<std::pair<std::string, JsonMaybeValue>> items();
+        Iterator<std::pair<std::string, JsonMaybeValue>> items();
     };
 
-    struct JsonMaybeObject : JsonMaybeSomething {
+    struct GEODE_DLL JsonMaybeObject : JsonMaybeSomething {
         std::set<std::string> m_knownKeys;
 
-        GEODE_DLL JsonMaybeObject(
+        JsonMaybeObject(
             JsonChecker& checker, json::Value& json, std::string const& hierarchy, bool hasValue
         );
 
-        GEODE_DLL JsonMaybeSomething& self();
+        JsonMaybeSomething& self();
 
-        GEODE_DLL void addKnownKey(std::string const& key);
+        void addKnownKey(std::string const& key);
 
-        GEODE_DLL json::Value& json();
+        json::Value& json();
 
-        GEODE_DLL JsonMaybeValue emptyValue();
+        JsonMaybeValue emptyValue();
 
-        GEODE_DLL JsonMaybeValue has(std::string const& key);
+        JsonMaybeValue has(std::string const& key);
 
-        GEODE_DLL JsonMaybeValue needs(std::string const& key);
+        JsonMaybeValue needs(std::string const& key);
 
-        GEODE_DLL void checkUnknownKeys();
+        void checkUnknownKeys();
     };
 
-    struct JsonChecker {
+    struct GEODE_DLL JsonChecker {
         std::variant<std::monostate, std::string> m_result;
         json::Value& m_json;
 
-        GEODE_DLL JsonChecker(json::Value& json);
+        JsonChecker(json::Value& json);
 
-        GEODE_DLL bool isError() const;
+        bool isError() const;
 
-        GEODE_DLL std::string getError() const;
+        std::string getError() const;
 
-        GEODE_DLL JsonMaybeValue root(std::string const& hierarchy);
+        JsonMaybeValue root(std::string const& hierarchy);
     };
 
 }
diff --git a/loader/include/Geode/utils/VersionInfo.hpp b/loader/include/Geode/utils/VersionInfo.hpp
index f94472ca..d9df8c88 100644
--- a/loader/include/Geode/utils/VersionInfo.hpp
+++ b/loader/include/Geode/utils/VersionInfo.hpp
@@ -14,24 +14,73 @@ namespace geode {
     };
 
     /**
-     * A version label, like v1.0.0-alpha or v2.3.4-prerelease. Purely semantic, 
-     * and not used in comparisons; so for example v1.0.0-alpha == v1.0.0.
+     * A version label, like v1.0.0-alpha or v2.3.4-prerelease. Limited to these 
+     * options; arbitary identifiers are not supported. Additional numbering 
+     * may be added after the identifier, such as v1.0.0-beta.1
      */
-    enum class VersionTag {
-        Alpha,
-        Beta,
-        Prerelease,
+    struct VersionTag {
+        enum {
+            Alpha,
+            Beta,
+            Prerelease,
+        } value;
+        std::optional<size_t> number;
+
+        using Type = decltype(value);
+
+        constexpr VersionTag(Type const& value) : value(value) {}
+        constexpr VersionTag(Type const& value, std::optional<size_t> number)
+          : value(value), number(number) {}
+
+        constexpr bool operator==(VersionTag const& other) const {
+            return value == other.value && number == other.number;
+        }
+        constexpr bool operator<(VersionTag const& other) const {
+            if (value == other.value) {
+                if (number && other.number) return number < other.number;
+                if (number) return true;
+                if (other.number) return false;
+                return false;
+            }
+            return value < other.value;
+        }
+        constexpr bool operator<=(VersionTag const& other) const {
+            if (value == other.value) {
+                if (number && other.number) return number <= other.number;
+                if (number) return true;
+                if (other.number) return false;
+                return true;
+            }
+            return value <= other.value;
+        }
+        constexpr bool operator>(VersionTag const& other) const {
+            if (value == other.value) {
+                if (number && other.number) return number > other.number;
+                if (number) return true;
+                if (other.number) return false;
+                return false;
+            }
+            return value > other.value;
+        }
+        constexpr bool operator>=(VersionTag const& other) const {
+            if (value == other.value) {
+                if (number && other.number) return number >= other.number;
+                if (number) return true;
+                if (other.number) return false;
+                return true;
+            }
+            return value >= other.value;
+        }
+
+        static Result<VersionTag> parse(std::stringstream& str);
+        std::string toSuffixString() const;
+        std::string toString() const;
     };
-    GEODE_DLL std::optional<VersionTag> versionTagFromString(std::string const& str);
-    GEODE_DLL std::string versionTagToSuffixString(VersionTag tag);
-    GEODE_DLL std::string versionTagToString(VersionTag tag);
 
     /**
-     * Class representing version information. Not strictly semver, notably in 
-     * regard to identifiers; identifiers are restricted to a few common ones, 
-     * and are purely semantic, i.e. not used in comparisons. See VersionTag 
-     * for details
-     * @class VersionInfo
+     * Class representing version information. Uses a limited subset of SemVer;  
+     * identifiers are restricted to a few predefined ones, and only one 
+     * identifier is allowed. See VersionTag for details
      */
     class GEODE_DLL VersionInfo final {
     protected:
@@ -78,24 +127,24 @@ namespace geode {
         // Apple clang does not support operator<=>! Yippee!
 
         constexpr bool operator==(VersionInfo const& other) const {
-            return std::tie(m_major, m_minor, m_patch) ==
-                std::tie(other.m_major, other.m_minor, other.m_patch);
+            return std::tie(m_major, m_minor, m_patch, m_tag) ==
+                std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag);
         }
         constexpr bool operator<(VersionInfo const& other) const {
-            return std::tie(m_major, m_minor, m_patch) <
-                std::tie(other.m_major, other.m_minor, other.m_patch);
+            return std::tie(m_major, m_minor, m_patch, m_tag) <
+                std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag);
         }
         constexpr bool operator<=(VersionInfo const& other) const {
-            return std::tie(m_major, m_minor, m_patch) <=
-                std::tie(other.m_major, other.m_minor, other.m_patch);
+            return std::tie(m_major, m_minor, m_patch, m_tag) <=
+                std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag);
         }
         constexpr bool operator>(VersionInfo const& other) const {
-            return std::tie(m_major, m_minor, m_patch) >
-                std::tie(other.m_major, other.m_minor, other.m_patch);
+            return std::tie(m_major, m_minor, m_patch, m_tag) >
+                std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag);
         }
         constexpr bool operator>=(VersionInfo const& other) const {
-            return std::tie(m_major, m_minor, m_patch) >=
-                std::tie(other.m_major, other.m_minor, other.m_patch);
+            return std::tie(m_major, m_minor, m_patch, m_tag) >=
+                std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag);
         }
 
         std::string toString(bool includeTag = true) const;
diff --git a/loader/include/Geode/utils/cocos.hpp b/loader/include/Geode/utils/cocos.hpp
index 401400fb..c16280e7 100644
--- a/loader/include/Geode/utils/cocos.hpp
+++ b/loader/include/Geode/utils/cocos.hpp
@@ -410,16 +410,31 @@ namespace geode::cocos {
      * or nullptr if index exceeds bounds
      */
     template <class Type = cocos2d::CCNode>
-    static Type* getChildOfType(cocos2d::CCNode* node, size_t index) {
+    static Type* getChildOfType(cocos2d::CCNode* node, int index) {
         size_t indexCounter = 0;
 
-        for (size_t i = 0; i < node->getChildrenCount(); ++i) {
-            auto obj = cast::typeinfo_cast<Type*>(node->getChildren()->objectAtIndex(i));
-            if (obj != nullptr) {
-                if (indexCounter == index) {
-                    return obj;
+        // start from end for negative index
+        if (index < 0) {
+            index = -index - 1;
+            for (size_t i = node->getChildrenCount() - 1; i >= 0; i--) {
+                auto obj = cast::typeinfo_cast<Type*>(node->getChildren()->objectAtIndex(i));
+                if (obj != nullptr) {
+                    if (indexCounter == index) {
+                        return obj;
+                    }
+                    ++indexCounter;
+                }
+            }
+        }
+        else {
+            for (size_t i = 0; i < node->getChildrenCount(); i++) {
+                auto obj = cast::typeinfo_cast<Type*>(node->getChildren()->objectAtIndex(i));
+                if (obj != nullptr) {
+                    if (indexCounter == index) {
+                        return obj;
+                    }
+                    ++indexCounter;
                 }
-                ++indexCounter;
             }
         }
 
@@ -548,86 +563,6 @@ namespace geode::cocos {
      */
     GEODE_DLL bool fileExistsInSearchPaths(char const* filename);
 
-    template <typename T>
-    struct CCArrayIterator {
-    public:
-        CCArrayIterator(T* p) : m_ptr(p) {}
-
-        T* m_ptr;
-
-        auto& operator*() {
-            return *m_ptr;
-        }
-
-        auto& operator*() const {
-            return *m_ptr;
-        }
-
-        auto operator->() {
-            return m_ptr;
-        }
-
-        auto operator->() const {
-            return m_ptr;
-        }
-
-        auto& operator++() {
-            ++m_ptr;
-            return *this;
-        }
-
-        auto& operator--() {
-            --m_ptr;
-            return *this;
-        }
-
-        auto& operator+=(size_t val) {
-            m_ptr += val;
-            return *this;
-        }
-
-        auto& operator-=(size_t val) {
-            m_ptr -= val;
-            return *this;
-        }
-
-        auto operator+(size_t val) const {
-            return CCArrayIterator<T>(m_ptr + val);
-        }
-
-        auto operator-(size_t val) const {
-            return CCArrayIterator<T>(m_ptr - val);
-        }
-
-        auto operator-(CCArrayIterator<T> const& other) const {
-            return m_ptr - other.m_ptr;
-        }
-
-        bool operator<(CCArrayIterator<T> const& other) const {
-            return m_ptr < other.m_ptr;
-        }
-
-        bool operator>(CCArrayIterator<T> const& other) const {
-            return m_ptr > other.m_ptr;
-        }
-
-        bool operator<=(CCArrayIterator<T> const& other) const {
-            return m_ptr <= other.m_ptr;
-        }
-
-        bool operator>=(CCArrayIterator<T> const& other) const {
-            return m_ptr >= other.m_ptr;
-        }
-
-        bool operator==(CCArrayIterator<T> const& other) const {
-            return m_ptr == other.m_ptr;
-        }
-
-        bool operator!=(CCArrayIterator<T> const& other) const {
-            return m_ptr != other.m_ptr;
-        }
-    };
-
     inline void ccDrawColor4B(cocos2d::ccColor4B const& color) {
         cocos2d::ccDrawColor4B(color.r, color.g, color.b, color.a);
     }
@@ -766,16 +701,6 @@ namespace std {
             return std::hash<T*>()(ref.data());
         }
     };
-
-    template <typename T>
-    struct iterator_traits<geode::cocos::CCArrayIterator<T>> {
-        using difference_type = ptrdiff_t;
-        using value_type = T;
-        using pointer = T*;
-        using reference = T&;
-        using iterator_category =
-            std::random_access_iterator_tag; // its random access but im too lazy to implement it
-    };
 }
 
 // more utils
@@ -807,9 +732,14 @@ namespace geode::cocos {
         using T = std::remove_pointer_t<_Type>;
 
     public:
+        using value_type = T;
+        using iterator = T**;
+        using const_iterator = const T**;
+
         CCArrayExt() : m_arr(cocos2d::CCArray::create()) {}
 
-        CCArrayExt(cocos2d::CCArray* arr) : m_arr(arr) {}
+        CCArrayExt(cocos2d::CCArray* arr)
+          : m_arr(arr) {}
 
         CCArrayExt(CCArrayExt const& a) : m_arr(a.m_arr) {}
 
@@ -819,18 +749,26 @@ namespace geode::cocos {
 
         ~CCArrayExt() {}
 
-        auto begin() {
+        T** begin() const {
             if (!m_arr) {
-                return CCArrayIterator<T*>(nullptr);
+                return nullptr;
             }
-            return CCArrayIterator<T*>(reinterpret_cast<T**>(m_arr->data->arr));
+            return reinterpret_cast<T**>(m_arr->data->arr);
         }
 
-        auto end() {
+        T** end() const {
             if (!m_arr) {
-                return CCArrayIterator<T*>(nullptr);
+                return nullptr;
             }
-            return CCArrayIterator<T*>(reinterpret_cast<T**>(m_arr->data->arr) + m_arr->count());
+            return reinterpret_cast<T**>(m_arr->data->arr) + m_arr->count();
+        }
+
+        auto rbegin() const {
+            return std::reverse_iterator(this->end());
+        }
+
+        auto rend() const {
+            return std::reverse_iterator(this->begin());
         }
 
         size_t size() const {
diff --git a/loader/include/Geode/utils/ranges.hpp b/loader/include/Geode/utils/ranges.hpp
index 0481285b..324abad4 100644
--- a/loader/include/Geode/utils/ranges.hpp
+++ b/loader/include/Geode/utils/ranges.hpp
@@ -289,23 +289,21 @@ namespace geode::utils::ranges {
         return member(*it);
     }
 
-    template <ValidConstContainer C>
-    struct ConstReverseWrapper {
-        C const& iter;
+    template <class C>
+    struct ReverseWrapper {
+        C iter;
+
+        decltype(auto) begin() {
+            return std::rbegin(iter);
+        }
+
+        decltype(auto) end() {
+            return std::rend(iter);
+        }
     };
 
-    template <ValidConstContainer C>
-    auto begin(ConstReverseWrapper<C> const& c) {
-        return std::rbegin(c.iter);
-    }
-
-    template <ValidConstContainer C>
-    auto end(ConstReverseWrapper<C> const& c) {
-        return std::rend(c.iter);
-    }
-
-    template <ValidConstContainer C>
-    ConstReverseWrapper<C> reverse(C const& iter) {
-        return { iter };
+    template <class C>
+    auto reverse(C&& iter) {
+        return ReverseWrapper<C>{std::forward<C>(iter)};
     }
 }
diff --git a/loader/resources/mod.json.in b/loader/resources/mod.json.in
index b4baa56d..5953f0c6 100644
--- a/loader/resources/mod.json.in
+++ b/loader/resources/mod.json.in
@@ -1,5 +1,5 @@
 {
-    "geode": "@PROJECT_VERSION@",
+    "geode": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@",
     "id": "geode.loader",
     "version": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@",
     "name": "Geode",
diff --git a/loader/src/cocos2d-ext/CCArray.cpp b/loader/src/cocos2d-ext/CCArray.cpp
index b863faf3..f288dfb4 100644
--- a/loader/src/cocos2d-ext/CCArray.cpp
+++ b/loader/src/cocos2d-ext/CCArray.cpp
@@ -16,4 +16,10 @@ void CCArray::removeFirstObject(bool bReleaseObj) {
     this->removeObjectAtIndex(0, bReleaseObj);
 }
  
+CCArray* CCArray::shallowCopy() {
+    auto r = CCArray::createWithCapacity(this->capacity());
+    r->addObjectsFromArray(this);
+    return r;
+}
+
 #pragma warning(pop)
diff --git a/loader/src/cocos2d-ext/Layout.cpp b/loader/src/cocos2d-ext/Layout.cpp
index c02b4cd3..e829754e 100644
--- a/loader/src/cocos2d-ext/Layout.cpp
+++ b/loader/src/cocos2d-ext/Layout.cpp
@@ -1,5 +1,9 @@
 #include <cocos2d.h>
 #include <Geode/utils/cocos.hpp>
+#include <Geode/utils/ranges.hpp>
+#include <Geode/loader/Log.hpp>
+#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
+#include <Geode/binding/CCMenuItemToggler.hpp>
 
 USE_GEODE_NAMESPACE();
 
@@ -7,155 +11,922 @@ USE_GEODE_NAMESPACE();
 
 void CCNode::swapChildIndices(CCNode* first, CCNode* second) {
     m_pChildren->exchangeObject(first, second);
+    std::swap(first->m_nZOrder, second->m_nZOrder);
+    std::swap(first->m_uOrderOfArrival, second->m_uOrderOfArrival);
 }
 
-void RowLayout::apply(CCArray* nodes, CCSize const& availableSize) {
-    float totalWidth = .0f;
+void CCNode::insertBefore(CCNode* child, CCNode* before) {
+    this->addChild(child);
+    if (
+        (before && m_pChildren->containsObject(before)) ||
+        (before = static_cast<CCNode*>(m_pChildren->firstObject()))
+    ) {
+        child->setZOrder(before->getZOrder());
+        child->setOrderOfArrival(before->getOrderOfArrival() - 1);
+    }
+}
+
+void CCNode::insertAfter(CCNode* child, CCNode* after) {
+    this->addChild(child);
+    if (m_pChildren->containsObject(after)) {
+        child->setZOrder(after->getZOrder());
+        child->setOrderOfArrival(after->getOrderOfArrival() + 1);
+    }
+}
+
+CCArray* Layout::getNodesToPosition(CCNode* on) {
+    if (!on->getChildren()) {
+        return CCArray::create();
+    }
+    return on->getChildren()->shallowCopy();
+}
+
+static AxisLayoutOptions const* axisOpts(CCNode* node) {
+    if (!node) return nullptr;
+    return typeinfo_cast<AxisLayoutOptions*>(node->getLayoutOptions());
+}
+
+static bool isOptsBreakLine(AxisLayoutOptions const* opts) {
+    if (opts) {
+        return opts->getBreakLine();
+    }
+    return false;
+}
+
+static bool isOptsSameLine(AxisLayoutOptions const* opts) {
+    if (opts) {
+        return opts->getSameLine();
+    }
+    return false;
+}
+
+static int optsScalePrio(AxisLayoutOptions const* opts) {
+    if (opts) {
+        return opts->getScalePriority();
+    }
+    return AXISLAYOUT_DEFAULT_PRIORITY;
+}
+
+static float optsMinScale(AxisLayoutOptions const* opts) {
+    if (opts) {
+        return opts->getMinScale();
+    }
+    return AXISLAYOUT_DEFAULT_MIN_SCALE;
+}
+
+static float optsMaxScale(AxisLayoutOptions const* opts) {
+    if (opts) {
+        return opts->getMaxScale();
+    }
+    return 1.f;
+}
+
+static float optsRelScale(AxisLayoutOptions const* opts) {
+    if (opts) {
+        return opts->getRelativeScale();
+    }
+    return 1.f;
+}
+
+static float scaleByOpts(AxisLayoutOptions const* opts, float scale, int prio, bool squishMode) {
+    if (prio > optsScalePrio(opts)) {
+        return optsMaxScale(opts) * optsRelScale(opts);
+    }
+    // otherwise if it matches scale it down by the factor
+    else if (!squishMode && prio == optsScalePrio(opts)) {
+        auto trueScale = scale;
+        auto min = optsMinScale(opts);
+        auto max = optsMaxScale(opts);
+        if (trueScale < min) {
+            trueScale = min;
+        }
+        if (trueScale > max) {
+            trueScale = max;
+        }
+        return trueScale * optsRelScale(opts);
+    }
+    // otherwise it's been scaled down to minimum
+    else {
+        return optsMinScale(opts) * optsRelScale(opts);
+    }
+}
+
+struct AxisLayout::Row : public CCObject {
+    float nextOverflowScaleDownFactor;
+    float nextOverflowSquishFactor;
+    float axisLength;
+    float crossLength;
+    float axisEndsLength;
+
+    // all layout calculations happen within a single frame so no Ref needed
+    CCArray* nodes;
+
+    // calculated values for scale, squish and prio to fit the nodes in this 
+    // row when positioning
+    float scale;
+    float squish;
+    float prio;
+
+    Row(
+        float scaleFactor,
+        float squishFactor,
+        float axisLength,
+        float crossLength,
+        float axisEndsLength,
+        CCArray* nodes,
+        float scale,
+        float squish,
+        float prio
+    ) : nextOverflowScaleDownFactor(scaleFactor),
+        nextOverflowSquishFactor(squishFactor),
+        axisLength(axisLength),
+        crossLength(crossLength),
+        axisEndsLength(axisEndsLength),
+        nodes(nodes),
+        scale(scale),
+        squish(squish),
+        prio(prio)
+    {
+        this->autorelease();
+    }
+};
+
+struct AxisPosition {
+    float axisLength;
+    float axisAnchor;
+    float crossLength;
+    float crossAnchor;
+};
+
+static AxisPosition nodeAxis(CCNode* node, Axis axis, float scale) {
+    auto scaledSize = node->getScaledContentSize() * scale;
+    std::optional<float> axisLength = std::nullopt;
+    if (auto opts = axisOpts(node)) {
+        axisLength = opts->getLength();
+    }
+    // CCMenuItemToggler is a common quirky class
+    if (auto toggle = typeinfo_cast<CCMenuItemToggler*>(node)) {
+        scaledSize = toggle->m_offButton->getScaledContentSize();
+    }
+    auto anchor = node->getAnchorPoint();
+    if (axis == Axis::Row) {
+        return AxisPosition {
+            .axisLength = axisLength.value_or(scaledSize.width),
+            .axisAnchor = anchor.x,
+            .crossLength = scaledSize.height,
+            .crossAnchor = anchor.y,
+        };
+    }
+    else {
+        return AxisPosition {
+            .axisLength = axisLength.value_or(scaledSize.height),
+            .axisAnchor = anchor.y,
+            .crossLength = scaledSize.width,
+            .crossAnchor = anchor.x,
+        };
+    }
+}
+
+float AxisLayout::nextGap(AxisLayoutOptions const* now, AxisLayoutOptions const* next) const {
+    std::optional<float> gap;
+    if (now) {
+        gap = now->getNextGap();
+    }
+    if (next && (!gap || gap.value() < next->getPrevGap())) {
+        gap = next->getPrevGap();
+    }
+    return gap.value_or(m_gap);
+}
+
+bool AxisLayout::shouldAutoScale(AxisLayoutOptions const* opts) const {
+    if (!opts) return m_autoScale;
+    return opts->getAutoScale().value_or(m_autoScale);
+}
+
+float AxisLayout::minScaleForPrio(CCArray* nodes, int prio) const {
+    float min = AXISLAYOUT_DEFAULT_MIN_SCALE;
+    bool first = true;
+    for (auto node : CCArrayExt<CCNode>(nodes)) {
+        auto scale = optsMinScale(axisOpts(node));
+        if (first) {
+            min = scale;
+            first = false;
+        }
+        else if (scale < min) {
+            min = scale;
+        }
+    }
+    return min;
+}
+
+float AxisLayout::maxScaleForPrio(CCArray* nodes, int prio) const {
+    float max = 1.f;
+    bool first = true;
+    for (auto node : CCArrayExt<CCNode>(nodes)) {
+        auto scale = optsMaxScale(axisOpts(node));
+        if (first) {
+            max = scale;
+            first = false;
+        }
+        else if (scale > max) {
+            max = scale;
+        }
+    }
+    return max;
+}
+
+AxisLayout::Row* AxisLayout::fitInRow(
+    CCNode* on, CCArray* nodes,
+    std::pair<int, int> const& minMaxPrios,
+    bool doAutoScale,
+    float scale, float squish, int prio
+) const {
+    float nextAxisScalableLength;
+    float nextAxisUnscalableLength;
+    float axisUnsquishedLength;
+    float axisLength;
+    float crossLength;
+    auto res = CCArray::create();
+
+    auto available = nodeAxis(on, m_axis, 1.f);
+
+    auto fit = [&](CCArray* nodes) {
+        nextAxisScalableLength = 0.f;
+        nextAxisUnscalableLength = 0.f;
+        axisUnsquishedLength = 0.f;
+        axisLength = 0.f;
+        crossLength = 0.f;
+        AxisLayoutOptions const* prev = nullptr;
+        size_t ix = 0;
+        for (auto& node : CCArrayExt<CCNode*>(nodes)) {
+            auto opts = axisOpts(node);
+            if (this->shouldAutoScale(opts)) {
+                node->setScale(1.f);
+            }
+            auto nodeScale = scaleByOpts(opts, scale, prio, false);
+            auto pos = nodeAxis(node, m_axis, nodeScale * squish);
+            auto squishPos = nodeAxis(node, m_axis, scaleByOpts(opts, scale, prio, true));
+            if (prio == optsScalePrio(opts)) {
+                nextAxisScalableLength += pos.axisLength;
+            }
+            else {
+                nextAxisUnscalableLength += pos.axisLength;
+            }
+            // if multiple rows are allowed and this row is full, time for the 
+            // next row
+            // also force at least one object to be added to this row, because if 
+            // it's too large for this row it's gonna be too large for all rows
+            if (
+                m_growCrossAxis && ((
+                    (nextAxisScalableLength + nextAxisUnscalableLength > available.axisLength) && 
+                    ix != 0 &&
+                    !isOptsSameLine(opts)
+                ))
+            ) {
+                break;
+            }
+            if (nodes != res) {
+                res->addObject(node);
+            }
+            if (ix) {
+                auto gap = nextGap(prev, opts);
+                // if we've exhausted all priority scale options, scale gap too
+                if (prio == minMaxPrios.first) {
+                    nextAxisScalableLength += gap * scale * squish;
+                    axisLength += gap * scale * squish;
+                    axisUnsquishedLength += gap * scale;
+                }
+                else {
+                    nextAxisUnscalableLength += gap * squish;
+                    axisLength += gap * squish;
+                    axisUnsquishedLength += gap;
+                }
+            }
+            axisLength += pos.axisLength;
+            axisUnsquishedLength += squishPos.axisLength;
+            // squishing doesn't affect cross length, that's done separately
+            if (pos.crossLength / squish > crossLength) {
+                crossLength = pos.crossLength / squish;
+            }
+            prev = opts;
+            if (m_growCrossAxis && isOptsBreakLine(opts)) {
+                break;
+            }
+            ix++;
+        }
+    };
+
+    fit(nodes);
+
+    // whoops! removing objects from a CCArray while iterating is totes potes UB
+    for (int i = 0; i < res->count(); i++) {
+        nodes->removeFirstObject();
+    }
+
+    auto scaleDownFactor = scale - .025f;
+    auto squishFactor = available.axisLength / (axisUnsquishedLength + .01f) * squish;
+
+    // calculate row scale, squish, and prio
+    int tries = 1000;
+    while (axisLength > available.axisLength) {
+        if (this->canTryScalingDown(
+            res, prio, scale, scale - .025f, minMaxPrios
+        )) {
+            scale -= .025f;
+        }
+        else {
+            squish = available.axisLength / (axisUnsquishedLength + .01f) * squish;
+        }
+        fit(res);
+        // Avoid infinite loops
+        if (tries-- <= 0) {
+            break;
+        }
+    }
+
+    // reverse row if needed
+    if (m_axisReverse) {
+        res->reverseObjects();
+    }
+
+    float axisEndsLength = 0.f;
+    if (res->count()) {
+        auto first = static_cast<CCNode*>(res->firstObject());
+        auto last = static_cast<CCNode*>(res->lastObject());
+        axisEndsLength = (
+            first->getScaledContentSize().width * 
+                scaleByOpts(axisOpts(first), scale, prio, false) / 2 +
+            last->getScaledContentSize().width * 
+                scaleByOpts(axisOpts(last), scale, prio, false) / 2
+        );
+    }
+
+    return new Row(
+        // how much should the nodes be scaled down to fit the next row
+        // the .01f is because floating point arithmetic is imprecise and you 
+        // end up in a situation where it confidently tells you that
+        // 241 > 241 == true
+        // todo: make this calculation more smart to avoid so much unnecessary recursion
+        scaleDownFactor,
+        // how much should the nodes be squished to fit the next item in this 
+        // row
+        squishFactor,
+        axisLength, crossLength, axisEndsLength,
+        res,
+        scale, squish, prio
+    );
+}
+
+bool AxisLayout::canTryScalingDown(
+    CCArray* nodes,
+    int& prio, float& scale,
+    float crossScaleDownFactor,
+    std::pair<int, int> const& minMaxPrios
+) const {
+    bool attemptRescale = false;
+    auto minScaleForPrio = this->minScaleForPrio(nodes, prio);
+    if (
+        // if the scale is less than the lowest min scale allowed, then 
+        // trying to scale will have no effect and not help anywmore
+        crossScaleDownFactor < minScaleForPrio ||
+        // if the scale down factor is the same as before, then we've 
+        // entered an infinite loop
+        crossScaleDownFactor == scale
+    ) {
+        // is there still some lower priority nodes we could try scaling?
+        if (prio > minMaxPrios.first) {
+            while (true) {
+                prio -= 1;
+                auto mscale = this->maxScaleForPrio(nodes, prio);
+                if (!mscale) {
+                    continue;
+                }
+                scale = mscale;
+                break;
+            }
+            attemptRescale = true;
+        }
+        // otherwise set scale to min and squish
+        else {
+            scale = minScaleForPrio;
+        }
+    }
+    // otherwise scale as usual
+    else {
+        attemptRescale = true;
+    }
+    return attemptRescale;
+}
+
+void AxisLayout::tryFitLayout(
+    CCNode* on, CCArray* nodes,
+    std::pair<int, int> const& minMaxPrios,
+    bool doAutoScale,
+    float scale, float squish, int prio
+) const {
+    // where do all of these magical calculations come from?
+    // idk i got tired of doing the math but they work so ¯\_(ツ)_/¯ 
+    // like i genuinely have no clue fr why some of these work tho, 
+    // i just threw in random equations and numbers until it worked
+
+    auto rows = CCArray::create();
+    float maxRowAxisLength = 0.f;
+    float totalRowCrossLength = 0.f;
+    float crossScaleDownFactor = 0.f;
+    float crossSquishFactor = 0.f;
+
+    // fit everything into rows while possible
     size_t ix = 0;
-    for (auto& node : CCArrayExt<CCNode*>(nodes)) {
-        totalWidth += node->getScaledContentSize().width;
+    auto newNodes = nodes->shallowCopy();
+    while (newNodes->count()) {
+        auto row = this->fitInRow(
+            on, newNodes,
+            minMaxPrios, doAutoScale,
+            scale, squish, prio
+        );
+        rows->addObject(row);
+        if (
+            row->nextOverflowScaleDownFactor > crossScaleDownFactor &&
+            row->nextOverflowScaleDownFactor <= scale
+        ) {
+            crossScaleDownFactor = row->nextOverflowScaleDownFactor;
+        }
+        if (
+            row->nextOverflowSquishFactor > crossSquishFactor &&
+            row->nextOverflowSquishFactor <= squish
+        ) {
+            crossSquishFactor = row->nextOverflowSquishFactor;
+        }
+        totalRowCrossLength += row->crossLength;
         if (ix) {
-            totalWidth += m_gap;
+            totalRowCrossLength += m_gap;
+        }
+        if (row->axisLength > maxRowAxisLength) {
+            maxRowAxisLength = row->axisLength;
         }
         ix++;
     }
+    newNodes->release();
 
-    float pos;
-    switch (m_alignment) {
-        default:
-        case Alignment::Center: pos = -totalWidth / 2; break;
-        case Alignment::Begin: pos = -totalWidth; break;
-        case Alignment::End: pos = 0.f; break;
+    if (!rows->count()) {
+        return;
     }
-    for (auto& node : CCArrayExt<CCNode*>(nodes)) {
-        auto sw = node->getScaledContentSize().width;
-        float disp;
-        switch (m_alignment) {
-            default:
-            case Alignment::Center: disp = sw * node->getAnchorPoint().x; break;
-            case Alignment::Begin:  disp = sw; break;
-            case Alignment::End:    disp = 0.f; break;
+
+    auto available = nodeAxis(on, m_axis, 1.f);
+
+    if (available.axisLength <= 0.f) {
+        return;
+    }
+
+    // if cross axis overflow not allowed and it's overflowing, try to scale 
+    // down layout if there are any nodes with auto-scale enabled (or 
+    // auto-scale is enabled by default)
+    if (
+        !m_allowCrossAxisOverflow && 
+        doAutoScale && 
+        totalRowCrossLength > available.crossLength
+    ) {
+        if (this->canTryScalingDown(
+            nodes, prio, scale, crossScaleDownFactor, minMaxPrios
+        )) {
+            rows->release();
+            return this->tryFitLayout(
+                on, nodes,
+                minMaxPrios, doAutoScale,
+                scale, squish, prio
+            );
         }
-        node->setPositionX(pos + disp);
-        if (m_alignVertically) {
-            node->setPositionY(m_alignVertically.value());
+    }
+
+    // if we're still overflowing, squeeze nodes closer together
+    if (
+        !m_allowCrossAxisOverflow &&
+        totalRowCrossLength > available.crossLength
+    ) {
+        // if squishing rows would take less squishing that squishing columns, 
+        // then squish rows
+        if (
+            !m_growCrossAxis ||
+            totalRowCrossLength / available.crossLength < crossSquishFactor
+        ) {
+            rows->release();
+            return this->tryFitLayout(
+                on, nodes,
+                minMaxPrios, doAutoScale,
+                scale, crossSquishFactor, prio
+            );
+        }
+    }
+
+    // if we're here, the nodes are ready to be positioned
+
+    if (m_crossReverse) {
+        rows->reverseObjects();
+    }
+
+    // resize cross axis if needed
+    if (m_allowCrossAxisOverflow) {
+        available.crossLength = totalRowCrossLength;
+        if (m_axis == Axis::Row) {
+            on->setContentSize({
+                available.axisLength,
+                totalRowCrossLength,
+            });
+        }
+        else {
+            on->setContentSize({
+                totalRowCrossLength,
+                available.axisLength,
+            });
+        }
+    }
+
+    float columnSquish = 1.f;
+    if (!m_allowCrossAxisOverflow && totalRowCrossLength > available.crossLength) {
+        columnSquish = available.crossLength / totalRowCrossLength;
+        totalRowCrossLength *= columnSquish;
+    }
+
+    float rowsEndsLength = 0.f;
+    if (rows->count()) {
+        auto first = static_cast<Row*>(rows->firstObject());
+        auto last = static_cast<Row*>(rows->lastObject());
+        rowsEndsLength = first->crossLength / 2 + last->crossLength / 2;
+    }
+
+    float rowCrossPos;
+    switch (m_crossAlignment) {
+        case AxisAlignment::Start: {
+            rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish);
+        } break;
+
+        case AxisAlignment::Even: {
+            totalRowCrossLength = available.crossLength;
+            rowCrossPos = totalRowCrossLength - rowsEndsLength * 1.5f * scale * (1.f - columnSquish);
+        } break;
+
+        case AxisAlignment::Center: {
+            rowCrossPos = available.crossLength / 2 + totalRowCrossLength / 2 - 
+                rowsEndsLength * 1.5f * scale * (1.f - columnSquish);
+        } break;
+
+        case AxisAlignment::End: {
+            rowCrossPos = available.crossLength - 
+                rowsEndsLength * 1.5f * scale * (1.f - columnSquish);
+        } break;
+    }
+
+    float rowEvenSpace = available.crossLength / rows->count();
+
+    for (auto row : CCArrayExt<Row*>(rows)) {
+        if (m_crossAlignment == AxisAlignment::Even) {
+            rowCrossPos -= rowEvenSpace / 2 + row->crossLength / 2;
+        }
+        else {
+            rowCrossPos -= row->crossLength * columnSquish;
+        }
+
+        float rowAxisPos;
+        switch (m_axisAlignment) {
+            case AxisAlignment::Start: { 
+                rowAxisPos = 0.f;
+            } break;
+
+            case AxisAlignment::Even: { 
+                rowAxisPos = 0.f;
+            } break;
+
+            case AxisAlignment::Center: {
+                rowAxisPos = available.axisLength / 2 - row->axisLength / 2;
+            } break;
+
+            case AxisAlignment::End: {
+                rowAxisPos = available.axisLength - row->axisLength;
+            } break;
+        }
+
+        float evenSpace = available.axisLength / row->nodes->count();
+
+        size_t ix = 0;
+        AxisLayoutOptions const* prev = nullptr;
+        for (auto& node : CCArrayExt<CCNode*>(row->nodes)) {
+            auto opts = axisOpts(node);
+            // rescale node if overflowing
+            if (this->shouldAutoScale(opts)) {
+                auto nodeScale = scaleByOpts(opts, row->scale, row->prio, false);
+                // CCMenuItemSpriteExtra is quirky af
+                if (auto btn = typeinfo_cast<CCMenuItemSpriteExtra*>(node)) {
+                    btn->m_baseScale = nodeScale;
+                }
+                node->setScale(nodeScale);
+            }
+            if (!ix) {
+                rowAxisPos += row->axisEndsLength * row->scale / 2 * (1.f - row->squish);
+            }
+            auto pos = nodeAxis(node, m_axis, row->squish);
+            float axisPos;
+            if (m_axisAlignment == AxisAlignment::Even) {
+                axisPos = rowAxisPos + evenSpace / 2 - pos.axisLength * (.5f - pos.axisAnchor);
+                rowAxisPos += evenSpace - 
+                    row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count();
+            }
+            else {
+                if (ix) {
+                    if (row->prio == minMaxPrios.first) {
+                        rowAxisPos += this->nextGap(prev, opts) * row->scale * row->squish;
+                    }
+                    else {
+                        rowAxisPos += this->nextGap(prev, opts) * row->squish;
+                    }
+                }
+                axisPos = rowAxisPos + pos.axisLength * pos.axisAnchor;
+                rowAxisPos += pos.axisLength - 
+                    row->axisEndsLength * row->scale * (1.f - row->squish) * 1.f / nodes->count();
+            }
+            float crossOffset;
+            switch (m_crossLineAlignment) {
+                case AxisAlignment::Start: {
+                    crossOffset = pos.crossLength * pos.crossAnchor;
+                } break;
+
+                case AxisAlignment::Center: case AxisAlignment::Even: {
+                    crossOffset = row->crossLength / 2 - pos.crossLength * (.5f - pos.crossAnchor);
+                } break;
+
+                case AxisAlignment::End: {
+                    crossOffset = row->crossLength - pos.crossLength * (1.f - pos.crossAnchor);
+                } break;
+            }
+            if (m_axis == Axis::Row) {
+                node->setPosition(axisPos, rowCrossPos + crossOffset);
+            }
+            else {
+                node->setPosition(rowCrossPos + crossOffset, axisPos);
+            }
+            prev = opts;
+            ix++;
+        }
+    
+        if (m_crossAlignment == AxisAlignment::Even) {
+            rowCrossPos -= rowEvenSpace / 2 - row->crossLength / 2 - 
+                rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count();
+        }
+        else {
+            rowCrossPos -= m_gap * columnSquish - 
+                rowsEndsLength * 1.5f * row->scale * (1.f - columnSquish) * 1.f / rows->count();
         }
-        pos += sw + m_gap;
     }
 }
 
-RowLayout* RowLayout::create(
-    float gap,
-    std::optional<float> alignVertically
-) {
-    auto ret = new RowLayout;
-    ret->m_gap = gap;
-    ret->m_alignVertically = alignVertically;
-    return ret;
+void AxisLayout::apply(CCNode* on) {
+    auto nodes = getNodesToPosition(on);
+    
+    std::pair<int, int> minMaxPrio;
+    bool doAutoScale = false;
+
+    bool first = true;
+    for (auto node : CCArrayExt<CCNode>(nodes)) {
+        int prio = 0;
+        if (auto opts = axisOpts(node)) {
+            prio = opts->getScalePriority();
+            // this does cause a recheck of m_autoScale every iteration but it 
+            // should be pretty fast and this correctly handles the situation 
+            // where auto-scale is enabled on the layout but explicitly 
+            // disabled on all its children
+            if (opts->getAutoScale().value_or(m_autoScale)) {
+                doAutoScale = true;
+            }
+        }
+        else {
+            if (m_autoScale) {
+                doAutoScale = true;
+            }
+        }
+        if (first) {
+            minMaxPrio = { prio, prio };
+            first = false;
+        }
+        else {
+            if (prio < minMaxPrio.first) {
+                minMaxPrio.first = prio;
+            }
+            if (prio > minMaxPrio.second) {
+                minMaxPrio.second = prio;
+            }
+        }
+    }
+
+    this->tryFitLayout(
+        on, nodes,
+        minMaxPrio, doAutoScale,
+        this->maxScaleForPrio(nodes, minMaxPrio.second), 1.f, minMaxPrio.second
+    );
 }
 
-RowLayout* RowLayout::setAlignment(Alignment align) {
-    m_alignment = align;
+AxisLayout::AxisLayout(Axis axis) : m_axis(axis) {}
+
+Axis AxisLayout::getAxis() const {
+    return m_axis;
+}
+
+AxisAlignment AxisLayout::getCrossAxisAlignment() const {
+    return m_crossAlignment;
+}
+
+AxisAlignment AxisLayout::getCrossAxisLineAlignment() const {
+    return m_crossLineAlignment;
+}
+
+AxisAlignment AxisLayout::getAxisAlignment() const {
+    return m_axisAlignment;
+}
+
+float AxisLayout::getGap() const {
+    return m_gap;
+}
+
+bool AxisLayout::getAxisReverse() const {
+    return m_axisReverse;
+}
+
+bool AxisLayout::getCrossAxisReverse() const {
+    return m_crossReverse;
+}
+
+bool AxisLayout::getAutoScale() const {
+    return m_autoScale;
+}
+
+bool AxisLayout::getGrowCrossAxis() const {
+    return m_growCrossAxis;
+}
+
+bool AxisLayout::getCrossAxisOverflow() const {
+    return m_allowCrossAxisOverflow;
+}
+
+AxisLayout* AxisLayout::setAxis(Axis axis) {
+    m_axis = axis;
     return this;
 }
 
-RowLayout* RowLayout::setGap(float gap) {
+AxisLayout* AxisLayout::setCrossAxisAlignment(AxisAlignment align) {
+    m_crossAlignment = align;
+    return this;
+}
+
+AxisLayout* AxisLayout::setCrossAxisLineAlignment(AxisAlignment align) {
+    m_crossLineAlignment = align;
+    return this;
+}
+
+AxisLayout* AxisLayout::setAxisAlignment(AxisAlignment align) {
+    m_axisAlignment = align;
+    return this;
+}
+
+AxisLayout* AxisLayout::setGap(float gap) {
     m_gap = gap;
     return this;
 }
 
-RowLayout* RowLayout::setAlignVertically(std::optional<float> align) {
-    m_alignVertically = align;
+AxisLayout* AxisLayout::setAxisReverse(bool reverse) {
+    m_axisReverse = reverse;
     return this;
 }
 
-void ColumnLayout::apply(CCArray* nodes, CCSize const& availableSize) {
-    float totalHeight = .0f;
-    size_t ix = 0;
-    for (auto& node : CCArrayExt<CCNode*>(nodes)) {
-        totalHeight += node->getScaledContentSize().height;
-        if (ix) {
-            totalHeight += m_gap;
-        }
-    }
-
-    float pos;
-    switch (m_alignment) {
-        default:
-        case Alignment::Center: pos = -totalHeight / 2; break;
-        case Alignment::Begin: pos = -totalHeight; break;
-        case Alignment::End: pos = 0.f; break;
-    }
-    for (auto& node : CCArrayExt<CCNode*>(nodes)) {
-        auto sh = node->getScaledContentSize().height;
-        float disp;
-        switch (m_alignment) {
-            default:
-            case Alignment::Center: disp = sh * node->getAnchorPoint().y; break;
-            case Alignment::Begin:  disp = sh; break;
-            case Alignment::End:    disp = 0.f; break;
-        }
-        node->setPositionY(pos + disp);
-        if (m_alignHorizontally) {
-            node->setPositionX(m_alignHorizontally.value());
-        }
-        pos += sh + m_gap;
-    }
-}
-
-ColumnLayout* ColumnLayout::create(
-    float gap,
-    std::optional<float> alignHorizontally
-) {
-    auto ret = new ColumnLayout;
-    ret->m_gap = gap;
-    ret->m_alignHorizontally = alignHorizontally;
-    return ret;
-}
-
-ColumnLayout* ColumnLayout::setAlignment(Alignment align) {
-    m_alignment = align;
+AxisLayout* AxisLayout::setCrossAxisReverse(bool reverse) {
+    m_crossReverse = reverse;
     return this;
 }
 
-ColumnLayout* ColumnLayout::setGap(float gap) {
-    m_gap = gap;
+AxisLayout* AxisLayout::setCrossAxisOverflow(bool fit) {
+    m_allowCrossAxisOverflow = fit;
     return this;
 }
 
-ColumnLayout* ColumnLayout::setAlignHorizontally(std::optional<float> align) {
-    m_alignHorizontally = align;
+AxisLayout* AxisLayout::setAutoScale(bool scale) {
+    m_autoScale = scale;
     return this;
 }
 
-void GridLayout::apply(CCArray* nodes, CCSize const& availableSize) {
-    // todo
-}
-
-GridLayout* GridLayout::create(
-    std::optional<size_t> rowSize,
-    GridAlignment alignment,
-    GridDirection direction
-) {
-    auto ret = new GridLayout;
-    ret->m_rowSize = rowSize;
-    ret->m_alignment = alignment;
-    ret->m_direction = direction;
-    return ret;
-}
-
-GridLayout* GridLayout::setDirection(GridDirection direction) {
-    m_direction = direction;
+AxisLayout* AxisLayout::setGrowCrossAxis(bool shrink) {
+    m_growCrossAxis = shrink;
     return this;
 }
 
-GridLayout* GridLayout::setAlignment(GridAlignment alignment) {
-    m_alignment = alignment;
+AxisLayout* AxisLayout::create(Axis axis) {
+    return new AxisLayout(axis);
+}
+
+// RowLayout
+
+RowLayout::RowLayout() : AxisLayout(Axis::Row) {}
+
+RowLayout* RowLayout::create() {
+    return new RowLayout();
+}
+
+// ColumnLayout
+
+ColumnLayout::ColumnLayout() : AxisLayout(Axis::Column) {}
+
+ColumnLayout* ColumnLayout::create() {
+    return new ColumnLayout();
+}
+
+// AxisLayoutOptions
+
+AxisLayoutOptions* AxisLayoutOptions::create() {
+    return new AxisLayoutOptions();
+}
+
+std::optional<bool> AxisLayoutOptions::getAutoScale() const {
+    return m_autoScale;
+}
+
+float AxisLayoutOptions::getMaxScale() const {
+    return m_maxScale;
+}
+
+float AxisLayoutOptions::getMinScale() const {
+    return m_minScale;
+}
+
+float AxisLayoutOptions::getRelativeScale() const {
+    return m_relativeScale;
+}
+
+std::optional<float> AxisLayoutOptions::getLength() const {
+    return m_length;
+}
+
+std::optional<float> AxisLayoutOptions::getPrevGap() const {
+    return m_prevGap;
+}
+
+std::optional<float> AxisLayoutOptions::getNextGap() const {
+    return m_nextGap;
+}
+
+bool AxisLayoutOptions::getBreakLine() const {
+    return m_breakLine;
+}
+
+bool AxisLayoutOptions::getSameLine() const {
+    return m_sameLine;
+}
+
+int AxisLayoutOptions::getScalePriority() const {
+    return m_scalePriority;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setMaxScale(float scale) {
+    m_maxScale = scale;
     return this;
 }
 
-GridLayout* GridLayout::setRowSize(std::optional<size_t> rowSize) {
-    m_rowSize = rowSize;
+AxisLayoutOptions* AxisLayoutOptions::setMinScale(float scale) {
+    m_minScale = scale;
     return this;
 }
 
+AxisLayoutOptions* AxisLayoutOptions::setRelativeScale(float scale) {
+    m_relativeScale = scale;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setAutoScale(std::optional<bool> enabled) {
+    m_autoScale = enabled;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setLength(std::optional<float> length) {
+    m_length = length;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setPrevGap(std::optional<float> gap) {
+    m_prevGap = gap;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setNextGap(std::optional<float> gap) {
+    m_nextGap = gap;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setBreakLine(bool enable) {
+    m_breakLine = enable;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setSameLine(bool enable) {
+    m_sameLine = enable;
+    return this;
+}
+
+AxisLayoutOptions* AxisLayoutOptions::setScalePriority(int priority) {
+    m_scalePriority = priority;
+    return this;
+}
diff --git a/loader/src/hooks/GeodeNodeMetadata.cpp b/loader/src/hooks/GeodeNodeMetadata.cpp
index 777635ab..f520a98b 100644
--- a/loader/src/hooks/GeodeNodeMetadata.cpp
+++ b/loader/src/hooks/GeodeNodeMetadata.cpp
@@ -20,7 +20,7 @@ private:
     Ref<cocos2d::CCObject> m_userObject;
     std::string m_id = "";
     std::unique_ptr<Layout> m_layout = nullptr;
-    PositionHint m_positionHint = PositionHint::Default;
+    std::unique_ptr<LayoutOptions> m_layoutOptions = nullptr;
     std::unordered_map<std::string, std::any> m_attributes;
 
     friend class ProxyCCNode;
@@ -118,7 +118,13 @@ CCNode* CCNode::getChildByIDRecursive(std::string const& id) {
     return nullptr;
 }
 
-void CCNode::setLayout(Layout* layout, bool apply) {
+void CCNode::setLayout(Layout* layout, bool apply, bool respectAnchor) {
+    if (respectAnchor && this->isIgnoreAnchorPointForPosition()) {
+        for (auto child : CCArrayExt<CCNode>(m_pChildren)) {
+            child->setPosition(child->getPosition() + this->getScaledContentSize());
+        }
+        this->ignoreAnchorPointForPosition(false);
+    }
     GeodeNodeMetadata::set(this)->m_layout.reset(layout);
     if (apply) {
         this->updateLayout();
@@ -129,26 +135,24 @@ Layout* CCNode::getLayout() {
     return GeodeNodeMetadata::set(this)->m_layout.get();
 }
 
-void CCNode::updateLayout() {
-    if (auto layout = GeodeNodeMetadata::set(this)->m_layout.get()) {
-        // nodes with absolute position should never be rearranged
-        auto filtered = CCArray::create();
-        for (auto& child : CCArrayExt<CCNode>(m_pChildren)) {
-            if (child->getPositionHint() != PositionHint::Absolute) {
-                filtered->addObject(child);
-            }
-        }
-        layout->apply(filtered, m_obContentSize);
-        filtered->release();
+void CCNode::setLayoutOptions(LayoutOptions* options, bool apply) {
+    GeodeNodeMetadata::set(this)->m_layoutOptions.reset(options);
+    if (apply && m_pParent) {
+        m_pParent->updateLayout();
     }
 }
 
-void CCNode::setPositionHint(PositionHint hint) {
-    GeodeNodeMetadata::set(this)->m_positionHint = hint;
+LayoutOptions* CCNode::getLayoutOptions() {
+    return GeodeNodeMetadata::set(this)->m_layoutOptions.get();
 }
 
-PositionHint CCNode::getPositionHint() {
-    return GeodeNodeMetadata::set(this)->m_positionHint;
+void CCNode::updateLayout(bool updateChildOrder) {
+    if (updateChildOrder) {
+        this->sortAllChildren();
+    }
+    if (auto layout = GeodeNodeMetadata::set(this)->m_layout.get()) {
+        layout->apply(this);
+    }
 }
 
 void CCNode::setAttribute(std::string const& attr, std::any value) {
diff --git a/loader/src/ids/AddIDs.hpp b/loader/src/ids/AddIDs.hpp
index 7f00b54b..77693e91 100644
--- a/loader/src/ids/AddIDs.hpp
+++ b/loader/src/ids/AddIDs.hpp
@@ -33,10 +33,13 @@ void setIDs(CCNode* node, int startIndex, Args... args) {
 }
 
 static void switchToMenu(CCNode* node, CCMenu* menu) {
+    if (!node || !menu) return;
+    
     auto worldPos = node->getParent()->convertToWorldSpace(node->getPosition());
 
     node->retain();
     node->removeFromParent();
+    node->setZOrder(0);
 
     menu->addChild(node);
     node->setPosition(menu->convertToNodeSpace(worldPos));
@@ -55,6 +58,14 @@ static void switchChildrenToMenu(CCNode* parent, CCMenu* menu, Args... args) {
 
 template <typename T, typename ...Args>
 static CCMenu* detachAndCreateMenu(CCNode* parent, const char* menuID, Layout* layout, T first, Args... args) {
+    if (!first) {
+        auto menu = CCMenu::create();
+        menu->setID(menuID);
+        menu->setLayout(layout);
+        parent->addChild(menu);
+        return menu;
+    }
+
     auto oldMenu = first->getParent();
 
     first->retain();
@@ -64,14 +75,25 @@ static CCMenu* detachAndCreateMenu(CCNode* parent, const char* menuID, Layout* l
     newMenu->setPosition(parent->convertToNodeSpace(oldMenu->convertToWorldSpace(first->getPosition())));
     newMenu->setID(menuID);
     newMenu->setZOrder(oldMenu->getZOrder());
-    newMenu->setLayout(layout);
     parent->addChild(newMenu);
 
     first->setPosition(0, 0);
+    first->setZOrder(0);
     newMenu->addChild(first);
     first->release();
 
     (switchToMenu(args, newMenu), ...);
+    
+    newMenu->setLayout(layout);
 
     return newMenu;
 }
+
+static CCSize getSizeSafe(CCNode* node) {
+    if (node) {
+        return node->getScaledContentSize();
+    }
+    else {
+        return CCSizeZero;
+    }
+}
diff --git a/loader/src/ids/CreatorLayer.cpp b/loader/src/ids/CreatorLayer.cpp
index d4745678..1b0b05a8 100644
--- a/loader/src/ids/CreatorLayer.cpp
+++ b/loader/src/ids/CreatorLayer.cpp
@@ -6,49 +6,118 @@
 
 USE_GEODE_NAMESPACE();
 
+template<class... Args>
+static void reorderButtons(Args... args) {
+    int ooa = 0;
+    for (auto& arg : { args... }) {
+        if (arg) {
+            arg->setOrderOfArrival(ooa);
+            ooa += 1;
+        }
+    }
+}
+
 $register_ids(CreatorLayer) {
     setIDSafe<CCSprite>(this, 0, "background");
 
+    auto winSize = CCDirector::get()->getWinSize();
+
     if (auto menu = getChildOfType<CCMenu>(this, 0)) {
         menu->setID("creator-buttons-menu");
 
-        setIDSafe(menu, 0, "create-button");
-        setIDSafe(menu, 1, "saved-button");
-        setIDSafe(menu, 2, "scores-button");
-        setIDSafe(menu, 3, "quests-button");
-        setIDSafe(menu, 4, "daily-button");
-        setIDSafe(menu, 5, "weekly-button");
-        setIDSafe(menu, 6, "featured-button");
-        setIDSafe(menu, 7, "hall-of-fame-button");
-        setIDSafe(menu, 8, "map-packs-button");
-        setIDSafe(menu, 9, "search-button");
-        setIDSafe(menu, 10, "gauntlets-button");
-
         // move vault button to its own menu
         if (auto lockBtn = setIDSafe(menu, -2, "vault-button")) {
-            detachAndCreateMenu(
+            auto menu = detachAndCreateMenu(
                 this,
                 "top-right-menu",
-                ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin),
+                ColumnLayout::create()
+                    ->setAxisReverse(true)
+                    ->setAxisAlignment(AxisAlignment::End),
                 lockBtn
             );
+            menu->setPositionY(
+                menu->getPositionY() - 150.f / 2 + 
+                    lockBtn->getScaledContentSize().height / 2
+            );
+            menu->setContentSize({ 60.f, 150.f });
+            menu->updateLayout();
         }
 
         // move treasure room button to its own menu
         if (auto roomBtn = setIDSafe(menu, -1, "treasure-room-button")) {
-            detachAndCreateMenu(
+            auto menu = detachAndCreateMenu(
                 this,
                 "bottom-right-menu",
-                ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End),
+                ColumnLayout::create()
+                    ->setAxisAlignment(AxisAlignment::Start),
                 roomBtn
             );
+            menu->setPositionY(
+                menu->getPositionY() + 125.f / 2 - 
+                    roomBtn->getScaledContentSize().height / 2
+            );
+            menu->setContentSize({ 60.f, 125.f });
+            menu->updateLayout();
         }
+
+        // row order is inverted because of layout
+
+        reorderButtons(
+            setIDSafe(menu, 6, "featured-button"),
+            setIDSafe(menu, 7, "hall-of-fame-button"),
+            setIDSafe(menu, 8, "map-packs-button"),
+
+            setIDSafe(menu, 9, "search-button"),
+            setIDSafe(menu, 3, "quests-button"),
+            setIDSafe(menu, 4, "daily-button"),
+            setIDSafe(menu, 5, "weekly-button"),
+
+            setIDSafe(menu, 10, "gauntlets-button"),
+            setIDSafe(menu, 0, "create-button"),
+            setIDSafe(menu, 1, "saved-button"),
+            setIDSafe(menu, 2, "scores-button")
+        );
+
+        if (winSize.width / winSize.height <= 5.1f / 3.f) {
+            menu->setContentSize({ winSize.width - 80.f, 310.f });
+        }
+        else {
+            menu->setContentSize({ winSize.width - 120.f, 310.f });
+        }
+        menu->setLayout(
+            RowLayout::create()
+                ->setGap(12.f)
+                ->setCrossAxisReverse(true)
+                ->setGrowCrossAxis(true)
+                ->setCrossAxisOverflow(false)
+        );
     }
 
     if (auto menu = getChildOfType<CCMenu>(this, 1)) {
         menu->setID("exit-menu");
-        setIDSafe(menu, 0, "exit-button");
+        auto exitBtn = setIDSafe(menu, 0, "exit-button");
+        menu->setPositionY(
+            menu->getPositionY() - 125.f / 2 + 
+                getSizeSafe(exitBtn).height / 2
+        );
+        menu->setContentSize({ 60.f, 125.f });
+        menu->setLayout(
+            ColumnLayout::create()
+                ->setAxisAlignment(AxisAlignment::End)
+        );
     }
+
+    // add a menu to the bottom left corner that is empty but prolly a place mods 
+    // want to add stuff to
+    auto menu = CCMenu::create();
+    menu->setPosition(24.f, 0.f + 125.f / 2);
+    menu->setID("bottom-left-menu");
+    menu->setContentSize({ 60.f, 125.f });
+    menu->setLayout(
+        ColumnLayout::create()
+            ->setAxisAlignment(AxisAlignment::Start)
+    );
+    this->addChild(menu);
 }
 
 struct CreatorLayerIDs : Modify<CreatorLayerIDs, CreatorLayer> {
diff --git a/loader/src/ids/EditLevelLayer.cpp b/loader/src/ids/EditLevelLayer.cpp
index 8901f4cb..5a0fc534 100644
--- a/loader/src/ids/EditLevelLayer.cpp
+++ b/loader/src/ids/EditLevelLayer.cpp
@@ -3,6 +3,7 @@
 #include <Geode/Bindings.hpp>
 #include <Geode/modify/EditLevelLayer.hpp>
 #include <Geode/utils/cocos.hpp>
+#include <Geode/ui/BasedButtonSprite.hpp>
 
 USE_GEODE_NAMESPACE();
 
@@ -18,22 +19,26 @@ $register_ids(EditLevelLayer) {
         "description-background",
         "description-input",
         "description-text-area",
-        "level-action-menu",
+        "level-edit-menu",
         "level-length",
         "level-song",
         "level-verified",
         "version-label",
         "level-id-label",
-        "right-side-menu",
-        "back-button-menu",
+        "level-actions-menu",
+        "back-menu",
         "info-button-menu"
     );
 
-    if (auto menu = this->getChildByID("level-action-menu")) {
+    auto winSize = CCDirector::get()->getWinSize();
+
+    if (auto menu = this->getChildByID("level-edit-menu")) {
         setIDs(menu, 0, "edit-button", "play-button", "share-button");
+        menu->setContentSize({ winSize.width - 160.f, 100.f });
+        menu->setLayout(RowLayout::create()->setGap(25.f));
     }
 
-    if (auto menu = this->getChildByID("right-side-menu")) {
+    if (auto menu = this->getChildByID("level-actions-menu")) {
         setIDs(
             menu,
             0,
@@ -44,12 +49,42 @@ $register_ids(EditLevelLayer) {
             "folder-button"
         );
 
-        detachAndCreateMenu(
-            menu, "folder-menu", ColumnLayout::create(), menu->getChildByID("folder-button")
+        auto folderMenu = detachAndCreateMenu(
+            this, "folder-menu",
+            ColumnLayout::create(),
+            menu->getChildByID("folder-button")
         );
+        folderMenu->setContentSize({ 50.f, 215.f });
+        folderMenu->updateLayout();
+
+        menu->setPosition(
+            menu->getPositionX() + static_cast<CCNode*>(
+                menu->getChildren()->firstObject()
+            )->getPositionX(),
+            winSize.height / 2
+        );
+        menu->setContentSize({ 60.f, winSize.height - 15.f });
+        menu->setLayout(
+            ColumnLayout::create()
+                ->setGap(7.f)
+                ->setAxisAlignment(AxisAlignment::End)
+                ->setAxisReverse(true)
+        );
+        menu->setZOrder(1);
     }
 
-    if (auto menu = this->getChildByID("back-button-menu")) setIDSafe(menu, 0, "back-button");
+    if (auto menu = this->getChildByID("back-menu")) {
+        auto backBtn = setIDSafe(menu, 0, "back-button");
+        menu->setPositionX(
+            menu->getPositionX() + 100.f / 2 - 
+                getSizeSafe(backBtn).width / 2
+        );
+        menu->setContentSize({ 100.f, 50.f });
+        menu->setLayout(
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+        );
+    }
 
     if (auto menu = this->getChildByID("info-button-menu")) setIDSafe(menu, 0, "info-button");
 }
diff --git a/loader/src/ids/EditorPauseLayer.cpp b/loader/src/ids/EditorPauseLayer.cpp
new file mode 100644
index 00000000..f1ae68fa
--- /dev/null
+++ b/loader/src/ids/EditorPauseLayer.cpp
@@ -0,0 +1,273 @@
+#include "AddIDs.hpp"
+
+#include <Geode/modify/EditorPauseLayer.hpp>
+
+USE_GEODE_NAMESPACE();
+
+// special class for this because making it a CCMenuItemToggler would be very UB 
+// (not gonna reinterpret_cast that into the members)
+class GuidelinesButton : public CCMenuItemSpriteExtra {
+protected:
+    bool init() {
+        if (!CCMenuItemSpriteExtra::init(
+            CCSprite::createWithSpriteFrameName("GJ_audioOffBtn_001.png"),
+            nullptr,
+            this, nullptr
+        )) return false;
+
+        this->updateSprite();
+        
+        return true;
+    }
+
+    void updateSprite() {
+        this->setNormalImage(CCSprite::createWithSpriteFrameName(
+            GameManager::get()->m_showSongMarkers ? 
+                "GJ_audioOnBtn_001.png" :
+                "GJ_audioOffBtn_001.png"
+        ));
+    }
+
+    void activate() override {
+        CCMenuItemSpriteExtra::activate();
+        GameManager::get()->m_showSongMarkers ^= 1;
+        this->updateSprite();
+    }
+
+public:
+    static GuidelinesButton* create() {
+        auto ret = new GuidelinesButton();
+        if (ret && ret->init()) {
+            ret->autorelease();
+            return ret;
+        }
+        CC_SAFE_DELETE(ret);
+        return nullptr;
+    }
+};
+
+$register_ids(EditorPauseLayer) {
+    auto winSize = CCDirector::get()->getWinSize();
+
+    if (auto menu = getChildOfType<CCMenu>(this, 0)) {
+        menu->setID("resume-menu");
+
+        setIDs(
+            menu, 0,
+            "resume-button",
+            "save-and-play-button",
+            "save-and-exit-button",
+            "save-button",
+            "exit-button"
+        );
+
+        menu->setContentSize({ 100.f, 220.f });
+        menu->setLayout(
+            ColumnLayout::create()
+                ->setGap(12.5f)
+                ->setAxisReverse(true)
+        );
+    }
+
+    setIDs(
+        this, 2,
+        "ignore-damage-label",
+        "follow-player-label",
+        "select-filter-label",
+        "show-grid-label",
+        "show-object-info-label",
+        "show-ground-label",
+        "preview-mode-label",
+
+        "object-count-label",
+        "length-label",
+        "length-name-label"
+    );
+
+    if (auto menu = getChildOfType<CCMenu>(this, 1)) {
+        menu->setID("bottom-menu");
+
+        setIDs(
+            menu, 0, 
+            "guidelines-enable-button",
+            "help-button",
+            "guidelines-disable-button",
+
+            "uncheck-portals-button",
+            "reset-unused-button",
+            "create-edges-button",
+            "create-outlines-button",
+            "create-base-button",
+            "build-helper-button",
+
+            "align-x-button",
+            "align-y-button",
+            "select-all-button",
+            "select-all-left-button",
+            "select-all-right-button",
+
+            "ignore-damage-toggle",
+            "follow-player-toggle",
+            "select-filter-toggle",
+            "show-grid-toggle",
+            "show-object-info-toggle",
+            "show-ground-toggle",
+            "preview-mode-toggle",
+
+            "keys-button",
+            "settings-button"
+        );
+
+        auto smallActionsMenu = detachAndCreateMenu(
+            this,
+            "small-actions-menu",
+            ColumnLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+                ->setAxisReverse(true),
+            menu->getChildByID("align-x-button"),
+            menu->getChildByID("align-y-button"),
+            menu->getChildByID("select-all-button"),
+            menu->getChildByID("select-all-left-button"),
+            menu->getChildByID("select-all-right-button")
+        );
+        smallActionsMenu->setContentSize({ 100.f, 240.f });
+        smallActionsMenu->setPositionY(130.f);
+        smallActionsMenu->updateLayout();
+
+        auto actionsMenu = detachAndCreateMenu(
+            this,
+            "actions-menu",
+            ColumnLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+                ->setAxisReverse(true),
+            menu->getChildByID("keys-button"),
+            menu->getChildByID("build-helper-button"),
+            menu->getChildByID("create-base-button"),
+            menu->getChildByID("create-outlines-button"),
+            menu->getChildByID("create-edges-button"),
+            menu->getChildByID("reset-unused-button"),
+            menu->getChildByID("uncheck-portals-button")
+        );
+        if (auto keysBtn = actionsMenu->getChildByID("keys-button")) {
+            keysBtn->setLayoutOptions(AxisLayoutOptions::create()->setPrevGap(10.f));
+        }
+        actionsMenu->setContentSize({ 100.f, 240.f });
+        actionsMenu->setPositionY(130.f);
+        actionsMenu->updateLayout();
+
+        auto optionsMenu = detachAndCreateMenu(
+            this,
+            "options-menu",
+            RowLayout::create()
+                ->setGap(0.f)
+                ->setAxisAlignment(AxisAlignment::Start)
+                ->setGrowCrossAxis(true)
+                ->setCrossAxisAlignment(AxisAlignment::Start)
+                ->setCrossAxisOverflow(false),
+            menu->getChildByID("preview-mode-toggle"),
+            this->getChildByID("preview-mode-label"),
+            menu->getChildByID("show-ground-toggle"),
+            this->getChildByID("show-ground-label"),
+            menu->getChildByID("show-object-info-toggle"),
+            this->getChildByID("show-object-info-label"),
+            menu->getChildByID("show-grid-toggle"),
+            this->getChildByID("show-grid-label"),
+            menu->getChildByID("select-filter-toggle"),
+            this->getChildByID("select-filter-label"),
+            menu->getChildByID("follow-player-toggle"),
+            this->getChildByID("follow-player-label"),
+            menu->getChildByID("ignore-damage-toggle"),
+            this->getChildByID("ignore-damage-label")
+        );
+        for (auto node : CCArrayExt<CCNode>(optionsMenu->getChildren())) {
+            if (auto label = typeinfo_cast<CCLabelBMFont*>(node)) {
+                label->setLayoutOptions(
+                    AxisLayoutOptions::create()
+                        ->setSameLine(true)
+                        ->setBreakLine(true)
+                        ->setPrevGap(5.f)
+                        ->setMinScale(.1f)
+                        ->setMaxScale(.5f)
+                        ->setScalePriority(1)
+                );
+            }
+        }
+        optionsMenu->setContentSize({ 120.f, winSize.height - 100.f });
+        optionsMenu->setPosition(70.f, winSize.height / 2 - 50.f + 10.f);
+        optionsMenu->updateLayout();
+
+        auto settingsMenu = detachAndCreateMenu(
+            this,
+            "settings-menu",
+            RowLayout::create()
+                ->setAxisReverse(true),
+            menu->getChildByID("settings-button")
+        );
+        settingsMenu->setContentSize({ 95.f, 50.f });
+        settingsMenu->updateLayout();
+
+        auto guidelinesMenu = menu;
+
+        // replace the two guidelines buttons with a single toggle
+        guidelinesMenu->getChildByID("guidelines-enable-button")->removeFromParent();
+        guidelinesMenu->getChildByID("guidelines-disable-button")->removeFromParent();
+        
+        auto glToggle = GuidelinesButton::create();
+        glToggle->setID("guidelines-enable-toggle");
+        guidelinesMenu->insertBefore(glToggle, nullptr);
+        m_guidelinesOffButton = m_guidelinesOnButton = glToggle;
+        this->updateSongButton();
+
+        guidelinesMenu->setID("guidelines-menu");
+        guidelinesMenu->setContentSize({ winSize.width / 2, 50.f });
+        guidelinesMenu->setLayout(RowLayout::create());
+
+        auto topMenu = CCMenu::create();
+        topMenu->setContentSize({ winSize.width / 2, 50.f });
+        topMenu->setPosition(winSize.width / 2, winSize.height - 30.f);
+        topMenu->setID("top-menu");
+        topMenu->setLayout(RowLayout::create());
+        this->addChild(topMenu);
+    }
+
+    if (auto menu = detachAndCreateMenu(
+        this, "info-menu",
+        ColumnLayout::create()
+            ->setGap(10.f)
+            ->setAxisAlignment(AxisAlignment::End)
+            ->setAxisReverse(true)
+            ->setCrossAxisOverflow(false)
+            ->setCrossAxisLineAlignment(AxisAlignment::Start),
+        this->getChildByID("object-count-label"),
+        this->getChildByID("length-label"),
+        this->getChildByID("length-name-label")
+    )) {
+        for (auto child : CCArrayExt<CCNode>(menu->getChildren())) {
+            child->setLayoutOptions(
+                AxisLayoutOptions::create()
+                    ->setMinScale(.1f)
+                    ->setMaxScale(.6f)
+                    ->setBreakLine(true)
+            );
+        }
+        menu->setContentSize({ 165.f, 100.f });
+        menu->setPosition(85.f, winSize.height - 55.f);
+        menu->updateLayout();
+    }
+}
+
+struct EditorPauseLayerIDs : Modify<EditorPauseLayerIDs, EditorPauseLayer> {
+    static void onModify(auto& self) {
+        if (!self.setHookPriority("EditorPauseLayer::init", GEODE_ID_PRIORITY)) {
+            log::warn("Failed to set EditorPauseLayer::init hook priority, node IDs may not work properly");
+        }
+    }
+
+    bool init(LevelEditorLayer* lel) {
+        if (!EditorPauseLayer::init(lel)) return false;
+
+        NodeIDs::get()->provide(this);
+
+        return true;
+    }
+};
diff --git a/loader/src/ids/EditorUI.cpp b/loader/src/ids/EditorUI.cpp
index 9ae84896..b6853f32 100644
--- a/loader/src/ids/EditorUI.cpp
+++ b/loader/src/ids/EditorUI.cpp
@@ -11,6 +11,8 @@ $register_ids(EditorUI) {
     setIDSafe(this, this->getChildrenCount() - 2, "layer-index-label");
     setIDSafe(this, this->getChildrenCount() - 1, "object-info-label");
 
+    auto winSize = CCDirector::get()->getWinSize();
+
     if (auto menu = getChildOfType<CCMenu>(this, 0)) {
         menu->setID("toolbar-categories-menu");
 
@@ -28,7 +30,7 @@ $register_ids(EditorUI) {
 
             "undo-button",
             "redo-button",
-            "delete-button",
+            "delete-trash-button",
 
             "music-playback-button",
 
@@ -42,51 +44,105 @@ $register_ids(EditorUI) {
             "unlink-button"
         );
 
-        detachAndCreateMenu(
+        auto toolbarTogglesMenu = detachAndCreateMenu(
             this,
             "toolbar-toggles-menu",
-            GridLayout::create(2, GridAlignment::Begin, GridDirection::Column),
+            RowLayout::create()
+                ->setCrossAxisOverflow(false)
+                ->setGrowCrossAxis(true)
+                ->setAxisAlignment(AxisAlignment::Center)
+                ->setCrossAxisAlignment(AxisAlignment::Center),
             menu->getChildByID("swipe-button"),
+            menu->getChildByID("rotate-button"),
             menu->getChildByID("free-move-button"),
-            menu->getChildByID("snap-button"),
-            menu->getChildByID("rotate-button")
+            menu->getChildByID("snap-button")
         );
+        toolbarTogglesMenu->setPosition(
+            winSize.width - 47.f,
+            45.f
+        );
+        toolbarTogglesMenu->setContentSize({ 90.f, 90.f });
+        toolbarTogglesMenu->updateLayout();
 
-        detachAndCreateMenu(
+        auto undoMenuWidth = winSize.width / 2 - 90.f;
+        auto undoMenu = detachAndCreateMenu(
             this,
-            "top-left-menu",
-            RowLayout::create(),
+            "undo-menu",
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+                ->setGap(10.f),
             menu->getChildByID("undo-button"),
             menu->getChildByID("redo-button"),
-            menu->getChildByID("delete-button")
+            menu->getChildByID("delete-trash-button")
         );
-
-        detachAndCreateMenu(
-            this, "playback-menu", RowLayout::create(), menu->getChildByID("music-playback-button")
+        undoMenu->setContentSize({ undoMenuWidth, 50.f });
+        undoMenu->setPositionX(
+            undoMenu->getPositionX() + undoMenuWidth / 2 - 
+                getSizeSafe(undoMenu->getChildByID("undo-button")).width / 2
         );
+        undoMenu->updateLayout();
 
-        detachAndCreateMenu(
+        auto playBackMenu = detachAndCreateMenu(
+            this,
+            "playback-menu",
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start),
+            menu->getChildByID("music-playback-button")
+        );
+        playBackMenu->setContentSize({ 100.f, 50.f });
+        playBackMenu->setPositionX(
+            playBackMenu->getPositionX() + 100.f / 2 - 
+                getSizeSafe(playBackMenu->getChildByID("music-playback-button")).width / 2
+        );
+        playBackMenu->updateLayout();
+
+        auto playTestMenu = detachAndCreateMenu(
             this,
             "playtest-menu",
-            RowLayout::create(),
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start),
             menu->getChildByID("playtest-button"),
             menu->getChildByID("stop-playtest-button")
         );
+        playTestMenu->setContentSize({ 100.f, 50.f });
+        playTestMenu->setPositionX(
+            playTestMenu->getPositionX() + 100.f / 2 - 
+                getSizeSafe(playTestMenu->getChildByID("playtest-button")).width / 2
+        );
+        playTestMenu->updateLayout();
 
-        detachAndCreateMenu(
+        auto zoomMenuHeight = winSize.height - 245.f;
+        auto zoomMenu = detachAndCreateMenu(
             this,
             "zoom-menu",
-            ColumnLayout::create(),
-            menu->getChildByID("zoom-in-button"),
-            menu->getChildByID("zoom-out-button")
+            ColumnLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start),
+            menu->getChildByID("zoom-out-button"),
+            menu->getChildByID("zoom-in-button")
         );
+        zoomMenu->setPositionY(150.f * winSize.height / 320);
+        zoomMenu->setContentSize({ 50.f, zoomMenuHeight });
+        zoomMenu->updateLayout();
 
-        detachAndCreateMenu(
+        auto linkMenu = detachAndCreateMenu(
             this,
             "link-menu",
-            ColumnLayout::create(),
-            menu->getChildByID("link-button"),
-            menu->getChildByID("unlink-button")
+            ColumnLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+                ->setGrowCrossAxis(true),
+            menu->getChildByID("unlink-button"),
+            menu->getChildByID("link-button")
+        );
+        linkMenu->setPositionY(150.f * winSize.height / 320);
+        linkMenu->setContentSize({ 125.f, zoomMenuHeight });
+        linkMenu->updateLayout();
+
+        menu->setPosition(42.f, 45.f);
+        menu->setContentSize({ 100.f, 90.f });
+        menu->setLayout(
+            ColumnLayout::create()
+                ->setGap(4.f)
+                ->setAxisReverse(true)
         );
     }
 
@@ -108,24 +164,43 @@ $register_ids(EditorUI) {
             "delete-help-icon"
         );
 
-        detachAndCreateMenu(
+        auto deleteButtonMenu = detachAndCreateMenu(
             menu,
             "delete-button-menu",
-            GridLayout::create(2, GridAlignment::Begin, GridDirection::Column),
+            ColumnLayout::create()
+                ->setCrossAxisOverflow(false)
+                ->setGrowCrossAxis(true)
+                ->setAxisReverse(true)
+                ->setCrossAxisReverse(true)
+                ->setAxisAlignment(AxisAlignment::End)
+                ->setCrossAxisAlignment(AxisAlignment::Center),
             menu->getChildByID("delete-button"),
             menu->getChildByID("delete-all-of-button"),
             menu->getChildByID("delete-startpos-button")
         );
-
-        detachAndCreateMenu(
+        deleteButtonMenu->setPosition(-88.5f, 0.f);
+        deleteButtonMenu->setContentSize({ winSize.width / 2 - 120.f, 80.f });
+        deleteButtonMenu->updateLayout();
+    	
+        auto filterMenuWidth = winSize.width / 2 - 150.f;
+        auto deleteFilterMenu = detachAndCreateMenu(
             menu,
             "delete-filter-menu",
-            GridLayout::create(2, GridAlignment::Begin, GridDirection::Column),
+            ColumnLayout::create()
+                ->setCrossAxisOverflow(false)
+                ->setGrowCrossAxis(true)
+                ->setAxisReverse(true)
+                ->setCrossAxisReverse(true)
+                ->setAxisAlignment(AxisAlignment::End)
+                ->setCrossAxisAlignment(AxisAlignment::Start),
             menu->getChildByID("delete-filter-none"),
             menu->getChildByID("delete-filter-static"),
             menu->getChildByID("delete-filter-detail"),
             menu->getChildByID("delete-filter-custom")
         );
+        deleteFilterMenu->setPosition(48.5f + filterMenuWidth / 2, 0.f);
+        deleteFilterMenu->setContentSize({ filterMenuWidth, 80.f });
+        deleteFilterMenu->updateLayout();
     }
 
     if (auto menu = getChildOfType<CCMenu>(this, 2)) {
@@ -134,20 +209,27 @@ $register_ids(EditorUI) {
         setIDs(
             menu,
             0,
-            "static-tab-1",
-            "static-tab-2",
-            "static-tab-3",
+            "block-tab",
+            "half-block-tab",
+            "outline-tab",
             "slope-tab",
             "hazard-tab",
             "3d-tab",
             "portal-tab",
-            "deco-tab-1",
-            "deco-tab-2",
+            "ground-deco-tab",
+            "air-deco-tab",
             "pulse-deco-tab",
             "sawblade-tab",
             "trigger-tab",
             "custom-tab"
         );
+
+        menu->setPosition(winSize.width / 2, 100.f);
+        menu->setContentSize({ winSize.width, 50.f });
+        menu->setLayout(
+            RowLayout::create()
+                ->setGap(0.f)
+        );
     }
 
     if (auto menu = getChildOfType<CCMenu>(this, 3)) {
@@ -174,18 +256,33 @@ $register_ids(EditorUI) {
             "all-layers-button"
         );
 
-        detachAndCreateMenu(
+        auto topRightMenuWidth = winSize.width / 2 - 140.f;
+        auto topRightMenu = detachAndCreateMenu(
             this,
-            "top-right-menu",
-            RowLayout::create()->setAlignment(Alignment::End),
+            "settings-menu",
+            RowLayout::create()
+                ->setAxisReverse(true)
+                ->setAxisAlignment(AxisAlignment::End),
             menu->getChildByID("pause-button"),
             menu->getChildByID("settings-button")
         );
+        topRightMenu->setContentSize({ topRightMenuWidth, 60.f });
+        topRightMenu->setPositionX(
+            topRightMenu->getPositionX() - topRightMenuWidth / 2 + 
+                getSizeSafe(topRightMenu->getChildByID("pause-button")).width / 2
+        );
+        topRightMenu->updateLayout();
 
-        detachAndCreateMenu(
+        auto rightMenu = detachAndCreateMenu(
             this,
             "editor-buttons-menu",
-            GridLayout::create(4, GridAlignment::End, GridDirection::Column),
+            ColumnLayout::create()
+                ->setAxisAlignment(AxisAlignment::End)
+                ->setCrossAxisAlignment(AxisAlignment::End)
+                ->setGap(-3.5f)
+                ->setGrowCrossAxis(true)
+                ->setCrossAxisOverflow(false)
+                ->setAxisReverse(true),
             menu->getChildByID("copy-paste-button"),
             menu->getChildByID("edit-object-button"),
             menu->getChildByID("paste-color-button"),
@@ -199,16 +296,35 @@ $register_ids(EditorUI) {
             menu->getChildByID("copy-values-button"),
             menu->getChildByID("hsv-button")
         );
+        for (auto btn : CCArrayExt<CCNode>(rightMenu->getChildren())) {
+            btn->setContentSize({ 40.f, 40.f });
+        }
+        rightMenu->setContentSize({ 210.f, 160.f });
+        rightMenu->setPosition(
+            winSize.width - 210.f / 2 - 5.f,
+            winSize.height / 2 + 42.5f
+        );
+        rightMenu->updateLayout();
 
-        detachAndCreateMenu(
+        this->getChildByID("layer-index-label")->setLayoutOptions(
+            AxisLayoutOptions::create()
+                ->setAutoScale(false)
+                ->setLength(25.f)
+        );
+
+        auto layerMenu = detachAndCreateMenu(
             this,
             "layer-menu",
-            RowLayout::create(),
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start),
             menu->getChildByID("all-layers-button"),
             menu->getChildByID("prev-layer-button"),
             this->getChildByID("layer-index-label"),
             menu->getChildByID("next-layer-button")
         );
+        layerMenu->setPositionX(winSize.width - 110.f / 2);
+        layerMenu->setContentSize({ 110.f, 30.f });
+        layerMenu->updateLayout();
     }
 }
 
diff --git a/loader/src/ids/GJGarageLayer.cpp b/loader/src/ids/GJGarageLayer.cpp
index 3d973602..55d3d663 100644
--- a/loader/src/ids/GJGarageLayer.cpp
+++ b/loader/src/ids/GJGarageLayer.cpp
@@ -10,8 +10,10 @@ $register_ids(GJGarageLayer) {
     setIDSafe(this, 2, "username-label");
     setIDSafe(this, 6, "player-icon");
 
+    auto winSize = CCDirector::get()->getWinSize();
+
     if (auto menu = getChildOfType<CCMenu>(this, 0)) {
-        menu->setID("icon-select-menu");
+        menu->setID("category-menu");
 
         setIDs(
             menu,
@@ -26,6 +28,13 @@ $register_ids(GJGarageLayer) {
             "trail-button",
             "death-effect-button"
         );
+
+        menu->setContentSize({ 320.f, 50.f });
+        menu->setLayout(
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+                ->setGap(-4.f)
+        );
     }
 
     setIDs(
@@ -47,13 +56,99 @@ $register_ids(GJGarageLayer) {
         "color-selection-menu"
     );
 
-    if (auto menu = getChildOfType<CCMenu>(this, 11)) {
+    if (auto menu = getChildOfType<CCMenu>(this, 1)) {
         menu->setID("top-left-menu");
 
         setIDs(menu, 0, "back-button", "shop-button", "shards-button");
 
-        detachAndCreateMenu(
-            menu, "shards-button-menu", ColumnLayout::create(), menu->getChildByID("shards-button")
+        auto backBtn = menu->getChildByID("back-button");
+        auto backMenu = detachAndCreateMenu(
+            this,
+            "back-menu",
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start),
+            backBtn
+        );
+        backMenu->setContentSize({ 100.f, 50.f });
+        backMenu->setPositionX(
+            backMenu->getPositionX() + 100.f / 2 - 
+                getSizeSafe(backBtn).width / 2
+        );
+        backMenu->updateLayout();
+
+        auto shardsBtn = menu->getChildByID("shards-button");
+        auto shardsMenu = detachAndCreateMenu(
+            this,
+            "shards-menu",
+            ColumnLayout::create()
+                ->setAxisReverse(true)
+                ->setAxisAlignment(AxisAlignment::End),
+            shardsBtn
+        );
+        shardsMenu->setContentSize({ 50.f, 100.f });
+        shardsMenu->setPositionY(
+            shardsMenu->getPositionY() - 100.f / 2 + 
+                getSizeSafe(shardsBtn).height / 2
+        );
+        shardsMenu->updateLayout();
+    }
+
+    auto bottomLeftMenu = CCMenu::create();
+    bottomLeftMenu->setID("bottom-left-menu");
+    bottomLeftMenu->setContentSize({ 50.f, 70.f });
+    bottomLeftMenu->setPosition(30.f, 115.f);
+    bottomLeftMenu->setLayout(
+        ColumnLayout::create()
+            ->setAxisAlignment(AxisAlignment::Start)
+    );
+    this->addChild(bottomLeftMenu);
+
+    auto bottomRightMenu = CCMenu::create();
+    bottomRightMenu->setID("bottom-right-menu");
+    bottomRightMenu->setContentSize({ 50.f, 110.f });
+    bottomRightMenu->setPosition(winSize.width - 30.f, 135.f);
+    bottomRightMenu->setLayout(
+        ColumnLayout::create()
+            ->setAxisAlignment(AxisAlignment::Start)
+    );
+    this->addChild(bottomRightMenu);
+
+    // aspect ratio responsiveness
+    if (winSize.width / winSize.height <= 5.1f / 3.f) {
+        bottomLeftMenu->setPosition(15.f, 115.f);
+        bottomRightMenu->setPosition(winSize.width - 15.f, 135.f);
+
+        if (auto shardsMenu = this->getChildByID("shards-menu")) {
+            shardsMenu->setContentSize({ 110.f, 50.f });
+            shardsMenu->setPosition(
+                shardsMenu->getPosition() + ccp(50.f, 30.f)
+            );
+            shardsMenu->setLayout(
+                RowLayout::create()
+                    ->setAxisAlignment(AxisAlignment::Start)
+            );
+        }
+    }
+    if (winSize.width / winSize.height <= 4.1f / 3.f) {
+        bottomLeftMenu->setContentSize({ 90.f, 50.f });
+        bottomLeftMenu->setPosition(
+            15.f + 110.f / 2,
+            85.f
+        );
+        bottomLeftMenu->setLayout(
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+        );
+
+        bottomRightMenu->setContentSize({ 90.f, 50.f });
+        bottomRightMenu->setPosition(
+            winSize.width - 15.f - 110.f / 2,
+            85.f
+        );
+        bottomRightMenu->setLayout(
+            RowLayout::create()
+                ->setAxisReverse(true)
+                ->setAxisAlignment(AxisAlignment::End)
         );
     }
 }
diff --git a/loader/src/ids/LevelBrowserLayer.cpp b/loader/src/ids/LevelBrowserLayer.cpp
index c1c74149..bd7ffdc4 100644
--- a/loader/src/ids/LevelBrowserLayer.cpp
+++ b/loader/src/ids/LevelBrowserLayer.cpp
@@ -7,28 +7,153 @@
 USE_GEODE_NAMESPACE();
 
 $register_ids(LevelBrowserLayer) {
+    auto winSize = CCDirector::get()->getWinSize();
+
     if (auto menu = getChildOfType<CCMenu>(this, 0)) {
-        menu->setID("go-back-menu");
-        setIDSafe(menu, 0, "back-button");
+        menu->setID("back-menu");
+        auto btn = setIDSafe(menu, 0, "back-button");
+        menu->setContentSize({ 100.f, 50.f });
+        menu->setPositionX(
+            menu->getPositionX() + 100.f / 2 - 
+                getSizeSafe(btn).width / 2
+        );
+        menu->setLayout(
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+        );
     }
 
     if (m_searchObject->m_searchType == SearchType::MyLevels) {
         if (auto menu = getChildOfType<CCMenu>(this, 2)) {
             menu->setID("new-level-menu");
-            setIDSafe(menu, 0, "new-level-button");
+            auto newLvlBtn = setIDSafe(menu, 0, "new-level-button");
 
             if (auto myLevelsBtn = setIDSafe(menu, 1, "my-levels-button")) {
-                detachAndCreateMenu(
+                auto menu = detachAndCreateMenu(
                     this,
                     "my-levels-menu",
-                    ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End),
+                    ColumnLayout::create()
+                        ->setAxisAlignment(AxisAlignment::Start),
                     myLevelsBtn
                 );
+                menu->setPositionY(
+                    menu->getPositionY() + 125.f / 2 - 
+                        myLevelsBtn->getScaledContentSize().height / 2
+                );
+                menu->setContentSize({ 50.f, 125.f });
+                menu->updateLayout();
             }
 
-            menu->setLayout(ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End));
+            menu->setLayout(
+                ColumnLayout::create()
+                    ->setAxisAlignment(AxisAlignment::Start)
+            );
+            menu->setPositionY(
+                menu->getPositionY() + 130.f / 2 - 
+                    getSizeSafe(newLvlBtn).height / 2
+            );
+            menu->setContentSize({ 50.f, 130.f });
+            menu->updateLayout();
+        }
+
+        if (auto menu = getChildOfType<CCMenu>(this, 1)) {
+            if (auto searchBtn = setIDSafe(menu, 5, "search-button")) {
+                auto clearBtn = setIDSafe(menu, 6, "clear-search-button");
+                // this is a hacky fix because for some reason adding children 
+                // before the clear button is made visible is inconsistent
+                if (clearBtn) {
+                    searchBtn->setZOrder(-1);
+                    clearBtn->setZOrder(-1);
+                }
+                auto searchMenu = detachAndCreateMenu(
+                    this,
+                    "search-menu",
+                    ColumnLayout::create()
+                        ->setAxisReverse(true)
+                        ->setCrossAxisReverse(true)
+                        ->setGrowCrossAxis(true)
+                        ->setCrossAxisOverflow(false)
+                        ->setCrossAxisAlignment(AxisAlignment::Start)
+                        ->setAxisAlignment(AxisAlignment::End),
+                    searchBtn,
+                    clearBtn
+                );
+                auto width = 45.f * winSize.aspect();
+                searchMenu->setPosition(
+                    searchMenu->getPositionX() + width / 2 - 
+                        searchBtn->getScaledContentSize().width / 2,
+                    searchMenu->getPositionY() - 80.f / 2 + 
+                        searchBtn->getScaledContentSize().height / 2
+                );
+                searchMenu->setContentSize({ width, 80.f });
+                searchMenu->updateLayout();
+            }
+
+            if (auto pageBtn = setIDSafe(menu, 2, "page-button")) {
+                auto folderBtn = setIDSafe(menu, 3, "folder-button");
+                auto lastPageBtn = setIDSafe(menu, 4, "last-page-button");
+                auto pageMenu = detachAndCreateMenu(
+                    this,
+                    "page-menu",
+                    ColumnLayout::create()
+                        ->setAxisReverse(true)
+                        ->setGrowCrossAxis(true)
+                        ->setAxisAlignment(AxisAlignment::End),
+                    pageBtn,
+                    folderBtn,
+                    lastPageBtn
+                );
+                pageMenu->setContentSize({ 40.f, 110.f });
+                pageMenu->setAnchorPoint({ 1.f, .5f });
+                pageMenu->setPosition(
+                    pageMenu->getPositionX() + 20.f,
+                    pageMenu->getPositionY() - 110.f / 2 + 12.5f
+                );
+                pageMenu->updateLayout();
+            }
+
+            auto navMenuWidth = 50.f * winSize.aspect();
+
+            if (auto prevPageBtn = setIDSafe(menu, 0, "prev-page-button")) {
+                auto navMenu = detachAndCreateMenu(
+                    this,
+                    "prev-page-menu",
+                    RowLayout::create()
+                        ->setAxisAlignment(AxisAlignment::Start),
+                    prevPageBtn
+                );
+                prevPageBtn->setZOrder(-1);
+                navMenu->setContentSize({ navMenuWidth, 40.f });
+                navMenu->setPositionX(
+                    navMenu->getPositionX() + navMenuWidth / 2 - 
+                        prevPageBtn->getScaledContentSize().width / 2
+                );
+                navMenu->updateLayout();
+            }
+
+            auto nextPageBtn = setIDSafe(menu, 0, "next-page-button");
+
+            menu->setID("next-page-menu");
+            menu->setLayout(
+                RowLayout::create()
+                    ->setAxisReverse(true)
+                    ->setAxisAlignment(AxisAlignment::End)
+            );
+            menu->setContentSize({ navMenuWidth, 40.f });
+            menu->setPositionX(
+                winSize.width - navMenuWidth / 2 - 5.f
+            );
+            menu->updateLayout();
         }
     }
+
+    auto bottomMenu = CCMenu::create();
+    bottomMenu->setID("bottom-menu");
+    bottomMenu->setContentSize({ 325.f + 20.f * winSize.aspect(), 50.f });
+    bottomMenu->setPosition(winSize.width / 2, 28.f);
+    bottomMenu->setZOrder(15);
+    bottomMenu->setLayout(RowLayout::create());
+    this->addChild(bottomMenu);
 }
 
 struct LevelBrowserLayerIDs : Modify<LevelBrowserLayerIDs, LevelBrowserLayer> {
diff --git a/loader/src/ids/LevelInfoLayer.cpp b/loader/src/ids/LevelInfoLayer.cpp
index 4a082700..d2f96629 100644
--- a/loader/src/ids/LevelInfoLayer.cpp
+++ b/loader/src/ids/LevelInfoLayer.cpp
@@ -35,26 +35,52 @@ $register_ids(LevelInfoLayer) {
     setIDSafe<CustomSongWidget>(this, 0, "custom-songs-widget");
 
     if (auto menu = getChildOfType<CCMenu>(this, 0)) {
-        menu->setID("exit-menu");
-        setIDSafe(menu, 0, "exit-button");
+        menu->setID("play-menu");
+        setIDSafe(menu, 0, "play-button");
+    }
+
+    if (auto menu = getChildOfType<CCMenu>(this, 2)) {
+        menu->setID("back-menu");
+        auto backBtn = setIDSafe(menu, 0, "back-button");
+        menu->setPositionX(
+            menu->getPositionX() + 100.f / 2 - 
+                getSizeSafe(backBtn).width / 2
+        );
+        menu->setContentSize({ 100.f, 50.f });
+        menu->setLayout(
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::Start)
+        );
     }
 
     if (auto menu = getChildOfType<CCMenu>(this, 1)) {
         menu->setID("right-side-menu");
 
         if (auto name = setIDSafe(menu, 0, "creator-name")) {
-            detachAndCreateMenu(
-                this, "creator-info-menu", ColumnLayout::create()->setAlignment(Alignment::Begin), name
+            auto menu = detachAndCreateMenu(
+                this,
+                "creator-info-menu",
+                ColumnLayout::create()
+                    ->setAxisReverse(true)
+                    ->setAxisAlignment(AxisAlignment::End),
+                name
             );
+            menu->setPositionY(
+                menu->getPositionY() - 40.f / 2 + 
+                    name->getScaledContentSize().height / 2
+            );
+            menu->setContentSize({ 60.f, 40.f });
+            menu->updateLayout();
         }
 
         auto leftSideMenu = CCMenu::create();
-        leftSideMenu->setPosition(winSize / 2 + ccp(-254.f, 30.f));
+        leftSideMenu->setPosition(30.f, winSize.height / 2);
         leftSideMenu->setLayout(ColumnLayout::create());
         leftSideMenu->setID("left-side-menu");
+        leftSideMenu->setContentSize({ 50.f, 225.f });
         this->addChild(leftSideMenu);
 
-        menu->setPosition(winSize / 2 + ccp(254.f, 0.f));
+        menu->setPosition(winSize.width - 30.f, winSize.height / 2);
 
         for (auto child : CCArrayExt<CCNode>(menu->getChildren())) {
             if (child->getPositionX() < 0.f) {
@@ -73,6 +99,20 @@ $register_ids(LevelInfoLayer) {
         setIDSafe(menu, 4, "like-button");
         setIDSafe(menu, 5, "rate-button");
 
+        menu->setPosition(
+            menu->getPositionX() + static_cast<CCNode*>(
+                menu->getChildren()->firstObject()
+            )->getPositionX(),
+            winSize.height / 2
+        );
+        menu->setContentSize({ 60.f, winSize.height - 15.f });
+        menu->setLayout(
+            ColumnLayout::create()
+                ->setGap(3.f)
+                ->setAxisAlignment(AxisAlignment::End)
+                ->setAxisReverse(true)
+        );
+
         setIDSafe(leftSideMenu, 0, "copy-button");
 
         menu->updateLayout();
diff --git a/loader/src/ids/LevelSettingsLayer.cpp b/loader/src/ids/LevelSettingsLayer.cpp
index ce857a83..a898d013 100644
--- a/loader/src/ids/LevelSettingsLayer.cpp
+++ b/loader/src/ids/LevelSettingsLayer.cpp
@@ -162,12 +162,15 @@ $register_ids(LevelSettingsLayer) {
                 menu->getChildByID("2-player-toggle")
             );
 
-            detachAndCreateMenu(
+            auto fontButtonMenu = detachAndCreateMenu(
                 this,
                 "font-button-menu",
-                RowLayout::create()->setAlignment(Alignment::End),
+                RowLayout::create()
+                    ->setAxisAlignment(AxisAlignment::End),
                 menu->getChildByID("font-button")
             );
+            fontButtonMenu->setPositionY(fontButtonMenu->getPositionY() - 100.f / 2);
+            fontButtonMenu->setContentSize({ 50.f, 100.f });
         }
     }
 
diff --git a/loader/src/ids/MenuLayer.cpp b/loader/src/ids/MenuLayer.cpp
index a42364cb..8bc228ed 100644
--- a/loader/src/ids/MenuLayer.cpp
+++ b/loader/src/ids/MenuLayer.cpp
@@ -2,6 +2,7 @@
 
 #include <Geode/modify/MenuLayer.hpp>
 #include <Geode/utils/cocos.hpp>
+#include <Geode/ui/BasedButtonSprite.hpp>
 
 USE_GEODE_NAMESPACE();
 
@@ -10,6 +11,8 @@ $register_ids(MenuLayer) {
     setIDSafe(this, 0, "main-menu-bg");
     setIDSafe<CCSprite>(this, 0, "main-title");
 
+    auto winSize = CCDirector::get()->getWinSize();
+
     // controller
     if (PlatformToolbox::isControllerConnected()) {
         setIDSafe<CCSprite>(this, 1, "play-gamepad-icon");
@@ -28,24 +31,42 @@ $register_ids(MenuLayer) {
     else {
         setIDSafe<CCLabelBMFont>(this, 0, "player-username");
     }
+    
     // main menu
     if (auto menu = getChildOfType<CCMenu>(this, 0)) {
         menu->setID("main-menu");
         auto playBtn = setIDSafe(menu, 0, "play-button");
         auto iconBtn = setIDSafe(menu, 1, "icon-kit-button");
 
+        setIDSafe(menu, 2, "editor-button");
+
+        if (auto pfp = setIDSafe(menu, 3, "profile-button")) {
+            auto profileMenu = detachAndCreateMenu(
+                this, "profile-menu",
+                RowLayout::create()
+                    ->setAxisAlignment(AxisAlignment::Start),
+                pfp
+            );
+            profileMenu->setContentSize({ 150.f, 50.f });
+            profileMenu->setPositionX(
+                profileMenu->getPositionX() + 150.f / 2 - 
+                    pfp->getScaledContentSize().height / 2
+            );
+            profileMenu->updateLayout();
+        }
+
         // the buttons are added in order play, icon, editor which doesn't work
         // well with setLayout that deals with children in order
         menu->swapChildIndices(playBtn, iconBtn);
 
-        setIDSafe(menu, 2, "editor-button");
-
-        if (auto pfp = setIDSafe(menu, 3, "profile-button")) {
-            pfp->setPositionHint(PositionHint::Absolute);
-        }
-
-        menu->setLayout(RowLayout::create(18.f, 0.f));
+        menu->setContentSize({ winSize.width - 140.f, 65.f });
+        menu->setLayout(
+            RowLayout::create()
+                ->setGap(18.f)
+                ->setCrossAxisOverflow(true)
+        );
     }
+
     // bottom menu
     if (auto menu = getChildOfType<CCMenu>(this, 1)) {
         menu->setID("bottom-menu");
@@ -57,11 +78,22 @@ $register_ids(MenuLayer) {
         // move daily chest to its own menu
 
         if (auto dailyChest = setIDSafe(menu, -1, "daily-chest-button")) {
-            detachAndCreateMenu(this, "right-side-menu", ColumnLayout::create(0.f, 0.f), dailyChest);
+            auto menu = detachAndCreateMenu(
+                this,
+                "right-side-menu",
+                ColumnLayout::create(),
+                dailyChest
+            );
+            menu->setContentSize({ 65.f, 180.f });
+            menu->updateLayout();
         }
 
-        menu->setLayout(RowLayout::create(5.f, ach->getPositionY()));
+        menu->setContentSize({ winSize.width - 220.f, 65.f });
+        menu->setLayout(
+            RowLayout::create()
+        );
     }
+    
     // social media menu
     if (auto menu = getChildOfType<CCMenu>(this, 2)) {
         menu->setID("social-media-menu");
@@ -70,19 +102,62 @@ $register_ids(MenuLayer) {
         setIDSafe(menu, 2, "twitter-button");
         setIDSafe(menu, 3, "youtube-button");
     }
+    
     // more games menu
     if (auto menu = getChildOfType<CCMenu>(this, 3)) {
         menu->setID("more-games-menu");
-        setIDSafe(menu, 0, "more-games-button");
+        auto moreGamesBtn = setIDSafe(menu, 0, "more-games-button");
 
         // move close button to its own menu
 
         if (auto closeBtn = setIDSafe(menu, 1, "close-button")) {
-            detachAndCreateMenu(
-                this, "close-menu", RowLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin), closeBtn
+            auto closeMenu = detachAndCreateMenu(
+                this,
+                "close-menu",
+                RowLayout::create()
+                    ->setAxisAlignment(AxisAlignment::Start),
+                closeBtn
             );
+            closeMenu->setContentSize({ 200.f, 50.f });
+            closeMenu->setPositionX(
+                closeMenu->getPositionX() + 200.f / 2 - 
+                    closeBtn->getScaledContentSize().width / 2
+            );
+            closeMenu->updateLayout();
         }
+    
+        menu->setContentSize({ 100.f, 50.f });
+        menu->setPositionX(
+            menu->getPositionX() - 100.f / 2 + 
+                getSizeSafe(moreGamesBtn).width / 2
+        );
+        menu->setLayout(
+            RowLayout::create()
+                ->setAxisAlignment(AxisAlignment::End)
+                ->setAxisReverse(true)
+        );
     }
+
+    // add a menu to the top right corner and middle left that are empty 
+    // but prolly a place mods want to add stuff
+
+    auto topRightMenu = CCMenu::create();
+    topRightMenu->setPosition(winSize.width - 200.f / 2, winSize.height - 50.f / 2);
+    topRightMenu->setID("top-right-menu");
+    topRightMenu->setContentSize({ 200.f, 50.f });
+    topRightMenu->setLayout(
+        RowLayout::create()
+            ->setAxisReverse(true)
+            ->setAxisAlignment(AxisAlignment::End)
+    );
+    this->addChild(topRightMenu);
+
+    auto middleLeftMenu = CCMenu::create();
+    middleLeftMenu->setPosition(25.f, 215.f);
+    middleLeftMenu->setID("side-menu");
+    middleLeftMenu->setContentSize({ 50.f, 120.f });
+    middleLeftMenu->setLayout(ColumnLayout::create());
+    this->addChild(middleLeftMenu);
 }
 
 // MenuLayer::init is hooked in ../hooks/MenuLayer.cpp
diff --git a/loader/src/internal/about.hpp.in b/loader/src/internal/about.hpp.in
index a0cdc50e..6da36c01 100644
--- a/loader/src/internal/about.hpp.in
+++ b/loader/src/internal/about.hpp.in
@@ -11,6 +11,6 @@ static constexpr geode::VersionInfo LOADER_VERSION = {
     @PROJECT_VERSION_MAJOR@,
     @PROJECT_VERSION_MINOR@,
     @PROJECT_VERSION_PATCH@,
-    @PROJECT_VERSION_TYPE@,
+    @PROJECT_VERSION_TAG_CONSTR@,
 };
 static constexpr const char* LOADER_MOD_JSON = R"JSON_SEPARATOR(@LOADER_MOD_JSON@)JSON_SEPARATOR";
diff --git a/loader/src/loader/ModInfo.cpp b/loader/src/loader/ModInfo.cpp
index dbfc4802..af6200e0 100644
--- a/loader/src/loader/ModInfo.cpp
+++ b/loader/src/loader/ModInfo.cpp
@@ -97,8 +97,7 @@ Result<ModInfo> ModInfo::Impl::createFromSchemaV010(ModJson const& rawJson) {
     root.has("unloadable").into(impl->m_supportsUnloading);
     root.has("early-load").into(impl->m_needsEarlyLoad);
     if (root.has("api")) {
-        // TODO: figure out what got wiped with merge
-        // impl->isAPI = true;
+        impl->m_isAPI = true;
     }
 
     for (auto& dep : root.has("dependencies").iterate()) {
@@ -194,11 +193,9 @@ Result<ModInfo> ModInfo::Impl::create(ModJson const& json) {
 
     return Err(
         "[mod.json] targets a version (" + schema.toString() +
-        ") that isn't "
-        "supported by this version (v" +
+        ") that isn't supported by this version (v" +
         LOADER_VERSION_STR +
-        ") of geode. "
-        "This is probably a bug; report it to "
+        ") of geode. This is probably a bug; report it to "
         "the Geode Development Team."
     );
 }
diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp
index a2b89bf1..b70c83eb 100644
--- a/loader/src/ui/internal/list/ModListCell.cpp
+++ b/loader/src/ui/internal/list/ModListCell.cpp
@@ -79,9 +79,7 @@ void ModListCell::setupInfo(
     this->addChild(versionLabel);
 
     if (auto tag = info.version().getTag()) {
-        auto tagLabel = TagNode::create(
-            versionTagToString(tag.value()).c_str()
-        );
+        auto tagLabel = TagNode::create(tag.value().toString().c_str());
         tagLabel->setAnchorPoint({ .0f, .5f });
         tagLabel->setScale(.3f);
         tagLabel->setPosition(
diff --git a/loader/src/utils/VersionInfo.cpp b/loader/src/utils/VersionInfo.cpp
index 13f8aad7..23269a82 100644
--- a/loader/src/utils/VersionInfo.cpp
+++ b/loader/src/utils/VersionInfo.cpp
@@ -10,31 +10,57 @@ USE_GEODE_NAMESPACE();
 
 // VersionTag
 
-std::optional<VersionTag> geode::versionTagFromString(std::string const& str) {
-    switch (hash(str.c_str())) {
-        case hash("alpha"): return VersionTag::Alpha;
-        case hash("beta"): return VersionTag::Beta;
-        case hash("prerelease"): return VersionTag::Prerelease;
-        default: return std::nullopt;
+Result<VersionTag> VersionTag::parse(std::stringstream& str) {
+    std::string iden;
+    while ('a' <= str.peek() && str.peek() <= 'z') {
+        iden += str.get();
     }
+    if (str.fail()) {
+        return Err("Unable to parse tag");
+    }
+    VersionTag tag = VersionTag::Alpha;
+    switch (hash(iden.c_str())) {
+        case hash("alpha"): tag = VersionTag::Alpha; break;
+        case hash("beta"): tag = VersionTag::Beta; break;
+        case hash("prerelease"): case hash("pr"): tag = VersionTag::Prerelease; break;
+        default: return Err("Invalid tag \"" + iden + "\"");
+    }
+    if (str.peek() == '.') {
+        str.get();
+        size_t num;
+        str >> num;
+        if (str.fail()) {
+            return Err("Unable to parse tag number");
+        }
+        tag.number = num;
+    }
+    return Ok(tag);
 }
 
-std::string geode::versionTagToSuffixString(VersionTag tag) {
-    switch (tag) {
-        case VersionTag::Alpha: return "-alpha";
-        case VersionTag::Beta: return "-beta";
-        case VersionTag::Prerelease: return "-prerelease";
+std::string VersionTag::toSuffixString() const {
+    std::string res = "";
+    switch (value) {
+        case Alpha: res += "-alpha"; break;
+        case Beta: res += "-beta"; break;
+        case Prerelease: res += "-prerelease"; break;
     }
-    return "";
+    if (number) {
+        res += "." + std::to_string(number.value());
+    }
+    return res;
 }
 
-std::string geode::versionTagToString(VersionTag tag) {
-    switch (tag) {
-        case VersionTag::Alpha: return "Alpha";
-        case VersionTag::Beta: return "Beta";
-        case VersionTag::Prerelease: return "Prerelease";
+std::string VersionTag::toString() const {
+    std::string res = "";
+    switch (value) {
+        case Alpha: res += "Alpha"; break;
+        case Beta: res += "Beta"; break;
+        case Prerelease: res += "Prerelease"; break;
     }
-    return "";
+    if (number) {
+        res += " " + std::to_string(number.value());
+    }
+    return res;
 }
 
 // VersionInfo
@@ -77,18 +103,13 @@ Result<VersionInfo> VersionInfo::parse(std::string const& string) {
     std::optional<VersionTag> tag;
     if (str.peek() == '-') {
         str.get();
-        std::string iden;
-        str >> iden;
-        if (str.fail()) {
-            return Err("Unable to parse tag");
-        }
-        if (auto t = versionTagFromString(iden)) {
-            tag = t;
-        }
-        else {
-            return Err("Invalid tag \"" + iden + "\"");
-        }
+        GEODE_UNWRAP_INTO(tag, VersionTag::parse(str));
     }
+
+    if (!str.eof()) {
+        return Err("Expected end of version, found '" + std::string(1, str.get()) + "'");
+    }
+
     return Ok(VersionInfo(major, minor, patch, tag));
 }
 
@@ -97,7 +118,7 @@ std::string VersionInfo::toString(bool includeTag) const {
         return fmt::format(
             "v{}.{}.{}{}",
             m_major, m_minor, m_patch,
-            versionTagToSuffixString(m_tag.value())
+            m_tag.value().toSuffixString()
         );
     }
     return fmt::format("v{}.{}.{}", m_major, m_minor, m_patch);