From 3e5b6a8170b7163d5262c7c87e6703374feaa18a Mon Sep 17 00:00:00 2001 From: prevter Date: Sun, 24 May 2026 00:13:11 +0300 Subject: [PATCH 1/3] Initial V2 commit --- .github/workflows/multi-platform.yml | 57 ++++++ .gitignore | 64 +++++++ CMakeLists.txt | 46 +++++ LICENSE.md | 229 ++++++++++++++++++++++ NOTICE | 22 +++ TRADEMARKS.md | 27 +++ include/Core/Config.hpp | 136 +++++++++++++ include/Core/InputManager.hpp | 74 +++++++ include/Core/Keybind.hpp | 103 ++++++++++ include/Core/Registry.hpp | 81 ++++++++ include/Core/Setting.hpp | 65 +++++++ include/Eclipse.hpp | 11 ++ include/Events.hpp | 106 +++++++++++ include/Macros.hpp | 22 +++ include/Prelude.hpp | 26 +++ include/UI/HeliosLayer.hpp | 33 ++++ include/Utils/FixedString.hpp | 15 ++ mod.json | 25 +++ src/API/Events.cpp | 24 +++ src/Core/InputManager.cpp | 275 +++++++++++++++++++++++++++ src/Core/Registry.cpp | 78 ++++++++ src/Hacks/Player/Noclip.cpp | 0 src/Main.cpp | 48 +++++ src/UI/HeliosLayer.cpp | 217 +++++++++++++++++++++ 24 files changed, 1784 insertions(+) create mode 100644 .github/workflows/multi-platform.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE.md create mode 100644 NOTICE create mode 100644 TRADEMARKS.md create mode 100644 include/Core/Config.hpp create mode 100644 include/Core/InputManager.hpp create mode 100644 include/Core/Keybind.hpp create mode 100644 include/Core/Registry.hpp create mode 100644 include/Core/Setting.hpp create mode 100644 include/Eclipse.hpp create mode 100644 include/Events.hpp create mode 100644 include/Macros.hpp create mode 100644 include/Prelude.hpp create mode 100644 include/UI/HeliosLayer.hpp create mode 100644 include/Utils/FixedString.hpp create mode 100644 mod.json create mode 100644 src/API/Events.cpp create mode 100644 src/Core/InputManager.cpp create mode 100644 src/Core/Registry.cpp create mode 100644 src/Hacks/Player/Noclip.cpp create mode 100644 src/Main.cpp create mode 100644 src/UI/HeliosLayer.cpp diff --git a/.github/workflows/multi-platform.yml b/.github/workflows/multi-platform.yml new file mode 100644 index 00000000..2e51bb1e --- /dev/null +++ b/.github/workflows/multi-platform.yml @@ -0,0 +1,57 @@ +name: Build Geode Mod + +on: + workflow_dispatch: + push: + branches: + - "**" + +jobs: + build: + strategy: + fail-fast: false + matrix: + config: + - name: Windows + os: windows-latest + + - name: macOS + os: macos-latest + + - name: iOS + os: macos-latest + target: iOS + + - name: Android32 + os: ubuntu-latest + target: Android32 + + - name: Android64 + os: ubuntu-latest + target: Android64 + + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Build the mod + uses: geode-sdk/build-geode-mod@main + with: + combine: true + target: ${{ matrix.config.target }} + + package: + name: Package builds + runs-on: ubuntu-latest + needs: ['build'] + + steps: + - uses: geode-sdk/build-geode-mod/combine@main + id: build + + - uses: actions/upload-artifact@v4 + with: + name: Build Output + path: ${{ steps.build.outputs.build-output }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2604c207 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Macos be like +**/.DS_Store + +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Ignore build folders +**/build +# Ignore platform specific build folders +build-*/ + +# Workspace files are user-specific +*.sublime-workspace + +# ILY vscode +**/.vscode + +# Local History for Visual Studio Code +.history/ + +# clangd +.cache/ + +# Visual Studio +.vs/ + +# CLion +.idea/ +/cmake-build-*/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..f68773f0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.27) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) + +if ("${CMAKE_SYSTEM_NAME}" STREQUAL "iOS" OR IOS) + set(CMAKE_OSX_ARCHITECTURES "arm64") +else() + set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") +endif() + +project(Eclipse LANGUAGES CXX) + +include(CheckIPOSupported) +check_ipo_supported(RESULT LTO_SUPPORTED OUTPUT LTO_ERROR) + +if(LTO_SUPPORTED) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) +else() + message(STATUS "LTO not supported: ${LTO_ERROR}") +endif() + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS src/*.cpp) +add_library(${PROJECT_NAME} SHARED ${SOURCES}) +target_include_directories(${PROJECT_NAME} PRIVATE src include) + +if (NOT DEFINED ENV{GEODE_SDK}) + message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") +else() + message(STATUS "Found Geode: $ENV{GEODE_SDK}") +endif() + +add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) + +CPMAddPackage("gh:prevter/HeliosUI#bf49a83") +target_compile_definitions(HeliosUI PUBLIC HELIOS_OPENGL_LOADER_INCLUDE="Geode/cocos/platform/CCGL.h") +target_link_libraries(HeliosUI PRIVATE geode-sdk) +target_link_libraries(${PROJECT_NAME} Helios::UI) + +setup_geode_mod(${PROJECT_NAME}) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/mod.json" MOD_JSON) +string(JSON MOD_VERSION GET "${MOD_JSON}" "version") +set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${MOD_VERSION} SOVERSION 2) +target_compile_definitions(${PROJECT_NAME} PRIVATE ECLIPSE_VERSION="${MOD_VERSION}") \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..b45a1f20 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,229 @@ +# Eclipse Menu Community License - v1.0 + +**This software is licensed under the Apache License, Version 2.0 (the "License"), +subject to the following Additional Terms. In the event of any conflict between the +Apache License and these Additional Terms, the Additional Terms control.** + +## Additional Terms + +### 1. Commercial Use Restriction + +Notwithstanding anything in the Apache License to the contrary, the rights granted to you +do not include, and the License does not grant you, the right to **Sell the Software**. + +**"Sell"** means practicing any or all of the rights granted to you under the License to provide +to third parties, for a fee or other charge (including a subscription), a product or service whose +value derives, entirely or substantially, from the functionality of the Software. + +### 2. Developer Freedom & Educational Exception + +**Right to Learn:** Nothing in this License restricts your right to view, study, and learn from +the source code of the Software. Educational use, reverse-engineering analysis for learning +purposes, and referencing the code to understand Geometry Dash modding techniques are fully +permitted and encouraged. + +**Good Faith Logic Reuse:** Notwithstanding the commercial restriction above, you may copy, modify, +and redistribute isolated code snippets, helper functions, or specific feature logic from this +Software for use in any project, including commercial ones, provided such use is done in good faith. + +**Standalone Repackaging Restriction:** This exception is strictly intended to allow developers +to integrate parts of Eclipse Menu's logic to enhance their own unique projects. You may not +copy a substantial, distinct feature of this Software to distribute it as a standalone modification, +or as a project where that copied feature constitutes the primary functionality. + +For allowed uses of minor snippets, attribution is encouraged but not required. +This exception strictly does not apply to the reproduction of the Software as a whole, +its primary interface framework, or to any files within the `/assets` directory. + +--- + +### Apache License, Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +**TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION** + +1. **Definitions.** + * **"License"** shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + * **"Licensor"** shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + * **"Legal Entity"** shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + * **"You"** (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + * **"Source"** form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + * **"Object"** form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + * **"Work"** shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + * **"Derivative Works"** shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + * **"Contribution"** shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + * **"Contributor"** shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. **Grant of Copyright License.** Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. **Grant of Patent License.** Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. **Redistribution.** You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + * (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + * (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + * (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + * (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. **Submission of Contributions.** Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. **Trademarks.** This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. **Disclaimer of Warranty.** Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. **Limitation of Liability.** In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. **Accepting Warranty or Additional Liability.** While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +**END OF TERMS AND CONDITIONS** + +--- + +**Copyright 2024-2026 Eclipse Menu Contributors** + +Licensed under the Apache License, Version 2.0 (the "License"), +as modified by the Additional Terms above. +You may not use this file except in compliance with the License. +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..b7bb3474 --- /dev/null +++ b/NOTICE @@ -0,0 +1,22 @@ +Eclipse Menu +Copyright (c) 2024-2026 Eclipse Menu Contributors + +This product includes software developed by the Eclipse Menu Team +within the Geometry Dash modding community. + +TRADEMARK & IDENTITY NOTICE: +The names "Eclipse" and "Eclipse Menu", when used in the context of +Geometry Dash software or modifications, are trademarks of the Eclipse Menu Team. +This License does not grant permission to use these trade names or associated +logos/visual assets except as required for reasonable origin attribution. + +COMMERCIAL RESTRICTION: +This software is subject to the "Additional Terms" as specified in the LICENSE.md file. +The right to Sell the Software (as defined in the License) is expressly withheld. +Value in this software derives substantially from its aggregate functionality. +Redistribution for a fee or charge is a violation of the License terms. + +LEGACY NOTICE: +Version 1.x of Eclipse Menu remains under the Eclipse Public License 2.0 (EPL-2.0). +This version (2.x+) is a substantial rewrite and is governed by the +Apache 2.0 + Commons Clause license. \ No newline at end of file diff --git a/TRADEMARKS.md b/TRADEMARKS.md new file mode 100644 index 00000000..db232c93 --- /dev/null +++ b/TRADEMARKS.md @@ -0,0 +1,27 @@ +# Eclipse Menu Trademark & Branding Policy + +## 1. Purpose + +This policy ensures the **Eclipse Menu** identity remains with the official development team +and prevents predatory redistribution. It also protects users from confusion and ensures they get +the authentic experience. + +## 2. Permitted Use (Contributions & Personal Use) +You are **not** required to rebrand or remove assets if: +* You are forking for private, personal use. +* You are contributing to the official Eclipse Menu project via Pull Request. +* You are making free extensions that clearly state they are "for Eclipse Menu." + +## 3. Prohibited Use (Paid/Commercial Forks) +Per the "Commons Clause" in our LICENSE, you may not sell this software +or its derivative works. If you destribute a fork commercially: +* **Distinct Identity:** You must change the name and remove all official logos/icons. +* **Originality:** You may not sell a "reskinned" version of Eclipse Menu, that relies on + a substantial portion (30% or more) of Eclipse's original modules without + significant original functionality. + +## 4. Asset Licensing +While the code is Apache 2.0, all files in the `/assets` directory are licensed under +**Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)**. +* You may use them for free, non-commercial purposes only. +* You may not modify these assets or use them in commercial projects. \ No newline at end of file diff --git a/include/Core/Config.hpp b/include/Core/Config.hpp new file mode 100644 index 00000000..687094fc --- /dev/null +++ b/include/Core/Config.hpp @@ -0,0 +1,136 @@ +#pragma once +#include + +#include "Registry.hpp" +#include "Setting.hpp" + +namespace eclipse { + #define ECLIPSE_CONFIG_TYPES(X) \ + X(bool, 0) \ + X(int64_t, 1) \ + X(double, 2) \ + X(std::string, 3) + + namespace __detail { + struct TypeOps { + using SerializeFn = matjson::Value(*)(void const* value); + using DeserializeFn = bool(*)(matjson::Value const& json, void* out, bool notify); + + SerializeFn serialize; + DeserializeFn deserialize; + }; + + template + matjson::Value serialize_type(void const* value) { + return matjson::Value(*static_cast(value)); + } + + template + bool deserialize_type(matjson::Value const& json, void* out, bool notify) { + auto res = json.as(); + if (!res) return false; + Setting* setting = static_cast*>(out); + if (notify) setting->set(std::move(*res)); + else setting->value() = std::move(*res); + return true; + } + + #define ECLIPSE_TYPE_OPS(type, index) { &serialize_type, &deserialize_type }, + #define ECLIPSE_DECL_INDEX(type, index) \ + template <> \ + struct index_for_type { \ + static constexpr size_t value = index; \ + }; + + inline constexpr auto type_table = std::to_array({ + ECLIPSE_CONFIG_TYPES(ECLIPSE_TYPE_OPS) + }); + + ECLIPSE_CONFIG_TYPES(ECLIPSE_DECL_INDEX) + + #undef ECLIPSE_DECL_INDEX + #undef ECLIPSE_TYPE_OPS + } + + template + T default_value() { + if constexpr (std::is_default_constructible_v) { + return T{}; + } else { + static_assert(sizeof(T) == 0, "Type must be default constructible or have a specialization of default_value"); + std::unreachable(); + } + } + + template + T default_value() { return default_value(); } + + template + class ConfigSetting : public Setting { + public: + using Setting::Setting; + + static ConfigSetting& get() { + static ConfigSetting instance; + return instance; + } + + constexpr static std::string_view name() noexcept { return Name; } + + private: + ConfigSetting() : Setting(default_value()) { + Registry::get()->bindSetting({ + .self = this, + .name = Name, + .type = __detail::index_for_type_v + }); + } + }; + + namespace config { + template + [[nodiscard]] ConfigSetting& ref() { + return ConfigSetting::get(); + } + + template + [[nodiscard]] T const& get() { + return ref().value(); + } + + template + void set(T&& value) { + ref().set(std::forward(value)); + } + + template + void set(T const& value) { + ref().set(value); + } + + template + size_t listen(typename ConfigSetting::ObserverFn fn) { + return ref().listen(std::move(fn)); + } + + template + void unlisten(size_t id) { + ref().unlisten(id); + } + + template + Result get(std::string_view name) { + return Registry::get()->get(name); + } + + template + Result set(std::string_view name, T&& value) { + return Registry::get()->set(name, std::forward(value)); + } + + template + Result set(std::string_view name, T const& value) { + return Registry::get()->set(name, value); + } + } +} \ No newline at end of file diff --git a/include/Core/InputManager.hpp b/include/Core/InputManager.hpp new file mode 100644 index 00000000..f3e8720d --- /dev/null +++ b/include/Core/InputManager.hpp @@ -0,0 +1,74 @@ +#pragma once +#include "Keybind.hpp" +#include "../Prelude.hpp" + +namespace eclipse { + struct SavedKeybind { Keybind keybind; KeybindMode mode; }; + + /// @brief Manages all keybindings and propagates input throughout the menu + class ECLIPSE_DLL InputManager { + public: + /// @brief Gets the global InputManager instance + /// @return The global InputManager instance + static InputManager* get() + ECLIPSE_EVENT_METHOD(InputManager::get); + + InputManager(InputManager const&) = delete; + InputManager& operator=(InputManager const&) = delete; + InputManager(InputManager&&) = delete; + InputManager& operator=(InputManager&&) = delete; + + using GlobalListener = Function; + + /// @brief Registers a keybind with the InputManager + /// @param id A unique string identifier for the keybind, used for saving/loading and displaying to the user + /// @param callback The function to call when the keybind is triggered + /// @param ephemeral Whether the keybind will be visible in the keybinds tab. Used for "internal" keybinds like StartPos Switcher, which have its own settings + void registerListener(std::string id, Binding::Action callback, bool ephemeral = false) + ECLIPSE_EVENT_METHOD(InputManager::registerListener, this, std::move(id), std::move(callback), ephemeral); + + /// @brief Should be invoked from keyboard listeners, handles the event and propagates it to registered keybinds + /// @param key The key that was pressed/released (see cocos2d::enumKeyCodes) + /// @param mods The keyboard modifiers active during the event + /// @param isDown Whether the key was pressed (true) or released (false) + /// @param timestamp The timestamp of the event (geode::utils::getInputTimestamp()) + /// @return true if the event was "swallowed" (shouldn't be passed further), false otherwise + bool onKeyEvent(cocos2d::enumKeyCodes key, KeyboardModifier mods, bool isDown, double timestamp) + ECLIPSE_EVENT_METHOD(InputManager::onKeyEvent, this, key, mods, isDown, timestamp); + + /// @brief Sets a global listener that will be called for every key event, regardless of registered keybinds. Used for rebind UI. + /// @param listener The function to call for every key event + void setGlobalListener(GlobalListener listener) + ECLIPSE_EVENT_METHOD(InputManager::setGlobalListener, this, std::move(listener)); + + void setDefaultKeybind(std::string id, Keybind keybind, KeybindMode mode = KeybindMode::Toggle) + ECLIPSE_EVENT_METHOD(InputManager::setDefaultKeybind, this, std::move(id), keybind, mode); + + private: + InputManager(); + + StringMap> m_bindings; + StringMap m_loadedKeybinds; + GlobalListener m_globalListener; + }; +} + +template <> +struct matjson::Serialize { + static Value toJson(eclipse::SavedKeybind const& saved) { + return makeObject({ + {"keybind", saved.keybind}, + {"mode", static_cast(saved.mode)} + }); + } + + static geode::Result fromJson(Value const& json) { + GEODE_UNWRAP_INTO(eclipse::Keybind keybind, json["keybind"].as()); + GEODE_UNWRAP_INTO(int modeInt, json["mode"].asInt()); + + return geode::Ok(eclipse::SavedKeybind{ + .keybind = keybind, + .mode = static_cast(modeInt) + }); + } +}; \ No newline at end of file diff --git a/include/Core/Keybind.hpp b/include/Core/Keybind.hpp new file mode 100644 index 00000000..0e70582e --- /dev/null +++ b/include/Core/Keybind.hpp @@ -0,0 +1,103 @@ +#pragma once +#include +#include "../Prelude.hpp" + +namespace eclipse { + using geode::KeyboardModifier; + + struct Keybind { + static constexpr cocos2d::enumKeyCodes UNBOUND = cocos2d::KEY_Unknown; + + cocos2d::enumKeyCodes key = UNBOUND; + KeyboardModifier mods = KeyboardModifier::None; + + constexpr Keybind() noexcept = default; + explicit(false) Keybind(cocos2d::enumKeyCodes key) noexcept : key(key) {} + constexpr Keybind(cocos2d::enumKeyCodes key, KeyboardModifier mods) noexcept : key(key), mods(mods) {} + + static constexpr Keybind unbound() noexcept { + return Keybind{ UNBOUND, KeyboardModifier::None }; + } + + [[nodiscard]] constexpr bool isBound() const noexcept { + return key != UNBOUND; + } + + [[nodiscard]] constexpr bool matches(cocos2d::enumKeyCodes k, KeyboardModifier m) const noexcept { + return key == k && (mods & m) == mods; + } + + [[nodiscard]] constexpr bool operator==(Keybind const&) const noexcept = default; + + [[nodiscard]] std::string toString() const; + }; + + enum class KeybindMode : uint8_t { + Toggle, + Enable, + Disable, + HoldOn, + HoldOff, + }; + + class Binding { + public: + using Action = Function; + + Binding(std::string id, Action callback) + : m_id(std::move(id)), m_callback(std::move(callback)) {} + + Binding(Binding const&) = delete; + Binding& operator=(Binding const&) = delete; + + [[nodiscard]] std::string const& id() const noexcept { return m_id; } + + [[nodiscard]] Keybind const& keybind() const noexcept { return m_keybind; } + void setKeybind(Keybind keybind) noexcept { m_keybind = keybind; } + + [[nodiscard]] KeybindMode mode() const noexcept { return m_mode; } + void setMode(KeybindMode mode) noexcept { m_mode = mode; } + + [[nodiscard]] bool isEphemeral() const noexcept { return m_ephemeral; } + void setEphemeral(bool ephemeral) noexcept { m_ephemeral = ephemeral; } + + [[nodiscard]] bool invoke(bool isDown, cocos2d::enumKeyCodes key, KeyboardModifier mods, double timestamp) { + if (!m_callback) return false; + if (!m_keybind.isBound() || !m_keybind.matches(key, mods)) return false; + return m_callback(m_mode, isDown, timestamp); + } + + private: + std::string m_id; + Action m_callback; + Keybind m_keybind; + KeybindMode m_mode = KeybindMode::Toggle; + bool m_ephemeral = false; // doesn't show up in keybinds list + }; +} + +template <> +struct matjson::Serialize { + static Value toJson(eclipse::Keybind const& keybind) { + if (!keybind.isBound()) return Value(nullptr); + + return makeObject({ + {"key", static_cast(keybind.key)}, + {"mods", keybind.mods.value} + }); + } + + static geode::Result fromJson(Value const& value) { + if (value.isNull()) { + return geode::Ok(eclipse::Keybind::unbound()); + } + + GEODE_UNWRAP_INTO(int32_t key, value["key"].asInt()); + GEODE_UNWRAP_INTO(geode::KeyboardModifier mods, value["mods"].asInt()); + + return geode::Ok(eclipse::Keybind{ + static_cast(key), + mods + }); + } +}; \ No newline at end of file diff --git a/include/Core/Registry.hpp b/include/Core/Registry.hpp new file mode 100644 index 00000000..6f5d38e9 --- /dev/null +++ b/include/Core/Registry.hpp @@ -0,0 +1,81 @@ +#pragma once +#include "Setting.hpp" +#include "../Prelude.hpp" + +namespace eclipse { + struct BoundSetting { + void* self; + std::string_view name; + size_t type; + }; + + namespace __detail { + template struct index_for_type {}; + template constexpr size_t index_for_type_v = index_for_type::value; + } + + enum class RegistryFindError { + NotFound, + TypeMismatch + }; + + constexpr std::string_view format_as(RegistryFindError error) { + switch (error) { + case RegistryFindError::NotFound: return "setting not found"; + case RegistryFindError::TypeMismatch: return "setting type mismatch"; + default: return "unknown error"; + } + } + + class ECLIPSE_DLL Registry { + public: + Registry(Registry const&) = delete; + Registry(Registry&&) = delete; + Registry& operator=(Registry const&) = delete; + Registry& operator=(Registry&&) = delete; + + static Registry* get() ECLIPSE_EVENT_METHOD(Registry::get); + + void bindSetting(BoundSetting const& setting) ECLIPSE_EVENT_METHOD(Registry::bindSetting, this, setting); + BoundSetting* findSetting(std::string_view name) ECLIPSE_EVENT_METHOD(Registry::findSetting, this, name); + + Result<> loadFromFile(std::filesystem::path const& path) ECLIPSE_EVENT_METHOD(Registry::loadFromFile, this, path); + Result<> saveToFile(std::filesystem::path const& path) ECLIPSE_EVENT_METHOD(Registry::saveToFile, this, path); + + template + Result*, RegistryFindError> find(std::string_view name) { + auto setting = this->findSetting(name); + if (!setting) { + return Err(RegistryFindError::NotFound); + } + + if (setting->type != __detail::index_for_type_v) { + return Err(RegistryFindError::TypeMismatch); + } + + return Ok(static_cast*>(setting->self)); + } + + template + Result get(std::string_view name) { + GEODE_UNWRAP_INTO(auto* setting, this->find(name)); + return Ok(setting->value()); + } + + template + Result set(std::string_view name, T&& value) { + GEODE_UNWRAP_INTO(auto* setting, this->find(name)); + setting->set(std::forward(value)); + return Ok(); + } + + private: + Registry(); + ~Registry(); + + private: + std::vector m_settings; + StringMap m_settingsMap; + matjson::Value m_json; + }; +} \ No newline at end of file diff --git a/include/Core/Setting.hpp b/include/Core/Setting.hpp new file mode 100644 index 00000000..fc910e38 --- /dev/null +++ b/include/Core/Setting.hpp @@ -0,0 +1,65 @@ +#pragma once +#include +#include "../Prelude.hpp" + +namespace eclipse { + template + class Setting { + public: + using value_type = T; + using ObserverFn = Function; + + constexpr Setting() noexcept(std::is_nothrow_default_constructible_v) = default; + explicit constexpr Setting(T initial) noexcept(std::is_nothrow_move_constructible_v) : m_value(std::move(initial)) {} + + Setting(Setting const&) = delete; + Setting(Setting&&) = delete; + Setting& operator=(Setting const&) = delete; + Setting& operator=(Setting&&) = delete; + + [[nodiscard]] constexpr T const& value() const noexcept { return m_value; } + [[nodiscard]] constexpr T& value() noexcept { return m_value; } + + void set(T&& value) { + if constexpr (requires{ m_value == value; }) { + if (m_value == value) return; + } + + m_value = std::forward(value); + this->notify(); + } + + void notify() { + for (auto& obs : m_observers) { + obs.fn(m_value); + } + } + + size_t listen(ObserverFn fn) { + size_t id = m_nextObserverId++; + m_observers.push_back({ id, std::move(fn) }); + return id; + } + + void unlisten(size_t id) { + auto it = std::ranges::find_if( + m_observers, + [id](Observer const& obs) { return obs.id == id; } + ); + + if (it != m_observers.end()) { + m_observers.erase(it); + } + } + + private: + struct Observer { + size_t id; + ObserverFn fn; + }; + + T m_value{}; + asp::SmallVec m_observers; + size_t m_nextObserverId = 0; + }; +} diff --git a/include/Eclipse.hpp b/include/Eclipse.hpp new file mode 100644 index 00000000..81f3ae6c --- /dev/null +++ b/include/Eclipse.hpp @@ -0,0 +1,11 @@ +#pragma once + +#ifdef ECLIPSE_USING_EVENT_API + #warning "Already using Event API, either remove \"Events.hpp\" include to use linked API, or don't include \"Eclipse.hpp\"" +#else + #ifndef ECLIPSE_USING_LINK_API + #define ECLIPSE_USING_LINK_API + #endif +#endif + +#include "Macros.hpp" \ No newline at end of file diff --git a/include/Events.hpp b/include/Events.hpp new file mode 100644 index 00000000..b83e8edd --- /dev/null +++ b/include/Events.hpp @@ -0,0 +1,106 @@ +#pragma once + +#ifdef ECLIPSE_USING_LINK_API + #warning "Already using linked API, either remove \"Eclipse.hpp\" include to use Event API, or don't include \"Events.hpp\"" +#else + #ifndef ECLIPSE_USING_EVENT_API + #define ECLIPSE_USING_EVENT_API + #endif +#endif + +#ifdef ECLIPSE_EVENT_METHOD + #undef ECLIPSE_EVENT_METHOD +#endif + +#include +#include +#include + +namespace eclipse { + struct BoundSetting; + class Registry; + class InputManager; + + namespace __detail::event { + constexpr size_t API_VERSION = 2; + + struct Table { + size_t version = API_VERSION; + + // == Registry == // + Registry*(*Registry_get)(); + void(Registry::*Registry_bindSetting)(BoundSetting const&); + BoundSetting*(Registry::*Registry_findSetting)(std::string_view); + geode::Result<>(Registry::*Registry_loadFromFile)(std::filesystem::path const&); + geode::Result<>(Registry::*Registry_saveToFile)(std::filesystem::path const&); + + // == InputManager == // + InputManager*(*InputManager_get)(); + }; + + struct FetchTableEvent : geode::Event { + using Event::Event; + }; + + inline Table* getTable() { + static Table* table = nullptr; + if (!table) FetchTableEvent().send(table); + return table; + } + + #define ECLIPSE_EVENT_SINGLETON_GET(Class) \ + inline eclipse::Class* get() { \ + static eclipse::Class* instance = nullptr; \ + if (!instance) { \ + if (auto table = getTable()) { \ + instance = table->Class##_get(); \ + } \ + } \ + return instance; \ + } + + #define ECLIPSE_EVENT_METHOD_VOID(cls, name, args, ...) \ + inline void name args { \ + if (!self) return; \ + if (auto table = getTable()) { \ + (self->*table->cls##_##name)(__VA_ARGS__); \ + } \ + } + + #define ECLIPSE_EVENT_METHOD_PTR(cls, name, type, args, ...) \ + inline type name args { \ + if (!self) return nullptr; \ + if (auto table = getTable()) { \ + return (self->*table->cls##_##name)(__VA_ARGS__); \ + } \ + return nullptr; \ + } + + #define ECLIPSE_EVENT_METHOD_RESULT(cls, name, type, args, ...) \ + inline geode::Result name args { \ + if (!self) return geode::Err("Instance not available"); \ + if (auto table = getTable()) { \ + return (self->*table->cls##_##name)(__VA_ARGS__); \ + } \ + return geode::Err("Event API not available"); \ + } + + namespace Registry { + ECLIPSE_EVENT_SINGLETON_GET(Registry) + + ECLIPSE_EVENT_METHOD_VOID(Registry, bindSetting, (eclipse::Registry* self, BoundSetting const& setting), setting) + ECLIPSE_EVENT_METHOD_PTR(Registry, findSetting, BoundSetting*, (eclipse::Registry* self, std::string_view name), name) + ECLIPSE_EVENT_METHOD_RESULT(Registry, loadFromFile,, (eclipse::Registry* self, std::filesystem::path const& path), path) + ECLIPSE_EVENT_METHOD_RESULT(Registry, saveToFile,, (eclipse::Registry* self, std::filesystem::path const& path), path) + } + + namespace InputManager { + ECLIPSE_EVENT_SINGLETON_GET(InputManager) + } + + #undef ECLIPSE_EVENT_SINGLETON_GET + #undef ECLIPSE_EVENT_METHOD_VOID + #undef ECLIPSE_EVENT_METHOD_PTR + #undef ECLIPSE_EVENT_METHOD_RESULT + } +} \ No newline at end of file diff --git a/include/Macros.hpp b/include/Macros.hpp new file mode 100644 index 00000000..d3f3c2d8 --- /dev/null +++ b/include/Macros.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +#ifndef ECLIPSE_DLL + #ifdef Eclipse_EXPORTS + #ifdef GEODE_IS_WINDOWS + #define ECLIPSE_DLL __declspec(dllexport) + #else + #define ECLIPSE_DLL __attribute__((visibility("default"))) + #endif + #else + #ifdef GEODE_IS_WINDOWS + #define ECLIPSE_DLL __declspec(dllimport) + #else + #define ECLIPSE_DLL + #endif + #endif +#endif + +#ifndef ECLIPSE_EVENT_METHOD + #define ECLIPSE_EVENT_METHOD(name, ...) +#endif \ No newline at end of file diff --git a/include/Prelude.hpp b/include/Prelude.hpp new file mode 100644 index 00000000..a3205af6 --- /dev/null +++ b/include/Prelude.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +#include "Macros.hpp" + +namespace eclipse { + using geode::Function; + using geode::utils::StringMap; + using geode::Result; + using geode::Ok; + using geode::Err; + using geode::Mod; + using geode::Loader; + + namespace log = geode::log; + namespace utils { + using namespace geode::utils; + } +} + +namespace eclipse::prelude { + using namespace eclipse; + using namespace cocos2d; +} \ No newline at end of file diff --git a/include/UI/HeliosLayer.hpp b/include/UI/HeliosLayer.hpp new file mode 100644 index 00000000..9f64c242 --- /dev/null +++ b/include/UI/HeliosLayer.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include "../Prelude.hpp" + +namespace eclipse { + class ECLIPSE_DLL HeliosLayer : public cocos2d::CCLayer { + public: + static HeliosLayer* get() ECLIPSE_EVENT_METHOD(HeliosLayer::get); + + void toggle() noexcept; + void setToggled(bool toggled) noexcept; + [[nodiscard]] bool isToggled() const noexcept { return m_toggled; } + + bool init() override; + + void draw() override; + void update(float dt) override; + + bool ccTouchBegan(cocos2d::CCTouch* pTouch, cocos2d::CCEvent* pEvent) override; + void ccTouchMoved(cocos2d::CCTouch* pTouch, cocos2d::CCEvent* pEvent) override; + void ccTouchEnded(cocos2d::CCTouch* pTouch, cocos2d::CCEvent* pEvent) override; + void ccTouchCancelled(cocos2d::CCTouch* pTouch, cocos2d::CCEvent* pEvent) override; + + void registerWithTouchDispatcher() override; + + private: + geode::ListenerHandle m_mouseMoveListener; + geode::ListenerHandle m_mouseClickListener; + geode::ListenerHandle m_keyboardListener; + + bool m_toggled = false; + }; +} diff --git a/include/Utils/FixedString.hpp b/include/Utils/FixedString.hpp new file mode 100644 index 00000000..97657152 --- /dev/null +++ b/include/Utils/FixedString.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +namespace eclipse { + template + struct FixedString { + char data[N + 1]{}; + explicit(false) constexpr FixedString(char const* str) { std::copy_n(str, N + 1, data); } + constexpr operator std::string_view() const { return { data, N }; } + }; + + template + FixedString(char const (&)[N]) -> FixedString; +} \ No newline at end of file diff --git a/mod.json b/mod.json new file mode 100644 index 00000000..f66bf1c0 --- /dev/null +++ b/mod.json @@ -0,0 +1,25 @@ +{ + "geode": "5.6.1", + "gd": { + "win": "2.2081", + "android": "2.2081", + "mac": "2.2081", + "ios": "2.2081" + }, + "id": "eclipse.eclipse-menu", + "name": "Eclipse", + "version": "v2.0.0", + "links": { + "homepage": "https://eclipse.menu", + "source": "https://github.com/EclipseMenu/EclipseMenu", + "community": "https://discord.gg/NnpwFRDMND" + }, + "developers": ["Eclipse Team", "ninXout", "prevter", "maxnu", "Firee", "SpaghettDev"], + "description": "A next-generation mod menu for Geometry Dash.", + "tags": ["cheat", "gameplay", "utility", "customization", "interface"], + "api": { "include": [ "include/*.hpp" ] }, + "early-load": true, + "dependencies": {}, + "resources": {}, + "settings": {} +} \ No newline at end of file diff --git a/src/API/Events.cpp b/src/API/Events.cpp new file mode 100644 index 00000000..09b070cb --- /dev/null +++ b/src/API/Events.cpp @@ -0,0 +1,24 @@ +#include + +#include + +namespace eclipse::__detail::event { + static Table& getGlobalTable() { + static Table table { + .version = API_VERSION, + + .Registry_get = &::eclipse::Registry::get, + .Registry_bindSetting = &::eclipse::Registry::bindSetting, + .Registry_findSetting = &::eclipse::Registry::findSetting, + .Registry_loadFromFile = &::eclipse::Registry::loadFromFile, + .Registry_saveToFile = &::eclipse::Registry::saveToFile + }; + return table; + } + + $execute { + FetchTableEvent() + .listen([](Table*& out) { out = &getGlobalTable(); }) + .leak(); + } +} diff --git a/src/Core/InputManager.cpp b/src/Core/InputManager.cpp new file mode 100644 index 00000000..468ca578 --- /dev/null +++ b/src/Core/InputManager.cpp @@ -0,0 +1,275 @@ +#include +#include +#include + +using namespace eclipse::prelude; + +namespace eclipse { + struct Names { std::string_view CTRL, ALT, SHIFT, SUPER; }; + static constexpr auto n = Names { + GEODE_WINDOWS("Ctrl+", "Alt+", "Shift+", "Win+") + GEODE_ANDROID("Ctrl+", "Alt+", "Shift+", "Super+") + GEODE_MACOS("Control+", "Option+", "Shift+", "Command+") + GEODE_IOS("Control+", "Option+", "Shift+", "Command+") + }; + + static void modsToString(utils::StringBuffer<64>& buffer, KeyboardModifier mods, enumKeyCodes key) { + auto test = [&](KeyboardModifier mod, std::array keys) { + if (!(mods & mod)) return false; + for (auto k : keys) { + if (key == k) return false; + } + return true; + }; + + if (test(KeyboardModifier::Control, {KEY_LeftControl, KEY_RightControl, KEY_Control})) buffer.append(n.CTRL); + if (test(KeyboardModifier::Alt, {KEY_LeftMenu, KEY_RightMenu, KEY_Alt})) buffer.append(n.ALT); + if (test(KeyboardModifier::Shift, {KEY_LeftShift, KEY_RightShift, KEY_Shift})) buffer.append(n.SHIFT); + if (test(KeyboardModifier::Super, {KEY_LeftWindowsKey, KEY_RightWindowsKey, KEY_None})) buffer.append(n.SUPER); + } + + static void keyToString(utils::StringBuffer<64>& buffer, enumKeyCodes key) { + if (key >= KEY_A && key <= KEY_Z) { + buffer.append(static_cast('A' + (key - KEY_A))); + } else if (key >= KEY_Zero && key <= KEY_Nine) { + buffer.append(static_cast('0' + (key - KEY_Zero))); + } else if (key >= KEY_NumPad0 && key <= KEY_NumPad9) { + buffer.append("Num {}", key - KEY_NumPad0); + } else if (key >= KEY_F1 && key <= KEY_F24) { + buffer.append("F{}", key - KEY_F1); + } else switch (key) { + case KEY_None: buffer.append("None"); break; + case KEY_Backspace: buffer.append("Backspace"); break; + case KEY_Tab: buffer.append("Tab"); break; + case KEY_Clear: buffer.append("Clear"); break; + case KEY_Enter: buffer.append("Enter"); break; + case KEY_Shift: buffer.append(n.SHIFT); break; + case KEY_Control: buffer.append(n.CTRL); break; + case KEY_Alt: buffer.append(n.ALT); break; + case KEY_Pause: buffer.append("Pause"); break; + case KEY_CapsLock: buffer.append("Caps Lock"); break; + case KEY_Escape: buffer.append("Escape"); break; + case KEY_Space: buffer.append("Space"); break; + case KEY_PageUp: buffer.append("Page Up"); break; + case KEY_PageDown: buffer.append("Page Down"); break; + case KEY_End: buffer.append("End"); break; + case KEY_Home: buffer.append("Home"); break; + case KEY_Left: buffer.append("Left"); break; + case KEY_Up: buffer.append("Up"); break; + case KEY_Right: buffer.append("Right"); break; + case KEY_Down: buffer.append("Down"); break; + case KEY_Select: buffer.append("Select"); break; + case KEY_Print: buffer.append("Print"); break; + case KEY_Execute: buffer.append("Execute"); break; + case KEY_PrintScreen: buffer.append("Print Screen"); break; + case KEY_Insert: buffer.append("Insert"); break; + case KEY_Delete: buffer.append("Delete"); break; + case KEY_Help: buffer.append("Help"); break; + case KEY_LeftWindowsKey: buffer.append(n.SUPER); break; + case KEY_RightWindowsKey: buffer.append("R{}", n.SUPER); break; + case KEY_ApplicationsKey: buffer.append("Menu"); break; + case KEY_Sleep: buffer.append("Sleep"); break; + case KEY_Multiply: buffer.append("Num *"); break; + case KEY_Add: buffer.append("Num +"); break; + case KEY_Seperator: buffer.append("Num ,"); break; + case KEY_Subtract: buffer.append("Num -"); break; + case KEY_Decimal: buffer.append("Num ."); break; + case KEY_Divide: buffer.append("Num /"); break; + case KEY_Numlock: buffer.append("Num Lock"); break; + case KEY_ScrollLock: buffer.append("Scroll Lock"); break; + case KEY_LeftShift: buffer.append(n.SHIFT); break; + case KEY_RightShift: buffer.append("R{}", n.SHIFT); break; + case KEY_LeftControl: buffer.append(n.CTRL); break; + case KEY_RightControl: buffer.append("R{}", n.CTRL); break; + case KEY_LeftMenu: buffer.append(n.ALT); break; + case KEY_RightMenu: buffer.append("R{}", n.ALT); break; + case KEY_BrowserBack: buffer.append("Browser Back"); break; + case KEY_BrowserForward: buffer.append("Browser Forward"); break; + case KEY_BrowserRefresh: buffer.append("Browser Refresh"); break; + case KEY_BrowserStop: buffer.append("Browser Stop"); break; + case KEY_BrowserSearch: buffer.append("Browser Search"); break; + case KEY_BrowserFavorites: buffer.append("Browser Favorites"); break; + case KEY_BrowserHome: buffer.append("Browser Home"); break; + case KEY_VolumeMute: buffer.append("Volume Mute"); break; + case KEY_VolumeDown: buffer.append("Volume Down"); break; + case KEY_VolumeUp: buffer.append("Volume Up"); break; + case KEY_NextTrack: buffer.append("Next Track"); break; + case KEY_PreviousTrack: buffer.append("Previous Track"); break; + case KEY_StopMedia: buffer.append("Stop Media"); break; + case KEY_PlayPause: buffer.append("Play/Pause"); break; + case KEY_LaunchMail: buffer.append("Launch Mail"); break; + case KEY_SelectMedia: buffer.append("Select Media"); break; + case KEY_LaunchApp1: buffer.append("Launch App 1"); break; + case KEY_LaunchApp2: buffer.append("Launch App 2"); break; + case KEY_OEM1: buffer.append("OEM 1"); break; + case KEY_OEMPlus: buffer.append("OEM Plus"); break; + case KEY_OEMComma: buffer.append("OEM Comma"); break; + case KEY_OEMMinus: buffer.append("OEM Minus"); break; + case KEY_OEMPeriod: buffer.append("OEM Period"); break; + case KEY_OEM2: buffer.append("OEM 2"); break; + case KEY_OEM3: buffer.append("OEM 3"); break; + case KEY_OEM4: buffer.append("OEM 4"); break; + case KEY_OEM5: buffer.append("OEM 5"); break; + case KEY_OEM6: buffer.append("OEM 6"); break; + case KEY_OEM7: buffer.append("OEM 7"); break; + case KEY_OEM8: buffer.append("OEM 8"); break; + case KEY_OEM102: buffer.append("OEM 102"); break; + case KEY_Process: buffer.append("Process"); break; + case KEY_Packet: buffer.append("Packet"); break; + case KEY_Attn: buffer.append("Attn"); break; + case KEY_CrSel: buffer.append("CrSel"); break; + case KEY_ExSel: buffer.append("ExSel"); break; + case KEY_EraseEOF: buffer.append("Erase EOF"); break; + case KEY_Play: buffer.append("Play"); break; + case KEY_Zoom: buffer.append("Zoom"); break; + case KEY_PA1: buffer.append("PA1"); break; + case KEY_OEMClear: buffer.append("OEM Clear"); break; + case KEY_ArrowUp: buffer.append("Up"); break; + case KEY_ArrowDown: buffer.append("Down"); break; + case KEY_ArrowLeft: buffer.append("Left"); break; + case KEY_ArrowRight: buffer.append("Right"); break; + case CONTROLLER_A: buffer.append("Gamepad A"); break; + case CONTROLLER_B: buffer.append("Gamepad B"); break; + case CONTROLLER_Y: buffer.append("Gamepad Y"); break; + case CONTROLLER_X: buffer.append("Gamepad X"); break; + case CONTROLLER_Start: buffer.append("Gamepad Start"); break; + case CONTROLLER_Back: buffer.append("Gamepad Back"); break; + case CONTROLLER_RB: buffer.append("Gamepad RB"); break; + case CONTROLLER_LB: buffer.append("Gamepad LB"); break; + case CONTROLLER_RT: buffer.append("Gamepad RT"); break; + case CONTROLLER_LT: buffer.append("Gamepad LT"); break; + case CONTROLLER_Up: buffer.append("Gamepad Up"); break; + case CONTROLLER_Down: buffer.append("Gamepad Down"); break; + case CONTROLLER_Left: buffer.append("Gamepad Left"); break; + case CONTROLLER_Right: buffer.append("Gamepad Right"); break; + case CONTROLLER_LTHUMBSTICK_UP: buffer.append("Gamepad LS Up"); break; + case CONTROLLER_LTHUMBSTICK_DOWN: buffer.append("Gamepad LS Down"); break; + case CONTROLLER_LTHUMBSTICK_LEFT: buffer.append("Gamepad LS Left"); break; + case CONTROLLER_LTHUMBSTICK_RIGHT: buffer.append("Gamepad LS Right"); break; + case CONTROLLER_RTHUMBSTICK_UP: buffer.append("Gamepad RS Up"); break; + case CONTROLLER_RTHUMBSTICK_DOWN: buffer.append("Gamepad RS Down"); break; + case CONTROLLER_RTHUMBSTICK_LEFT: buffer.append("Gamepad RS Left"); break; + case CONTROLLER_RTHUMBSTICK_RIGHT: buffer.append("Gamepad RS Right"); break; + case KEY_GraveAccent: buffer.append("`"); break; + case KEY_OEMEqual: buffer.append("="); break; + case KEY_LeftBracket: buffer.append("["); break; + case KEY_RightBracket: buffer.append("]"); break; + case KEY_Backslash: buffer.append("\\"); break; + case KEY_Semicolon: buffer.append(";"); break; + case KEY_Apostrophe: buffer.append("'"); break; + case KEY_Slash: buffer.append("/"); break; + case KEY_Equal: buffer.append("="); break; + case KEY_NumEnter: buffer.append("Num Enter"); break; + case KEY_World1: buffer.append("World 1"); break; + case KEY_World2: buffer.append("World 2"); break; + case MOUSE_4: buffer.append("Mouse 4"); break; + case MOUSE_5: buffer.append("Mouse 5"); break; + case MOUSE_6: buffer.append("Mouse 6"); break; + case MOUSE_7: buffer.append("Mouse 7"); break; + case MOUSE_8: buffer.append("Mouse 8"); break; + + default: buffer.append("Unknown({})", static_cast(key)); break; + } + } + + std::string Keybind::toString() const { + if (!this->isBound()) return ""; + + utils::StringBuffer<64> buffer; + + modsToString(buffer, mods, key); + keyToString(buffer, key); + + return buffer.str(); + } + + InputManager* InputManager::get() { + static InputManager instance; + return &instance; + } + + void InputManager::registerListener(std::string id, Binding::Action callback, bool ephemeral) { + std::string idCopy = id; + auto [it, _] = m_bindings.insert_or_assign( + std::move(idCopy), + std::make_unique(std::move(id), std::move(callback)) + ); + + it->second->setEphemeral(ephemeral); + + auto loadedIt = m_loadedKeybinds.find(it->first); + if (loadedIt != m_loadedKeybinds.end()) { + it->second->setKeybind(loadedIt->second.keybind); + it->second->setMode(loadedIt->second.mode); + } + } + + bool InputManager::onKeyEvent(enumKeyCodes key, KeyboardModifier mods, bool isDown, double timestamp) { + if (m_globalListener && m_globalListener(key, mods, isDown)) { + return true; + } + + for (auto& binding : m_bindings | std::views::values) { + if (binding->invoke(isDown, key, mods, timestamp)) { + return true; + } + } + + return false; + } + + void InputManager::setGlobalListener(GlobalListener listener) { + m_globalListener = std::move(listener); + } + + void InputManager::setDefaultKeybind(std::string id, Keybind keybind, KeybindMode mode) { + if (!m_loadedKeybinds.contains(id)) { + m_loadedKeybinds.insert_or_assign(std::move(id), SavedKeybind{ keybind, mode }); + } + } + + InputManager::InputManager() { + geode::KeyboardInputEvent() + .listen([this](geode::KeyboardInputData& data) { + if (data.action == geode::KeyboardInputData::Action::Repeat) return false; + return this->onKeyEvent( + data.key, + data.modifiers, + data.action == geode::KeyboardInputData::Action::Press, + data.timestamp + ); + }) + .leak(); + + geode::ModStateEvent(geode::ModEventType::DataSaved, Mod::get()) + .listen([this] { + matjson::Value toSave; + for (auto& [id, binding] : m_bindings) { + if (!binding->keybind().isBound()) continue; + toSave.set(id, SavedKeybind{ binding->keybind(), binding->mode() }); + } + + auto str = toSave.dump(); + auto res = utils::file::writeStringSafe( + Mod::get()->getSaveDir() / "keybinds.json", + str + ); + + if (res.isErr()) { + log::error("Failed to save keybinds: {}", res.unwrapErr()); + } + }) + .leak(); + + auto res = utils::file::readFromJson>( + Mod::get()->getSaveDir() / "keybinds.json" + ); + + if (res.isErr()) { + log::error("Failed to load keybinds: {}", res.unwrapErr()); + return; + } + + m_loadedKeybinds = std::move(res).unwrap(); + } +} diff --git a/src/Core/Registry.cpp b/src/Core/Registry.cpp new file mode 100644 index 00000000..2a809e7d --- /dev/null +++ b/src/Core/Registry.cpp @@ -0,0 +1,78 @@ +#include +#include + +namespace eclipse { + Registry* Registry::get() { + static Registry instance; + return &instance; + } + + static bool loadSetting(BoundSetting const& setting, matjson::Value const& value, bool notify = true) { + using namespace __detail; + if (setting.type >= type_table.size()) return false; + auto& ops = type_table[setting.type]; + return ops.deserialize(value, setting.self, notify); + } + + static matjson::Value saveSetting(BoundSetting const& setting) { + using namespace __detail; + if (setting.type >= type_table.size()) return matjson::Value(); + auto& ops = type_table[setting.type]; + return ops.serialize(setting.self); + } + + void Registry::bindSetting(BoundSetting const& setting) { + m_settings.push_back(setting); + m_settingsMap[std::string(setting.name)] = m_settings.size() - 1; + loadSetting(setting, m_json[setting.name], false); + } + + BoundSetting* Registry::findSetting(std::string_view name) { + auto it = m_settingsMap.find(name); + return it != m_settingsMap.end() ? &m_settings[it->second] : nullptr; + } + + Result<> Registry::loadFromFile(std::filesystem::path const& path) { + GEODE_UNWRAP_INTO(m_json, geode::utils::file::readJson(path)); + + for (auto const& setting : m_settings) { + if (m_json.contains(setting.name)) { + if (!loadSetting(setting, m_json[setting.name])) { + log::error("Failed to load setting \"{}\" from config file", setting.name); + } + } + } + + return Ok(); + } + + Result<> Registry::saveToFile(std::filesystem::path const& path) { + for (auto const& setting : m_settings) { + m_json[setting.name] = saveSetting(setting); + } + + auto str = m_json.dump(4); + return geode::utils::file::writeStringSafe(path, str); + } + + Registry::Registry() { + auto path = geode::Mod::get()->getSaveDir() / "config.json"; + std::error_code ec; + if (std::filesystem::exists(path, ec)) { + auto res = this->loadFromFile(path); + if (!res) { + log::error("Failed to load config: {}", res.unwrapErr()); + } + } + } + + Registry::~Registry() = default; + + $on_mod(DataSaved) { + auto path = geode::Mod::get()->getSaveDir() / "config.json"; + auto res = Registry::get()->saveToFile(path); + if (!res) { + log::error("Failed to save config: {}", res.unwrapErr()); + } + } +} diff --git a/src/Hacks/Player/Noclip.cpp b/src/Hacks/Player/Noclip.cpp new file mode 100644 index 00000000..e69de29b diff --git a/src/Main.cpp b/src/Main.cpp new file mode 100644 index 00000000..fa7e5a09 --- /dev/null +++ b/src/Main.cpp @@ -0,0 +1,48 @@ +#include +#include +#include + +using namespace eclipse::prelude; + +// template <> +// bool eclipse::default_value() { return true; } +// +// $execute { +// auto& noclip = config::ref(); +// auto& also_noclip = ConfigSetting::get(); +// +// noclip.listen([](bool enabled) { +// log::info("Noclip {}", enabled ? "enabled" : "disabled"); +// }); +// +// also_noclip.set(true); +// +// if (config::get()) { +// config::listen([](bool enabled) { +// // second listener +// }); +// } +// +// config::set(false); +// +// auto value = Registry::get()->find("player.noclip").unwrap(); +// value->set(true); +// } + +$on_game(Loaded) { + // Create and attach the UI interface + (void) HeliosLayer::get(); + + auto inputManager = InputManager::get(); + inputManager->setDefaultKeybind("toggle-ui", KEY_Tab); + inputManager->registerListener( + "toggle-ui", + [](KeybindMode, bool isDown, double) -> bool { + if (!isDown) return false; + if (auto layer = HeliosLayer::get()) { + layer->toggle(); + } + return false; + } + ); +} \ No newline at end of file diff --git a/src/UI/HeliosLayer.cpp b/src/UI/HeliosLayer.cpp new file mode 100644 index 00000000..0446791f --- /dev/null +++ b/src/UI/HeliosLayer.cpp @@ -0,0 +1,217 @@ +#include + +#include +#include + +#include +#include + +using namespace eclipse::prelude; + +static void heliosLog(Helios::LogLevel level, std::string_view message) { + using Helios::LogLevel; + switch (level) { + case LogLevel::Trace: log::trace("[Helios] {}", message); break; + case LogLevel::Debug: log::debug("[Helios] {}", message); break; + case LogLevel::Info: log::info("[Helios] {}", message); break; + case LogLevel::Warning: log::warn("[Helios] {}", message); break; + case LogLevel::Error: log::error("[Helios] {}", message); break; + } +} + +namespace eclipse { + class TestWidget : public Helios::Widget { + public: + void onDraw(Helios::DrawList& dl) override { + dl.fillRoundedRectGradient( + {0, 0, m_size.x, m_size.y}, 8.f, + Helios::Color::red(), + Helios::Color::green(), + Helios::Color::blue(), + m_hovered ? + Helios::Color::fromFloats(1.f, 1.f, 0.f, 0.5f) : + Helios::Color::transparent() + ); + } + + void onMouseEnter() override { + m_hovered = true; + this->markGeometryDirty(); + } + + void onMouseLeave() override { + m_hovered = false; + this->markGeometryDirty(); + } + + bool m_hovered = false; + }; + + static Helios::Vec2 cocosToFrame(CCPoint const& pos) { + auto frameSize = CCEGLView::get()->m_obScreenSize * geode::utils::getDisplayFactor(); + auto winSize = CCDirector::get()->m_obWinSizeInPoints; + return Helios::Vec2{ + pos.x / winSize.width * frameSize.width, + (1.f - pos.y / winSize.height) * frameSize.height + }; + } + + static Helios::Vec2 getMousePosition() { + return cocosToFrame(geode::cocos::getMousePos()); + } + + HeliosLayer* HeliosLayer::get() { + static HeliosLayer* instance = nullptr; + if (!instance) { + instance = new HeliosLayer(); + if (!instance->init()) { + delete instance; + instance = nullptr; + } + instance->autorelease(); + } + return instance; + } + + void HeliosLayer::toggle() noexcept { + this->setToggled(!m_toggled); + } + + void HeliosLayer::setToggled(bool toggled) noexcept { + m_toggled = toggled; + this->setVisible(m_toggled); + } + + bool HeliosLayer::init() { + if (!CCLayer::init()) return false; + + auto gl = CCEGLView::get(); + if (!Helios::Director::get().init( + gl->m_obScreenSize.width, + gl->m_obScreenSize.height + )) { + return false; + } + + Helios::Director::get() + .setCursorCallback([](Helios::MouseCursor cursor) { + using Helios::MouseCursor; + // TODO: set cursor + }); + + this->setID("interface"_spr); + + m_mouseMoveListener = geode::MouseMoveEvent().listen([](int32_t x, int32_t y) { + Helios::Director::get().handleMouseMoved(getMousePosition()); + }); + + m_mouseClickListener = geode::MouseInputEvent().listen([](geode::MouseInputData& data) { + using namespace geode; + if (data.button == MouseInputData::Button::Left) return ListenerResult::Propagate; + return Helios::Director::get().handleMouseButton( + static_cast(data.button), + data.action == MouseInputData::Action::Press, + getMousePosition() + ); + }); + + m_keyboardListener = geode::KeyboardInputEvent().listen([](geode::KeyboardInputData& data) { + // TODO + }); + + this->setTouchEnabled(true); + this->scheduleUpdate(); + + geode::OverlayManager::get()->addChild(this); + + Helios::Director::get() + .addWidget() + ->setRect({10, 10}, {150, 100}); + + return true; + } + + void HeliosLayer::draw() { + Helios::Director::get().render(); + } + + void HeliosLayer::update(float) { + // cocos delta time might be speed-hacked, so for accuracy we're counting our own + static auto lastTime = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + std::chrono::duration delta = now - lastTime; + lastTime = now; + + Helios::Director::get().update(delta.count()); + } + + bool HeliosLayer::ccTouchBegan(CCTouch* pTouch, CCEvent* pEvent) { + if (!m_bVisible) return false; + #ifdef GEODE_IS_DESKTOP + return Helios::Director::get().handleMouseButton( + Helios::MouseButton::Left, true, + cocosToFrame(pTouch->getLocation()) + ); + #else + return Helios::Director::get().handleTouchBegin( + pTouch->getID(), + cocosToFrame(pTouch->getLocation()) + ); + #endif + } + + void HeliosLayer::ccTouchMoved(CCTouch* pTouch, CCEvent* pEvent) { + #ifndef GEODE_IS_DESKTOP + Helios::Director::get().handleTouchMoved( + pTouch->getID(), + cocosToFrame(pTouch->getLocation()) + ); + #endif + } + + void HeliosLayer::ccTouchEnded(CCTouch* pTouch, CCEvent* pEvent) { + #ifdef GEODE_IS_DESKTOP + Helios::Director::get().handleMouseButton( + Helios::MouseButton::Left, false, + cocosToFrame(pTouch->getLocation()) + ); + #else + Helios::Director::get().handleTouchEnded( + pTouch->getID(), + cocosToFrame(pTouch->getLocation()) + ); + #endif + } + + void HeliosLayer::ccTouchCancelled(CCTouch* pTouch, CCEvent* pEvent) { + #ifdef GEODE_IS_DESKTOP + Helios::Director::get().handleMouseButton( + Helios::MouseButton::Left, false, + cocosToFrame(pTouch->getLocation()) + ); + #else + Helios::Director::get().handleTouchCancelled(pTouch->getID()); + #endif + } + + void HeliosLayer::registerWithTouchDispatcher() { + CCTouchDispatcher::get()->addTargetedDelegate(this, -1000, true); + } +} + +class $modify(HeliosCCEGLVP, CCEGLViewProtocol) { + void setFrameSize(float width, float height) override { + CCEGLViewProtocol::setFrameSize(width, height); + Helios::Director::get().updateViewport(width, height); + } +}; + +#ifndef GEODE_IS_IOS +class $modify(HeliosCCMD, CCMouseDispatcher) { + bool dispatchScrollMSG(float y, float x) { + static constexpr float scrollMult = 1.f / 10.f; + Helios::Director::get().handleMouseWheel({x * scrollMult, -y * scrollMult}); + return CCMouseDispatcher::dispatchScrollMSG(y, x); + } +}; +#endif \ No newline at end of file From e502add3ebde362114d55905f0db0017d366557c Mon Sep 17 00:00:00 2001 From: prevter Date: Sun, 31 May 2026 00:09:19 +0300 Subject: [PATCH 2/3] Define module with toggling --- include/Core/Module.hpp | 89 +++++++++++++++++++++++++++++++++++++ include/Core/Setting.hpp | 9 ++++ src/Hacks/Player/Noclip.cpp | 20 +++++++++ src/Main.cpp | 29 ++++++++++++ src/UI/HeliosLayer.cpp | 4 +- 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 include/Core/Module.hpp diff --git a/include/Core/Module.hpp b/include/Core/Module.hpp new file mode 100644 index 00000000..fab43667 --- /dev/null +++ b/include/Core/Module.hpp @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include "Config.hpp" +#include "../Prelude.hpp" + +namespace eclipse { + enum class MenuTab : uint8_t { + Global, + Player, + Level, + Creator, + Bypass, + Shortcuts + }; + + enum class ModuleType : uint8_t { + Toggle, + Button, + IntToggle, + FloatToggle, + InputFloat, + Color, + }; + + struct OptionDef { + std::string id; + ModuleType type = ModuleType::Toggle; + }; + + struct ModuleDef { + MenuTab tab; + ModuleType type = ModuleType::Toggle; + bool cheat = false; + std::vector options; + }; + + class ModuleRegistry { + public: + static ModuleRegistry* get() { + static ModuleRegistry instance; + return &instance; + } + + void emplace(std::string_view id, ModuleDef&& def) { + // TODO: actually save it lol + } + }; + + struct HackRegistar { + std::string_view id; + bool operator<<(ModuleDef&& def) const { + ModuleRegistry::get()->emplace(id, std::move(def)); + return true; + } + }; + + #define $hack(Name, Id) \ + namespace { struct Name { static constexpr std::string_view id = Id; }; } \ + static bool const Name##_registered = HackRegistar{Id} << ModuleDef + + inline void RegisterHooksHighPrio( + StringMap>& hooks, + Setting& setting + ) { + auto value = setting.value(); + + asp::SmallVec hookPtrs; + for (auto& hook : hooks | std::views::values) { + hook->setAutoEnable(value); + hookPtrs.push_back(hook.get()); + } + + setting.listen([hooks = std::move(hookPtrs)](bool enabled) { + for (auto hook : hooks) { + (void) hook->toggle(enabled); + } + }); + } + + #define BIND_MODULE_HIGHPRIO(Module) \ + static void onModify(auto& self) { \ + ::eclipse::RegisterHooksHighPrio( \ + self.m_hooks,\ + ::eclipse::config::ref{Module::id.data()}>() \ + ); \ + } + +} diff --git a/include/Core/Setting.hpp b/include/Core/Setting.hpp index fc910e38..ef4ec033 100644 --- a/include/Core/Setting.hpp +++ b/include/Core/Setting.hpp @@ -29,6 +29,15 @@ namespace eclipse { this->notify(); } + void set(T const& value) { + if constexpr (requires{ m_value == value; }) { + if (m_value == value) return; + } + + m_value = value; + this->notify(); + } + void notify() { for (auto& obs : m_observers) { obs.fn(m_value); diff --git a/src/Hacks/Player/Noclip.cpp b/src/Hacks/Player/Noclip.cpp index e69de29b..86f636e0 100644 --- a/src/Hacks/Player/Noclip.cpp +++ b/src/Hacks/Player/Noclip.cpp @@ -0,0 +1,20 @@ +#include +#include + +using namespace eclipse::prelude; + +$hack(Noclip, "player.noclip") { + .tab = MenuTab::Player, + .type = ModuleType::Toggle, + .cheat = true, +}; + +class $modify(NoclipPL, PlayLayer) { + BIND_MODULE_HIGHPRIO(Noclip); + + void destroyPlayer(PlayerObject* player, GameObject* object) override { + if (object == m_anticheatSpike) { + return PlayLayer::destroyPlayer(player, object); + } + } +}; \ No newline at end of file diff --git a/src/Main.cpp b/src/Main.cpp index fa7e5a09..6d89b176 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -45,4 +45,33 @@ using namespace eclipse::prelude; return false; } ); + + // temporary + inputManager->setDefaultKeybind("player.noclip", KEY_N); + inputManager->registerListener( + "player.noclip", + [](KeybindMode mode, bool isDown, double) -> bool { + switch (mode) { + case KeybindMode::Toggle: + if (isDown) config::set(!config::get()); + break; + case KeybindMode::Enable: + if (isDown) config::set(true); + break; + case KeybindMode::Disable: + if (isDown) config::set(false); + break; + case KeybindMode::HoldOn: + config::set(isDown); + break; + case KeybindMode::HoldOff: + config::set(!isDown); + break; + default: + break; + } + + return false; + } + ); } \ No newline at end of file diff --git a/src/UI/HeliosLayer.cpp b/src/UI/HeliosLayer.cpp index 0446791f..b8d37557 100644 --- a/src/UI/HeliosLayer.cpp +++ b/src/UI/HeliosLayer.cpp @@ -48,7 +48,7 @@ namespace eclipse { }; static Helios::Vec2 cocosToFrame(CCPoint const& pos) { - auto frameSize = CCEGLView::get()->m_obScreenSize * geode::utils::getDisplayFactor(); + auto frameSize = CCEGLView::get()->m_obScreenSize * utils::getDisplayFactor(); auto winSize = CCDirector::get()->m_obWinSizeInPoints; return Helios::Vec2{ pos.x / winSize.width * frameSize.width, @@ -93,6 +93,8 @@ namespace eclipse { return false; } + this->setToggled(false); + Helios::Director::get() .setCursorCallback([](Helios::MouseCursor cursor) { using Helios::MouseCursor; From 8570eb39fd865b58de7f64f6ceef9354e3a68e69 Mon Sep 17 00:00:00 2001 From: prevter Date: Sun, 31 May 2026 00:19:19 +0300 Subject: [PATCH 3/3] Bump helios commit --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f68773f0..4d21f643 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,7 @@ endif() add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) -CPMAddPackage("gh:prevter/HeliosUI#bf49a83") +CPMAddPackage("gh:prevter/HeliosUI#49c6864") target_compile_definitions(HeliosUI PUBLIC HELIOS_OPENGL_LOADER_INCLUDE="Geode/cocos/platform/CCGL.h") target_link_libraries(HeliosUI PRIVATE geode-sdk) target_link_libraries(${PROJECT_NAME} Helios::UI)