diff --git a/loader/include/Geode/cocos/base_nodes/CCNode.h b/loader/include/Geode/cocos/base_nodes/CCNode.h index 351f9a15..ab944f7e 100644 --- a/loader/include/Geode/cocos/base_nodes/CCNode.h +++ b/loader/include/Geode/cocos/base_nodes/CCNode.h @@ -916,13 +916,24 @@ public: * @note Geode addition */ GEODE_DLL CCNode* getChildByID(std::string_view id); + /** + * Get a child by its string ID + * @param id ID of the child + * @param considerProxies If true, proxy IDs will be checked first + * @returns The child, or nullptr if none was found + * @note Geode addition + * @todo in v5: merge this with getChildByID and add default value considerProxies = true + */ + GEODE_DLL CCNode* getChildByID(std::string_view id, bool considerProxies); /** * Get a child by its string ID. Recursively searches all the children * @param id ID of the child * @returns The child, or nullptr if none was found * @note Geode addition + * @todo in v5: remove */ + [[deprecated("Use CCNode::querySelector instead")]] GEODE_DLL CCNode* getChildByIDRecursive(std::string_view id); /** @@ -945,9 +956,51 @@ public: * Removes a child from the container by its ID. * @param id The ID of the node * @note Geode addition + * @note Will not follow proxies but just delete the proxy; if you want to + * actually remove the node the proxy points to itself, use + * `getChildByID(id)->removeFromParent()` */ GEODE_DLL void removeChildByID(std::string_view id); + /** + * Add a Proxy ID to this node. A Proxy ID is an ID of a node that is + * logically the child of this node, but might not actually be. In other + * words, when using `CCNode::getChildByID` or other related functions, it + * will first check if there is a proxy ID on that node, and follow those + * if one does exist. + * + * For example, if a mod moves a button to another CCMenu, it should leave + * behind a proxy ID into the old CCMenu so any other mod trying to find the + * button in the old menu will still get it. + * + * Another example: if a mod makes a CCMenu paged, it will probably add lots + * of wrapper nodes into the node tree, but any buttons of the CCMenu still + * logically stay children of the CCMenu. That mod would then use proxy IDs + * so the menu will appear like nothing has changed for other mods which try + * to work with it. + * + * @param id The ID of the proxy node + * @param getter When the ID is queried, returns the actual node it points + * to. Needs not be a child of this node. May return nullptr if the node no + * longer exists + * @returns True if the ID was added, or false if it this ID has already + * been added + */ + GEODE_DLL bool addProxyID(std::string_view id, std::function getter); + /** + * Remove a Proxy ID from this node + * @param id The ID of the Proxy. If the ID does not point to a proxy, + * nothing happens + */ + GEODE_DLL void removeProxyID(std::string_view id); + /** + * Check if the given ID is of a proxy node or of an actual child + * @returns True if the ID is of a proxy + * @note Technically there may also be children with the same ID, but this + * function will return true if there is a proxy + */ + GEODE_DLL bool isProxyID(std::string_view id); + /** * Add a child before a specified existing child * @param child The node to add. The node may not be a child of another diff --git a/loader/src/hooks/GeodeNodeMetadata.cpp b/loader/src/hooks/GeodeNodeMetadata.cpp index bd4de0de..00c2b659 100644 --- a/loader/src/hooks/GeodeNodeMetadata.cpp +++ b/loader/src/hooks/GeodeNodeMetadata.cpp @@ -24,6 +24,7 @@ private: std::unordered_map> m_userObjects; std::unordered_set> m_eventListeners; std::unordered_map> m_idEventListeners; + std::unordered_map> m_proxies; friend class ProxyCCNode; friend class cocos2d::CCNode; @@ -117,6 +118,15 @@ void CCNode::setID(std::string&& id) { } CCNode* CCNode::getChildByID(std::string_view id) { + return this->getChildByID(id, true); +} +CCNode* CCNode::getChildByID(std::string_view id, bool considerProxies) { + if (considerProxies) { + auto meta = GeodeNodeMetadata::set(this); + if (meta->m_proxies.contains(std::string(id))) { + return meta->m_proxies.at(std::string(id))(this); + } + } for (auto child : CCArrayExt(this->getChildren())) { if (child->getID() == id) { return child; @@ -124,7 +134,6 @@ CCNode* CCNode::getChildByID(std::string_view id) { } return nullptr; } - CCNode* CCNode::getChildByIDRecursive(std::string_view id) { if (auto child = this->getChildByID(id)) { return child; @@ -294,9 +303,35 @@ CCNode* CCNode::querySelector(std::string_view queryStr) { } void CCNode::removeChildByID(std::string_view id) { - if (auto child = this->getChildByID(id)) { - this->removeChild(child); + // Remove proxies with this ID + if (this->isProxyID(id)) { + this->removeProxyID(id); } + // Technically there can be multiple children with the same ID in addition + // to the proxy ID. However, this is very bad practice, and removing actual + // children only when there's no proxies with the ID mirrors the fact that + // if there's four children with the same ID, you need to call + // `removeChildByID` four times to remove them all + else { + if (auto child = this->getChildByID(id, false)) { + this->removeChild(child); + } + } +} + +bool CCNode::addProxyID(std::string_view id, std::function getter) { + auto meta = GeodeNodeMetadata::set(this); + if (meta->m_proxies.contains(std::string(id))) { + return false; + } + meta->m_proxies.insert({ std::string(id), getter }); + return true; +} +void CCNode::removeProxyID(std::string_view id) { + GeodeNodeMetadata::set(this)->m_proxies.erase(std::string(id)); +} +bool CCNode::isProxyID(std::string_view id) { + return GeodeNodeMetadata::set(this)->m_proxies.contains(std::string(id)); } void CCNode::setLayout(Layout* layout, bool apply, bool respectAnchor) {