diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c01e824f..93ed5d0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: - name: Configure CMake run: | - ${{ matrix.config.prefixes }} cmake -B ${{ github.workspace }}/build ${{ matrix.config.extra_flags }} + ${{ matrix.config.prefixes }} cmake -B ${{ github.workspace }}/build ${{ matrix.config.extra_flags }} -DGEODE_DONT_PACKAGE_RESOURCES - name: Build run: | diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..b4a0a1f3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "loader/md4c"] + path = loader/md4c + url = https://github.com/mity/md4c diff --git a/cmake/GeodeFile.cmake b/cmake/GeodeFile.cmake index 03581218..3c040da9 100644 --- a/cmake/GeodeFile.cmake +++ b/cmake/GeodeFile.cmake @@ -23,3 +23,18 @@ function(create_geode_file proname) endif() endfunction() + +function(package_geode_resources proname src dest prefix) + message(STATUS "Packaging resources from ${src} with prefix ${prefix} into ${dest}") + + if(GEODE_CLI STREQUAL "GEODE_CLI-NOTFOUND") + message(WARNING "package_geode_resources called, but Geode CLI was not found - You will need to manually package the resources") + else() + + add_custom_target(${proname}_PACKAGE ALL + DEPENDS ${proname} + COMMAND ${GEODE_CLI} resources ${src} ${dest} --prefix ${prefix} --cached + VERBATIM USES_TERMINAL + ) + endif() +endfunction() diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index cf645617..4f8021f7 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -3,9 +3,11 @@ cmake_minimum_required(VERSION 3.21 FATAL_ERROR) project(geode-loader VERSION 0.2.0 LANGUAGES C CXX) set(PROJECT_VERSION_TYPE Alpha) +# Package info file for internal representation file(READ resources/about.md LOADER_ABOUT_MD) configure_file(src/internal/about.hpp.in ${CMAKE_CURRENT_SOURCE_DIR}/src/internal/about.hpp) +# Source files file(GLOB CORE_SOURCES src/cocos2d-ext/*.cpp src/core/*.cpp @@ -22,6 +24,13 @@ file(GLOB CORE_SOURCES src/utils/*.cpp src/utils/windows/*.cpp src/utils/zip/*.cpp + src/index/*.cpp + src/ui/nodes/*.cpp + src/ui/internal/credits/*.cpp + src/ui/internal/dev/*.cpp + src/ui/internal/info/*.cpp + src/ui/internal/list/*.cpp + src/ui/internal/settings/*.cpp ) file(GLOB OBJC_SOURCES @@ -33,7 +42,7 @@ file(GLOB OBJC_SOURCES src/utils/mac/*.mm ) -# embed version info in binary +# Embed version info in binary if (WIN32) configure_file(src/internal/windows/info.rc.in info.rc) set(CORE_SOURCES ${CORE_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/info.rc) @@ -55,6 +64,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${GEODE_BIN_PATH}" ) +# Move compiled binary into `bin/nightly` directory if (APPLE) add_custom_command( COMMAND @@ -73,10 +83,23 @@ elseif(WIN32) ) endif() +if (NOT GEODE_DONT_PACKAGE_RESOURCES) + # Package resources for UI + package_geode_resources( + ${PROJECT_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/resources + ${GEODE_BIN_PATH}/nightly/resources + "geode.loader" + ) +endif() + target_include_directories(${PROJECT_NAME} PRIVATE src/internal/ src/platform/ src/gui/ + src/index/ + md4c/src/ + hash/ ./ # lilac ) @@ -85,10 +108,15 @@ set_property(TARGET ${PROJECT_NAME} PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAN target_compile_definitions(${PROJECT_NAME} PUBLIC GEODE_EXPORTING GEODE_PLATFORM_CONSOLE) +# Markdown support +add_subdirectory(md4c) +target_link_libraries(${PROJECT_NAME} md4c) + +# Lilac (hooking) add_subdirectory(lilac) +target_link_libraries(${PROJECT_NAME} z lilac_hook geode-sdk) -target_link_libraries(${PROJECT_NAME} z lilac_hook geode-sdk) # lilac - +# Use precompiled headers for faster builds set_source_files_properties(${OBJC_SOURCES} PROPERTIES SKIP_PRECOMPILE_HEADERS ON) target_precompile_headers(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/Geode/DefaultInclude.hpp" @@ -96,6 +124,7 @@ target_precompile_headers(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/Geode/cocos/cocos2dx/include/cocos2d.h" ) +# Create launcher if (APPLE) if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm" OR GEODE_TARGET_PLATFORM STREQUAL "iOS") add_custom_command(TARGET geode-loader @@ -116,8 +145,12 @@ elseif (WIN32) target_link_libraries(${PROJECT_NAME} dbghelp) endif() +# Build test mods if needed if(NOT GEODE_DONT_BUILD_TEST_MODS) if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) add_subdirectory(test) endif() endif() + +# Build index hashing algorithm test program +add_subdirectory(hash) diff --git a/loader/hash/CMakeLists.txt b/loader/hash/CMakeLists.txt new file mode 100644 index 00000000..8d1fe4ac --- /dev/null +++ b/loader/hash/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.0 FATAL_ERROR) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED On) + +project(GeodeChecksum VERSION 1.0) + +add_executable(${PROJECT_NAME} hash.cpp) + +message(STATUS "Building Checksum Exe") diff --git a/loader/hash/LICENSE b/loader/hash/LICENSE new file mode 100644 index 00000000..76a17d78 --- /dev/null +++ b/loader/hash/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/loader/hash/hash.cpp b/loader/hash/hash.cpp new file mode 100644 index 00000000..d55dbdba --- /dev/null +++ b/loader/hash/hash.cpp @@ -0,0 +1,11 @@ +#include <iostream> +#include "hash.hpp" + +int main(int ac, char* av[]) { + if (ac < 2) { + std::cout << "Usage: \"checksum <file>\"\n"; + return 1; + } + std::cout << calculateHash(av[1]) << std::hex; + return 0; +} diff --git a/loader/hash/hash.hpp b/loader/hash/hash.hpp new file mode 100644 index 00000000..b6caa528 --- /dev/null +++ b/loader/hash/hash.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include <string> +#include <fstream> +#include <ciso646> +#include "picosha3.h" +#include <vector> + +static std::string calculateHash(std::string const& path) { + std::vector<uint8_t> s(picosha3::bits_to_bytes(256)); + auto sha3_256 = picosha3::get_sha3_generator<256>(); + std::ifstream file(path); + return sha3_256.get_hex_string(file); +} diff --git a/loader/hash/picosha3.h b/loader/hash/picosha3.h new file mode 100644 index 00000000..54ac7b59 --- /dev/null +++ b/loader/hash/picosha3.h @@ -0,0 +1,361 @@ +#ifndef PICOSHA3_H +#define PICOSHA3_H + +#include <array> +#include <cassert> +#include <fstream> +#include <iomanip> +#include <sstream> + +namespace picosha3 { + constexpr size_t bits_to_bytes(size_t bits) { return bits / 8; }; + constexpr static size_t b_bytes = bits_to_bytes(1600); + constexpr static uint64_t RC[24] = { + 0x0000000000000001ull, 0x0000000000008082ull, 0x800000000000808Aull, + 0x8000000080008000ull, 0x000000000000808Bull, 0x0000000080000001ull, + 0x8000000080008081ull, 0x8000000000008009ull, 0x000000000000008Aull, + 0x0000000000000088ull, 0x0000000080008009ull, 0x000000008000000Aull, + 0x000000008000808Bull, 0x800000000000008Bull, 0x8000000000008089ull, + 0x8000000000008003ull, 0x8000000000008002ull, 0x8000000000000080ull, + 0x000000000000800Aull, 0x800000008000000Aull, 0x8000000080008081ull, + 0x8000000000008080ull, 0x0000000080000001ull, 0x8000000080008008ull}; + + using byte_t = uint8_t; + using state_t = std::array<std::array<uint64_t, 5>, 5>; + + inline void theta(state_t& A) { + uint64_t C[5] = {0, 0, 0, 0, 0}; + for(size_t x = 0; x < 5; ++x) { + C[x] = A[x][0] ^ A[x][1] ^ A[x][2] ^ A[x][3] ^ A[x][4]; + }; + uint64_t D[5] = {0, 0, 0, 0, 0}; + D[0] = C[4] ^ (C[1] << 1 | C[1] >> (64 - 1)); + D[1] = C[0] ^ (C[2] << 1 | C[2] >> (64 - 1)); + D[2] = C[1] ^ (C[3] << 1 | C[3] >> (64 - 1)); + D[3] = C[2] ^ (C[4] << 1 | C[4] >> (64 - 1)); + D[4] = C[3] ^ (C[0] << 1 | C[0] >> (64 - 1)); + for(size_t x = 0; x < 5; ++x) { + for(size_t y = 0; y < 5; ++y) { + A[x][y] ^= D[x]; + } + } + }; + + inline void rho(state_t& A) { + size_t x{1}; + size_t y{0}; + for(size_t t = 0; t < 24; ++t) { + size_t offset = ((t + 1) * (t + 2) / 2) % 64; + A[x][y] = (A[x][y] << offset) | (A[x][y] >> (64 - offset)); + size_t tmp{y}; + y = (2 * x + 3 * y) % 5; + x = tmp; + }; + }; + + inline void pi(state_t& A) { + state_t tmp{A}; + for(size_t x = 0; x < 5; ++x) { + for(size_t y = 0; y < 5; ++y) { + A[x][y] = tmp[(x + 3 * y) % 5][x]; + } + } + }; + + inline void chi(state_t& A) { + state_t tmp{A}; + for(size_t x = 0; x < 5; ++x) { + for(size_t y = 0; y < 5; ++y) { + A[x][y] = + tmp[x][y] ^ (~(tmp[(x + 1) % 5][y]) & tmp[(x + 2) % 5][y]); + } + } + }; + + inline void iota(state_t& A, size_t round_index) { + A[0][0] ^= RC[round_index]; + }; + + inline void keccak_p(state_t& A) { + for(size_t round_index = 0; round_index < 24; ++round_index) { + theta(A); + rho(A); + pi(A); + chi(A); + iota(A, round_index); + } + }; + + namespace { + inline void next(size_t& x, size_t& y, size_t& i) { + if(++i != 8) { + return; + } + i = 0; + if(++x != 5) { + return; + } + x = 0; + if(++y != 5) { + return; + } + } + } // namespace + + template <typename InIter> + void absorb(InIter first, InIter last, state_t& A) { + size_t x = 0; + size_t y = 0; + size_t i = 0; + for(; first != last && y < 5; ++first) { + auto tmp = static_cast<uint64_t>(*first); + A[x][y] ^= (tmp << (i * 8)); + next(x, y, i); + }; + } + + template <typename InContainer> + void absorb(const InContainer& src, state_t& A) { + absorb(src.cbegin(), src.cend(), A); + }; + + template <typename OutIter> + OutIter squeeze(const state_t& A, OutIter first, OutIter last, + size_t rate_bytes) { + size_t x = 0; + size_t y = 0; + size_t i = 0; + for(size_t read_bytes = 0; + first != last && y < 5 && read_bytes < rate_bytes; + ++read_bytes, ++first) { + auto tmp = static_cast<uint64_t>(A[x][y]); + auto p = reinterpret_cast<byte_t*>(&tmp); + *first = *(p + i); + next(x, y, i); + } + return first; + }; + + template <typename OutContainer> + typename OutContainer::iterator + squeeze(const state_t& A, OutContainer& dest, size_t rate_bytes) { + return squeeze(A, dest.begin(), dest.end(), rate_bytes); + } + + enum class PaddingType { + SHA, + SHAKE, + }; + + template <typename InIter> + std::string bytes_to_hex_string(InIter first, InIter last) { + std::stringstream ss; + ss << std::hex; + for(; first != last; ++first) { + ss << std::setw(2) << std::setfill('0') + << static_cast<uint64_t>(*first); + } + return ss.str(); + } + + template <typename InContainer> + std::string bytes_to_hex_string(const InContainer& src) { + return bytes_to_hex_string(src.cbegin(), src.cend()); + } + + template <size_t rate_bytes, size_t d_bytes, PaddingType padding_type> + class HashGenerator { + public: + HashGenerator() + : buffer_{}, buffer_pos_{buffer_.begin()}, A_{}, hash_{}, + is_finished_{false} {} + + void clear() { + clear_state(); + clear_buffer(); + is_finished_ = false; + } + + template <typename InIter> + void process(InIter first, InIter last) { + static_assert( + sizeof(typename std::iterator_traits<InIter>::value_type) == 1, + "The size of input iterator value_type must be one byte."); + + for(; first != last; ++first) { + *buffer_pos_ = *first; + if(++buffer_pos_ == buffer_.end()) { + absorb(buffer_, A_); + keccak_p(A_); + clear_buffer(); + } + } + }; + + void finish() { + add_padding(); + absorb(buffer_, A_); + keccak_p(A_); + squeeze_(); + is_finished_ = true; + }; + + template <typename OutIter> + void get_hash_bytes(OutIter first, OutIter last) { + if(!is_finished_) { + throw std::runtime_error("Not finished!"); + } + std::copy(hash_.cbegin(), hash_.cend(), first); + }; + + template <typename OutCotainer> + void get_hash_bytes(OutCotainer& dest) { + get_hash_bytes(dest.begin(), dest.end()); + }; + + template <typename InIter, typename OutIter> + void operator()(InIter in_first, InIter in_last, OutIter out_first, + OutIter out_last) { + static_assert( + sizeof(typename std::iterator_traits<InIter>::value_type) == 1, + "The size of input iterator value_type must be one byte."); + static_assert( + sizeof(typename std::iterator_traits<OutIter>::value_type) == 1, + "The size of output iterator value_type must be one byte."); + process(in_first, in_last); + finish(); + std::copy(hash_.cbegin(), hash_.cend(), out_first); + clear(); + }; + + template <typename InIter, typename OutCotainer> + void operator()(InIter in_first, InIter in_last, OutCotainer& dest) { + operator()(in_first, in_last, dest.begin(), dest.end()); + }; + + template <typename InContainer, typename OutIter> + void operator()(const InContainer& src, OutIter out_first, + OutIter out_last) { + operator()(src.cbegin(), src.cend(), out_first, out_last); + }; + + template <typename InContainer, typename OutContainer> + void operator()(const InContainer& src, OutContainer& dest) { + operator()(src.cbegin(), src.cend(), dest.begin(), dest.end()); + }; + + template <typename OutIter> + void operator()(std::ifstream& ifs, OutIter out_first, + OutIter out_last) { + auto in_first = std::istreambuf_iterator<char>(ifs); + auto in_last = std::istreambuf_iterator<char>(); + operator()(in_first, in_last, out_first, out_last); + }; + + template <typename OutCotainer> + void operator()(std::ifstream& ifs, OutCotainer& dest) { + operator()(ifs, dest.begin(), dest.end()); + }; + + std::string get_hex_string() { + if(!is_finished_) { + throw std::runtime_error("Not finished!"); + } + return bytes_to_hex_string(hash_); + }; + + template <typename InIter> + std::string get_hex_string(InIter in_first, InIter in_last) { + process(in_first, in_last); + finish(); + auto hash = get_hex_string(); + clear(); + return hash; + }; + + template <typename InContainer> + std::string get_hex_string(const InContainer& src) { + return get_hex_string(src.cbegin(), src.cend()); + }; + + std::string get_hex_string(std::ifstream& ifs) { + auto in_first = std::istreambuf_iterator<char>(ifs); + auto in_last = std::istreambuf_iterator<char>(); + return get_hex_string(in_first, in_last); + }; + + private: + void clear_buffer() { + buffer_.fill(0); + buffer_pos_ = buffer_.begin(); + }; + + void clear_state() { + for(auto& row : A_) { + row.fill(0); + } + }; + + void add_padding() { + const auto q = + buffer_.size() - std::distance(buffer_pos_, buffer_.begin()); + + if(padding_type == PaddingType::SHA) { + if(q == 1) { + *buffer_pos_ = 0x86; + } else { + *buffer_pos_ = 0x06; + buffer_.back() = 0x80; + } + } else if(padding_type == PaddingType::SHAKE) { + if(q == 1) { + *buffer_pos_ = 0x9F; + } else { + *buffer_pos_ = 0x1F; + buffer_.back() = 0x80; + } + } + }; + + void squeeze_() { + auto first = hash_.begin(); + auto last = hash_.end(); + first = squeeze(A_, first, last, rate_bytes); + while(first != last) { + keccak_p(A_); + first = squeeze(A_, first, last, rate_bytes); + } + }; + + std::array<byte_t, rate_bytes> buffer_; + typename decltype(buffer_)::iterator buffer_pos_; + state_t A_; + std::array<byte_t, d_bytes> hash_; + bool is_finished_; + }; + + template <size_t d_bits> + auto get_sha3_generator() { + static_assert( + d_bits == 224 or d_bits == 256 or d_bits == 384 or d_bits == 512, + "SHA3 only accepts digest message length 224, 256 384 or 512 bits."); + constexpr auto d_bytes = bits_to_bytes(d_bits); + constexpr auto capacity_bytes = d_bytes * 2; + constexpr auto rate_bytes = b_bytes - capacity_bytes; + return HashGenerator<rate_bytes, d_bytes, PaddingType::SHA>{}; + } + + template <size_t strength_bits, size_t d_bits> + auto get_shake_generator() { + static_assert(strength_bits == 128 or strength_bits == 256, + "SHAKE only accepts strength 128 or 256 bits."); + constexpr auto strength_bytes = bits_to_bytes(strength_bits); + constexpr auto capacity_bytes = strength_bytes * 2; + constexpr auto rate_bytes = b_bytes - capacity_bytes; + constexpr auto d_bytes = bits_to_bytes(d_bits); + return HashGenerator<rate_bytes, d_bytes, PaddingType::SHAKE>{}; + } + +} // namespace picosha3 + +#endif diff --git a/loader/include/Geode/Geode.hpp b/loader/include/Geode/Geode.hpp index 67aa9999..19315f47 100644 --- a/loader/include/Geode/Geode.hpp +++ b/loader/include/Geode/Geode.hpp @@ -3,4 +3,5 @@ #include "Bindings.hpp" #include "Utils.hpp" #include "Loader.hpp" -#include "Modify.hpp" \ No newline at end of file +#include "Modify.hpp" +#include "UI.hpp" diff --git a/loader/include/Geode/UI.hpp b/loader/include/Geode/UI.hpp new file mode 100644 index 00000000..4e8522ee --- /dev/null +++ b/loader/include/Geode/UI.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "ui/BasedButton.hpp" +#include "ui/BasedButtonSprite.hpp" +#include "ui/IconButtonSprite.hpp" +#include "ui/InputNode.hpp" +#include "ui/ListView.hpp" +#include "ui/MDTextArea.hpp" +#include "ui/MenuInputNode.hpp" +#include "ui/Notification.hpp" +#include "ui/Popup.hpp" +#include "ui/SceneManager.hpp" +#include "ui/Scrollbar.hpp" +#include "ui/ScrollLayer.hpp" +#include "ui/TextRenderer.hpp" diff --git a/loader/include/Geode/Utils.hpp b/loader/include/Geode/Utils.hpp index 22f578fb..c7f80fb0 100644 --- a/loader/include/Geode/Utils.hpp +++ b/loader/include/Geode/Utils.hpp @@ -13,4 +13,5 @@ #include "utils/ext.hpp" #include "utils/convert.hpp" #include "utils/cocos.hpp" -#include "utils/operators.hpp" \ No newline at end of file +#include "utils/operators.hpp" +#include "utils/Ref.hpp" diff --git a/loader/include/Geode/ui/BasedButton.hpp b/loader/include/Geode/ui/BasedButton.hpp new file mode 100644 index 00000000..58ea43ac --- /dev/null +++ b/loader/include/Geode/ui/BasedButton.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "BasedButtonSprite.hpp" + +#pragma warning(disable : 4275) + +namespace geode { + class GEODE_DLL TabButton : public CCMenuItemToggler { + public: + static TabButton* create( + TabBaseColor unselected, + TabBaseColor selected, + const char* text, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler callback + ); + + static TabButton* create( + const char* text, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler callback + ); + }; +} // namespace geode diff --git a/loader/include/Geode/ui/BasedButtonSprite.hpp b/loader/include/Geode/ui/BasedButtonSprite.hpp new file mode 100644 index 00000000..a492cc68 --- /dev/null +++ b/loader/include/Geode/ui/BasedButtonSprite.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + enum class CircleBaseSize { + Tiny = 0, // Equivalent to the tiny delete button + Small = 1, // Equivalent to most circular buttons in the editor + Small2 = 2, // Equivalent to the trash button in the editor + Medium = 3, // Equivalent to most buttons + Medium2 = 4, // Equivalent to the bottom buttons in MenuLayer + Big = 5, // Equivalent to the New button + Big2 = 6, // Equivalent to the Account button + Large = 7, // Equivalent to the big Play Button + }; + + enum class CrossBaseSize { + Small = 0, + Huge = 1, + }; + + enum class CircleBaseColor { + Green = 0, + Pink = 1, + Gray = 2, + Blue = 3, + Cyan = 4, + Geode = 5, + }; + + enum class AccountBaseColor { + Blue = 0, + Gray = 1, + Purple = 2, + }; + + enum class IconSelectBaseColor { + Gray = 0, + Selected = 1, + }; + + enum class EditorBaseColor { + LightBlue = 0, + Green = 1, + Orange = 2, + DarkGray = 3, + Gray = 4, + Pink = 5, + Teal = 6, + Aqua = 7, + Cyan = 8, + }; + + enum class TabBaseColor { + Unselected = 0, + Selected = 1, + UnselectedDark = 2, + }; + + enum class BaseType { + Circle = 0, + Cross = 1, + Account = 2, + IconSelect = 3, + GlobalThing = 4, + Editor = 5, + Tab = 6, + }; + + /** + * Represents a GD button sprite where there's + * an icon sprite on top another default sprite. + * You know, it has a base. It's based. + * lmao trademark lizbith + */ + class GEODE_DLL BasedButtonSprite : public cocos2d::CCSprite { + protected: + int m_type; + int m_size; + int m_color; + cocos2d::CCNode* m_onTop = nullptr; + + bool init(cocos2d::CCNode* ontop, int type, int size, int color); + bool initWithSprite(const char* sprName, float sprScale, int type, int size, int color); + bool initWithSpriteFrameName(const char* sprName, float sprScale, int type, int size, int color); + + cocos2d::CCPoint getTopOffset() const; + + virtual ~BasedButtonSprite(); + + public: + static BasedButtonSprite* create(cocos2d::CCNode* ontop, int type, int size, int color); + }; + + class GEODE_DLL CircleButtonSprite : public BasedButtonSprite { + public: + static CircleButtonSprite* create( + cocos2d::CCNode* top, + CircleBaseColor color = CircleBaseColor::Green, + CircleBaseSize size = CircleBaseSize::Medium + ); + static CircleButtonSprite* createWithSprite( + const char* sprName, + float sprScale = 1.f, + CircleBaseColor color = CircleBaseColor::Green, + CircleBaseSize size = CircleBaseSize::Medium + ); + static CircleButtonSprite* createWithSpriteFrameName( + const char* sprName, + float sprScale = 1.f, + CircleBaseColor color = CircleBaseColor::Green, + CircleBaseSize size = CircleBaseSize::Medium + ); + }; + + class GEODE_DLL EditorButtonSprite : public BasedButtonSprite { + public: + static EditorButtonSprite* create(cocos2d::CCNode* top, EditorBaseColor color); + static EditorButtonSprite* createWithSprite( + const char* sprName, + float sprScale = 1.f, + EditorBaseColor color = EditorBaseColor::Green + ); + static EditorButtonSprite* createWithSpriteFrameName( + const char* sprName, + float sprScale = 1.f, + EditorBaseColor color = EditorBaseColor::Green + ); + }; + + class GEODE_DLL TabButtonSprite : public BasedButtonSprite { + public: + static TabButtonSprite* create(const char* text, TabBaseColor color); + }; +} diff --git a/loader/include/Geode/ui/IconButtonSprite.hpp b/loader/include/Geode/ui/IconButtonSprite.hpp new file mode 100644 index 00000000..6d823646 --- /dev/null +++ b/loader/include/Geode/ui/IconButtonSprite.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + class GEODE_DLL IconButtonSprite : + public cocos2d::CCSprite, + public cocos2d::CCLabelProtocol + { + protected: + cocos2d::extension::CCScale9Sprite* m_bg = nullptr; + cocos2d::CCLabelBMFont* m_label = nullptr; + cocos2d::CCNode* m_icon = nullptr; + + bool init( + const char* bg, + bool bgIsFrame, + cocos2d::CCNode* icon, + const char* text, + const char* font + ); + + void updateLayout(); + + IconButtonSprite() = default; + IconButtonSprite(IconButtonSprite&&) = delete; + IconButtonSprite& operator=(IconButtonSprite&&) = delete; + + public: + static IconButtonSprite* create( + const char* bg, + cocos2d::CCNode* icon, + const char* text, + const char* font + ); + static IconButtonSprite* createWithSpriteFrameName( + const char* bg, + cocos2d::CCNode* icon, + const char* text, + const char* font + ); + + void setBG(const char* bg, bool isFrame); + + void setIcon(cocos2d::CCNode* icon); + cocos2d::CCNode* getIcon() const; + + void setString(const char* label) override; + const char* getString() override; + }; +} diff --git a/loader/include/Geode/ui/InputNode.hpp b/loader/include/Geode/ui/InputNode.hpp new file mode 100644 index 00000000..3819d706 --- /dev/null +++ b/loader/include/Geode/ui/InputNode.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + class GEODE_DLL InputNode : public cocos2d::CCNode { + protected: + cocos2d::extension::CCScale9Sprite* m_bgSprite; + CCTextInputNode* m_input; + + bool init(float, float, const char*, const char*, std::string const&, int); + bool init(float, const char*, const char*, std::string const&, int); + + public: + static InputNode* create( + float width, + const char* placeholder, + const char* fontFile, + std::string const& filter, + int limit + ); + static InputNode* create( + float width, + const char* placeholder, + std::string const& filter, + int limit + ); + static InputNode* create( + float width, + const char* placeholder, + std::string const& filter + ); + static InputNode* create( + float width, + const char* placeholder, + const char* fontFile + ); + static InputNode* create( + float width, + const char* placeholder + ); + + CCTextInputNode* getInputNode() const; + cocos2d::extension::CCScale9Sprite* getBGSprite() const; + + void setEnabled(bool); + + void setString(const char*); + const char* getString(); + }; +} + + diff --git a/loader/include/Geode/ui/ListView.hpp b/loader/include/Geode/ui/ListView.hpp new file mode 100644 index 00000000..f436dda9 --- /dev/null +++ b/loader/include/Geode/ui/ListView.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + class GEODE_DLL GenericListCell : public TableViewCell { + protected: + GenericListCell(const char* name, cocos2d::CCSize size); + + void draw() override; + + public: + static GenericListCell* create(const char* key, cocos2d::CCSize size); + + void updateBGColor(int index); + }; + + /** + * Class for a generic scrollable list of + * items like the level list in GD + */ + class GEODE_DLL ListView : public CustomListView { + protected: + void setupList() override; + TableViewCell* getListCell(const char* key) override; + void loadCell(TableViewCell* cell, unsigned int index) override; + + public: + /** + * Create a generic scrollable list of + * items + * @param items Nodes to add as children + * @param itemHeight Height of each child + * @param width Width of the list + * @param height Height of the list + * @returns The created ListView, or nullptr + * on error + */ + static ListView* create( + cocos2d::CCArray* items, + float itemHeight = 40.f, + float width = 358.f, + float height = 220.f + ); + }; +} + + diff --git a/loader/include/Geode/ui/MDTextArea.hpp b/loader/include/Geode/ui/MDTextArea.hpp new file mode 100644 index 00000000..184e921c --- /dev/null +++ b/loader/include/Geode/ui/MDTextArea.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include <Geode/Geode.hpp> +#include "TextRenderer.hpp" +#include "ScrollLayer.hpp" + +struct MDParser; + +namespace geode { + /** + * TextArea for static markdown content. Supports the + * following features: + * - Links + * - Images (sprites & spritesheets) + * - Headings + * - Paragraphs + * - Code blocks + * - Code spans + * - TextArea color tags (<cr>, <cy>, etc.) + * - Strikethrough + * - Underline + * - Bold & italic + * - Horizontal rules + * - Lists + * Note that links also have some special protocols. + * Use `user:<id>` or `user:<name>` to link to a GD + * account; `level:<id>` to link to a GD level and + * `mod:<id>` to link to another Geode mod. + */ + class GEODE_DLL MDTextArea : + public cocos2d::CCLayer, + public cocos2d::CCLabelProtocol, + public FLAlertLayerProtocol + { + protected: + std::string m_text; + cocos2d::CCSize m_size; + cocos2d::extension::CCScale9Sprite* m_bgSprite = nullptr; + cocos2d::CCMenu* m_content = nullptr; + CCScrollLayerExt* m_scrollLayer = nullptr; + TextRenderer* m_renderer = nullptr; + + bool init(std::string const& str, cocos2d::CCSize const& size); + + virtual ~MDTextArea(); + + void onLink(CCObject*); + void onGDProfile(CCObject*); + void FLAlert_Clicked(FLAlertLayer*, bool btn) override; + + friend struct ::MDParser; + + public: + /** + * Create a markdown text area. See class + * documentation for details on supported + * features & notes. + * @param str String to render + * @param size Size of the textarea + */ + static MDTextArea* create(std::string const& str, cocos2d::CCSize const& size); + + /** + * Update the label's content; call + * sparingly as rendering may be slow + */ + void updateLabel(); + + void setString(const char* text) override; + const char* getString() override; + + CCScrollLayerExt* getScrollLayer() const; + }; +} diff --git a/loader/include/Geode/ui/MenuInputNode.hpp b/loader/include/Geode/ui/MenuInputNode.hpp new file mode 100644 index 00000000..c046226d --- /dev/null +++ b/loader/include/Geode/ui/MenuInputNode.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + /** + * Simple wrapper around CCTextInputNode that + * turns it into a CCMenuItem that can be used + * in a CCMenu. Can help with touch dispatcher + * issues. Also comes with a background sprite + */ + class GEODE_DLL MenuInputNode : public cocos2d::CCMenuItem { + protected: + cocos2d::extension::CCScale9Sprite* m_bgSprite = nullptr; + CCTextInputNode* m_input; + + bool init( + float width, + float height, + const char* placeholder, + const char* font, + bool bg + ); + + public: + static MenuInputNode* create( + float width, + float height, + const char* placeholder, + const char* font, + bool bg = false + ); + + void selected() override; + + CCTextInputNode* getInput() const; + }; +} + diff --git a/loader/include/Geode/ui/Notification.hpp b/loader/include/Geode/ui/Notification.hpp new file mode 100644 index 00000000..dc4b2a13 --- /dev/null +++ b/loader/include/Geode/ui/Notification.hpp @@ -0,0 +1,188 @@ +#pragma once + +#include <Geode/Geode.hpp> +#include "SceneManager.hpp" +#include <chrono> +#include "../utils/Ref.hpp" + +namespace geode { + enum class NotificationLocation { + TopLeft, + TopCenter, + TopRight, + BottomLeft, + BottomCenter, + BottomRight, + }; + + static constexpr float DEFAULT_NOTIFICATION_TIME = 4.f; + static constexpr NotificationLocation PLATFORM_NOTIFICATION_LOCATION = + #ifdef GEODE_IS_DESKTOP + NotificationLocation::BottomRight; + #else + NotificationLocation::TopCenter; + #endif; + + class Notification; + class NotificationManager; + + struct GEODE_DLL NotificationBuilder { + Mod* m_owner = Mod::get(); + std::string m_title = ""; + std::string m_text = ""; + std::string m_icon = "GJ_infoIcon_001.png"; + Ref<cocos2d::CCNode> m_iconNode = nullptr; + std::string m_bg = "GJ_square02.png"; + std::function<void(Notification*)> m_callback = nullptr; + float m_time = DEFAULT_NOTIFICATION_TIME; + NotificationLocation m_location = PLATFORM_NOTIFICATION_LOCATION; + bool m_hideOnClick = true; + + inline NotificationBuilder& from(Mod* owner) { + m_owner = owner; + return *this; + } + inline NotificationBuilder& title(std::string const& title) { + m_title = title; + return *this; + } + inline NotificationBuilder& text(std::string const& text) { + m_text = text; + return *this; + } + inline NotificationBuilder& icon(std::string const& icon) { + m_icon = icon; + m_iconNode = nullptr; + return *this; + } + inline NotificationBuilder& icon(cocos2d::CCNode* icon) { + m_icon = ""; + m_iconNode = icon; + return *this; + } + inline NotificationBuilder& loading() { + auto spr = cocos2d::CCSprite::create("loadingCircle.png"); + spr->runAction(cocos2d::CCRepeat::create( + cocos2d::CCRotateBy::create(1.f, 360.f), 40000 + )); + spr->setBlendFunc({ GL_ONE, GL_ONE }); + return this->icon(spr); + } + inline NotificationBuilder& bg(std::string const& bg) { + m_bg = bg; + return *this; + } + inline NotificationBuilder& location(NotificationLocation location) { + m_location = location; + return *this; + } + inline NotificationBuilder& time(float time) { + m_time = time; + return *this; + } + inline NotificationBuilder& stay() { + m_time = .0f; + return *this; + } + inline NotificationBuilder& clicked( + std::function<void(Notification*)> cb, + bool hide = true + ) { + m_callback = cb; + m_hideOnClick = hide; + return *this; + } + Notification* show(); + }; + + class GEODE_DLL Notification : public cocos2d::CCLayer { + protected: + Mod* m_owner; + std::function<void(Notification*)> m_callback = nullptr; + cocos2d::extension::CCScale9Sprite* m_bg; + cocos2d::CCNode* m_icon = nullptr; + cocos2d::CCLabelBMFont* m_title = nullptr; + Ref<cocos2d::CCArray> m_labels = nullptr; + cocos2d::CCPoint m_showDest; + cocos2d::CCPoint m_hideDest; + cocos2d::CCPoint m_posAtTouchStart; + NotificationLocation m_location; + float m_time; + bool m_hiding = false; + bool m_clicking; + bool m_hovered; + bool m_hideOnClicked = true; + float m_targetScale = 1.f; + + bool init( + Mod* owner, + std::string const& title, + std::string const& text, + cocos2d::CCNode* icon, + const char* bg, + std::function<void(Notification*)> callback, + bool hideOnClick + ); + + Notification(); + virtual ~Notification(); + + bool ccTouchBegan( + cocos2d::CCTouch* touch, cocos2d::CCEvent* event + ) override; + void ccTouchEnded( + cocos2d::CCTouch* touch, cocos2d::CCEvent* event + ) override; + void ccTouchMoved( + cocos2d::CCTouch* touch, cocos2d::CCEvent* event + ) override; + void registerWithTouchDispatcher() override; + + void clicked(); + + void animateIn(); + void animateOut(); + void animateOutClicked(); + void animateClicking(); + + void hidden(); + void showForReal(); + + friend class NotificationManager; + + public: + static Notification* create( + Mod* owner, + std::string const& title, + std::string const& text, + cocos2d::CCNode* icon, + const char* bg, + std::function<void(Notification*)> callback, + bool hideOnClick + ); + static NotificationBuilder build(); + + void show( + NotificationLocation = PLATFORM_NOTIFICATION_LOCATION, + float time = DEFAULT_NOTIFICATION_TIME + ); + void hide(); + }; + + class NotificationManager { + protected: + std::unordered_map< + NotificationLocation, std::vector<Ref<Notification>> + > m_notifications; + + void push(Notification*); + void pop(Notification*); + + bool isInQueue(Notification*); + + friend class Notification; + + public: + static NotificationManager* get(); + }; +} diff --git a/loader/include/Geode/ui/Popup.hpp b/loader/include/Geode/ui/Popup.hpp new file mode 100644 index 00000000..127d04a5 --- /dev/null +++ b/loader/include/Geode/ui/Popup.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + template<typename T, typename... InitArgs> + class GEODE_DLL Popup : public FLAlertLayer { + protected: + cocos2d::CCSize m_size; + cocos2d::extension::CCScale9Sprite* m_bgSprite; + + bool init( + float width, + float height, + InitArgs... args, + const char* bg = "GJ_square01.png" + ) { + auto winSize = cocos2d::CCDirector::sharedDirector()->getWinSize(); + m_size = cocos2d::CCSize{width, height}; + + if (!this->initWithColor({0, 0, 0, 105})) return false; + m_mainLayer = cocos2d::CCLayer::create(); + this->addChild(m_mainLayer); + + m_bgSprite = cocos2d::extension::CCScale9Sprite::create(bg, {0.0f, 0.0f, 80.0f, 80.0f}); + m_bgSprite->setContentSize(m_size); + m_bgSprite->setPosition(winSize.width / 2, winSize.height / 2); + m_mainLayer->addChild(m_bgSprite); + + m_buttonMenu = cocos2d::CCMenu::create(); + m_mainLayer->addChild(m_buttonMenu); + + cocos2d::CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + this->setup(args...); + + auto closeSpr = cocos2d::CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); + closeSpr->setScale(.8f); + + auto closeBtn = CCMenuItemSpriteExtra::create(closeSpr, this, (cocos2d::SEL_MenuHandler)(&Popup::onClose)); + closeBtn->setPosition(-m_size.width / 2 + 3.f, m_size.height / 2 - 3.f); + m_buttonMenu->addChild(closeBtn); + + this->setKeypadEnabled(true); + this->setTouchEnabled(true); + + return true; + } + + virtual void setup(InitArgs... args) = 0; + + void keyDown(cocos2d::enumKeyCodes key) { + if (key == cocos2d::enumKeyCodes::KEY_Escape) return this->onClose(nullptr); + if (key == cocos2d::enumKeyCodes::KEY_Space) return; + return FLAlertLayer::keyDown(key); + } + + virtual void onClose(cocos2d::CCObject*) { + this->setKeyboardEnabled(false); + this->removeFromParentAndCleanup(true); + } + }; + + void GEODE_DLL createQuickPopup( + const char* title, + std::string const& content, + const char* btn1, + const char* btn2, + std::function<void(FLAlertLayer*, bool)> selected + ); +} + + diff --git a/loader/include/Geode/ui/SceneManager.hpp b/loader/include/Geode/ui/SceneManager.hpp new file mode 100644 index 00000000..36b1aef1 --- /dev/null +++ b/loader/include/Geode/ui/SceneManager.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + class GEODE_DLL SceneManager { + protected: + cocos2d::CCArray* m_persistedNodes; + + bool setup(); + + virtual ~SceneManager(); + + public: + static SceneManager* get(); + + void keepAcrossScenes(cocos2d::CCNode* node); + void forget(cocos2d::CCNode* node); + + void willSwitchToScene(cocos2d::CCScene* scene); + }; +} diff --git a/loader/include/Geode/ui/ScrollLayer.hpp b/loader/include/Geode/ui/ScrollLayer.hpp new file mode 100644 index 00000000..3a49c78d --- /dev/null +++ b/loader/include/Geode/ui/ScrollLayer.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + /** + * CCContentLayer expects all of its children + * to be TableViewCells, which is not ideal for + * a generic content layer + */ + class GEODE_DLL GenericContentLayer : public CCContentLayer { + public: + static GenericContentLayer* create( + float width, float height + ); + + void setPosition(cocos2d::CCPoint const& pos) override; + }; + + class GEODE_DLL ScrollLayer : public CCScrollLayerExt { + protected: + bool m_scrollWheelEnabled; + + ScrollLayer( + cocos2d::CCRect const& rect, + bool scrollWheelEnabled, + bool vertical + ); + + public: + static ScrollLayer* create( + cocos2d::CCRect const& rect, + bool scrollWheelEnabled = true, + bool vertical = true + ); + static ScrollLayer* create( + cocos2d::CCSize const& size, + bool scrollWheelEnabled = true, + bool vertical = true + ); + + void scrollWheel(float y, float) override; + void enableScrollWheel(bool enable = true); + }; +} + diff --git a/loader/include/Geode/ui/Scrollbar.hpp b/loader/include/Geode/ui/Scrollbar.hpp new file mode 100644 index 00000000..5b7c8ea9 --- /dev/null +++ b/loader/include/Geode/ui/Scrollbar.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + class GEODE_DLL Scrollbar : + public cocos2d::CCLayer + // public ExtMouseDelegate + { + protected: + CCScrollLayerExt* m_target = nullptr; + cocos2d::extension::CCScale9Sprite* m_track; + cocos2d::extension::CCScale9Sprite* m_thumb; + cocos2d::CCPoint m_clickOffset; + float m_width; + bool m_resizeThumb; + bool m_trackIsRotated; + bool m_hoverHighlight; + + // bool mouseDownExt(MouseEvent, cocos2d::CCPoint const&) override; + // bool mouseUpExt(MouseEvent, cocos2d::CCPoint const&) override; + // void mouseMoveExt(cocos2d::CCPoint const&) override; + void scrollWheel(float y, float x) override; + + void draw() override; + + bool init(CCScrollLayerExt*); + + public: + void setTarget(CCScrollLayerExt* list); + + static Scrollbar* create(CCScrollLayerExt* list); + }; +} diff --git a/loader/include/Geode/ui/TextRenderer.hpp b/loader/include/Geode/ui/TextRenderer.hpp new file mode 100644 index 00000000..a5f664b0 --- /dev/null +++ b/loader/include/Geode/ui/TextRenderer.hpp @@ -0,0 +1,398 @@ +#pragma once + +#include <Geode/Geode.hpp> + +namespace geode { + enum class TextAlignment { + Begin, + Center, + End, + }; + + enum class TextCapitalization { + Normal, + AllUpper, + AllLower, + }; + + // enum only as these are flags + enum TextStyle { + TextStyleRegular = 0b0, + TextStyleBold = 0b1, + TextStyleItalic = 0b10, + }; + + // enum only as these are flags + enum TextDecoration { + TextDecorationNone = 0b0, + TextDecorationUnderline = 0b1, + TextDecorationStrikethrough= 0b10, + }; + + class TextDecorationWrapper; + class TextLinkedButtonWrapper; + + /** + * Utility class for creating rich text content. + * Use to incrementally render strings, and push + * variables to modify the renderer's state. Use + * `begin` to start rendering to a target and + * `end` to finish rendering. + * + * Works for any type of label, although relies + * heavily on content sizes for labels and nodes. + * + * Not too well-performant and the rendering is + * done linearly without so this is not suitable + * for dynamic content. For something like a + * static rich text -area though this can prove + * useful. Used in MDTextArea. + */ + class GEODE_DLL TextRenderer : public cocos2d::CCObject { + public: + /** + * Represents a label. As CCLabelBMFont and + * CCLabelTTF have different inheritance + * structures, this class can handle either + * one universally. All relevant vtables are + * stored in-class to avoid needing to + * `dynamic_cast` everything. + */ + struct Label { + /** + * Label's CCNode vtable + */ + cocos2d::CCNode* m_node; + /** + * Label's CCLabelProtocol vtable + */ + cocos2d::CCLabelProtocol* m_labelProtocol; + /** + * Label's CCRGBAProtocol vtable + */ + cocos2d::CCRGBAProtocol* m_rgbaProtocol; + /** + * Line height. If 0, the renderer will dynamically + * calculate line height based on content size. + */ + float m_lineHeight; + + explicit inline Label() { + m_node = nullptr; + m_labelProtocol = nullptr; + m_rgbaProtocol = nullptr; + m_lineHeight = .0f; + } + + template<class T> + Label(T* label, float lineHeight = .0f) { + static_assert(std::is_base_of_v<cocos2d::CCNode, T>, "Label must inherit from CCNode!"); + static_assert(std::is_base_of_v<cocos2d::CCLabelProtocol, T>, "Label must inherit from CCLabelProtocol!"); + static_assert(std::is_base_of_v<cocos2d::CCRGBAProtocol, T>, "Label must inherit from CCRGBAProtocol!"); + m_node = label; + m_labelProtocol = label; + m_rgbaProtocol = label; + if (lineHeight) { + m_lineHeight = lineHeight; + } else { + if constexpr (std::is_same_v<cocos2d::CCLabelBMFont, T>) { + m_lineHeight = label->getConfiguration()->m_nCommonHeight / cocos2d::CC_CONTENT_SCALE_FACTOR(); + } + } + } + }; + /** + * Label generator function. The `int` parameter + * represents the current text style flags. Use + * to distinguish between bold, italic and + * regular text. + */ + using Font = std::function<Label(int)>; + + protected: + cocos2d::CCPoint m_origin = cocos2d::CCPointZero; + cocos2d::CCSize m_size = cocos2d::CCSizeZero; + cocos2d::CCPoint m_cursor = cocos2d::CCPointZero; + cocos2d::CCNode* m_target = nullptr; + std::vector<Font> m_fontStack; + std::vector<float> m_scaleStack; + std::vector<int> m_styleStack; + std::vector<cocos2d::ccColor3B> m_colorStack; + std::vector<GLubyte> m_opacityStack; + std::vector<int> m_decorationStack; + std::vector<TextCapitalization> m_capsStack; + std::vector<Label> m_lastRendered; + std::vector<float> m_indentationStack; + std::vector<float> m_wrapOffsetStack; + std::vector<TextAlignment> m_hAlignmentStack; + std::vector<TextAlignment> m_vAlignmentStack; + std::vector<cocos2d::CCNode*> m_renderedLine; + cocos2d::CCNode* m_lastRenderedNode = nullptr; + + bool init(); + + Label addWrappers( + Label const& label, + bool isButton, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler callback + ); + bool render(std::string const& word, cocos2d::CCNode* to, cocos2d::CCLabelProtocol* label); + float adjustLineAlignment(); + + public: + /** + * Create a TextRenderer + * @returns Created TextRenderer + */ + static TextRenderer* create(); + virtual ~TextRenderer(); + + /** + * Initialize renderer + * @param target Target node to render to. If nullptr, + * a new CCNode will be created. + * @param pos Position to render to + * @param size Size of the render area. Needed for + * text wrapping & alignment + */ + void begin( + cocos2d::CCNode* target, + cocos2d::CCPoint const& pos = cocos2d::CCPointZero, + cocos2d::CCSize const& size = cocos2d::CCSizeZero + ); + /** + * Finish rendering and clean up renderer + * @param fitToContent Resize the target's content + * size to match the rendered content + * @param horizontalAlign Horizontal alignment of + * the rendered text + * @param verticalAlign Vertical alignment of + * the rendered text + * @returns Target that was rendered onto + */ + cocos2d::CCNode* end( + bool fitToContent = true, + TextAlignment horizontalAlign = TextAlignment::Begin, + TextAlignment verticalAlign = TextAlignment::Begin + ); + + /** + * Render a string with specific settings, bypassing + * current variable stacks. + * @param str String to render + * @param font Font function to use + * @param scale Scale of label + * @param color Label color + * @param opacity Label opacity + * @param style Label style (TextStyle enum) + * @param deco Label decorations (TextDecoration enum) + * @param caps String capitalization + * @param addToTarget Whether to add the created label(s) + * onto the target + * @param isButton If the label should be created as an + * interactive linked button + * @param buttonTarget Target for the label if isButton is + * true, defaults to current renderer target + * @param callback Callback for the label if isButton is + * true + * @returns Vector of rendered labels. The label may be + * split on multiple lines if it exceeds bounds + */ + std::vector<Label> renderStringEx( + std::string const& str, + Font font, + float scale, + cocos2d::ccColor3B color = { 255, 255, 255 }, + GLubyte opacity = 255, + int style = TextStyleRegular, + int deco = TextDecorationNone, + TextCapitalization caps = TextCapitalization::Normal, + bool addToTarget = true, + bool isButton = false, + cocos2d::CCObject* buttonTarget = nullptr, + cocos2d::SEL_MenuHandler callback = nullptr + ); + /** + * Render a string to target. Uses current variable stacks + * for styling and parameters + * @param str String to render + * @returns Vector of rendered labels. The label may be + * split on multiple lines if it exceeds bounds + */ + std::vector<Label> renderString(std::string const& str); + /** + * Render a string to target as a button. Note that the + * target should be a CCMenu for the button to do + * anything. Uses current variable stacks for styling + * and parameters + * @param str String to render + * @param buttonTarget Target for the label if isButton is + * true, defaults to current renderer target + * @param callback Callback for the label if isButton is + * true + * @returns Vector of rendered labels. The label may be + * split on multiple lines if it exceeds bounds + */ + std::vector<Label> renderStringInteractive( + std::string const& str, + cocos2d::CCObject* buttonTarget, + cocos2d::SEL_MenuHandler callback + ); + /** + * Render a node to the current target, use for adding + * images & other content in the middle of text + * @param node Node to render + * @returns Rendered node + */ + cocos2d::CCNode* renderNode(cocos2d::CCNode* node); + /** + * Start next line + * @param y Y offset amount from previous line. If 0, + * will dynamically figure out based on content size + */ + void breakLine(float y = .0f); + + /** + * Helper for pushing a CCLabelBMFont. Make + * sure the const char* outlives the renderer. + */ + void pushBMFont(const char* bmFont); + void pushFont(Font const& font); + void popFont(); + Font getCurrentFont() const; + + void pushScale(float scale); + void popScale(); + float getCurrentScale() const; + + void pushStyleFlags(int style); + void popStyleFlags(); + int getCurrentStyle() const; + + void pushColor(cocos2d::ccColor3B const& color); + void popColor(); + cocos2d::ccColor3B getCurrentColor() const; + + void pushOpacity(GLubyte opacity); + void popOpacity(); + GLubyte getCurrentOpacity() const; + + void pushDecoFlags(int deco); + void popDecoFlags(); + int getCurrentDeco() const; + + void pushCaps(TextCapitalization caps); + void popCaps(); + TextCapitalization getCurrentCaps() const; + + void pushIndent(float indent); + void popIndent(); + float getCurrentIndent() const; + + void pushWrapOffset(float wrapOffset); + void popWrapOffset(); + float getCurrentWrapOffset() const; + + void pushVerticalAlign(TextAlignment align); + void popVerticalAlign(); + TextAlignment getCurrentVerticalAlign() const; + + void pushHorizontalAlign(TextAlignment align); + void popHorizontalAlign(); + TextAlignment getCurrentHorizontalAlign() const; + + void moveCursor(cocos2d::CCPoint const& pos); + cocos2d::CCPoint const& getCursorPos(); + }; + + /** + * Wrapper node for adding decorations (strikethrough, + * underline) to an arbitary label. Is not agnostic of + * font and as such will always render simple lines + */ + class TextDecorationWrapper : public cocos2d::CCNodeRGBA, public cocos2d::CCLabelProtocol { + protected: + int m_deco; + TextRenderer::Label m_label; + + bool init( + TextRenderer::Label const& label, + int decoration, + cocos2d::ccColor3B const& color, + GLubyte opacity + ); + + void draw() override; + + public: + static TextDecorationWrapper* create( + TextRenderer::Label const& label, + int decoration, + cocos2d::ccColor3B const& color, + GLubyte opacity + ); + static TextDecorationWrapper* wrap( + TextRenderer::Label const& label, + int decoration, + cocos2d::ccColor3B const& color, + GLubyte opacity + ); + + void setColor(cocos2d::ccColor3B const& color) override; + void setOpacity(GLubyte opacity) override; + void updateDisplayedColor(cocos2d::ccColor3B const& color) override; + void updateDisplayedOpacity(GLubyte opacity) override; + + void setString(const char* text) override; + const char* getString() override; + }; + + /** + * Wrapper node for making a label clickable. + * Note that this should always be the top + * wrapper above all other wrappers + */ + class TextLinkedButtonWrapper : + public cocos2d::CCMenuItemSprite, + public cocos2d::CCLabelProtocol + { + protected: + TextRenderer::Label m_label; + GLubyte m_opacity; + cocos2d::ccColor3B m_color; + std::vector<TextLinkedButtonWrapper*> m_linked; + + bool init( + TextRenderer::Label const& label, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler handler + ); + + public: + static TextLinkedButtonWrapper* create( + TextRenderer::Label const& label, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler handler + ); + static TextLinkedButtonWrapper* wrap( + TextRenderer::Label const& label, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler handler + ); + + void link(TextLinkedButtonWrapper* other); + + void selectedWithoutPropagation(bool selected); + void selected() override; + void unselected() override; + + void setColor(cocos2d::ccColor3B const& color) override; + void setOpacity(GLubyte opacity) override; + void updateDisplayedColor(cocos2d::ccColor3B const& color) override; + void updateDisplayedOpacity(GLubyte opacity) override; + + void setString(const char* text) override; + const char* getString() override; + }; +} diff --git a/loader/include/Geode/utils/Ref.hpp b/loader/include/Geode/utils/Ref.hpp new file mode 100644 index 00000000..94fac82e --- /dev/null +++ b/loader/include/Geode/utils/Ref.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include <cocos2d.h> + +namespace geode { + /** + * A smart pointer to a managed CCObject-deriving class. Retains shared + * ownership over the managed instance. Releases the object when the Ref + * is destroyed, or assigned another object or nullptr. + * + * Use-cases include, for example, non-CCNode class members, or nodes that + * are not always in the scene tree. + * + * @example class MyNode : public CCNode { + * protected: + * // no need to manually call retain or + * // release on this array; Ref manages it + * // for you :3 + * Ref<CCArray> m_list = CCArray::create(); + * }; + */ + template<class T> + class Ref final { + static_assert( + std::is_base_of_v<cocos2d::CCObject, T>, + "Ref can only be used with a CCObject-inheriting class!" + ); + + T* m_obj = nullptr; + + public: + /** + * Construct a Ref of an object. The object will be retained and + * managed until Ref goes out of scope + */ + Ref(T* obj) : m_obj(obj) { + CC_SAFE_RETAIN(obj); + } + Ref(Ref<T> const& other) : Ref(other.data()) {} + Ref(Ref<T>&& other) : m_obj(other.m_obj) { + other.m_obj = nullptr; + } + /** + * Construct an empty Ref (the managed object will be null) + */ + Ref() = default; + ~Ref() { + CC_SAFE_RELEASE(m_obj); + } + + /** + * Swap the managed object with another object. The managed object + * will be released, and the new object retained + * @param other The new object to swap to + */ + void swap(T* other) { + CC_SAFE_RELEASE(m_obj); + m_obj = other; + CC_SAFE_RETAIN(other); + } + /** + * Return the managed object + * @returns The managed object + */ + T* data() const { + return m_obj; + } + + operator T*() const { + return m_obj; + } + T* operator*() const { + return m_obj; + } + T* operator->() const { + return m_obj; + } + T* operator=(T* obj) { + this->swap(obj); + return obj; + } + Ref<T>& operator=(Ref<T> const& other) { + this->swap(other.data()); + return *this; + } + Ref<T>& operator=(Ref<T>&& other) { + this->swap(other.data()); + return *this; + } + bool operator==(T* other) const { + return m_obj == other; + } + bool operator==(Ref<T> const& other) const { + return m_obj == other.m_obj; + } + }; +} diff --git a/loader/md4c b/loader/md4c new file mode 160000 index 00000000..e9ff661f --- /dev/null +++ b/loader/md4c @@ -0,0 +1 @@ +Subproject commit e9ff661ff818ee94a4a231958d9b6768dc6882c9 diff --git a/loader/resources/blanks/GEODE_blank00_00_00-uhd.png b/loader/resources/blanks/GEODE_blank00_00_00-uhd.png new file mode 100644 index 00000000..1d0a28d7 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_00_01-uhd.png b/loader/resources/blanks/GEODE_blank00_00_01-uhd.png new file mode 100644 index 00000000..02f86ec8 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_00_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_00_02-uhd.png b/loader/resources/blanks/GEODE_blank00_00_02-uhd.png new file mode 100644 index 00000000..1bd93285 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_00_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_00_03-uhd.png b/loader/resources/blanks/GEODE_blank00_00_03-uhd.png new file mode 100644 index 00000000..f1d70b2b Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_00_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_00_04-uhd.png b/loader/resources/blanks/GEODE_blank00_00_04-uhd.png new file mode 100644 index 00000000..f953b2fc Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_00_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_01_00-uhd.png b/loader/resources/blanks/GEODE_blank00_01_00-uhd.png new file mode 100644 index 00000000..8f1690ec Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_01_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_01_01-uhd.png b/loader/resources/blanks/GEODE_blank00_01_01-uhd.png new file mode 100644 index 00000000..fd80a292 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_01_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_01_02-uhd.png b/loader/resources/blanks/GEODE_blank00_01_02-uhd.png new file mode 100644 index 00000000..de8b71dc Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_01_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_01_03-uhd.png b/loader/resources/blanks/GEODE_blank00_01_03-uhd.png new file mode 100644 index 00000000..00e86d9e Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_01_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_01_04-uhd.png b/loader/resources/blanks/GEODE_blank00_01_04-uhd.png new file mode 100644 index 00000000..83c21119 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_01_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_02_00-uhd.png b/loader/resources/blanks/GEODE_blank00_02_00-uhd.png new file mode 100644 index 00000000..831acf9b Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_02_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_02_01-uhd.png b/loader/resources/blanks/GEODE_blank00_02_01-uhd.png new file mode 100644 index 00000000..d139fd9c Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_02_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_02_02-uhd.png b/loader/resources/blanks/GEODE_blank00_02_02-uhd.png new file mode 100644 index 00000000..dcc712c6 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_02_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_02_03-uhd.png b/loader/resources/blanks/GEODE_blank00_02_03-uhd.png new file mode 100644 index 00000000..958f148e Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_02_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_02_04-uhd.png b/loader/resources/blanks/GEODE_blank00_02_04-uhd.png new file mode 100644 index 00000000..c777f6ab Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_02_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_03_00-uhd.png b/loader/resources/blanks/GEODE_blank00_03_00-uhd.png new file mode 100644 index 00000000..b50d38f7 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_03_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_03_01-uhd.png b/loader/resources/blanks/GEODE_blank00_03_01-uhd.png new file mode 100644 index 00000000..cf609277 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_03_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_03_02-uhd.png b/loader/resources/blanks/GEODE_blank00_03_02-uhd.png new file mode 100644 index 00000000..e2b59434 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_03_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_03_03-uhd.png b/loader/resources/blanks/GEODE_blank00_03_03-uhd.png new file mode 100644 index 00000000..fb174e72 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_03_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_03_04-uhd.png b/loader/resources/blanks/GEODE_blank00_03_04-uhd.png new file mode 100644 index 00000000..374fef8f Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_03_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_04_00-uhd.png b/loader/resources/blanks/GEODE_blank00_04_00-uhd.png new file mode 100644 index 00000000..de9bcacd Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_04_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_04_01-uhd.png b/loader/resources/blanks/GEODE_blank00_04_01-uhd.png new file mode 100644 index 00000000..2734e40b Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_04_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_04_02-uhd.png b/loader/resources/blanks/GEODE_blank00_04_02-uhd.png new file mode 100644 index 00000000..ceb93598 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_04_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_04_03-uhd.png b/loader/resources/blanks/GEODE_blank00_04_03-uhd.png new file mode 100644 index 00000000..667a1f59 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_04_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_04_04-uhd.png b/loader/resources/blanks/GEODE_blank00_04_04-uhd.png new file mode 100644 index 00000000..61ffcd7d Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_04_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_05_00-uhd.png b/loader/resources/blanks/GEODE_blank00_05_00-uhd.png new file mode 100644 index 00000000..4bec4026 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_05_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_05_01-uhd.png b/loader/resources/blanks/GEODE_blank00_05_01-uhd.png new file mode 100644 index 00000000..eade39db Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_05_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_05_02-uhd.png b/loader/resources/blanks/GEODE_blank00_05_02-uhd.png new file mode 100644 index 00000000..85ce3a8b Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_05_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_05_03-uhd.png b/loader/resources/blanks/GEODE_blank00_05_03-uhd.png new file mode 100644 index 00000000..21957129 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_05_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_05_04-uhd.png b/loader/resources/blanks/GEODE_blank00_05_04-uhd.png new file mode 100644 index 00000000..a06d3814 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_05_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_06_00-uhd.png b/loader/resources/blanks/GEODE_blank00_06_00-uhd.png new file mode 100644 index 00000000..7c042572 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_06_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_06_01-uhd.png b/loader/resources/blanks/GEODE_blank00_06_01-uhd.png new file mode 100644 index 00000000..886024e3 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_06_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_06_02-uhd.png b/loader/resources/blanks/GEODE_blank00_06_02-uhd.png new file mode 100644 index 00000000..ea73faba Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_06_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_06_03-uhd.png b/loader/resources/blanks/GEODE_blank00_06_03-uhd.png new file mode 100644 index 00000000..205e4fdd Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_06_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_06_04-uhd.png b/loader/resources/blanks/GEODE_blank00_06_04-uhd.png new file mode 100644 index 00000000..d5a0ad42 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_06_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_07_00-uhd.png b/loader/resources/blanks/GEODE_blank00_07_00-uhd.png new file mode 100644 index 00000000..b8c12356 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_07_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_07_01-uhd.png b/loader/resources/blanks/GEODE_blank00_07_01-uhd.png new file mode 100644 index 00000000..efd3874a Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_07_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_07_02-uhd.png b/loader/resources/blanks/GEODE_blank00_07_02-uhd.png new file mode 100644 index 00000000..0cd467cc Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_07_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_07_03-uhd.png b/loader/resources/blanks/GEODE_blank00_07_03-uhd.png new file mode 100644 index 00000000..fa5d25f6 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_07_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank00_07_04-uhd.png b/loader/resources/blanks/GEODE_blank00_07_04-uhd.png new file mode 100644 index 00000000..50d04df2 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank00_07_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank01_00_00-uhd.png b/loader/resources/blanks/GEODE_blank01_00_00-uhd.png new file mode 100644 index 00000000..1d44a8d3 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank01_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank01_01_00-uhd.png b/loader/resources/blanks/GEODE_blank01_01_00-uhd.png new file mode 100644 index 00000000..619eb833 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank01_01_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank02_00_00-uhd.png b/loader/resources/blanks/GEODE_blank02_00_00-uhd.png new file mode 100644 index 00000000..559538ec Binary files /dev/null and b/loader/resources/blanks/GEODE_blank02_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank02_00_01-uhd.png b/loader/resources/blanks/GEODE_blank02_00_01-uhd.png new file mode 100644 index 00000000..74a96ca2 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank02_00_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank02_00_02-uhd.png b/loader/resources/blanks/GEODE_blank02_00_02-uhd.png new file mode 100644 index 00000000..97b90e4f Binary files /dev/null and b/loader/resources/blanks/GEODE_blank02_00_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank03_00_00-uhd.png b/loader/resources/blanks/GEODE_blank03_00_00-uhd.png new file mode 100644 index 00000000..5dd3b93c Binary files /dev/null and b/loader/resources/blanks/GEODE_blank03_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank03_00_01-uhd.png b/loader/resources/blanks/GEODE_blank03_00_01-uhd.png new file mode 100644 index 00000000..02d2ac37 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank03_00_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank04_00_00-uhd.png b/loader/resources/blanks/GEODE_blank04_00_00-uhd.png new file mode 100644 index 00000000..ded180c0 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank04_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_00-uhd.png b/loader/resources/blanks/GEODE_blank05_00_00-uhd.png new file mode 100644 index 00000000..00ccfe5d Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_01-uhd.png b/loader/resources/blanks/GEODE_blank05_00_01-uhd.png new file mode 100644 index 00000000..9162a71f Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_02-uhd.png b/loader/resources/blanks/GEODE_blank05_00_02-uhd.png new file mode 100644 index 00000000..b2ddde8d Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_02-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_03-uhd.png b/loader/resources/blanks/GEODE_blank05_00_03-uhd.png new file mode 100644 index 00000000..a8ed3f93 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_03-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_04-uhd.png b/loader/resources/blanks/GEODE_blank05_00_04-uhd.png new file mode 100644 index 00000000..faeeb5a7 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_04-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_05-uhd.png b/loader/resources/blanks/GEODE_blank05_00_05-uhd.png new file mode 100644 index 00000000..8a5abb5c Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_05-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_06-uhd.png b/loader/resources/blanks/GEODE_blank05_00_06-uhd.png new file mode 100644 index 00000000..4b492b41 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_06-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_07-uhd.png b/loader/resources/blanks/GEODE_blank05_00_07-uhd.png new file mode 100644 index 00000000..1feb76a2 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_07-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank05_00_08-uhd.png b/loader/resources/blanks/GEODE_blank05_00_08-uhd.png new file mode 100644 index 00000000..c726ca74 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank05_00_08-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank06_00_00-uhd.png b/loader/resources/blanks/GEODE_blank06_00_00-uhd.png new file mode 100644 index 00000000..6d64eb61 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank06_00_00-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank06_00_01-uhd.png b/loader/resources/blanks/GEODE_blank06_00_01-uhd.png new file mode 100644 index 00000000..ef2b6e25 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank06_00_01-uhd.png differ diff --git a/loader/resources/blanks/GEODE_blank06_00_02-uhd.png b/loader/resources/blanks/GEODE_blank06_00_02-uhd.png new file mode 100644 index 00000000..e7966ad4 Binary files /dev/null and b/loader/resources/blanks/GEODE_blank06_00_02-uhd.png differ diff --git a/loader/resources/filters.png b/loader/resources/filters.png new file mode 100644 index 00000000..f4b718ce Binary files /dev/null and b/loader/resources/filters.png differ diff --git a/loader/resources/fonts/Ubuntu-Bold.ttf b/loader/resources/fonts/Ubuntu-Bold.ttf new file mode 100644 index 00000000..c2293d5c Binary files /dev/null and b/loader/resources/fonts/Ubuntu-Bold.ttf differ diff --git a/loader/resources/fonts/Ubuntu-BoldItalic.ttf b/loader/resources/fonts/Ubuntu-BoldItalic.ttf new file mode 100644 index 00000000..ce6e784d Binary files /dev/null and b/loader/resources/fonts/Ubuntu-BoldItalic.ttf differ diff --git a/loader/resources/fonts/Ubuntu-Italic.ttf b/loader/resources/fonts/Ubuntu-Italic.ttf new file mode 100644 index 00000000..a599244e Binary files /dev/null and b/loader/resources/fonts/Ubuntu-Italic.ttf differ diff --git a/loader/resources/fonts/Ubuntu-Regular.ttf b/loader/resources/fonts/Ubuntu-Regular.ttf new file mode 100644 index 00000000..f98a2dab Binary files /dev/null and b/loader/resources/fonts/Ubuntu-Regular.ttf differ diff --git a/loader/resources/fonts/UbuntuMono-Regular.ttf b/loader/resources/fonts/UbuntuMono-Regular.ttf new file mode 100644 index 00000000..4977028d Binary files /dev/null and b/loader/resources/fonts/UbuntuMono-Regular.ttf differ diff --git a/loader/resources/images/GE_button_01.png b/loader/resources/images/GE_button_01.png new file mode 100644 index 00000000..1d7a451a Binary files /dev/null and b/loader/resources/images/GE_button_01.png differ diff --git a/loader/resources/images/GE_button_02.png b/loader/resources/images/GE_button_02.png new file mode 100644 index 00000000..03c9d787 Binary files /dev/null and b/loader/resources/images/GE_button_02.png differ diff --git a/loader/resources/images/GE_button_03.png b/loader/resources/images/GE_button_03.png new file mode 100644 index 00000000..c482845c Binary files /dev/null and b/loader/resources/images/GE_button_03.png differ diff --git a/loader/resources/images/GE_button_04.png b/loader/resources/images/GE_button_04.png new file mode 100644 index 00000000..0911ae63 Binary files /dev/null and b/loader/resources/images/GE_button_04.png differ diff --git a/loader/resources/images/scrollbar.png b/loader/resources/images/scrollbar.png new file mode 100644 index 00000000..27bd3cb7 Binary files /dev/null and b/loader/resources/images/scrollbar.png differ diff --git a/loader/resources/info-alert.png b/loader/resources/info-alert.png new file mode 100644 index 00000000..75a1f799 Binary files /dev/null and b/loader/resources/info-alert.png differ diff --git a/loader/resources/info-warning.png b/loader/resources/info-warning.png new file mode 100644 index 00000000..64fab516 Binary files /dev/null and b/loader/resources/info-warning.png differ diff --git a/loader/resources/install.png b/loader/resources/install.png new file mode 100644 index 00000000..b009173a Binary files /dev/null and b/loader/resources/install.png differ diff --git a/loader/resources/logos/geode-circle.png b/loader/resources/logos/geode-circle.png new file mode 100644 index 00000000..0378f7e8 Binary files /dev/null and b/loader/resources/logos/geode-circle.png differ diff --git a/loader/resources/logos/geode-logo-color.png b/loader/resources/logos/geode-logo-color.png new file mode 100644 index 00000000..77200dde Binary files /dev/null and b/loader/resources/logos/geode-logo-color.png differ diff --git a/loader/resources/logos/geode-logo-outline-gold.png b/loader/resources/logos/geode-logo-outline-gold.png new file mode 100644 index 00000000..8e100790 Binary files /dev/null and b/loader/resources/logos/geode-logo-outline-gold.png differ diff --git a/loader/resources/logos/geode-logo-outline.png b/loader/resources/logos/geode-logo-outline.png new file mode 100644 index 00000000..f09f936b Binary files /dev/null and b/loader/resources/logos/geode-logo-outline.png differ diff --git a/loader/resources/logos/geode-logo.png b/loader/resources/logos/geode-logo.png new file mode 100644 index 00000000..0b861859 Binary files /dev/null and b/loader/resources/logos/geode-logo.png differ diff --git a/loader/resources/logos/logo-base.png b/loader/resources/logos/logo-base.png new file mode 100644 index 00000000..adce6cc9 Binary files /dev/null and b/loader/resources/logos/logo-base.png differ diff --git a/loader/resources/logos/no-logo.png b/loader/resources/logos/no-logo.png new file mode 100644 index 00000000..af4fcf8c Binary files /dev/null and b/loader/resources/logos/no-logo.png differ diff --git a/loader/resources/resources.json b/loader/resources/resources.json new file mode 100644 index 00000000..f212e27d --- /dev/null +++ b/loader/resources/resources.json @@ -0,0 +1,39 @@ +{ + "fonts": { + "mdFont": { + "path": "fonts/Ubuntu-Regular.ttf", + "size": 80 + }, + "mdFontB": { + "path": "fonts/Ubuntu-Bold.ttf", + "size": 80 + }, + "mdFontI": { + "path": "fonts/Ubuntu-Italic.ttf", + "size": 80 + }, + "mdFontBI": { + "path": "fonts/Ubuntu-BoldItalic.ttf", + "size": 80 + }, + "mdFontMono": { + "path": "fonts/UbuntuMono-Regular.ttf", + "size": 80 + } + }, + "files": [ + "images/*.png", + "sounds/*.ogg" + ], + "spritesheets": { + "LogoSheet": [ + "logos/*.png" + ], + "APISheet": [ + "*.png" + ], + "BlankSheet": [ + "blanks/*.png" + ] + } +} \ No newline at end of file diff --git a/loader/resources/sounds/byeNotif00.ogg b/loader/resources/sounds/byeNotif00.ogg new file mode 100644 index 00000000..89774d54 Binary files /dev/null and b/loader/resources/sounds/byeNotif00.ogg differ diff --git a/loader/resources/sounds/newNotif00.ogg b/loader/resources/sounds/newNotif00.ogg new file mode 100644 index 00000000..af2613b2 Binary files /dev/null and b/loader/resources/sounds/newNotif00.ogg differ diff --git a/loader/resources/sounds/newNotif01.ogg b/loader/resources/sounds/newNotif01.ogg new file mode 100644 index 00000000..02f667ce Binary files /dev/null and b/loader/resources/sounds/newNotif01.ogg differ diff --git a/loader/resources/sounds/newNotif02.ogg b/loader/resources/sounds/newNotif02.ogg new file mode 100644 index 00000000..9fe0de2c Binary files /dev/null and b/loader/resources/sounds/newNotif02.ogg differ diff --git a/loader/resources/sounds/newNotif03.ogg b/loader/resources/sounds/newNotif03.ogg new file mode 100644 index 00000000..a69143e1 Binary files /dev/null and b/loader/resources/sounds/newNotif03.ogg differ diff --git a/loader/resources/updates-available.png b/loader/resources/updates-available.png new file mode 100644 index 00000000..1272026c Binary files /dev/null and b/loader/resources/updates-available.png differ diff --git a/loader/resources/updates-failed.png b/loader/resources/updates-failed.png new file mode 100644 index 00000000..d301d465 Binary files /dev/null and b/loader/resources/updates-failed.png differ diff --git a/loader/resources/updates-installed.png b/loader/resources/updates-installed.png new file mode 100644 index 00000000..fb2c4b9a Binary files /dev/null and b/loader/resources/updates-installed.png differ diff --git a/loader/src/hooks/LoadingLayer.cpp b/loader/src/hooks/LoadingLayer.cpp new file mode 100644 index 00000000..5ff4f5d5 --- /dev/null +++ b/loader/src/hooks/LoadingLayer.cpp @@ -0,0 +1,29 @@ +#include <Geode/Geode.hpp> +#include <array> + +USE_GEODE_NAMESPACE(); + +class $modify(CustomLoadingLayer, LoadingLayer) { + bool init(bool fromReload) { + if (!LoadingLayer::init(fromReload)) + return false; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + auto count = Loader::get()->getAllMods().size(); + + auto label = CCLabelBMFont::create( + CCString::createWithFormat( + "Geode: Loaded %lu mods", + static_cast<unsigned long>(count) + )->getCString(), + "goldFont.fnt" + ); + label->setPosition(winSize.width / 2, 30.f); + label->setScale(.45f); + label->setTag(5); + this->addChild(label); + + return true; + } +}; diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp new file mode 100644 index 00000000..2f7e9088 --- /dev/null +++ b/loader/src/hooks/MenuLayer.cpp @@ -0,0 +1,221 @@ +#include <Geode/Geode.hpp> +#include "../ui/internal/list/ModListLayer.hpp" +#include <Geode/utils/WackyGeodeMacros.hpp> +#include <Geode/ui/BasedButtonSprite.hpp> +#include <Index.hpp> +#include <Geode/ui/Notification.hpp> + +USE_GEODE_NAMESPACE(); + +class CustomMenuLayer; + +static Ref<Notification> g_indexUpdateNotif = nullptr; +static Ref<CCSprite> g_geodeButton = nullptr; + +static void addUpdateIcon(const char* icon = "updates-available.png"_spr) { + if (g_geodeButton && Index::get()->areUpdatesAvailable()) { + auto updateIcon = CCSprite::createWithSpriteFrameName(icon); + updateIcon->setPosition( + g_geodeButton->getContentSize() - CCSize { 10.f, 10.f } + ); + updateIcon->setZOrder(99); + updateIcon->setScale(.5f); + g_geodeButton->addChild(updateIcon); + } +} + +static void updateModsProgress( + UpdateStatus status, + std::string const& info, + uint8_t progress +) { + if (status == UpdateStatus::Failed) { + g_indexUpdateNotif->hide(); + g_indexUpdateNotif = nullptr; + NotificationBuilder() + .title("Some Updates Failed") + .text("Some mods failed to update, click for details") + .icon("info-alert.png"_spr) + .clicked([info](auto) -> void { + FLAlertLayer::create("Info", info, "OK")->show(); + }) + .show(); + addUpdateIcon("updates-failed.png"_spr); + } + + if (status == UpdateStatus::Finished) { + g_indexUpdateNotif->hide(); + g_indexUpdateNotif = nullptr; + NotificationBuilder() + .title("Updates Installed") + .text( + "Mods have been updated, please " + "restart to apply changes" + ) + .icon("updates-available.png"_spr) + .clicked([info](auto) -> void { + FLAlertLayer::create("Info", info, "OK")->show(); + }) + .show(); + } +} + +static void updateIndexProgress( + UpdateStatus status, + std::string const& info, + uint8_t progress +) { + if (status == UpdateStatus::Failed) { + g_indexUpdateNotif->hide(); + g_indexUpdateNotif = nullptr; + NotificationBuilder() + .title("Index Update") + .text("Index update failed :(") + .icon("info-alert.png"_spr) + .show(); + addUpdateIcon("updates-failed.png"_spr); + } + + if (status == UpdateStatus::Finished) { + g_indexUpdateNotif->hide(); + g_indexUpdateNotif = nullptr; + if (Index::get()->areUpdatesAvailable()) { + // todo: uncomment and fix crash + // if (Mod::get()->getDataStore()["enable-auto-updates"]) { + // auto ticket = Index::get()->installUpdates(updateModsProgress); + // if (!ticket) { + // NotificationBuilder() + // .title("Unable to auto-update") + // .text("Unable to update mods :(") + // .icon("updates-failed.png"_spr) + // .show(); + // } else { + // g_indexUpdateNotif = NotificationBuilder() + // .title("Installing updates") + // .text("Installing updates...") + // .clicked([ticket](auto) -> void { + // createQuickPopup( + // "Cancel Updates", + // "Do you want to <cr>cancel</c> updates?", + // "Don't Cancel", "Cancel Updates", + // [ticket](auto, bool btn2) -> void { + // if (g_indexUpdateNotif && btn2) { + // ticket.value()->cancel(); + // } + // } + // ); + // }, false) + // .loading() + // .stay() + // .show(); + // } + // } else { + // NotificationBuilder() + // .title("Updates available") + // .text("Some mods have updates available!") + // .icon("updates-available.png"_spr) + // .clicked([](auto) -> void { + // ModListLayer::scene(); + // }) + // .show(); + // } + // addUpdateIcon(); + } + } +} + +class $modify(CustomMenuLayer, MenuLayer) { + void destructor() { + g_geodeButton = nullptr; + MenuLayer::~MenuLayer(); + } + + bool init() { + if (!MenuLayer::init()) + return false; + + CCMenu* bottomMenu = nullptr; + + size_t indexCounter = 0; + for (size_t i = 0; i < this->getChildren()->count(); i++) { + auto obj = typeinfo_cast<CCMenu*>(this->getChildren()->objectAtIndex(i)); + if (obj != nullptr) { + ++indexCounter; + if (indexCounter == 2) { + bottomMenu = obj; + break; + } + } + } + + // if (!GameManager::sharedState()->m_clickedGarage) { + // bottomMenu = getChild<CCMenu*>(this, 4); + // } + // else { + // bottomMenu = getChild<CCMenu*>(this, 3); + // } + + auto chest = getChild<>(bottomMenu, -1); + if (chest) { + chest->retain(); + chest->removeFromParent(); + } + + auto y = getChild<>(bottomMenu, 0)->getPositionY(); + + g_geodeButton = CircleButtonSprite::createWithSpriteFrameName( + "geode-logo-outline-gold.png"_spr, + 1.0f, + CircleBaseColor::Green, + CircleBaseSize::Medium2 + ); + if (!g_geodeButton) { + g_geodeButton = ButtonSprite::create("!!"); + } + addUpdateIcon(); + auto btn = CCMenuItemSpriteExtra::create( + g_geodeButton.data(), this, menu_selector(CustomMenuLayer::onGeode) + ); + bottomMenu->addChild(btn); + + bottomMenu->alignItemsHorizontallyWithPadding(3.f); + + CCARRAY_FOREACH_B_TYPE(bottomMenu->getChildren(), node, CCNode) { + node->setPositionY(y); + } + if (chest) { + bottomMenu->addChild(chest); + chest->release(); + } + + auto failed = Loader::get()->getFailedMods(); + if (failed.size()) { + auto layer = FLAlertLayer::create( + "Notice", + "Some mods failed to load; see <cy>Geode</c> for details", + "OK" + ); + layer->m_scene = this; + layer->m_noElasticity = true; + layer->show(); + } + + if (!g_indexUpdateNotif && !Index::get()->isIndexUpdated()) { + g_indexUpdateNotif = NotificationBuilder() + .title("Index Update") + .text("Updating index...") + .loading() + .stay() + .show(); + + Index::get()->updateIndex(updateIndexProgress); + } + + return true; + } + + void onGeode(CCObject*) { + ModListLayer::scene(); + } +}; + diff --git a/loader/src/hooks/persist.cpp b/loader/src/hooks/persist.cpp new file mode 100644 index 00000000..274f3081 --- /dev/null +++ b/loader/src/hooks/persist.cpp @@ -0,0 +1,11 @@ +#include <Geode/Geode.hpp> +#include <Geode/ui/SceneManager.hpp> + +USE_GEODE_NAMESPACE(); + +class $modify(AchievementNotifier) { + void willSwitchToScene(CCScene* scene) { + AchievementNotifier::willSwitchToScene(scene); + SceneManager::get()->willSwitchToScene(scene); + } +}; diff --git a/loader/src/hooks/update_resources.cpp b/loader/src/hooks/updateResources.cpp similarity index 100% rename from loader/src/hooks/update_resources.cpp rename to loader/src/hooks/updateResources.cpp diff --git a/loader/src/index/Index.cpp b/loader/src/index/Index.cpp new file mode 100644 index 00000000..672d676e --- /dev/null +++ b/loader/src/index/Index.cpp @@ -0,0 +1,655 @@ +#include "Index.hpp" +#include <thread> +#include <Geode/utils/json.hpp> +#include "fetch.hpp" + +#define GITHUB_DONT_RATE_LIMIT_ME_PLS 0 + +static Result<nlohmann::json> readJSON(ghc::filesystem::path const& path) { + auto indexJsonData = file_utils::readString(path); + if (!indexJsonData) { + return Err("Unable to read " + path.string()); + } + try { + return Ok(nlohmann::json::parse(indexJsonData.value())); + } catch(std::exception& e) { + return Err("Error parsing JSON: " + std::string(e.what())); + } +} + +static Result<> unzipTo( + ghc::filesystem::path const& from, + ghc::filesystem::path const& to +) { + // unzip downloaded + auto unzip = ZipFile(from.string()); + if (!unzip.isLoaded()) { + return Err("Unable to unzip index.zip"); + } + + for (auto file : unzip.getAllFiles()) { + // this is a very bad check for seeing + // if file is a directory. it seems to + // work on windows at least. idk why + // getAllFiles returns the directories + // aswell now + if ( + string_utils::endsWith(file, "\\") || + string_utils::endsWith(file, "/") + ) continue; + + auto zipPath = file; + + // dont include the github repo folder + file = file.substr(file.find_first_of("/") + 1); + + auto path = ghc::filesystem::path(file); + if (path.has_parent_path()) { + if ( + !ghc::filesystem::exists(to / path.parent_path()) && + !ghc::filesystem::create_directories(to / path.parent_path()) + ) { + return Err( + "Unable to create directories \"" + + path.parent_path().string() + "\"" + ); + } + } + unsigned long size; + auto data = unzip.getFileData(zipPath, &size); + if (!data || !size) { + return Err("Unable to read \"" + std::string(zipPath) + "\""); + } + auto wrt = file_utils::writeBinary( + to / file, + byte_array(data, data + size) + ); + if (!wrt) { + return Err("Unable to write \"" + file + "\": " + wrt.error()); + } + } + + return Ok(); +} + + +Index* Index::get() { + static auto ret = new Index(); + return ret; +} + +bool Index::isIndexUpdated() const { + return m_upToDate; +} + + +void Index::updateIndexThread(bool force) { + auto indexDir = Loader::get()->getGeodeDirectory() / "index"; + + // download index + +#if GITHUB_DONT_RATE_LIMIT_ME_PLS == 0 + + indexUpdateProgress( + UpdateStatus::Progress, "Fetching index metadata", 0 + ); + + // get all commits in index repo + auto commit = fetchJSON( + "https://api.github.com/repos/geode-sdk/mods/commits" + ); + if (!commit) { + return indexUpdateProgress(UpdateStatus::Failed, commit.error()); + } + auto json = commit.value(); + if ( + json.is_object() && + json.contains("documentation_url") && + json.contains("message") + ) { + // whoops! got rate limited + return indexUpdateProgress( + UpdateStatus::Failed, + json["message"].get<std::string>() + ); + } + + indexUpdateProgress( + UpdateStatus::Progress, "Checking index status", 25 + ); + + // read sha of latest commit + + if (!json.is_array()) { + return indexUpdateProgress( + UpdateStatus::Failed, + "Fetched commits, expected 'array', got '" + + std::string(json.type_name()) + "'. " + "Report this bug to the Geode developers!" + ); + } + + if (!json.at(0).is_object()) { + return indexUpdateProgress( + UpdateStatus::Failed, + "Fetched commits, expected 'array.object', got 'array." + + std::string(json.type_name()) + "'. " + "Report this bug to the Geode developers!" + ); + } + + if (!json.at(0).contains("sha")) { + return indexUpdateProgress( + UpdateStatus::Failed, + "Fetched commits, missing '0.sha'. " + "Report this bug to the Geode developers!" + ); + } + + auto upcomingCommitSHA = json.at(0)["sha"]; + + // read sha of currently installed commit + std::string currentCommitSHA = ""; + if (ghc::filesystem::exists(indexDir / "current")) { + auto data = file_utils::readString(indexDir / "current"); + if (data) { + currentCommitSHA = data.value(); + } + } + + // update if forced or latest commit has + // different sha + if (force || currentCommitSHA != upcomingCommitSHA) { + // save new sha in file + file_utils::writeString(indexDir / "current", upcomingCommitSHA); + + // download latest commit (by downloading + // the repo as a zip) + + indexUpdateProgress( + UpdateStatus::Progress, + "Downloading index", + 50 + ); + auto gotZip = fetchFile( + "https://github.com/geode-sdk/mods/zipball/main", + indexDir / "index.zip" + ); + if (!gotZip) { + return indexUpdateProgress( + UpdateStatus::Failed, + gotZip.error() + ); + } + + // delete old index + if (ghc::filesystem::exists(indexDir / "index")) { + ghc::filesystem::remove_all(indexDir / "index"); + } + + auto unzip = unzipTo(indexDir / "index.zip", indexDir); + if (!unzip) { + return indexUpdateProgress( + UpdateStatus::Failed, unzip.error() + ); + } + } +#endif + + // update index + + indexUpdateProgress( + UpdateStatus::Progress, + "Updating index", + 75 + ); + this->updateIndexFromLocalCache(); + + m_upToDate = true; + m_updating = false; + + indexUpdateProgress( + UpdateStatus::Finished, + "", + 100 + ); +} + +void Index::indexUpdateProgress( + UpdateStatus status, + std::string const& info, + uint8_t percentage +) { + Loader::get()->queueInGDThread([this, status, info, percentage]() -> void { + m_callbacksMutex.lock(); + for (auto& d : m_callbacks) { + d(status, info, percentage); + } + if ( + status == UpdateStatus::Finished || + status == UpdateStatus::Failed + ) { + m_callbacks.clear(); + } + m_callbacksMutex.unlock(); + }); +} + +void Index::updateIndex(IndexUpdateCallback callback, bool force) { + // if already updated and no force, let + // delegate know + if (!force && m_upToDate) { + if (callback) { + callback( + UpdateStatus::Finished, + "Index already updated", + 100 + ); + } + return; + } + + // add delegate thread-safely if it's not + // added already + if (callback) { + m_callbacksMutex.lock(); + m_callbacks.push_back(callback); + m_callbacksMutex.unlock(); + } + + // if already updating, let delegate know + // and return + if (m_updating) { + if (callback) { + callback( + UpdateStatus::Progress, + "Waiting for update info", + 0 + ); + } + return; + } + m_updating = true; + + // create directory for the local clone of + // the index + auto indexDir = Loader::get()->getGeodeDirectory() / "index"; + ghc::filesystem::create_directories(indexDir); + + // update index in another thread to avoid + // pausing UI + std::thread(&Index::updateIndexThread, this, force).detach(); +} + +PlatformID platformFromString(std::string const& str) { + switch (hash(string_utils::trim(string_utils::toLower(str)).c_str())) { + default: + case hash("unknown"): return PlatformID::Unknown; + case hash("windows"): return PlatformID::Windows; + case hash("macos"): return PlatformID::MacOS; + case hash("ios"): return PlatformID::iOS; + case hash("android"): return PlatformID::Android; + case hash("linux"): return PlatformID::Linux; + } +} + +void Index::addIndexItemFromFolder(ghc::filesystem::path const& dir) { + if (ghc::filesystem::exists(dir / "index.json")) { + + auto readJson = readJSON(dir / "index.json"); + if (!readJson) { + Log::get() << Severity::Warning + << "Error reading index.json: " + << readJson.error() << ", skipping"; + return; + } + auto json = readJson.value(); + if (!json.is_object()) { + Log::get() << Severity::Warning + << "[index.json] is not an object, skipping"; + return; + } + + auto readModJson = readJSON(dir / "mod.json"); + if (!readModJson) { + Log::get() << Severity::Warning + << "Error reading mod.json: " + << readModJson.error() << ", skipping"; + return; + } + auto info = ModInfo::create(readModJson.value()); + if (!info) { + Log::get() << Severity::Warning + << info.error() << ", skipping"; + return; + } + + IndexItem item; + + item.m_path = dir; + item.m_info = info.value(); + + if (json.contains("download") && json["download"].is_object()) { + auto download = json["download"]; + std::set<PlatformID> unsetPlatforms = { + PlatformID::Windows, + PlatformID::MacOS, + PlatformID::iOS, + PlatformID::Android, + PlatformID::Linux, + }; + for (auto& key : std::initializer_list<std::string> { + "windows", + "macos", + "android", + "ios", + "*", + }) { + if (download.contains(key)) { + auto platformDownload = download[key]; + if (!platformDownload.is_object()) { + Log::get() << Severity::Warning + << "[index.json].download." + << key + << " is not an object, skipping"; + return; + } + if ( + !platformDownload.contains("url") || + !platformDownload["url"].is_string() + ) { + Log::get() << Severity::Warning + << "[index.json].download." + << key + << ".url is not a string, skipping"; + return; + } + if ( + !platformDownload.contains("name") || + !platformDownload["name"].is_string() + ) { + Log::get() << Severity::Warning + << "[index.json].download." + << key + << ".name is not a string, skipping"; + return; + } + if ( + !platformDownload.contains("hash") || + !platformDownload["hash"].is_string() + ) { + Log::get() << Severity::Warning + << "[index.json].download." + << key + << ".hash is not a string, skipping"; + return; + } + IndexItem::Download down; + down.m_url = platformDownload["url"]; + down.m_filename = platformDownload["name"]; + down.m_hash = platformDownload["hash"]; + if (key == "*") { + for (auto& platform : unsetPlatforms) { + item.m_download[platform] = down; + } + } else { + auto id = platformFromString(key); + item.m_download[id] = down; + unsetPlatforms.erase(id); + } + } + } + } else { + Log::get() << Severity::Warning + << "[index.json] is missing \"download\", adding anyway"; + } + + m_items.push_back(item); + + } else { + Log::get() << Severity::Warning << "Index directory " + << dir << " is missing index.json, skipping"; + } +} + +void Index::updateIndexFromLocalCache() { + m_items.clear(); + auto indexDir = Loader::get()->getGeodeDirectory() / "index" / "index"; + for (auto const& dir : ghc::filesystem::directory_iterator(indexDir)) { + if (ghc::filesystem::is_directory(dir)) { + this->addIndexItemFromFolder(dir); + } + } +} + +std::vector<IndexItem> const& Index::getItems() const { + return m_items; +} + +std::vector<IndexItem> Index::getUninstalledItems() const { + std::vector<IndexItem> items; + for (auto& item : m_items) { + if (!Loader::get()->isModInstalled(item.m_info.m_id)) { + if (item.m_download.count(GEODE_PLATFORM_TARGET)) { + items.push_back(item); + } + } + } + return items; +} + +bool Index::isKnownItem(std::string const& id) const { + for (auto& item : m_items) { + if (item.m_info.m_id == id) return true; + } + return false; +} + +IndexItem Index::getKnownItem(std::string const& id) const { + for (auto& item : m_items) { + if (item.m_info.m_id == id) { + return item; + } + } + return IndexItem(); +} + +struct UninstalledDependency { + std::string m_id; + bool m_isInIndex; +}; + +static void getUninstalledDependenciesRecursive( + ModInfo const& info, + std::vector<UninstalledDependency>& deps +) { + for (auto& dep : info.m_dependencies) { + UninstalledDependency d; + d.m_isInIndex = Index::get()->isKnownItem(dep.m_id); + if (!Loader::get()->isModInstalled(dep.m_id)) { + d.m_id = dep.m_id; + deps.push_back(d); + } + if (d.m_isInIndex) { + getUninstalledDependenciesRecursive( + Index::get()->getKnownItem(dep.m_id).m_info, + deps + ); + } + } +} + +Result<std::vector<std::string>> Index::checkDependenciesForItem( + IndexItem const& item +) { + // todo: check versions + std::vector<UninstalledDependency> deps; + getUninstalledDependenciesRecursive(item.m_info, deps); + if (deps.size()) { + std::vector<std::string> unknownDeps; + for (auto& dep : deps) { + if (!dep.m_isInIndex) { + unknownDeps.push_back(dep.m_id); + } + } + if (unknownDeps.size()) { + std::string list = ""; + for (auto& ud : unknownDeps) { + list += "<cp>" + ud + "</c>, "; + } + list.pop_back(); + list.pop_back(); + return Err( + "This mod or its dependencies <cb>depends</c> on the " + "following unknown mods: " + list + ". You will have " + "to manually install these mods before you can install " + "this one." + ); + } + std::vector<std::string> list = {}; + for (auto& d : deps) { + list.push_back(d.m_id); + } + list.push_back(item.m_info.m_id); + return Ok(list); + } else { + return Ok<std::vector<std::string>>({ item.m_info.m_id }); + } +} + +Result<InstallTicket*> Index::installItems( + std::vector<IndexItem> const& items, + ItemInstallCallback progress +) { + std::vector<std::string> ids {}; + for (auto& item : items) { + if (!item.m_download.count(GEODE_PLATFORM_TARGET)) { + return Err( + "This mod is not available on your " + "current platform \"" GEODE_PLATFORM_NAME "\" - Sorry! :(" + ); + } + auto download = item.m_download.at(GEODE_PLATFORM_TARGET); + if (!download.m_url.size()) { + return Err( + "Download URL not set! Report this bug to " + "the Geode developers - this should not happen, ever." + ); + } + if (!download.m_filename.size()) { + return Err( + "Download filename not set! Report this bug to " + "the Geode developers - this should not happen, ever." + ); + } + if (!download.m_hash.size()) { + return Err( + "Checksum not set! Report this bug to " + "the Geode developers - this should not happen, ever." + ); + } + auto list = checkDependenciesForItem(item); + if (!list) { + return Err(list.error()); + } + vector_utils::push(ids, list.value()); + } + return Ok(new InstallTicket(this, ids, progress)); +} + +Result<InstallTicket*> Index::installItem( + IndexItem const& item, + ItemInstallCallback progress +) { + return this->installItems({ item }, progress); +} + +bool Index::isUpdateAvailableForItem(std::string const& id) const { + if (!Loader::get()->isModInstalled(id)) { + return false; + } + if (!this->isKnownItem(id)) { + return false; + } + return + this->getKnownItem(id).m_info.m_version > + Loader::get()->getInstalledMod(id)->getVersion(); +} + +bool Index::isUpdateAvailableForItem(IndexItem const& item) const { + if (!Loader::get()->isModInstalled(item.m_info.m_id)) { + return false; + } + return + item.m_info.m_version > + Loader::get()->getInstalledMod(item.m_info.m_id)->getVersion(); +} + +bool Index::areUpdatesAvailable() const { + for (auto& item : m_items) { + if (this->isUpdateAvailableForItem(item.m_info.m_id)) { + return true; + } + } + return false; +} + +Result<InstallTicket*> Index::installUpdates( + IndexUpdateCallback callback, bool force +) { + // find items that need updating + std::vector<IndexItem> itemsToUpdate {}; + for (auto& item : m_items) { + if (this->isUpdateAvailableForItem(item)) { + itemsToUpdate.push_back(item); + } + } + + // generate ticket + auto ticket = this->installItems( + itemsToUpdate, + [itemsToUpdate, callback]( + InstallTicket*, + UpdateStatus status, + std::string const& info, + uint8_t progress + ) -> void { + switch (status) { + case UpdateStatus::Failed: { + callback( + UpdateStatus::Failed, + "Updating failed: " + info, + 0 + ); + } break; + + case UpdateStatus::Finished: { + std::string updatedStr = ""; + for (auto& item : itemsToUpdate) { + updatedStr += item.m_info.m_name + " (" + + item.m_info.m_id + ")\n"; + } + callback( + UpdateStatus::Finished, + "Updated the following mods: " + + updatedStr + + "Please restart to apply changes.", + 100 + ); + } break; + + case UpdateStatus::Progress: { + callback(UpdateStatus::Progress, info, progress); + } break; + } + } + ); + if (!ticket) { + return Err(ticket.error()); + } + + // install updates concurrently + ticket.value()->start(InstallMode::Concurrent); + + return ticket; +} diff --git a/loader/src/index/Index.hpp b/loader/src/index/Index.hpp new file mode 100644 index 00000000..058a79e8 --- /dev/null +++ b/loader/src/index/Index.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include <Geode/Geode.hpp> +#include <mutex> + +USE_GEODE_NAMESPACE(); + +class Index; +struct ModInstallUpdate; +class InstallTicket; + +// todo: make index use events + +enum class UpdateStatus { + Progress, + Failed, + Finished, +}; + +using ItemInstallCallback = std::function<void( + InstallTicket*, UpdateStatus, std::string const&, uint8_t +)>; +using IndexUpdateCallback = std::function<void( + UpdateStatus, std::string const&, uint8_t +)>; + +struct IndexItem { + struct Download { + std::string m_url; + std::string m_filename; + std::string m_hash; + }; + + ghc::filesystem::path m_path; + ModInfo m_info; + std::unordered_map<PlatformID, Download> m_download; + std::string m_installFailed; +}; + +enum class InstallMode { + Order, // download & install one-by-one + Concurrent, // download & install all simultaneously +}; + +/** + * Used for working with a currently + * happening mod installation from + * the index. Note that once the + * installation is finished / failed, + * the ticket will free its own memory, + * so make sure to let go of any + * pointers you may have to it. + */ +class InstallTicket { +protected: + ItemInstallCallback m_progress; + const std::vector<std::string> m_installList; + mutable std::mutex m_cancelMutex; + bool m_cancelling = false; + bool m_installing = false; + bool m_replaceFiles = true; + Index* m_index; + + void postProgress( + UpdateStatus status, + std::string const& info = "", + uint8_t progress = 0 + ); + void install(std::string const& id); + + friend class Index; + +public: + /** + * Create a new ticket for installing a list of mods. This method + * should not be called manually; instead, you should always use + * `Index::installItem`. Note that once the installation is + * finished / failed, the ticket will free its own memory, so make + * sure to let go of any pointers you may have to it. + */ + InstallTicket( + Index* index, + std::vector<std::string> const& list, + ItemInstallCallback progress + ); + + /** + * Get list of mods to install + */ + std::vector<std::string> const& getInstallList() const; + + /** + * Cancel all pending installations and revert finished ones. This + * function is thread-safe + */ + void cancel(); + + /** + * Begin installation. Note that this function is *not* + * thread-safe + * @param mode Whether to install the list of mods + * provided concurrently or in order + * @note Use InstallTicket::cancel to cancel the + * installation + */ + void start(InstallMode mode = InstallMode::Concurrent); +}; + +class Index { +protected: + bool m_upToDate = false; + bool m_updating = false; + mutable std::mutex m_callbacksMutex; + std::vector<IndexUpdateCallback> m_callbacks; + std::vector<IndexItem> m_items; + + void indexUpdateProgress( + UpdateStatus status, + std::string const& info = "", + uint8_t percentage = 0 + ); + + void updateIndexThread(bool force); + void addIndexItemFromFolder(ghc::filesystem::path const& dir); + void updateIndexFromLocalCache(); + + Result<std::vector<std::string>> checkDependenciesForItem( + IndexItem const& item + ); + +public: + static Index* get(); + + std::vector<IndexItem> const& getItems() const; + std::vector<IndexItem> getUninstalledItems() const; + bool isKnownItem(std::string const& id) const; + IndexItem getKnownItem(std::string const& id) const; + Result<InstallTicket*> installItems( + std::vector<IndexItem> const& item, + ItemInstallCallback progress = nullptr + ); + Result<InstallTicket*> installItem( + IndexItem const& item, + ItemInstallCallback progress = nullptr + ); + bool isUpdateAvailableForItem(std::string const& id) const; + bool isUpdateAvailableForItem(IndexItem const& item) const; + bool areUpdatesAvailable() const; + Result<InstallTicket*> installUpdates( + IndexUpdateCallback callback = nullptr, + bool force = false + ); + + bool isIndexUpdated() const; + void updateIndex(IndexUpdateCallback callback, bool force = false); +}; diff --git a/loader/src/index/InstallTicket.cpp b/loader/src/index/InstallTicket.cpp new file mode 100644 index 00000000..f9cc7b1b --- /dev/null +++ b/loader/src/index/InstallTicket.cpp @@ -0,0 +1,196 @@ +#include "Index.hpp" +#include <thread> +#include <Geode/utils/json.hpp> +#include <hash.hpp> +#include "fetch.hpp" + +void InstallTicket::postProgress( + UpdateStatus status, + std::string const& info, + uint8_t percentage +) { + if (m_progress) { + Loader::get()->queueInGDThread([this, status, info, percentage]() -> void { + m_progress(this, status, info, percentage); + }); + } + if (status == UpdateStatus::Failed || status == UpdateStatus::Finished) { + Log::get() << "Deleting InstallTicket at " << this; + // clean up own memory + delete this; + } +} + +InstallTicket::InstallTicket( + Index* index, + std::vector<std::string> const& list, + ItemInstallCallback progress +) : m_index(index), + m_installList(list), + m_progress(progress) {} + +std::vector<std::string> const& InstallTicket::getInstallList() const { + return m_installList; +} + +void InstallTicket::install(std::string const& id) { + // run installing in another thread in order + // to render progress on screen while installing + auto indexDir = Loader::get()->getGeodeDirectory() / "index"; + + auto item = m_index->getKnownItem(id); + auto download = item.m_download.at(GEODE_PLATFORM_TARGET); + + this->postProgress(UpdateStatus::Progress, "Checking status", 0); + + // download to temp file in index dir + auto tempFile = indexDir / download.m_filename; + + this->postProgress(UpdateStatus::Progress, "Fetching binary", 0); + auto res = fetchFile( + download.m_url, + tempFile, + [this, tempFile](double now, double total) -> int { + // check if cancelled + m_cancelMutex.lock(); + if (m_cancelling) { + try { ghc::filesystem::remove(tempFile); } catch(...) {} + m_cancelMutex.unlock(); + return 1; + } + m_cancelMutex.unlock(); + + this->postProgress( + UpdateStatus::Progress, + "Downloading binary", + static_cast<uint8_t>(now / total * 100.0) + ); + return 0; + } + ); + if (!res) { + try { ghc::filesystem::remove(tempFile); } catch(...) {} + return this->postProgress( + UpdateStatus::Failed, + "Downloading failed: " + res.error() + ); + } + + // check if cancelled + m_cancelMutex.lock(); + if (m_cancelling) { + ghc::filesystem::remove(tempFile); + m_cancelMutex.unlock(); + return; + } + m_cancelMutex.unlock(); + + // check for 404 + auto notFound = file_utils::readString(tempFile); + if (notFound && notFound.value() == "Not Found") { + try { ghc::filesystem::remove(tempFile); } catch(...) {} + return this->postProgress( + UpdateStatus::Failed, + "Binary file download returned \"Not found\". Report " + "this to the Geode development team." + ); + } + + // verify checksum + this->postProgress(UpdateStatus::Progress, "Verifying", 100); + + auto checksum = ::calculateHash(tempFile.string()); + + if (checksum != download.m_hash) { + try { ghc::filesystem::remove(tempFile); } catch(...) {} + return this->postProgress( + UpdateStatus::Failed, + "Checksum mismatch! (Downloaded file did not match what " + "was expected. Try again, and if the download fails another time, " + "report this to the Geode development team." + ); + } + + // move temp file to geode directory + try { + auto modDir = Loader::get()->getGeodeDirectory() / "mods"; + auto targetFile = modDir / download.m_filename; + + // find valid filename that doesn't exist yet + if (!m_replaceFiles) { + auto filename = ghc::filesystem::path( + download.m_filename + ).replace_extension("").string(); + + size_t number = 0; + while (ghc::filesystem::exists(targetFile)) { + targetFile = modDir / + (filename + std::to_string(number) + ".geode"); + number++; + } + } + + // move file + ghc::filesystem::rename(tempFile, targetFile); + } catch(std::exception& e) { + try { ghc::filesystem::remove(tempFile); } catch(...) {} + return this->postProgress( + UpdateStatus::Failed, + "Unable to move downloaded file to mods directory: \"" + + std::string(e.what()) + " \" " + "(This might be due to insufficient permissions to " + "write files under SteamLibrary, try running GD as " + "administrator)" + ); + } + + // call next in queue or post finish message + Loader::get()->queueInGDThread([this, id]() -> void { + // todo: Loader::get()->refreshMods(m_updateMod); + // where the Loader unloads the mod binary and + // reloads it from disk (this should prolly be + // done only for the installed mods) + Loader::get()->refreshMods(); + // already in GD thread, so might aswell do the + // progress posting manually + if (m_progress) { + (m_progress)( + this, UpdateStatus::Finished, "", 100 + ); + } + // clean up memory + delete this; + }); +} + +void InstallTicket::cancel() { + m_cancelMutex.lock(); + m_cancelling = true; + m_cancelMutex.unlock(); +} + +void InstallTicket::start(InstallMode mode) { + if (m_installing) return; + // make sure we have stuff to install + if (!m_installList.size()) { + return this->postProgress( + UpdateStatus::Failed, "Nothing to install", 0 + ); + } + m_installing = true; + switch (mode) { + case InstallMode::Concurrent: { + for (auto& id : m_installList) { + std::thread(&InstallTicket::install, this, id).detach(); + } + } break; + + case InstallMode::Order: { + std::thread([this]() -> void { + for (auto& id : m_installList) { + this->install(id); + } + }).detach(); + } break; + } +} diff --git a/loader/src/index/fetch.cpp b/loader/src/index/fetch.cpp new file mode 100644 index 00000000..c6504ee6 --- /dev/null +++ b/loader/src/index/fetch.cpp @@ -0,0 +1,98 @@ +#include "fetch.hpp" +#include <curl/curl.h> + +namespace fetch_utils { + static size_t writeData(char* data, size_t size, size_t nmemb, void* str) { + as<std::string*>(str)->append(data, size * nmemb); + return size * nmemb; + } + + static size_t writeBinaryData(char* data, size_t size, size_t nmemb, void* file) { + as<std::ofstream*>(file)->write(data, size * nmemb); + return size * nmemb; + } + + static int progress(void* ptr, double total, double now, double, double) { + return (*as<std::function<int(double, double)>*>(ptr))(now, total); + } +} + +Result<> fetchFile( + std::string const& url, + ghc::filesystem::path const& into, + std::function<int(double, double)> prog +) { + auto curl = curl_easy_init(); + + if (!curl) return Err("Curl not initialized!"); + + std::ofstream file(into, std::ios::out | std::ios::binary); + + if (!file.is_open()) { + return Err("Unable to open output file"); + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fetch_utils::writeBinaryData); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); + if (prog) { + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, fetch_utils::progress); + curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &prog); + } + auto res = curl_easy_perform(curl); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + return Err("Fetch failed"); + } + + char* ct; + res = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + if ((res == CURLE_OK) && ct) { + curl_easy_cleanup(curl); + return Ok(); + } + curl_easy_cleanup(curl); + return Err("Error getting info: " + std::string(curl_easy_strerror(res))); +} + +Result<std::string> fetch(std::string const& url) { + auto curl = curl_easy_init(); + + if (!curl) return Err("Curl not initialized!"); + + std::string ret; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fetch_utils::writeData); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); + auto res = curl_easy_perform(curl); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + return Err("Fetch failed"); + } + + char* ct; + res = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + if ((res == CURLE_OK) && ct) { + curl_easy_cleanup(curl); + return Ok(ret); + } + curl_easy_cleanup(curl); + return Err("Error getting info: " + std::string(curl_easy_strerror(res))); +} + +Result<nlohmann::json> fetchJSON(std::string const& url) { + auto res = fetch(url); + if (!res) return Err(res.error()); + try { + return Ok(nlohmann::json::parse(res.value())); + } catch(std::exception& e) { + return Err(e.what()); + } +} + diff --git a/loader/src/index/fetch.hpp b/loader/src/index/fetch.hpp new file mode 100644 index 00000000..f64a9687 --- /dev/null +++ b/loader/src/index/fetch.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <Geode/Geode.hpp> + +USE_GEODE_NAMESPACE(); + +Result<std::string> fetch(std::string const& url); +Result<nlohmann::json> fetchJSON(std::string const& url); +Result<> fetchFile( + std::string const& url, + ghc::filesystem::path const& into, + std::function<int(double, double)> prog = nullptr +); + + diff --git a/loader/src/internal/InternalMod.cpp b/loader/src/internal/InternalMod.cpp index 8a76f2f3..1823d058 100644 --- a/loader/src/internal/InternalMod.cpp +++ b/loader/src/internal/InternalMod.cpp @@ -1,7 +1,7 @@ #include "InternalMod.hpp" #include "about.hpp" -ModInfo getInternalModInfo() { +static ModInfo getInternalModInfo() { ModInfo info; info.m_id = "geode.loader"; @@ -11,6 +11,11 @@ ModInfo getInternalModInfo() { info.m_details = LOADER_ABOUT_MD; info.m_version = LOADER_VERSION; info.m_supportsDisabling = false; + info.m_spritesheets = { + "geode.loader_LogoSheet", + "geode.loader_APISheet", + "geode.loader_BlankSheet" + }; return info; } diff --git a/loader/src/internal/InternalMod.hpp b/loader/src/internal/InternalMod.hpp index 72713705..5b3eb199 100644 --- a/loader/src/internal/InternalMod.hpp +++ b/loader/src/internal/InternalMod.hpp @@ -6,12 +6,8 @@ class InternalMod; USE_GEODE_NAMESPACE(); -class InternalLoader; - class InternalMod : public Mod { protected: - friend class InternalLoade; - InternalMod(); virtual ~InternalMod(); diff --git a/loader/src/load/Loader.cpp b/loader/src/load/Loader.cpp index 0a0c1b88..4f567560 100644 --- a/loader/src/load/Loader.cpp +++ b/loader/src/load/Loader.cpp @@ -62,8 +62,11 @@ void Loader::addModResourcesPath(Mod* mod) { } void Loader::updateResourcePaths() { + // add own resources directory auto resDir = this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY; CCFileUtils::sharedFileUtils()->addSearchPath(resDir.string().c_str()); + + // add mods' resources directories for (auto const& [_, mod] : m_mods) { this->addModResourcesPath(mod); } @@ -97,6 +100,10 @@ void Loader::updateModResources(Mod* mod) { } void Loader::updateResources() { + // add own spritesheets + this->updateModResources(InternalMod::get()); + + // add mods' spritesheets for (auto const& [_, mod] : m_mods) { this->updateModResources(mod); } @@ -316,6 +323,11 @@ bool Loader::setup() { this->loadSettings(); this->refreshMods(); + // add resources on startup + this->queueInGDThread([]() { + Loader::get()->updateResourcePaths(); + }); + if (crashlog::setupPlatformHandler()) { InternalMod::get()->log() << Severity::Debug diff --git a/loader/src/ui/internal/credits/link.cpp b/loader/src/ui/internal/credits/link.cpp new file mode 100644 index 00000000..22daa335 --- /dev/null +++ b/loader/src/ui/internal/credits/link.cpp @@ -0,0 +1,28 @@ +#include <Geode/Geode.hpp> + +USE_GEODE_NAMESPACE(); + +std::string expandKnownLink(std::string const& link) { + switch (hash(string_utils::toLower(link).c_str())) { + case hash("github"): + if (!string_utils::contains(link, "/")) { + return "https://github.com/" + link; + } break; + + case hash("youtube"): + if (!string_utils::contains(link, "/")) { + return "https://youtube.com/channel/" + link; + } break; + + case hash("twitter"): + if (!string_utils::contains(link, "/")) { + return "https://twitter.com/" + link; + } break; + + case hash("newgrounds"): + if (!string_utils::contains(link, "/")) { + return "https://" + link + ".newgrounds.com"; + } break; + } + return link; +} diff --git a/loader/src/ui/internal/dev/HookListLayer.cpp b/loader/src/ui/internal/dev/HookListLayer.cpp new file mode 100644 index 00000000..3750a073 --- /dev/null +++ b/loader/src/ui/internal/dev/HookListLayer.cpp @@ -0,0 +1,26 @@ +#include "HookListLayer.hpp" + +bool HookListLayer::init(Mod* mod) { + if (!GJDropDownLayer::init("Hooks", 220.f)) + return false; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + auto hooks = CCArray::create(); + for (auto const& hook : mod->getHooks()) { + hooks->addObject(new HookItem(hook)); + } + this->m_listLayer->m_listView = HookListView::create(hooks, mod, 356.f, 220.f); + this->m_listLayer->addChild(this->m_listLayer->m_listView); + + return true; +} + +HookListLayer* HookListLayer::create(Mod* mod) { + auto ret = new HookListLayer; + if (ret && ret->init(mod)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/internal/dev/HookListLayer.hpp b/loader/src/ui/internal/dev/HookListLayer.hpp new file mode 100644 index 00000000..c63961f0 --- /dev/null +++ b/loader/src/ui/internal/dev/HookListLayer.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "HookListView.hpp" + +class HookListLayer : public GJDropDownLayer { + protected: + bool init(Mod* mod); + + public: + static HookListLayer* create(Mod* mod); +}; diff --git a/loader/src/ui/internal/dev/HookListView.cpp b/loader/src/ui/internal/dev/HookListView.cpp new file mode 100644 index 00000000..c366cf5e --- /dev/null +++ b/loader/src/ui/internal/dev/HookListView.cpp @@ -0,0 +1,142 @@ +#include "HookListView.hpp" + +HookCell::HookCell(const char* name, CCSize size) : + TableViewCell(name, size.width, size.height) {} + +void HookCell::draw() { + reinterpret_cast<StatsCell*>(this)->StatsCell::draw(); +} + + +void HookCell::updateBGColor(int index) { + if (index & 1) m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e)); + else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c)); + m_backgroundLayer->setOpacity(0xff); +} + +void HookCell::onEnable(CCObject* pSender) { + auto toggle = as<CCMenuItemToggler*>(pSender); + if (!toggle->isToggled()) { + auto res = this->m_mod->enableHook(this->m_hook); + if (!res) { + FLAlertLayer::create( + nullptr, "Error Enabling Hook", + std::string(res.error()), + "OK", nullptr, + 280.f + )->show(); + } + } else { + auto res = this->m_mod->disableHook(this->m_hook); + if (!res) { + FLAlertLayer::create( + nullptr, "Error Disabling Hook", + std::string(res.error()), + "OK", nullptr, + 280.f + )->show(); + } + } + toggle->toggle(!this->m_hook->isEnabled()); +} + +void HookCell::loadFromHook(Hook* hook, Mod* Mod) { + this->m_hook = hook; + this->m_mod = Mod; + + this->m_mainLayer->setVisible(true); + this->m_backgroundLayer->setOpacity(255); + + auto menu = CCMenu::create(); + menu->setPosition(this->m_width - this->m_height, this->m_height / 2); + this->m_mainLayer->addChild(menu); + + auto enableBtn = CCMenuItemToggler::createWithStandardSprites( + this, menu_selector(HookCell::onEnable), .6f + ); + enableBtn->setPosition(0, 0); + enableBtn->toggle(hook->isEnabled()); + menu->addChild(enableBtn); + + std::stringstream moduleName; + + + // #ifdef GEODE_IS_WINDOWS // add other platforms? + // HMODULE module; + // if (GetModuleHandleExA( + // GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + // GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + // as<LPCSTR>(hook->getAddress()), + // &module + // )) { + // addr -= as<uintptr_t>(module); + // char name[MAX_PATH]; + // if (GetModuleFileNameA( + // module, name, sizeof name + // )) { + // auto fileName = std::filesystem::path(name).stem(); + // moduleName << fileName.string() << " + "; + // } + // } + // #endif + if (hook->getDisplayName() != "") + moduleName << hook->getDisplayName(); + else + moduleName << "0x" << std::hex << hook->getAddress(); + auto label = CCLabelBMFont::create(moduleName.str().c_str(), "chatFont.fnt"); + label->setPosition(this->m_height / 2, this->m_height / 2); + label->setScale(.7f); + label->setAnchorPoint({ .0f, .5f }); + this->m_mainLayer->addChild(label); +} + +HookCell* HookCell::create(const char* key, CCSize size) { + auto pRet = new HookCell(key, size); + if (pRet) { + return pRet; + } + CC_SAFE_DELETE(pRet); + return nullptr; +} + + +void HookListView::setupList() { + this->m_itemSeparation = 30.0f; + + if (!this->m_entries->count()) return; + + this->m_tableView->reloadData(); + + if (this->m_entries->count() == 1) + this->m_tableView->moveToTopWithOffset(this->m_itemSeparation); + + this->m_tableView->moveToTop(); +} + +TableViewCell* HookListView::getListCell(const char* key) { + return HookCell::create(key, CCSize { this->m_width, this->m_itemSeparation }); +} + +void HookListView::loadCell(TableViewCell* cell, unsigned int index) { + as<HookCell*>(cell)->loadFromHook( + as<HookItem*>(this->m_entries->objectAtIndex(index))->m_hook, + this->m_mod + ); + as<HookCell*>(cell)->updateBGColor(index); +} + +HookListView* HookListView::create( + CCArray* hooks, Mod* Mod, + float width, float height +) { + auto pRet = new HookListView; + if (pRet) { + pRet->m_mod = Mod; + if (pRet->init(hooks, kBoomListType_Hooks, width, height)) { + pRet->autorelease(); + return pRet; + } + } + CC_SAFE_DELETE(pRet); + return nullptr; +} diff --git a/loader/src/ui/internal/dev/HookListView.hpp b/loader/src/ui/internal/dev/HookListView.hpp new file mode 100644 index 00000000..3c59f4c1 --- /dev/null +++ b/loader/src/ui/internal/dev/HookListView.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include <Geode/Geode.hpp> + +USE_GEODE_NAMESPACE(); + +static constexpr const BoomListType kBoomListType_Hooks + = static_cast<BoomListType>(0x358); + +struct HookItem : public CCObject { + Hook* m_hook; + + HookItem(Hook* h) : m_hook(h) { + this->autorelease(); + } +}; + +class HookCell : public TableViewCell { + protected: + Mod* m_mod; + Hook* m_hook; + + HookCell(const char* name, CCSize size); + + void draw() override; + + + void onEnable(CCObject*); + + public: + void updateBGColor(int index); + void loadFromHook(Hook*, Mod*); + + static HookCell* create(const char* key, CCSize size); +}; + +class HookListView : public CustomListView { + protected: + Mod* m_mod; + + void setupList() override; + TableViewCell* getListCell(const char* key) override; + void loadCell(TableViewCell* cell, unsigned int index) override; + + public: + static HookListView* create( + CCArray* hooks, Mod* Mod, float width, float height + ); +}; diff --git a/loader/src/ui/internal/dev/HotReloadLayer.cpp b/loader/src/ui/internal/dev/HotReloadLayer.cpp new file mode 100644 index 00000000..54eabd6b --- /dev/null +++ b/loader/src/ui/internal/dev/HotReloadLayer.cpp @@ -0,0 +1,47 @@ +#include "HotReloadLayer.hpp" + +bool HotReloadLayer::init(std::string const& name) { + if (!CCLayer::init()) + return false; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + auto bg = CCSprite::create("GJ_gradientBG.png"); + auto bgSize = bg->getTextureRect().size; + + bg->setAnchorPoint({ 0.0f, 0.0f }); + bg->setScaleX((winSize.width + 10.0f) / bgSize.width); + bg->setScaleY((winSize.height + 10.0f) / bgSize.height); + bg->setPosition({ -5.0f, -5.0f }); + bg->setColor({ 0, 102, 255 }); + + this->addChild(bg); + + std::string text = "Reloading " + name + "..."; + auto label = CCLabelBMFont::create(text.c_str(), "bigFont.fnt"); + label->setPosition(this->getContentSize() / 2); + label->setZOrder(1); + label->setScale(.5f); + this->addChild(label); + + return true; +} + +HotReloadLayer* HotReloadLayer::create(std::string const& name) { + auto ret = new HotReloadLayer; + if (ret && ret->init(name)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +HotReloadLayer* HotReloadLayer::scene(std::string const& name) { + auto scene = CCScene::create(); + auto layer = HotReloadLayer::create(name); + scene->addChild(layer); + CCDirector::sharedDirector()->replaceScene(scene); + return layer; +} + diff --git a/loader/src/ui/internal/dev/HotReloadLayer.hpp b/loader/src/ui/internal/dev/HotReloadLayer.hpp new file mode 100644 index 00000000..cab1cd7a --- /dev/null +++ b/loader/src/ui/internal/dev/HotReloadLayer.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include <Geode/Geode.hpp> + +USE_GEODE_NAMESPACE(); + +class HotReloadLayer : public CCLayer { +protected: + bool init(std::string const& name); + +public: + static HotReloadLayer* create(std::string const& name); + static HotReloadLayer* scene(std::string const& name); +}; diff --git a/loader/src/ui/internal/info/ModInfoLayer.cpp b/loader/src/ui/internal/info/ModInfoLayer.cpp new file mode 100644 index 00000000..8ef27bd7 --- /dev/null +++ b/loader/src/ui/internal/info/ModInfoLayer.cpp @@ -0,0 +1,708 @@ +#include "ModInfoLayer.hpp" +#include "../dev/HookListLayer.hpp" +#include "../settings/ModSettingsLayer.hpp" +#include <Geode/ui/BasedButton.hpp> +#include <Geode/ui/MDTextArea.hpp> +#include "../list/ModListView.hpp" +#include <Geode/ui/Scrollbar.hpp> +#include <Geode/utils/WackyGeodeMacros.hpp> +// #include <settings/Setting.hpp> +#include <Geode/ui/IconButtonSprite.hpp> + +// TODO: die +#undef min +#undef max + +static constexpr const int TAG_CONFIRM_INSTALL = 4; +static constexpr const int TAG_CONFIRM_UNINSTALL = 5; +static constexpr const int TAG_DELETE_SAVEDATA = 6; + +bool DownloadStatusNode::init() { + if (!CCNode::init()) + return false; + + this->setContentSize({ 150.f, 25.f }); + + auto bg = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bg->setScale(.33f); + bg->setColor({ 0, 0, 0 }); + bg->setOpacity(75); + bg->setContentSize(m_obContentSize * 3); + this->addChild(bg); + + m_bar = Slider::create(this, nullptr, .6f); + m_bar->setValue(.0f); + m_bar->updateBar(); + m_bar->setPosition(0.f, -5.f); + m_bar->m_touchLogic->m_thumb->setVisible(false); + this->addChild(m_bar); + + m_label = CCLabelBMFont::create("", "bigFont.fnt"); + m_label->setAnchorPoint({ .0f, .5f }); + m_label->setScale(.45f); + m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f); + this->addChild(m_label); + + return true; +} + +DownloadStatusNode* DownloadStatusNode::create() { + auto ret = new DownloadStatusNode(); + if (ret && ret->init()) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void DownloadStatusNode::setProgress(uint8_t progress) { + m_bar->setValue(progress / 100.f); + m_bar->updateBar(); +} + +void DownloadStatusNode::setStatus(std::string const& text) { + m_label->setString(text.c_str()); + m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f); +} + + +bool ModInfoLayer::init(ModObject* obj, ModListView* list) { + m_noElasticity = true; + m_list = list; + m_mod = obj->m_mod; + + bool isInstalledMod; + switch (obj->m_type) { + case ModObjectType::Mod: { + m_info = obj->m_mod->getModInfo(); + isInstalledMod = true; + } break; + + case ModObjectType::Index: { + m_info = obj->m_index.m_info; + isInstalledMod = false; + } break; + + default: return false; + } + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + CCSize size { 440.f, 290.f }; + + if (!this->initWithColor({ 0, 0, 0, 105 })) return false; + m_mainLayer = CCLayer::create(); + this->addChild(m_mainLayer); + + auto bg = CCScale9Sprite::create( + "GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bg->setContentSize(size); + bg->setPosition(winSize.width / 2, winSize.height / 2); + m_mainLayer->addChild(bg); + + m_buttonMenu = CCMenu::create(); + m_mainLayer->addChild(m_buttonMenu); + + constexpr float logoSize = 40.f; + constexpr float logoOffset = 10.f; + + auto nameLabel = CCLabelBMFont::create( + m_info.m_name.c_str(), "bigFont.fnt" + ); + nameLabel->setScale(.7f); + nameLabel->setAnchorPoint({ .0f, .5f }); + m_mainLayer->addChild(nameLabel, 2); + + auto logoSpr = this->createLogoSpr(obj); + logoSpr->setScale(logoSize / logoSpr->getContentSize().width); + m_mainLayer->addChild(logoSpr); + + auto developerStr = "by " + m_info.m_developer; + auto developerLabel = CCLabelBMFont::create( + developerStr.c_str(), "goldFont.fnt" + ); + developerLabel->setScale(.5f); + developerLabel->setAnchorPoint({ .0f, .5f }); + m_mainLayer->addChild(developerLabel); + + auto logoTitleWidth = std::max( + nameLabel->getScaledContentSize().width, + developerLabel->getScaledContentSize().width + ) + logoSize + logoOffset; + + nameLabel->setPosition( + winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, + winSize.height / 2 + 125.f + ); + logoSpr->setPosition({ + winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2, + winSize.height / 2 + 115.f + }); + developerLabel->setPosition( + winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, + winSize.height / 2 + 105.f + ); + + auto versionLabel = CCLabelBMFont::create( + m_info.m_version.toString().c_str(), "bigFont.fnt" + ); + versionLabel->setAnchorPoint({ .0f, .5f }); + versionLabel->setScale(.4f); + versionLabel->setPosition( + nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f, + winSize.height / 2 + 125.f + ); + versionLabel->setColor({ 0, 255, 0 }); + m_mainLayer->addChild(versionLabel); + + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + auto details = MDTextArea::create( + m_info.m_details.size() ? + m_info.m_details : + "### No description provided.", + { 350.f, 137.5f } + ); + details->setPosition( + winSize.width / 2 - details->getScaledContentSize().width / 2, + winSize.height / 2 - details->getScaledContentSize().height / 2 - 20.f + ); + m_mainLayer->addChild(details); + + auto detailsBar = Scrollbar::create(details->getScrollLayer()); + detailsBar->setPosition( + winSize.width / 2 + details->getScaledContentSize().width / 2 + 20.f, + winSize.height / 2 - 20.f + ); + m_mainLayer->addChild(detailsBar); + + + auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + infoSpr->setScale(.85f); + + auto infoBtn = CCMenuItemSpriteExtra::create( + infoSpr, this, menu_selector(ModInfoLayer::onInfo) + ); + infoBtn->setPosition(size.width / 2 - 25.f, size.height / 2 - 25.f); + m_buttonMenu->addChild(infoBtn); + + + if (isInstalledMod) { + auto settingsSpr = CCSprite::createWithSpriteFrameName( + "GJ_optionsBtn_001.png" + ); + settingsSpr->setScale(.65f); + + auto settingsBtn = CCMenuItemSpriteExtra::create( + settingsSpr, this, menu_selector(ModInfoLayer::onSettings) + ); + settingsBtn->setPosition( + -size.width / 2 + 25.f, + -size.height / 2 + 25.f + ); + m_buttonMenu->addChild(settingsBtn); + + // if (!SettingManager::with(m_mod)->hasSettings()) { + // settingsSpr->setColor({ 150, 150, 150 }); + // settingsBtn->setTarget( + // this, menu_selector(ModInfoLayer::onNoSettings) + // ); + // } + + auto devSpr = ButtonSprite::create( + "Dev Options", "bigFont.fnt", "GJ_button_05.png", .6f + ); + devSpr->setScale(.5f); + + auto devBtn = CCMenuItemSpriteExtra::create( + devSpr, this, nullptr + ); + devBtn->setPosition(size.width / 2 - 50.f, -size.height / 2 + 25.f); + m_buttonMenu->addChild(devBtn); + + + auto enableBtnSpr = ButtonSprite::create( + "Enable", "bigFont.fnt", "GJ_button_01.png", .6f + ); + enableBtnSpr->setScale(.6f); + + auto disableBtnSpr = ButtonSprite::create( + "Disable", "bigFont.fnt", "GJ_button_06.png", .6f + ); + disableBtnSpr->setScale(.6f); + + auto enableBtn = CCMenuItemToggler::create( + disableBtnSpr, enableBtnSpr, + this, menu_selector(ModInfoLayer::onEnableMod) + ); + enableBtn->setPosition(-155.f, 75.f); + enableBtn->toggle(!obj->m_mod->isEnabled()); + m_buttonMenu->addChild(enableBtn); + + if (!m_info.m_supportsDisabling) { + enableBtn->setTarget( + this, menu_selector(ModInfoLayer::onDisablingNotSupported) + ); + enableBtnSpr->setColor({ 150, 150, 150 }); + disableBtnSpr->setColor({ 150, 150, 150 }); + } + + if ( + m_mod != Loader::get()->getInternalMod() && + m_mod != Mod::get() + ) { + auto uninstallBtnSpr = ButtonSprite::create( + "Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f + ); + uninstallBtnSpr->setScale(.6f); + + auto uninstallBtn = CCMenuItemSpriteExtra::create( + uninstallBtnSpr, this, + menu_selector(ModInfoLayer::onUninstall) + ); + uninstallBtn->setPosition(-85.f, 75.f); + m_buttonMenu->addChild(uninstallBtn); + + // api and loader should be updated through the installer + // todo: show update button on them that invokes the installer + if (Index::get()->isUpdateAvailableForItem(m_info.m_id)) { + m_installBtnSpr = IconButtonSprite::create( + "GE_button_01.png"_spr, + CCSprite::createWithSpriteFrameName("install.png"_spr), + "Update", + "bigFont.fnt" + ); + m_installBtnSpr->setScale(.6f); + + m_installBtn = CCMenuItemSpriteExtra::create( + m_installBtnSpr, this, + menu_selector(ModInfoLayer::onInstallMod) + ); + m_installBtn->setPosition(-8.0f, 75.f); + m_buttonMenu->addChild(m_installBtn); + + m_installStatus = DownloadStatusNode::create(); + m_installStatus->setPosition( + winSize.width / 2 + 105.f, + winSize.height / 2 + 75.f + ); + m_installStatus->setVisible(false); + m_mainLayer->addChild(m_installStatus); + + auto incomingVersion = Index::get()->getKnownItem(m_info.m_id).m_info.m_version.toString(); + + m_updateVersionLabel = CCLabelBMFont::create( + ("Available: " + incomingVersion).c_str(), "bigFont.fnt" + ); + m_updateVersionLabel->setScale(.35f); + m_updateVersionLabel->setAnchorPoint({ .0f, .5f }); + m_updateVersionLabel->setColor({ 94, 219, 255 }); + m_updateVersionLabel->setPosition( + winSize.width / 2 + 35.f, + winSize.height / 2 + 75.f + ); + m_mainLayer->addChild(m_updateVersionLabel); + } + } + } else { + m_installBtnSpr = IconButtonSprite::create( + "GE_button_01.png"_spr, + CCSprite::createWithSpriteFrameName("install.png"_spr), + "Install", + "bigFont.fnt" + ); + m_installBtnSpr->setScale(.6f); + + m_installBtn = CCMenuItemSpriteExtra::create( + m_installBtnSpr, this, + menu_selector(ModInfoLayer::onInstallMod) + ); + m_installBtn->setPosition(-143.0f, 75.f); + m_buttonMenu->addChild(m_installBtn); + + m_installStatus = DownloadStatusNode::create(); + m_installStatus->setPosition( + winSize.width / 2 - 25.f, + winSize.height / 2 + 75.f + ); + m_installStatus->setVisible(false); + m_mainLayer->addChild(m_installStatus); + } + + auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); + closeSpr->setScale(.8f); + + auto closeBtn = CCMenuItemSpriteExtra::create( + closeSpr, this, menu_selector(ModInfoLayer::onClose) + ); + closeBtn->setPosition(-size.width / 2 + 3.f, size.height / 2 - 3.f); + m_buttonMenu->addChild(closeBtn); + + this->setKeypadEnabled(true); + this->setTouchEnabled(true); + + return true; +} + +void ModInfoLayer::onEnableMod(CCObject* pSender) { + // if (!APIInternal::get()->m_shownEnableWarning) { + // APIInternal::get()->m_shownEnableWarning = true; + // FLAlertLayer::create( + // "Notice", + // "<cb>Disabling</c> a <cy>mod</c> removes its hooks & patches and " + // "calls its user-defined disable function if one exists. You may " + // "still see some effects of the mod left however, and you may " + // "need to <cg>restart</c> the game to have it fully unloaded.", + // "OK" + // )->show(); + // if (m_list) m_list->updateAllStates(nullptr); + // return; + // } + if (as<CCMenuItemToggler*>(pSender)->isToggled()) { + auto res = m_mod->load(); + if (!res) { + FLAlertLayer::create( + nullptr, + "Error Loading Mod", + res.error(), + "OK", nullptr + )->show(); + } + else { + auto res = m_mod->enable(); + if (!res) { + FLAlertLayer::create( + nullptr, + "Error Enabling Mod", + res.error(), + "OK", nullptr + )->show(); + } + } + + } else { + auto res = m_mod->disable(); + if (!res) { + FLAlertLayer::create( + nullptr, + "Error Disabling Mod", + res.error(), + "OK", nullptr + )->show(); + } + } + if (m_list) m_list->updateAllStates(nullptr); + as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled()); +} + +void ModInfoLayer::onInstallMod(CCObject*) { + auto ticketRes = Index::get()->installItem( + Index::get()->getKnownItem(m_info.m_id), + [this](InstallTicket* ticket, UpdateStatus status, std::string const& info, uint8_t progress) -> void { + this->modInstallProgress(ticket, status, info, progress); + } + ); + if (!ticketRes) { + return FLAlertLayer::create( + "Unable to install", + ticketRes.error(), + "OK" + )->show(); + } + m_ticket = ticketRes.value(); + + auto layer = FLAlertLayer::create( + this, + "Install", + "The following <cb>mods</c> will be installed: " + + vector_utils::join(m_ticket->getInstallList(), ",") + ".", + "Cancel", "OK", 360.f + ); + layer->setTag(TAG_CONFIRM_INSTALL); + layer->show(); +} + +void ModInfoLayer::onCancelInstall(CCObject*) { + m_installBtn->setEnabled(false); + m_installBtnSpr->setString("Cancelling"); + + if (m_ticket) { + m_ticket->cancel(); + } + if (m_updateVersionLabel) { + m_updateVersionLabel->setVisible(true); + } +} + +void ModInfoLayer::onUninstall(CCObject*) { + auto layer = FLAlertLayer::create( + this, + "Confirm Uninstall", + "Are you sure you want to uninstall <cr>" + m_info.m_name + "</c>?", + "Cancel", "OK" + ); + layer->setTag(TAG_CONFIRM_UNINSTALL); + layer->show(); +} + +void ModInfoLayer::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) { + switch (layer->getTag()) { + case TAG_CONFIRM_INSTALL: { + if (btn2) { + this->install(); + } else { + this->updateInstallStatus("", 0); + } + } break; + + case TAG_CONFIRM_UNINSTALL: { + if (btn2) { + this->uninstall(); + } + } break; + + case TAG_DELETE_SAVEDATA: { + if (btn2) { + if (ghc::filesystem::remove_all(m_mod->getSaveDir())) { + FLAlertLayer::create( + "Deleted", + "The mod's save data was deleted.", + "OK" + )->show(); + } else { + FLAlertLayer::create( + "Error", + "Unable to delete mod's save directory!", + "OK" + )->show(); + } + } + m_list->refreshList(); + this->onClose(nullptr); + } break; + } +} + +void ModInfoLayer::updateInstallStatus( + std::string const& status, + uint8_t progress +) { + if (status.size()) { + m_installStatus->setVisible(true); + m_installStatus->setStatus(status); + m_installStatus->setProgress(progress); + } else { + m_installStatus->setVisible(false); + } +} + +void ModInfoLayer::modInstallProgress( + InstallTicket*, + UpdateStatus status, + std::string const& info, + uint8_t percentage +) { + switch (status) { + case UpdateStatus::Failed: { + FLAlertLayer::create( + "Installation failed :(", info, "OK" + )->show(); + this->updateInstallStatus("", 0); + + m_installBtn->setEnabled(true); + m_installBtn->setTarget( + this, menu_selector(ModInfoLayer::onInstallMod) + ); + m_installBtnSpr->setString("Install"); + m_installBtnSpr->setBG("GE_button_01.png"_spr, false); + + m_ticket = nullptr; + } break; + + case UpdateStatus::Finished: { + this->updateInstallStatus("", 100); + + FLAlertLayer::create( + "Install complete", + "Mod succesfully installed! :) " + "(You may need to <cy>restart the game</c> " + "for the mod to take full effect)", + "OK" + )->show(); + + m_ticket = nullptr; + + m_list->refreshList(); + this->onClose(nullptr); + } break; + + default: { + this->updateInstallStatus(info, percentage); + } break; + } +} + +void ModInfoLayer::install() { + if (m_ticket) { + if (m_updateVersionLabel) { + m_updateVersionLabel->setVisible(false); + } + this->updateInstallStatus("Starting install", 0); + + m_installBtn->setTarget( + this, menu_selector(ModInfoLayer::onCancelInstall) + ); + m_installBtnSpr->setString("Cancel"); + m_installBtnSpr->setBG("GJ_button_06.png", false); + + m_ticket->start(); + } +} + +void ModInfoLayer::uninstall() { + auto res = m_mod->uninstall(); + if (!res) { + return FLAlertLayer::create( + "Uninstall failed :(", + res.error(), + "OK" + )->show(); + } + auto layer = FLAlertLayer::create( + this, + "Uninstall complete", + "Mod was succesfully uninstalled! :) " + "(You may need to <cy>restart the game</c> " + "for the mod to take full effect). " + "<co>Would you also like to delete the mod's " + "save data?</c>", + "Cancel", "Delete", 350.f + ); + layer->setTag(TAG_DELETE_SAVEDATA); + layer->show(); +} + +void ModInfoLayer::onDisablingNotSupported(CCObject* pSender) { + FLAlertLayer::create( + "Unsupported", + "<cr>Disabling</c> is not supported for this mod.", + "OK" + )->show(); + as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled()); +} + +void ModInfoLayer::onHooks(CCObject*) { + auto layer = HookListLayer::create(this->m_mod); + this->addChild(layer); + layer->showLayer(false); +} + +void ModInfoLayer::onSettings(CCObject*) { + //ModSettingsLayer::create(this->m_mod)->show(); + // FIXME: No settings yet +} + +void ModInfoLayer::onNoSettings(CCObject*) { + FLAlertLayer::create( + "No Settings Found", + "This mod has no customizable settings.", + "OK" + )->show(); +} + +void ModInfoLayer::onInfo(CCObject*) { + FLAlertLayer::create( + nullptr, + ("About " + m_info.m_name).c_str(), + "<cr>ID: " + m_info.m_id + "</c>\n" + "<cg>Version: " + m_info.m_version.toString() + "</c>\n" + "<cp>Developer: " + m_info.m_developer + "</c>\n" + "<cb>Path: " + m_info.m_path.string() + "</c>\n", + "OK", nullptr, 400.f + )->show(); +} + +void ModInfoLayer::keyDown(enumKeyCodes key) { + if (key == KEY_Escape) + return this->onClose(nullptr); + if (key == KEY_Space) + return; + + return FLAlertLayer::keyDown(key); +} + +void ModInfoLayer::onClose(CCObject* pSender) { + this->setKeyboardEnabled(false); + this->removeFromParentAndCleanup(true); +}; + +ModInfoLayer* ModInfoLayer::create(Mod* mod, ModListView* list) { + auto ret = new ModInfoLayer; + if (ret && ret->init(new ModObject(mod), list)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +ModInfoLayer* ModInfoLayer::create(ModObject* obj, ModListView* list) { + auto ret = new ModInfoLayer; + if (ret && ret->init(obj, list)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CCNode* ModInfoLayer::createLogoSpr(ModObject* modObj) { + switch (modObj->m_type) { + case ModObjectType::Mod: { + return ModInfoLayer::createLogoSpr(modObj->m_mod); + } break; + + case ModObjectType::Index: { + return ModInfoLayer::createLogoSpr(modObj->m_index); + } break; + + default: { + auto spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); + if (!spr) { + return CCLabelBMFont::create("OwO", "goldFont.fnt"); + } + return spr; + } break; + } +} + +CCNode* ModInfoLayer::createLogoSpr(Mod* mod) { + CCNode* spr = nullptr; + if (mod == Loader::getInternalMod()) { + spr = CCSprite::create("geode.api.png"); + } else { + spr = CCSprite::create( + CCString::createWithFormat( + "%s.png", + mod->getID().c_str() + )->getCString() + ); + } + if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); + if (!spr) spr = CCLabelBMFont::create("OwO", "goldFont.fnt"); + return spr; +} + +CCNode* ModInfoLayer::createLogoSpr(IndexItem const& item) { + CCNode* spr = nullptr; + auto logoPath = ghc::filesystem::absolute(item.m_path / "logo.png"); + spr = CCSprite::create(logoPath.string().c_str()); + if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); + if (!spr) spr = CCLabelBMFont::create("OwO", "goldFont.fnt"); + return spr; +} diff --git a/loader/src/ui/internal/info/ModInfoLayer.hpp b/loader/src/ui/internal/info/ModInfoLayer.hpp new file mode 100644 index 00000000..7af442e5 --- /dev/null +++ b/loader/src/ui/internal/info/ModInfoLayer.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include <Geode/Geode.hpp> +#include <Index.hpp> + +USE_GEODE_NAMESPACE(); + +class ModListView; +class ModObject; + +class DownloadStatusNode : public CCNode { +protected: + Slider* m_bar; + CCLabelBMFont* m_label; + + bool init(); + +public: + static DownloadStatusNode* create(); + + void setProgress(uint8_t progress); + void setStatus(std::string const& text); +}; + +class ModInfoLayer : + public FLAlertLayer, + public FLAlertLayerProtocol +{ +protected: + Mod* m_mod = nullptr; + ModInfo m_info; + bool m_isIndexMod = false; + ModListView* m_list = nullptr; + DownloadStatusNode* m_installStatus = nullptr; + IconButtonSprite* m_installBtnSpr; + CCMenuItemSpriteExtra* m_installBtn; + CCLabelBMFont* m_updateVersionLabel = nullptr; + InstallTicket* m_ticket = nullptr; + + void onHooks(CCObject*); + void onSettings(CCObject*); + void onNoSettings(CCObject*); + void onInfo(CCObject*); + void onEnableMod(CCObject*); + void onInstallMod(CCObject*); + void onCancelInstall(CCObject*); + void onUninstall(CCObject*); + void onDisablingNotSupported(CCObject*); + void install(); + void uninstall(); + void updateInstallStatus(std::string const& status, uint8_t progress); + + void modInstallProgress( + InstallTicket*, + UpdateStatus status, + std::string const& info, + uint8_t percentage + ); + void FLAlert_Clicked(FLAlertLayer*, bool) override; + + bool init(ModObject* obj, ModListView* list); + + void keyDown(cocos2d::enumKeyCodes) override; + void onClose(cocos2d::CCObject*); + +public: + static ModInfoLayer* create(Mod* mod, ModListView* list); + static ModInfoLayer* create(ModObject* obj, ModListView* list); + + static CCNode* createLogoSpr(ModObject* modObj); + static CCNode* createLogoSpr(Mod* mod); + static CCNode* createLogoSpr(IndexItem const& item); +}; diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp new file mode 100644 index 00000000..64e33b3f --- /dev/null +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -0,0 +1,431 @@ +#include "ModListLayer.hpp" +#include <Geode/ui/BasedButton.hpp> +#include "SearchFilterPopup.hpp" +#include <Geode/ui/Notification.hpp> + +static ModListType g_tab = ModListType::Installed; +static ModListLayer* g_instance = nullptr; + +bool ModListLayer::init() { + if (!CCLayer::init()) + return false; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + // create background + auto bg = CCSprite::create("GJ_gradientBG.png"); + auto bgSize = bg->getTextureRect().size; + + bg->setAnchorPoint({ 0.0f, 0.0f }); + bg->setScaleX((winSize.width + 10.0f) / bgSize.width); + bg->setScaleY((winSize.height + 10.0f) / bgSize.height); + bg->setPosition({ -5.0f, -5.0f }); + bg->setColor({ 0, 102, 255 }); + + this->addChild(bg); + + // create menus + m_menu = CCMenu::create(); + m_topMenu = CCMenu::create(); + + // add back button + auto backBtn = CCMenuItemSpriteExtra::create( + CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"), + this, + menu_selector(ModListLayer::onExit) + ); + backBtn->setPosition(-winSize.width / 2 + 25.0f, winSize.height / 2 - 25.0f); + m_menu->addChild(backBtn); + + // add refresh mods button + auto reloadSpr = CCSprite::createWithSpriteFrameName("GJ_updateBtn_001.png"); + reloadSpr->setScale(.8f); + auto reloadBtn = CCMenuItemSpriteExtra::create( + reloadSpr, this, menu_selector(ModListLayer::onReload) + ); + reloadBtn->setPosition(-winSize.width / 2 + 30.0f, - winSize.height / 2 + 30.0f); + m_menu->addChild(reloadBtn); + + // add open folder button + auto openSpr = CircleButtonSprite::createWithSpriteFrameName( + "gj_folderBtn_001.png", .7f, + CircleBaseColor::Green, CircleBaseSize::Small + ); + auto openBtn = CCMenuItemSpriteExtra::create( + openSpr, this, menu_selector(ModListLayer::onOpenFolder) + ); + openBtn->setPosition(-winSize.width / 2 + 30.0f, - winSize.height / 2 + 80.0f); + this->m_menu->addChild(openBtn); + + + // add list status label + m_listLabel = CCLabelBMFont::create("", "bigFont.fnt"); + + m_listLabel->setPosition(winSize / 2); + m_listLabel->setScale(.6f); + m_listLabel->setVisible(false); + m_listLabel->setZOrder(1001); + + this->addChild(m_listLabel); + + + // add index update status label + m_indexUpdateLabel = CCLabelBMFont::create("", "goldFont.fnt"); + + m_indexUpdateLabel->setPosition(winSize.width / 2, winSize.height / 2 - 80.f); + m_indexUpdateLabel->setScale(.5f); + m_indexUpdateLabel->setZOrder(1001); + + this->addChild(m_indexUpdateLabel); + + + // tabs + m_installedTabBtn = TabButton::create( + "Installed", this, menu_selector(ModListLayer::onTab) + ); + m_installedTabBtn->setPosition(-95.f, 138.5f); + m_installedTabBtn->setTag(static_cast<int>(ModListType::Installed)); + m_menu->addChild(m_installedTabBtn); + + m_downloadTabBtn = TabButton::create( + "Download", this, menu_selector(ModListLayer::onTab) + ); + m_downloadTabBtn->setPosition(0.f, 138.5f); + m_downloadTabBtn->setTag(static_cast<int>(ModListType::Download)); + m_menu->addChild(m_downloadTabBtn); + + m_featuredTabBtn = TabButton::create( + "Featured", this, menu_selector(ModListLayer::onTab) + ); + m_featuredTabBtn->setPosition(95.f, 138.5f); + m_featuredTabBtn->setTag(static_cast<int>(ModListType::Featured)); + m_menu->addChild(m_featuredTabBtn); + + // add menus + m_menu->setZOrder(0); + m_topMenu->setZOrder(10); + + this->addChild(m_menu); + this->addChild(m_topMenu); + + // select first tab + this->onTab(nullptr); + + // enable keyboard + this->setKeyboardEnabled(true); + this->setKeypadEnabled(true); + + return true; +} + +std::tuple<CCNode*, CCTextInputNode*> ModListLayer::createSearchControl() { + auto layer = CCLayerColor::create({ 194, 114, 62, 255 }, 358.f, 30.f); + + auto menu = CCMenu::create(); + menu->setPosition(340.f, 15.f); + + auto filterSpr = EditorButtonSprite::createWithSpriteFrameName( + "filters.png"_spr, 1.0f, EditorBaseColor::Gray + ); + filterSpr->setScale(.7f); + + auto filterBtn = CCMenuItemSpriteExtra::create( + filterSpr, this, menu_selector(ModListLayer::onSearchFilters) + ); + filterBtn->setPosition(-8.f, 0.f); + menu->addChild(filterBtn); + + auto searchSpr = CCSprite::createWithSpriteFrameName("gj_findBtn_001.png"); + searchSpr->setScale(.7f); + + m_searchBtn = CCMenuItemSpriteExtra::create(searchSpr, this, nullptr); + m_searchBtn->setPosition(-35.f, 0.f); + menu->addChild(m_searchBtn); + + auto searchClearSpr = CCSprite::createWithSpriteFrameName("gj_findBtnOff_001.png"); + searchClearSpr->setScale(.7f); + + m_searchClearBtn = CCMenuItemSpriteExtra::create( + searchClearSpr, this, menu_selector(ModListLayer::onResetSearch) + ); + m_searchClearBtn->setPosition(-35.f, 0.f); + m_searchClearBtn->setVisible(false); + menu->addChild(m_searchClearBtn); + + auto inputBG = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + inputBG->setColor({ 126, 59, 7 }); + inputBG->setContentSize({ 530.f, 40.f }); + inputBG->setPosition(153.f, 15.f); + inputBG->setScale(.5f); + layer->addChild(inputBG); + + auto input = CCTextInputNode::create(250.f, 20.f, "Search Mods...", "bigFont.fnt"); + input->setLabelPlaceholderColor({ 150, 150, 150 }); + input->setLabelPlaceholderScale(.4f); + input->setMaxLabelScale(.4f); + input->setDelegate(this); + input->m_textField->setAnchorPoint({ .0f, .5f }); + input->m_placeholderLabel->setAnchorPoint({ .0f, .5f }); + + layer->addChild(menu); + return { layer, input }; +} + +void ModListLayer::indexUpdateProgress( + UpdateStatus status, + std::string const& info, + uint8_t percentage +) { + // if we have a check for updates button + // visible, disable it from being clicked + // again + if (m_checkForUpdatesBtn) { + m_checkForUpdatesBtn->setEnabled(false); + as<ButtonSprite*>( + m_checkForUpdatesBtn->getNormalImage() + )->setString("Updating Index"); + } + + // if finished, refresh list + if (status == UpdateStatus::Finished) { + m_indexUpdateLabel->setVisible(false); + this->reloadList(); + + // make sure to release global instance + // and set it back to null + CC_SAFE_RELEASE_NULL(g_instance); + } + else { + m_indexUpdateLabel->setVisible(true); + m_indexUpdateLabel->setString(info.c_str()); + } + + if (status == UpdateStatus::Failed) { + FLAlertLayer::create( + "Error Updating Index", + info, "OK" + )->show(); + + // make sure to release global instance + // and set it back to null + CC_SAFE_RELEASE(g_instance); + } +} + +void ModListLayer::reloadList() { + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + // remove old list + if (m_list) { + if (m_searchBG) m_searchBG->retain(); + m_list->removeFromParent(); + } + + // create new list + const char* filter = m_searchInput ? m_searchInput->getString() : nullptr; + auto list = ModListView::create(g_tab, 358.f, 190.f, filter, m_searchFlags); + list->setLayer(this); + + // set list status + auto status = list->getStatusAsString(); + if (status.size()) { + m_listLabel->setVisible(true); + m_listLabel->setString(status.c_str()); + } else { + m_listLabel->setVisible(false); + } + + // update index if needed + if (g_tab == ModListType::Download && !Index::get()->isIndexUpdated()) { + m_listLabel->setString("Updating index..."); + if (!m_loadingCircle) { + m_loadingCircle = LoadingCircle::create(); + + m_loadingCircle->setPosition(.0f, -40.f); + m_loadingCircle->setScale(.7f); + m_loadingCircle->setZOrder(1001); + + m_loadingCircle->show(); + } + this->onCheckForUpdates(nullptr); + } else { + if (m_loadingCircle) { + m_loadingCircle->fadeAndRemove(); + m_loadingCircle = nullptr; + } + } + + // set list + m_list = GJListLayer::create( + list, nullptr, { 0, 0, 0, 180 }, + 358.f, 220.f + ); + m_list->setZOrder(2); + m_list->setPosition( + winSize / 2 - m_list->getScaledContentSize() / 2 + ); + this->addChild(m_list); + + // add search input to list + if (!m_searchInput) { + auto search = this->createSearchControl(); + + m_searchBG = std::get<0>(search); + m_searchBG->setPosition(0.f, 190.f); + m_list->addChild(m_searchBG); + + m_searchInput = std::get<1>(search); + m_searchInput->setPosition( + winSize.width / 2 - 155.f, + winSize.height / 2 + 95.f + ); + m_searchInput->setZOrder(60); + this->addChild(m_searchInput); + } else { + m_list->addChild(m_searchBG); + m_searchBG->release(); + } + + // check if the user has searched something, + // and show visual indicator if so + auto hasQuery = filter && strlen(filter); + m_searchBtn->setVisible(!hasQuery); + m_searchClearBtn->setVisible(hasQuery); + + // add/remove "Check for Updates" button + if ( + // only show it on the install tab + g_tab == ModListType::Installed && + // check if index is updated, and if not + // add button if it doesn't exist yet + !Index::get()->isIndexUpdated() + ) { + if (!m_checkForUpdatesBtn) { + auto checkSpr = ButtonSprite::create("Check for Updates"); + checkSpr->setScale(.7f); + m_checkForUpdatesBtn = CCMenuItemSpriteExtra::create( + checkSpr, + this, + menu_selector(ModListLayer::onCheckForUpdates) + ); + m_checkForUpdatesBtn->setPosition(0, -winSize.height / 2 + 40.f); + m_topMenu->addChild(m_checkForUpdatesBtn); + } + } + // otherwise remove the button if it + // exists + else if (m_checkForUpdatesBtn) { + m_checkForUpdatesBtn->removeFromParent(); + m_checkForUpdatesBtn = nullptr; + } +} + +void ModListLayer::onCheckForUpdates(CCObject*) { + // store instance in a global so the + // layer stays in memory even if the + // user leaves the layer and we don't + // end up trying to update the UI of + // a deleted layer + g_instance = this; + g_instance->retain(); + + // update index + Index::get()->updateIndex( + []( + UpdateStatus status, + std::string const& info, + uint8_t progress + ) -> void { + g_instance->indexUpdateProgress(status, info, progress); + } + ); +} + +void ModListLayer::textChanged(CCTextInputNode* input) { + this->reloadList(); +} + +void ModListLayer::onExit(CCObject*) { + CCDirector::sharedDirector()->replaceScene( + CCTransitionFade::create(.5f, MenuLayer::scene(false)) + ); +} + +void ModListLayer::onReload(CCObject*) { + Loader::get()->refreshMods(); + this->reloadList(); +} + +void ModListLayer::onOpenFolder(CCObject*) { + dirs::openFolder( + ghc::filesystem::canonical(Loader::get()->getGeodeDirectory() / "mods") + ); +} + +void ModListLayer::onResetSearch(CCObject*) { + m_searchInput->setString(""); +} + +void ModListLayer::keyDown(enumKeyCodes key) { + if (key == KEY_Escape) { + this->onExit(nullptr); + } +} + +void ModListLayer::onTab(CCObject* pSender) { + if (pSender) { + g_tab = static_cast<ModListType>(pSender->getTag()); + } + this->reloadList(); + + auto toggleTab = [this](CCMenuItemToggler* member) -> void { + auto isSelected = member->getTag() == static_cast<int>(g_tab); + auto targetMenu = isSelected ? m_topMenu : m_menu; + member->toggle(isSelected); + if (member->getParent() != targetMenu) { + member->retain(); + member->removeFromParent(); + targetMenu->addChild(member); + member->release(); + } + }; + + toggleTab(m_downloadTabBtn); + toggleTab(m_installedTabBtn); + toggleTab(m_featuredTabBtn); +} + +void ModListLayer::onSearchFilters(CCObject*) { + SearchFilterPopup::create(this)->show(); +} + +ModListLayer* ModListLayer::create() { + // return global instance if one exists + if (g_instance) return g_instance; + + // otherwise create new instance like a + // normal person + auto ret = new ModListLayer(); + if (ret && ret->init()) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +ModListLayer* ModListLayer::scene() { + auto scene = CCScene::create(); + auto layer = ModListLayer::create(); + scene->addChild(layer); + CCDirector::sharedDirector()->replaceScene( + CCTransitionFade::create(.5f, scene) + ); + return layer; +} + +ModListLayer::~ModListLayer() { + removeAllChildrenWithCleanup(true); +} diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp new file mode 100644 index 00000000..bf81cfd2 --- /dev/null +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include <Geode/Geode.hpp> +#include "ModListView.hpp" +#include <Index.hpp> + +USE_GEODE_NAMESPACE(); + +class SearchFilterPopup; + +class ModListLayer : public CCLayer, public TextInputDelegate { +protected: + GJListLayer* m_list = nullptr; + CCLabelBMFont* m_listLabel; + CCLabelBMFont* m_indexUpdateLabel; + CCMenu* m_menu; + CCMenu* m_topMenu; + CCMenuItemToggler* m_installedTabBtn; + CCMenuItemToggler* m_downloadTabBtn; + CCMenuItemToggler* m_featuredTabBtn; + CCMenuItemSpriteExtra* m_searchBtn; + CCMenuItemSpriteExtra* m_searchClearBtn; + CCMenuItemSpriteExtra* m_checkForUpdatesBtn = nullptr; + CCNode* m_searchBG = nullptr; + CCTextInputNode* m_searchInput = nullptr; + LoadingCircle* m_loadingCircle = nullptr; + int m_searchFlags = ModListView::s_allFlags; + + virtual ~ModListLayer(); + + bool init() override; + + void onExit(CCObject*); + void onReload(CCObject*); + void onCheckForUpdates(CCObject*); + void onOpenFolder(CCObject*); + void onResetSearch(CCObject*); + void keyDown(enumKeyCodes) override; + void onTab(CCObject*); + void onSearchFilters(CCObject*); + void textChanged(CCTextInputNode*) override; + void indexUpdateProgress( + UpdateStatus status, + std::string const& info, + uint8_t percentage + ); + std::tuple<CCNode*, CCTextInputNode*> createSearchControl(); + + friend class SearchFilterPopup; + +public: + static ModListLayer* create(); + static ModListLayer* scene(); + + void reloadList(); +}; diff --git a/loader/src/ui/internal/list/ModListView.cpp b/loader/src/ui/internal/list/ModListView.cpp new file mode 100644 index 00000000..4ab9916e --- /dev/null +++ b/loader/src/ui/internal/list/ModListView.cpp @@ -0,0 +1,539 @@ +#include "ModListView.hpp" +#include "../info/ModInfoLayer.hpp" +#include <Geode/utils/WackyGeodeMacros.hpp> +#include <Index.hpp> +#include "ModListLayer.hpp" + +ModCell::ModCell(const char* name, CCSize size) : + TableViewCell(name, size.width, size.height) {} + +void ModCell::draw() { + reinterpret_cast<StatsCell*>(this)->StatsCell::draw(); +} + +void ModCell::onFailedInfo(CCObject*) { + FLAlertLayer::create( + this, + "Error Info", + m_obj->m_info.m_reason.size() ? + m_obj->m_info.m_reason : + m_obj->m_mod->getLoadErrorInfo(), + "OK", "Remove file", + 360.f + )->show(); +} + +void ModCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) { + if (btn2) { + try { + if (ghc::filesystem::remove(m_obj->m_info.m_file)) { + FLAlertLayer::create( + "File removed", + "Removed <cy>" + m_obj->m_info.m_file + "</c>", + "OK" + )->show(); + } else { + FLAlertLayer::create( + "Unable to remove file", + "Unable to remove <cy>" + m_obj->m_info.m_file + "</c>", + "OK" + )->show(); + } + } catch(std::exception& e) { + FLAlertLayer::create( + "Unable to remove file", + "Unable to remove <cy>" + + m_obj->m_info.m_file + "</c>: <cr>" + + std::string(e.what()) + "</c>", + "OK" + )->show(); + } + Loader::get()->refreshMods(); + m_list->refreshList(); + } +} + +void ModCell::setupUnloaded() { + m_mainLayer->setVisible(true); + + auto menu = CCMenu::create(); + menu->setPosition(m_width - m_height, m_height / 2); + m_mainLayer->addChild(menu); + + auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt"); + titleLabel->setAnchorPoint({ .0f, .5f }); + titleLabel->setScale(.5f); + titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f); + m_mainLayer->addChild(titleLabel); + + auto pathLabel = CCLabelBMFont::create( + m_obj->m_info.m_file.c_str(), "chatFont.fnt" + ); + pathLabel->setAnchorPoint({ .0f, .5f }); + pathLabel->setScale(.43f); + pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f); + pathLabel->setColor({ 255, 255, 0 }); + m_mainLayer->addChild(pathLabel); + + auto whySpr = ButtonSprite::create( + "Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f + ); + whySpr->setScale(.65f); + + auto viewBtn = CCMenuItemSpriteExtra::create( + whySpr, this, menu_selector(ModCell::onFailedInfo) + ); + menu->addChild(viewBtn); +} + +void ModCell::setupLoadedButtons() { + auto viewSpr = m_obj->m_mod->wasSuccesfullyLoaded() ? + ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f) : + ButtonSprite::create("Why", "bigFont.fnt", "GJ_button_06.png", .8f); + viewSpr->setScale(.65f); + + auto viewBtn = CCMenuItemSpriteExtra::create( + viewSpr, this, + m_obj->m_mod->wasSuccesfullyLoaded() ? + menu_selector(ModCell::onInfo) : + menu_selector(ModCell::onFailedInfo) + ); + m_menu->addChild(viewBtn); + + if ( + m_obj->m_mod->wasSuccesfullyLoaded() && + m_obj->m_mod->supportsDisabling() + ) { + m_enableToggle = CCMenuItemToggler::createWithStandardSprites( + this, menu_selector(ModCell::onEnable), .7f + ); + m_enableToggle->setPosition(-50.f, 0.f); + m_menu->addChild(m_enableToggle); + } + + auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png"); + exMark->setScale(.5f); + + m_unresolvedExMark = CCMenuItemSpriteExtra::create( + exMark, this, menu_selector(ModCell::onUnresolvedInfo) + ); + m_unresolvedExMark->setPosition(-80.f, 0.f); + m_menu->addChild(m_unresolvedExMark); + + if (!m_obj->m_mod->wasSuccesfullyLoaded()) { + m_unresolvedExMark->setVisible(false); + } else { + if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) { + viewSpr->updateBGImage("GE_button_01.png"_spr); + + auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); + updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f }); + updateIcon->setZOrder(99); + updateIcon->setScale(.5f); + viewSpr->addChild(updateIcon); + } + } +} + +void ModCell::setupIndexButtons() { + auto viewSpr = ButtonSprite::create( + "View", "bigFont.fnt", "GJ_button_01.png", .8f + ); + viewSpr->setScale(.65f); + + auto viewBtn = CCMenuItemSpriteExtra::create( + viewSpr, this, menu_selector(ModCell::onInfo) + ); + m_menu->addChild(viewBtn); +} + +void ModCell::loadFromObject(ModObject* modobj) { + m_obj = modobj; + + if (modobj->m_type == ModObjectType::Unloaded) { + return this->setupUnloaded(); + } + + m_mainLayer->setVisible(true); + m_backgroundLayer->setOpacity(255); + + m_menu = CCMenu::create(); + m_menu->setPosition(m_width - m_height, m_height / 2); + m_mainLayer->addChild(m_menu); + + auto logoSize = m_height - 12.f; + + auto logoSpr = ModInfoLayer::createLogoSpr(modobj); + logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); + logoSpr->setScale(logoSize / logoSpr->getContentSize().width); + m_mainLayer->addChild(logoSpr); + + ModInfo info; + switch (modobj->m_type) { + case ModObjectType::Mod: + info = modobj->m_mod->getModInfo(); + break; + + case ModObjectType::Index: + info = modobj->m_index.m_info; + break; + + default: return; + } + + auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt"); + titleLabel->setAnchorPoint({ .0f, .5f }); + titleLabel->setPosition(m_height / 2 + logoSize, m_height / 2 + 7.f); + titleLabel->limitLabelWidth(m_width / 2 - 30.f, .5f, .1f); + m_mainLayer->addChild(titleLabel); + + auto versionLabel = CCLabelBMFont::create( + info.m_version.toString().c_str(), "bigFont.fnt" + ); + versionLabel->setAnchorPoint({ .0f, .5f }); + versionLabel->setScale(.3f); + versionLabel->setPosition( + titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f, + m_height / 2 + 7.f + ); + versionLabel->setColor({ 0, 255, 0 }); + m_mainLayer->addChild(versionLabel); + + auto creatorStr = "by " + info.m_developer; + auto creatorLabel = CCLabelBMFont::create( + creatorStr.c_str(), "goldFont.fnt" + ); + creatorLabel->setAnchorPoint({ .0f, .5f }); + creatorLabel->setScale(.43f); + creatorLabel->setPosition(m_height / 2 + logoSize, m_height / 2 - 7.f); + m_mainLayer->addChild(creatorLabel); + + switch (modobj->m_type) { + case ModObjectType::Mod: + this->setupLoadedButtons(); + break; + + case ModObjectType::Index: + this->setupIndexButtons(); + break; + + default: + break; + } + this->updateState(); +} + +void ModCell::onInfo(CCObject*) { + ModInfoLayer::create(m_obj, m_list)->show(); +} + +void ModCell::updateBGColor(int index) { + if (index & 1) m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e)); + else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c)); + m_backgroundLayer->setOpacity(0xff); +} + +void ModCell::onEnable(CCObject* pSender) { + // if (!APIInternal::get()->m_shownEnableWarning) { + // APIInternal::get()->m_shownEnableWarning = true; + // FLAlertLayer::create( + // "Notice", + // "<cb>Disabling</c> a <cy>mod</c> removes its hooks & patches and " + // "calls its user-defined disable function if one exists. You may " + // "still see some effects of the mod left however, and you may " + // "need to <cg>restart</c> the game to have it fully unloaded.", + // "OK" + // )->show(); + // m_list->updateAllStates(this); + // return; + // } + if (!as<CCMenuItemToggler*>(pSender)->isToggled()) { + auto res = m_obj->m_mod->load(); + if (!res) { + FLAlertLayer::create( + nullptr, + "Error Loading Mod", + res.error(), + "OK", nullptr + )->show(); + } + else { + auto res = m_obj->m_mod->enable(); + if (!res) { + FLAlertLayer::create( + nullptr, + "Error Enabling Mod", + res.error(), + "OK", nullptr + )->show(); + } + } + } else { + auto res = m_obj->m_mod->disable(); + if (!res) { + FLAlertLayer::create( + nullptr, + "Error Disabling Mod", + res.error(), + "OK", nullptr + )->show(); + } + } + m_list->updateAllStates(this); +} + +void ModCell::onUnresolvedInfo(CCObject* pSender) { + std::string info = + "This mod has the following " + "<cr>unresolved dependencies</c>: "; + for (auto const& dep : m_obj->m_mod->getUnresolvedDependencies()) { + info += + "<cg>" + dep.m_id + "</c> " + "(<cy>" + dep.m_version.toString() + "</c>), "; + } + info.pop_back(); + info.pop_back(); + FLAlertLayer::create( + nullptr, + "Unresolved Dependencies", + info, + "OK", nullptr, + 400.f + )->show(); +} + +bool ModCell::init(ModListView* list) { + m_list = list; + return true; +} + +void ModCell::updateState(bool invert) { + if (m_obj->m_type == ModObjectType::Mod) { + bool unresolved = m_obj->m_mod->hasUnresolvedDependencies(); + if (m_enableToggle) { + m_enableToggle->toggle(m_obj->m_mod->isEnabled() ^ invert); + m_enableToggle->setEnabled(!unresolved); + m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255); + m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255)); + m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255); + m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255)); + } + m_unresolvedExMark->setVisible(unresolved); + } +} + +ModCell* ModCell::create(ModListView* list, const char* key, CCSize size) { + auto pRet = new ModCell(key, size); + if (pRet && pRet->init(list)) { + return pRet; + } + CC_SAFE_DELETE(pRet); + return nullptr; +} + + +void ModListView::updateAllStates(ModCell* toggled) { + CCARRAY_FOREACH_B_TYPE(m_tableView->m_cellArray, cell, ModCell) { + cell->updateState(toggled == cell); + } +} + +void ModListView::setupList() { + m_itemSeparation = 40.0f; + + if (!m_entries->count()) return; + + m_tableView->reloadData(); + + // fix content layer content size so the + // list is properly aligned to the top + auto coverage = calculateChildCoverage(m_tableView->m_contentLayer); + m_tableView->m_contentLayer->setContentSize({ + -coverage.origin.x + coverage.size.width, + -coverage.origin.y + coverage.size.height + }); + + if (m_entries->count() == 1) { + m_tableView->moveToTopWithOffset(m_itemSeparation * 2); + } else if (m_entries->count() == 2) { + m_tableView->moveToTopWithOffset(-m_itemSeparation); + } else { + m_tableView->moveToTop(); + } +} + +TableViewCell* ModListView::getListCell(const char* key) { + return ModCell::create(this, key, { m_width, m_itemSeparation }); +} + +void ModListView::loadCell(TableViewCell* cell, unsigned int index) { + auto obj = as<ModObject*>(m_entries->objectAtIndex(index)); + as<ModCell*>(cell)->loadFromObject(obj); + if (obj->m_type == ModObjectType::Mod) { + if (obj->m_mod->wasSuccesfullyLoaded()) { + as<ModCell*>(cell)->updateBGColor(index); + } else { + cell->m_backgroundLayer->setOpacity(255); + cell->m_backgroundLayer->setColor({ 153, 0, 0 }); + } + if (obj->m_mod->isUninstalled()) { + cell->m_backgroundLayer->setColor({ 50, 50, 50 }); + } + } else { + as<ModCell*>(cell)->updateBGColor(index); + } +} + +bool ModListView::filter(ModInfo const& info, const char* searchFilter, int searchFlags) { + if (!searchFilter || !strlen(searchFilter)) return true; + auto check = [searchFlags, searchFilter](SearchFlags flag, std::string const& name) -> bool { + if (!(searchFlags & flag)) return false; + return string_utils::contains( + string_utils::toLower(name), + string_utils::toLower(searchFilter) + ); + }; + if (check(SearchFlags::Name, info.m_name)) return true; + if (check(SearchFlags::ID, info.m_id)) return true; + if (check(SearchFlags::Developer, info.m_developer)) return true; + if (check(SearchFlags::Description, info.m_description)) return true; + if (check(SearchFlags::Details, info.m_details)) return true; + return false; +} + +static void sortInstalledMods(std::vector<Mod*>& mods) { + if (!mods.size()) return; + // keep track of first object + size_t frontIndex = 0; + auto front = mods.front(); + for (auto mod = mods.begin(); mod != mods.end(); mod++) { + // move mods with updates to front + if (Index::get()->isUpdateAvailableForItem((*mod)->getID())) { + // swap first object and updatable mod + // if the updatable mod is the first object, + // nothing changes + std::rotate(mods.begin(), mod, mod + 1); + + // get next object at front for next mod + // to sort + frontIndex++; + front = mods[frontIndex]; + } + } +} + +static std::vector<Mod*> sortedInstalledMods() { + auto mods = Loader::get()->getAllMods(); + sortInstalledMods(mods); + return std::move(mods); +} + +bool ModListView::init( + CCArray* mods, + ModListType type, + float width, + float height, + const char* searchFilter, + int searchFlags +) { + if (!mods) { + switch (type) { + case ModListType::Installed: { + mods = CCArray::create(); + // failed mods first + for (auto const& mod : Loader::get()->getFailedMods()) { + mods->addObject(new ModObject(mod)); + } + // internal geode representation always at the top + auto imod = Loader::getInternalMod(); + if (this->filter(imod->getModInfo(), searchFilter, searchFlags)) { + mods->addObject(new ModObject(imod)); + } + // then other mods + for (auto const& mod : sortedInstalledMods()) { + // if the mod is no longer installed nor + // loaded, it's as good as not existing + // (because it doesn't) + if (mod->isUninstalled() && !mod->isLoaded()) continue; + if (this->filter(mod->getModInfo(), searchFilter, searchFlags)) { + mods->addObject(new ModObject(mod)); + } + } + if (!mods->count()) { + m_status = Status::SearchEmpty; + } + } break; + + case ModListType::Download: { + mods = CCArray::create(); + for (auto const& item : Index::get()->getUninstalledItems()) { + mods->addObject(new ModObject(item)); + } + if (!mods->count()) { + m_status = Status::NoModsFound; + } + } break; + + case ModListType::Featured: { + mods = CCArray::create(); + m_status = Status::NoModsFound; + } break; + + default: return false; + } + } + return CustomListView::init(mods, BoomListType::Default, width, height); +} + +ModListView* ModListView::create( + CCArray* mods, + ModListType type, + float width, + float height, + const char* searchFilter, + int searchFlags +) { + auto pRet = new ModListView; + if (pRet) { + if (pRet->init(mods, type, width, height, searchFilter, searchFlags)) { + pRet->autorelease(); + return pRet; + } + } + CC_SAFE_DELETE(pRet); + return nullptr; +} + +ModListView* ModListView::create( + ModListType type, + float width, + float height, + const char* searchFilter, + int searchFlags +) { + return ModListView::create(nullptr, type, width, height, searchFilter, searchFlags); +} + +ModListView::Status ModListView::getStatus() const { + return m_status; +} + +std::string ModListView::getStatusAsString() const { + switch (m_status) { + case Status::OK: return ""; + case Status::Unknown: return "Unknown Issue"; + case Status::NoModsFound: return "No Mods Found"; + case Status::SearchEmpty: return "No Mods Match Search Query"; + } + return "Unrecorded Status"; +} + +void ModListView::setLayer(ModListLayer* layer) { + m_layer = layer; +} + +void ModListView::refreshList() { + if (m_layer) { + m_layer->reloadList(); + } +} diff --git a/loader/src/ui/internal/list/ModListView.hpp b/loader/src/ui/internal/list/ModListView.hpp new file mode 100644 index 00000000..c7fb6973 --- /dev/null +++ b/loader/src/ui/internal/list/ModListView.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include <Geode/Geode.hpp> +#include <Index.hpp> + +USE_GEODE_NAMESPACE(); + +enum class ModListType { + Installed, + Download, + Featured, +}; + +enum class ModObjectType { + Mod, + Unloaded, + Index, +}; + +class ModListLayer; + +// Wrapper so you can pass Mods in a CCArray +struct ModObject : public CCObject { + ModObjectType m_type; + Mod* m_mod; + Loader::FailedModInfo m_info; + IndexItem m_index; + + inline ModObject(Mod* mod) + : m_mod(mod), m_type(ModObjectType::Mod) { + this->autorelease(); + }; + inline ModObject(Loader::FailedModInfo const& info) + : m_info(info), m_type(ModObjectType::Unloaded) { + this->autorelease(); + }; + inline ModObject(IndexItem const& index) + : m_index(index), m_type(ModObjectType::Index) { + this->autorelease(); + }; +}; + +class ModListView; + +class ModCell : public TableViewCell, public FLAlertLayerProtocol { +protected: + ModListView* m_list; + ModObject* m_obj; + CCMenu* m_menu; + CCMenuItemToggler* m_enableToggle = nullptr; + CCMenuItemSpriteExtra* m_unresolvedExMark; + + ModCell(const char* name, CCSize size); + + void draw() override; + void onInfo(CCObject*); + void onFailedInfo(CCObject*); + void onEnable(CCObject*); + void onUnresolvedInfo(CCObject*); + + void setupUnloaded(); + void setupLoadedButtons(); + void setupIndexButtons(); + + void FLAlert_Clicked(FLAlertLayer*, bool btn2) override; + + bool init(ModListView* list); + +public: + void updateBGColor(int index); + void loadFromObject(ModObject*); + void updateState(bool invert = false); + + static ModCell* create(ModListView* list, const char* key, CCSize size); +}; + +class ModListView : public CustomListView { +public: + // this is not enum class so | works + enum SearchFlags { + Name = 0b1, + ID = 0b10, + Developer = 0b100, + Credits = 0b1000, + Description = 0b10000, + Details = 0b100000, + }; + static constexpr int s_allFlags = + SearchFlags::Name | + SearchFlags::ID | + SearchFlags::Developer | + SearchFlags::Credits | + SearchFlags::Description | + SearchFlags::Details; + +protected: + enum class Status { + OK, + Unknown, + NoModsFound, + SearchEmpty, + }; + + Status m_status = Status::OK; + ModListLayer* m_layer = nullptr; + + void setupList() override; + TableViewCell* getListCell(const char* key) override; + void loadCell(TableViewCell* cell, unsigned int index) override; + + bool init( + CCArray* mods, + ModListType type, + float width, + float height, + const char* searchFilter, + int searchFlags + ); + bool filter(ModInfo const& info, const char* searchFilter, int searchFlags); + +public: + static ModListView* create( + CCArray* mods, + ModListType type = ModListType::Installed, + float width = 358.f, + float height = 220.f, + const char* searchFilter = nullptr, + int searchFlags = 0 + ); + static ModListView* create( + ModListType type, + float width = 358.f, + float height = 220.f, + const char* searchFilter = nullptr, + int searchFlags = 0 + ); + + void updateAllStates(ModCell* toggled = nullptr); + void setLayer(ModListLayer* layer); + void refreshList(); + + Status getStatus() const; + std::string getStatusAsString() const; +}; diff --git a/loader/src/ui/internal/list/SearchFilterPopup.cpp b/loader/src/ui/internal/list/SearchFilterPopup.cpp new file mode 100644 index 00000000..d60386ab --- /dev/null +++ b/loader/src/ui/internal/list/SearchFilterPopup.cpp @@ -0,0 +1,102 @@ +#include "SearchFilterPopup.hpp" +#include "ModListLayer.hpp" +#include "ModListView.hpp" + +bool SearchFilterPopup::init(ModListLayer* layer) { + this->m_noElasticity = true; + + this->m_modLayer = layer; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + CCSize size { 280.f, 250.f }; + + if (!this->initWithColor({ 0, 0, 0, 105 })) return false; + this->m_mainLayer = CCLayer::create(); + this->addChild(this->m_mainLayer); + + auto bg = CCScale9Sprite::create("GJ_square05.png", { 0.0f, 0.0f, 80.0f, 80.0f }); + bg->setContentSize(size); + bg->setPosition(winSize.width / 2, winSize.height / 2); + this->m_mainLayer->addChild(bg); + + this->m_buttonMenu = CCMenu::create(); + this->m_mainLayer->addChild(this->m_buttonMenu); + + auto nameLabel = CCLabelBMFont::create("Search Filters", "goldFont.fnt"); + nameLabel->setPosition(winSize.width / 2, winSize.height / 2 + 100.f); + nameLabel->setScale(.7f); + this->m_mainLayer->addChild(nameLabel, 2); + + this->m_pos = CCPoint { winSize.width / 2 - 45.f, 86.f }; + + this->addToggle("Name", ModListView::SearchFlags::Name); + this->addToggle("ID", ModListView::SearchFlags::ID); + this->addToggle("Credits", ModListView::SearchFlags::Credits); + this->addToggle("Description", ModListView::SearchFlags::Description); + this->addToggle("Details", ModListView::SearchFlags::Details); + this->addToggle("Developer", ModListView::SearchFlags::Developer); + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); + closeSpr->setScale(1.0f); + + auto closeBtn = CCMenuItemSpriteExtra::create( + closeSpr, + this, + (SEL_MenuHandler)&SearchFilterPopup::onClose + ); + closeBtn->setUserData(reinterpret_cast<void*>(this)); + + this->m_buttonMenu->addChild(closeBtn); + + closeBtn->setPosition( - size.width / 2, size.height / 2 ); + + this->setKeypadEnabled(true); + this->setTouchEnabled(true); + + return true; +} + +void SearchFilterPopup::addToggle(const char* title, int flag) { + GameToolbox::createToggleButton( + title, menu_selector(SearchFilterPopup::onToggle), + this->m_modLayer->m_searchFlags & flag, + this->m_buttonMenu, this->m_pos, this, + this->m_buttonMenu, .5f, .5f, 100.f, + { 10.f, .0f }, nullptr, false, flag, nullptr + )->setTag(flag); + this->m_pos.y += 25.f; +} + +void SearchFilterPopup::keyDown(cocos2d::enumKeyCodes key) { + if (key == KEY_Escape) + return onClose(nullptr); + if (key == KEY_Space) + return; + return FLAlertLayer::keyDown(key); +} + +void SearchFilterPopup::onClose(cocos2d::CCObject*) { + this->setKeyboardEnabled(false); + this->removeFromParentAndCleanup(true); +} + +void SearchFilterPopup::onToggle(cocos2d::CCObject* pSender) { + if (as<CCMenuItemToggler*>(pSender)->isToggled()) { + this->m_modLayer->m_searchFlags &= ~pSender->getTag(); + } else { + this->m_modLayer->m_searchFlags |= pSender->getTag(); + } +} + +SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer) { + auto ret = new SearchFilterPopup(); + if (ret && ret->init(layer)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/internal/list/SearchFilterPopup.hpp b/loader/src/ui/internal/list/SearchFilterPopup.hpp new file mode 100644 index 00000000..a74a58c7 --- /dev/null +++ b/loader/src/ui/internal/list/SearchFilterPopup.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include <Geode/Geode.hpp> + +USE_GEODE_NAMESPACE(); + +class ModListLayer; + +class SearchFilterPopup : public FLAlertLayer { + protected: + ModListLayer* m_modLayer; + CCPoint m_pos; + + bool init(ModListLayer* layer); + void addToggle(const char* title, int flag); + + void keyDown(cocos2d::enumKeyCodes) override; + void onClose(cocos2d::CCObject*); + void onToggle(cocos2d::CCObject*); + + public: + static SearchFilterPopup* create(ModListLayer* layer); +}; + diff --git a/loader/src/ui/internal/settings/GeodeSettingNode.cpp b/loader/src/ui/internal/settings/GeodeSettingNode.cpp new file mode 100644 index 00000000..337b6bb9 --- /dev/null +++ b/loader/src/ui/internal/settings/GeodeSettingNode.cpp @@ -0,0 +1,760 @@ +/*#pragma warning(disable: 4067) + +#include "GeodeSettingNode.hpp" +#include <general/TextRenderer.hpp> +#include <random> +#include <utils/WackyGeodeMacros.hpp> + +USE_GEODE_NAMESPACE(); + +#define GEODE_GENERATE_SETTING_CREATE(_sett_, _height_) \ + _sett_##Node* _sett_##Node::create(_sett_* setting, float width) { \ + auto ret = new _sett_##Node(width, _height_); \ + if (ret && ret->init(setting)) { \ + ret->autorelease(); \ + return ret; \ + } CC_SAFE_DELETE(ret); return nullptr; } + +template <typename T> +std::string toStringWithPrecision(const T a_value, const size_t n = 6, bool cutZeros = true) { + std::ostringstream out; + out.precision(n); + out << std::fixed << a_value; + auto str = out.str(); + while ( + cutZeros && + string_utils::contains(str, '.') && + (str.back() == '0' || str.back() == '.') + ) { + str = str.substr(0, str.size() - 1); + } + return str; +} + +// bool + +bool BoolSettingNode::init(BoolSetting* setting) { + if (!GeodeSettingNode<BoolSetting>::init(setting)) + return false; + + m_toggle = CCMenuItemToggler::createWithStandardSprites( + this, menu_selector(BoolSettingNode::onToggle), .65f + ); + m_toggle->setPosition(-m_toggle->m_onButton->getScaledContentSize().width / 2, 0); + m_toggle->toggle(setting->getValue()); + m_buttonMenu->addChild(m_toggle); + + return true; +} + +void BoolSettingNode::onToggle(CCObject* pSender) { + m_value = !m_toggle->isToggled(); + this->updateSettingsList(); +} + +void BoolSettingNode::updateState() { + m_toggle->toggle(m_value); +} + +// int + +bool IntSettingNode::init(IntSetting* setting) { + if (!GeodeSettingNode<IntSetting>::init(setting)) + return false; + + auto controls = CCArray::create(); + CCScale9Sprite* bgSprite = nullptr; + if (setting->hasInput()) { + bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.25f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setContentSize({ 45.f * 4, m_height * 3 }); + bgSprite->setPosition(-20, 0); + m_buttonMenu->addChild(bgSprite); + controls->addObject(bgSprite); + } + + m_valueInput = MenuInputNode::create(45.f, m_height, "Num", "bigFont.fnt"); + m_valueInput->setPosition(-20.f, .0f); + m_valueInput->getInput()->setAllowedChars("0123456789+-. "); + m_valueInput->getInput()->setMaxLabelScale(.5f); + m_valueInput->getInput()->setLabelPlaceholderColor({ 150, 150, 150 }); + m_valueInput->getInput()->setLabelPlaceholderScale(.75f); + m_valueInput->getInput()->setDelegate(this); + m_valueInput->setEnabled(setting->hasInput()); + m_buttonMenu->addChild(m_valueInput); + controls->addObject(m_valueInput); + + if (setting->hasArrows()) { + m_valueInput->setPositionX(-30.f); + if (bgSprite) bgSprite->setPositionX(-30.f); + + auto decSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + decSpr->setScale(.3f); + decSpr->setFlipX(true); + + auto decBtn = CCMenuItemSpriteExtra::create( + decSpr, this, menu_selector(IntSettingNode::onArrow) + ); + decBtn->setTag(-1); + decBtn->setPosition(-60.f, 0); + m_buttonMenu->addChild(decBtn); + controls->addObject(decBtn); + + auto incSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + incSpr->setScale(.3f); + + auto incBtn = CCMenuItemSpriteExtra::create( + incSpr, this, menu_selector(IntSettingNode::onArrow) + ); + incBtn->setTag(1); + incBtn->setPosition(0.f, 0); + m_buttonMenu->addChild(incBtn); + controls->addObject(incBtn); + } + + if (setting->hasSlider()) { + m_height += 20.f; + this->setContentSize({ m_width, m_height }); + m_backgroundLayer->setContentSize(this->getContentSize()); + + m_nameLabel->setPositionY(m_nameLabel->getPositionY() + 10.f); + if (m_descButton) { + m_descButton->setPositionY(m_descButton->getPositionY() - 8.f); + } + m_buttonMenu->setPositionY(m_buttonMenu->getPositionY() + 18.f); + + CCARRAY_FOREACH_B_TYPE(controls, node, CCNode) { + node->setPositionX(node->getPositionX() - (setting->hasArrows() ? 35.f : 45.f)); + } + + m_slider = Slider::create(this, menu_selector(IntSettingNode::onSlider), .65f); + m_slider->setPosition(-65.f, -22.f); + m_slider->setValue( + // normalized to 0-1 + (m_value - m_setting->getMin()) / + (m_setting->getMax() - m_setting->getMin()) + ); + m_buttonMenu->addChild(m_slider); + } + + this->updateValue(); + + return true; +} + +void IntSettingNode::textChanged(CCTextInputNode* input) { + try { + m_value = std::stoi(input->getString()); + } catch(...) {} + this->updateValue(false); +} + +void IntSettingNode::textInputClosed(CCTextInputNode* input) { + try { + m_value = std::stoi(input->getString()); + } catch(...) {} + this->updateValue(); +} + +void IntSettingNode::onSlider(CCObject* pSender) { + m_value = + as<SliderThumb*>(pSender)->getValue() * + (m_setting->getMax() - m_setting->getMin()) + + m_setting->getMin(); + m_valueInput->getInput()->detachWithIME(); + this->updateValue(); +} + +void IntSettingNode::onArrow(CCObject* pSender) { + m_value = m_value + pSender->getTag() * m_setting->getStep(); + m_valueInput->getInput()->detachWithIME(); + this->updateValue(); +} + +void IntSettingNode::updateValue(bool updateInput) { + if (m_value < m_setting->getMin()) { + m_value = m_setting->getMin(); + } + if (m_value > m_setting->getMax()) { + m_value = m_setting->getMax(); + } + if (updateInput) { + m_valueInput->getInput()->setString( + std::to_string(m_value).c_str() + ); + } + if (m_slider) { + m_slider->setValue( + static_cast<float>(m_value - m_setting->getMin()) / + (m_setting->getMax() - m_setting->getMin()) + ); + m_slider->updateBar(); + } + this->updateSettingsList(); +} + +void IntSettingNode::updateState() { + this->updateValue(); +} + +// float + +bool FloatSettingNode::init(FloatSetting* setting) { + if (!GeodeSettingNode<FloatSetting>::init(setting)) + return false; + + auto controls = CCArray::create(); + CCScale9Sprite* bgSprite = nullptr; + if (setting->hasInput()) { + bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.25f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setContentSize({ 45.f * 4, m_height * 3 }); + bgSprite->setPosition(-20, 0); + m_buttonMenu->addChild(bgSprite); + controls->addObject(bgSprite); + } + + m_valueInput = MenuInputNode::create(45.f, m_height, "Num", "bigFont.fnt"); + m_valueInput->setPosition(-20.f, .0f); + m_valueInput->getInput()->setAllowedChars("0123456789+-. "); + m_valueInput->getInput()->setMaxLabelScale(.5f); + m_valueInput->getInput()->setLabelPlaceholderColor({ 150, 150, 150 }); + m_valueInput->getInput()->setLabelPlaceholderScale(.5f); + m_valueInput->getInput()->setDelegate(this); + m_valueInput->setEnabled(setting->hasInput()); + m_buttonMenu->addChild(m_valueInput); + controls->addObject(m_valueInput); + + CCMenuItemSpriteExtra* decBtn = nullptr; + CCMenuItemSpriteExtra* incBtn = nullptr; + if (setting->hasArrows()) { + m_valueInput->setPositionX(-30.f); + if (bgSprite) bgSprite->setPositionX(-30.f); + + auto decSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + decSpr->setScale(.3f); + decSpr->setFlipX(true); + + decBtn = CCMenuItemSpriteExtra::create( + decSpr, this, menu_selector(FloatSettingNode::onArrow) + ); + decBtn->setTag(-1); + decBtn->setPosition(-60.f, 0); + m_buttonMenu->addChild(decBtn); + controls->addObject(decBtn); + + auto incSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + incSpr->setScale(.3f); + + incBtn = CCMenuItemSpriteExtra::create( + incSpr, this, menu_selector(FloatSettingNode::onArrow) + ); + incBtn->setTag(1); + incBtn->setPosition(0.f, 0); + m_buttonMenu->addChild(incBtn); + controls->addObject(incBtn); + } + + if (setting->hasSlider()) { + m_height += 20.f; + this->setContentSize({ m_width, m_height }); + m_backgroundLayer->setContentSize(this->getContentSize()); + + m_nameLabel->setPositionY(m_nameLabel->getPositionY() + 10.f); + if (m_descButton) { + m_descButton->setPositionY(m_descButton->getPositionY() - 8.f); + } + m_buttonMenu->setPositionY(m_buttonMenu->getPositionY() + 18.f); + + CCARRAY_FOREACH_B_TYPE(controls, node, CCNode) { + node->setPositionX(node->getPositionX() - (setting->hasArrows() ? 35.f : 45.f)); + } + + m_slider = Slider::create(this, menu_selector(FloatSettingNode::onSlider), .65f); + m_slider->setPosition(-65.f, -22.f); + m_slider->setValue( + // normalized to 0-1 + (m_value - m_setting->getMin()) / + (m_setting->getMax() - m_setting->getMin()) + ); + m_buttonMenu->addChild(m_slider); + } + + this->updateValue(); + + return true; +} + +void FloatSettingNode::textChanged(CCTextInputNode* input) { + try { + m_value = std::stof(input->getString()); + } catch(...) {} + this->updateValue(false); +} + +void FloatSettingNode::textInputClosed(CCTextInputNode* input) { + try { + m_value = std::stof(input->getString()); + } catch(...) {} + this->updateValue(); +} + +void FloatSettingNode::onSlider(CCObject* pSender) { + m_value = + as<SliderThumb*>(pSender)->getValue() * + (m_setting->getMax() - m_setting->getMin()) + + m_setting->getMin(); + m_valueInput->getInput()->detachWithIME(); + this->updateValue(); +} + +void FloatSettingNode::onArrow(CCObject* pSender) { + m_value = m_value + pSender->getTag() * m_setting->getStep(); + this->updateValue(); +} + +void FloatSettingNode::updateValue(bool updateInput) { + if (m_value < m_setting->getMin()) { + m_value = m_setting->getMin(); + } + if (m_value > m_setting->getMax()) { + m_value = m_setting->getMax(); + } + if (updateInput) { + m_valueInput->getInput()->setString(toStringWithPrecision( + m_value, m_setting->getPrecision() + ).c_str()); + } + if (m_slider) { + m_slider->setValue( + // normalized to 0-1 + (m_value - m_setting->getMin()) / + (m_setting->getMax() - m_setting->getMin()) + ); + m_slider->updateBar(); + } + this->updateSettingsList(); +} + +void FloatSettingNode::updateState() { + this->updateValue(); +} + +// string + +bool StringSettingNode::init(StringSetting* setting) { + if (!GeodeSettingNode<StringSetting>::init(setting)) + return false; + + auto bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.25f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setContentSize({ (m_width / 2 - 30.f) * 4, m_height * 3 }); + bgSprite->setPosition(-m_width / 4 + 18.f, 0); + m_buttonMenu->addChild(bgSprite); + + m_input = MenuInputNode::create( + m_width / 2 - 30.f, m_height, + "...", "bigFont.fnt" + ); + m_input->setPositionX(-m_width / 4 + 18.f); + m_input->getInput()->setAllowedChars(setting->getFilter()); + m_input->getInput()->setMaxLabelScale(.5f); + m_input->getInput()->setLabelPlaceholderColor({ 150, 150, 150 }); + m_input->getInput()->setLabelPlaceholderScale(.5f); + m_input->getInput()->setDelegate(this); + m_buttonMenu->addChild(m_input); + + m_input->getInput()->setString(m_value.c_str()); + + return true; +} + +void StringSettingNode::textChanged(CCTextInputNode* input) { + m_value = input->getString(); + this->updateSettingsList(); +} + +void StringSettingNode::updateState() { + m_input->getInput()->setString(m_value); +} + +// color + +bool ColorSettingNode::init(ColorSetting* setting) { + if (!GeodeSettingNode<ColorSetting>::init(setting)) + return false; + + m_colorSprite = ColorChannelSprite::create(); + m_colorSprite->setColor(setting->getValue()); + m_colorSprite->setScale(.65f); + + auto button = CCMenuItemSpriteExtra::create( + m_colorSprite, this, menu_selector(ColorSettingNode::onPickColor) + ); + button->setPosition({ -m_colorSprite->getScaledContentSize().width / 2, 0 }); + m_buttonMenu->addChild(button); + + return true; +} + +void ColorSettingNode::onPickColor(CCObject*) { + ColorPickPopup::create(this)->show(); +} + +void ColorSettingNode::updateState() { + m_colorSprite->setColor(m_value); +} + +// rgba + +bool ColorAlphaSettingNode::init(ColorAlphaSetting* setting) { + if (!GeodeSettingNode<ColorAlphaSetting>::init(setting)) + return false; + + m_colorSprite = ColorChannelSprite::create(); + m_colorSprite->setColor(to3B(setting->getValue())); + m_colorSprite->updateOpacity(setting->getValue().a / 255.f); + m_colorSprite->setScale(.65f); + + auto button = CCMenuItemSpriteExtra::create( + m_colorSprite, this, menu_selector(ColorAlphaSettingNode::onPickColor) + ); + button->setPosition({ -m_colorSprite->getScaledContentSize().width / 2, 0 }); + m_buttonMenu->addChild(button); + + return true; +} + +void ColorAlphaSettingNode::onPickColor(CCObject*) { + ColorPickPopup::create(this)->show(); +} + +void ColorAlphaSettingNode::updateState() { + m_colorSprite->setColor(to3B(m_value)); +} + +// path + +bool PathSettingNode::init(PathSetting* setting) { + if (!GeodeSettingNode<PathSetting>::init(setting)) + return false; + + auto bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.25f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setContentSize({ (m_width / 2 - 50.f) * 4, m_height * 3 }); + bgSprite->setPosition(-m_width / 4, 0); + m_buttonMenu->addChild(bgSprite); + + m_input = MenuInputNode::create( + m_width / 2 - 50.f, m_height, + "...", "chatFont.fnt" + ); + m_input->setPositionX(-m_width / 4); + m_input->getInput()->setAllowedChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,/\\_-.+%#@!';"); + m_input->getInput()->setMaxLabelScale(.7f); + m_input->getInput()->setLabelPlaceholderColor({ 150, 150, 150 }); + m_input->getInput()->setLabelPlaceholderScale(.7f); + m_input->getInput()->setDelegate(this); + m_buttonMenu->addChild(m_input); + + auto folder = CCSprite::createWithSpriteFrameName("gj_folderBtn_001.png"); + folder->setScale(.65f); + auto button = CCMenuItemSpriteExtra::create( + folder, this, menu_selector(PathSettingNode::onSelectFile) + ); + button->setPosition({ -folder->getScaledContentSize().width / 2, 0 }); + m_buttonMenu->addChild(button); + + m_input->getInput()->setString(m_value.string().c_str()); + + return true; +} + +void PathSettingNode::onSelectFile(CCObject*) { + FLAlertLayer::create("todo", "implement file picker", "OK")->show(); + this->updateSettingsList(); +} + +void PathSettingNode::textChanged(CCTextInputNode* input) { + m_value = input->getString(); + this->updateSettingsList(); +} + +void PathSettingNode::updateState() { + m_input->getInput()->setString(m_value.string()); +} + +// string[] + +bool StringSelectSettingNode::init(StringSelectSetting* setting) { + if (!GeodeSettingNode<StringSelectSetting>::init(setting)) + return false; + + m_selectedLabel = CCLabelBMFont::create(setting->getValue().c_str(), "bigFont.fnt"); + m_selectedLabel->setPosition(-m_width / 4 + 20.f, .0f); + m_selectedLabel->limitLabelWidth(m_width / 2 - 60.f, .5f, .1f); + m_buttonMenu->addChild(m_selectedLabel); + + auto decSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + decSpr->setScale(.3f); + decSpr->setFlipX(true); + + auto decBtn = CCMenuItemSpriteExtra::create( + decSpr, this, menu_selector(StringSelectSettingNode::onChange) + ); + decBtn->setTag(-1); + decBtn->setPosition(-m_width / 2 + 40.f, 0); + m_buttonMenu->addChild(decBtn); + + auto incSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + incSpr->setScale(.3f); + + auto incBtn = CCMenuItemSpriteExtra::create( + incSpr, this, menu_selector(StringSelectSettingNode::onChange) + ); + incBtn->setTag(1); + incBtn->setPosition(0.f, 0); + m_buttonMenu->addChild(incBtn); + + return true; +} + +void StringSelectSettingNode::onChange(CCObject* pSender) { + if (pSender) { + long newValue = m_value + pSender->getTag(); + if (newValue < 0) m_value = m_setting->getOptions().size() - 1; + else if (newValue > static_cast<long>(m_setting->getOptions().size() - 1)) m_value = 0; + else m_value = newValue; + } + + m_selectedLabel->setString(m_setting->getValueAt(m_value).c_str()); + m_selectedLabel->limitLabelWidth(m_width / 2 - 60.f, .5f, .1f); + this->updateSettingsList(); +} + +void StringSelectSettingNode::updateState() { + this->onChange(nullptr); +} + +// custom + +bool CustomSettingPlaceHolderNode::init(CustomSettingPlaceHolder* setting, bool isLoaded) { + if (!CCNode::init()) + return false; + + auto pad = m_height; + this->setContentSize({ m_width, m_height }); + m_backgroundLayer->setContentSize(this->getContentSize()); + + // i'm using TextRenderer instead of TextArea because + // i couldn't get TextArea to work for some reason + // and TextRenderer should be fast enough for short + // static text + + auto render = TextRenderer::create(); + + render->begin(this, CCPointZero, { m_width - pad * 2, m_height }); + + render->pushFont( + [](int) -> TextRenderer::Label { + return CCLabelBMFont::create("", "chatFont.fnt"); + } + ); + render->pushHorizontalAlign(TextAlignment::Begin); + render->pushVerticalAlign(TextAlignment::Begin); + render->pushScale(.7f); + auto rendered = render->renderString( + isLoaded ? + "This setting (id: " + setting->getKey() + ") is a " + "custom setting which has no registered setting node. " + "This is likely a bug in the mod; report it to the " + "developer." : + "This setting (id: " + setting->getKey() + ") is a " + "custom setting, which means that you need to enable " + "& load the mod to change its value." + ); + + render->end(true, TextAlignment::Center, TextAlignment::Center); + render->release(); + + m_height = this->getContentSize().height + pad / 4; + m_width = this->getContentSize().width; + m_width += pad * 2; + this->setContentSize({ m_width, m_height }); + m_backgroundLayer->setContentSize(this->getContentSize()); + + for (auto& label : rendered) { + label.m_node->setPositionX(pad * 1.5f); + label.m_node->setPositionY(label.m_node->getPositionY() + pad / 8); + } + + auto bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.5f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setZOrder(-1); + bgSprite->setContentSize(m_obContentSize * 2 - CCSize { pad, 0 }); + bgSprite->setPosition(m_obContentSize / 2); + this->addChild(bgSprite); + + auto iconSprite = CCSprite::createWithSpriteFrameName( + isLoaded ? "info-warning.png"_spr : "GJ_infoIcon_001.png" + ); + iconSprite->setPosition({ pad * .9f, m_height / 2 }); + iconSprite->setScale(.8f); + this->addChild(iconSprite); + + return true; +} + +CustomSettingPlaceHolderNode* CustomSettingPlaceHolderNode::create(CustomSettingPlaceHolder* setting, bool isLoaded, float width) { + auto ret = new CustomSettingPlaceHolderNode(width, 30.f); + if (ret && ret->init(setting, isLoaded)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void ColorPickPopup::setup( + ColorSettingNode* colorNode, + ColorAlphaSettingNode* alphaNode +) { + m_noElasticity = true; + + m_colorNode = colorNode; + m_colorAlphaNode = alphaNode; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + auto picker = CCControlColourPicker::colourPicker(); + picker->setColorValue( + colorNode ? + colorNode->m_value : + to3B(alphaNode->m_value) + ); + picker->setColorTarget(colorNode ? + colorNode->m_colorSprite : + alphaNode->m_colorSprite + ); + picker->setPosition(winSize.width / 2, winSize.height / 2 + (colorNode ? 0.f : 20.f)); + picker->setDelegate(this); + m_mainLayer->addChild(picker); + + if (alphaNode) { + m_alphaSlider = Slider::create(this, menu_selector(ColorPickPopup::onSlider), .75f); + m_alphaSlider->setPosition(winSize.width / 2, winSize.height / 2 - 90.f); + m_alphaSlider->setValue(alphaNode->m_value.a / 255.f); + m_alphaSlider->updateBar(); + m_mainLayer->addChild(m_alphaSlider); + + auto bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.25f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setContentSize({ 200.f, 100.f }); + bgSprite->setPosition(winSize.width / 2 + 115.f, winSize.height / 2 - 90.f); + m_mainLayer->addChild(bgSprite); + + m_alphaInput = MenuInputNode::create(50.f, 25.f, "...", "bigFont.fnt"); + m_alphaInput->setPosition(winSize.width / 2 + 115.f, winSize.height / 2 - 90.f); + m_alphaInput->getInput()->setDelegate(this); + m_alphaInput->getInput()->setAllowedChars("0123456789. "); + m_alphaInput->getInput()->setString( + toStringWithPrecision(alphaNode->m_value.a / 255.f, 2, false) + ); + m_alphaInput->setScale(.8f); + m_mainLayer->addChild(m_alphaInput); + } +} + +void ColorPickPopup::onSlider(CCObject* pSender) { + if (m_colorAlphaNode) { + m_colorAlphaNode->m_value = to4B( + to3B(m_colorAlphaNode->m_value), + static_cast<GLubyte>(as<SliderThumb*>(pSender)->getValue() * 255.f) + ); + m_alphaInput->getInput()->setString( + toStringWithPrecision(as<SliderThumb*>(pSender)->getValue(), 2, false) + ); + m_colorAlphaNode->m_colorSprite->updateOpacity( + m_colorAlphaNode->m_value.a / 255.f + ); + m_colorAlphaNode->updateSettingsList(); + } +} + +void ColorPickPopup::textChanged(CCTextInputNode* input) { + try { + m_colorAlphaNode->m_value = to4B( + to3B(m_colorAlphaNode->m_value), + static_cast<GLubyte>(std::stof(input->getString()) * 255.f) + ); + m_colorAlphaNode->m_colorSprite->updateOpacity( + m_colorAlphaNode->m_value.a / 255.f + ); + m_alphaSlider->setValue(std::stof(input->getString())); + m_colorAlphaNode->updateSettingsList(); + } catch(...) {} +} + +void ColorPickPopup::colorValueChanged(ccColor3B color) { + if (m_colorNode) { + m_colorNode->m_value = color; + m_colorNode->updateSettingsList(); + } else { + m_colorAlphaNode->m_value = to4B(color, m_colorAlphaNode->m_value.a); + m_colorAlphaNode->updateSettingsList(); + } +} + +ColorPickPopup* ColorPickPopup::create( + ColorSettingNode* colorNode, + ColorAlphaSettingNode* colorAlphaNode +) { + auto ret = new ColorPickPopup; + if (ret && ret->init(320.f, 250.f, colorNode, colorAlphaNode, "GJ_square02.png")) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +ColorPickPopup* ColorPickPopup::create(ColorSettingNode* colorNode) { + return ColorPickPopup::create(colorNode, nullptr); +} + +ColorPickPopup* ColorPickPopup::create(ColorAlphaSettingNode* colorNode) { + return ColorPickPopup::create(nullptr, colorNode); +} + +GEODE_GENERATE_SETTING_CREATE(BoolSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(IntSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(FloatSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(StringSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(ColorSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(ColorAlphaSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(PathSetting, 30.f); +GEODE_GENERATE_SETTING_CREATE(StringSelectSetting, 30.f);*/ diff --git a/loader/src/ui/internal/settings/GeodeSettingNode.hpp b/loader/src/ui/internal/settings/GeodeSettingNode.hpp new file mode 100644 index 00000000..6c000a5e --- /dev/null +++ b/loader/src/ui/internal/settings/GeodeSettingNode.hpp @@ -0,0 +1,282 @@ +/*#pragma once + +#pragma warning(disable: 4067) + +#include <Geode.hpp> +#include <nodes/MenuInputNode.hpp> +#include <nodes/Popup.hpp> + +USE_GEODE_NAMESPACE(); + +namespace geode { + class ColorPickPopup; + + template<class SettingClass> + class GeodeSettingNode : public SettingNode { + protected: + SettingClass* m_setting; + CCMenu* m_buttonMenu; + CCLabelBMFont* m_nameLabel; + CCMenuItemSpriteExtra* m_descButton = nullptr; + typename SettingClass::value_type_t m_value; + + bool init(SettingClass* setting) { + if (!CCNode::init()) + return false; + + m_setting = setting; + if constexpr (std::is_same_v<typename SettingClass::value_type_t, size_t>) { + m_value = m_setting->getIndex(); + } else { + m_value = m_setting->getValue(); + } + + this->setContentSize({ m_width, m_height }); + + auto text = setting->getName().size() ? setting->getName() : setting->getKey(); + m_nameLabel = CCLabelBMFont::create(text.c_str(), "bigFont.fnt"); + m_nameLabel->setAnchorPoint({ .0f, .5f }); + m_nameLabel->setPosition(m_height / 2, m_height / 2); + m_nameLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f); + this->addChild(m_nameLabel); + + m_buttonMenu = CCMenu::create(); + m_buttonMenu->setPosition(m_width - m_height / 2, m_height / 2); + this->addChild(m_buttonMenu); + + if (m_setting->getDescription().size()) { + auto descBtnSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + descBtnSpr->setScale(.7f); + m_descButton = CCMenuItemSpriteExtra::create( + descBtnSpr, this, menu_selector(GeodeSettingNode::onDescription) + ); + m_descButton->setPosition( + -m_buttonMenu->getPositionX() + + m_nameLabel->getPositionX() + + m_nameLabel->getScaledContentSize().width + + m_height / 2, + .0f + ); + m_buttonMenu->addChild(m_descButton); + } + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + return true; + } + + void onDescription(CCObject*) { + FLAlertLayer::create( + m_nameLabel->getString(), + m_setting->getDescription(), + "OK" + )->show(); + } + + bool hasUnsavedChanges() const override { + if constexpr (std::is_same_v<typename SettingClass::value_type_t, size_t>) { + return m_value != m_setting->getIndex(); + } else { + return m_value != m_setting->getValue(); + } + } + + void commitChanges() override { + if constexpr (std::is_same_v<typename SettingClass::value_type_t, size_t>) { + m_setting->setIndex(m_value); + } else { + m_setting->setValue(m_value); + } + } + + void resetToDefault() override { + m_setting->resetToDefault(); + if constexpr (std::is_same_v<typename SettingClass::value_type_t, size_t>) { + m_value = m_setting->getIndex(); + } else { + m_value = m_setting->getValue(); + } + this->updateState(); + } + + virtual void updateState() = 0; + + ~GeodeSettingNode() { + CCDirector::sharedDirector()->getTouchDispatcher()->decrementForcePrio(2); + } + + GeodeSettingNode(float width, float height) : SettingNode(width, height) {} + }; + + class BoolSettingNode : public GeodeSettingNode<BoolSetting> { + protected: + CCMenuItemToggler* m_toggle; + + bool init(BoolSetting* setting); + + void onToggle(CCObject*); + void updateState() override; + + BoolSettingNode(float width, float height) : GeodeSettingNode<BoolSetting>(width, height) {} + + public: + static BoolSettingNode* create(BoolSetting* setting, float width); + }; + + class IntSettingNode : public GeodeSettingNode<IntSetting>, public TextInputDelegate { + protected: + MenuInputNode* m_valueInput; + Slider* m_slider = nullptr; + + bool init(IntSetting* setting); + + void updateValue(bool updateInput = true); + void onArrow(CCObject*); + void onSlider(CCObject*); + void textInputClosed(CCTextInputNode* input) override; + void textChanged(CCTextInputNode* input) override; + void updateState() override; + + IntSettingNode(float width, float height) : GeodeSettingNode<IntSetting>(width, height) {} + + public: + static IntSettingNode* create(IntSetting* setting, float width); + }; + + class FloatSettingNode : public GeodeSettingNode<FloatSetting>, public TextInputDelegate { + protected: + MenuInputNode* m_valueInput; + Slider* m_slider = nullptr; + + bool init(FloatSetting* setting); + + void updateValue(bool updateInput = true); + void onArrow(CCObject*); + void onSlider(CCObject*); + void textInputClosed(CCTextInputNode* input) override; + void textChanged(CCTextInputNode* input) override; + void updateState() override; + + FloatSettingNode(float width, float height) : GeodeSettingNode<FloatSetting>(width, height) {} + + public: + static FloatSettingNode* create(FloatSetting* setting, float width); + }; + + class StringSettingNode : public GeodeSettingNode<StringSetting>, public TextInputDelegate { + protected: + MenuInputNode* m_input; + + bool init(StringSetting* setting); + void textChanged(CCTextInputNode* input) override; + void updateState() override; + + StringSettingNode(float width, float height) : GeodeSettingNode<StringSetting>(width, height) {} + + public: + static StringSettingNode* create(StringSetting* setting, float width); + }; + + class ColorSettingNode : public GeodeSettingNode<ColorSetting> { + protected: + ColorChannelSprite* m_colorSprite; + + bool init(ColorSetting* setting); + void onPickColor(CCObject*); + void updateState() override; + + ColorSettingNode(float width, float height) : GeodeSettingNode<ColorSetting>(width, height) {} + + friend class ColorPickPopup; + + public: + static ColorSettingNode* create(ColorSetting* setting, float width); + }; + + class ColorAlphaSettingNode : public GeodeSettingNode<ColorAlphaSetting> { + protected: + ColorChannelSprite* m_colorSprite; + + bool init(ColorAlphaSetting* setting); + void onPickColor(CCObject*); + void updateState() override; + + ColorAlphaSettingNode(float width, float height) : GeodeSettingNode<ColorAlphaSetting>(width, height) {} + + friend class ColorPickPopup; + + + public: + static ColorAlphaSettingNode* create(ColorAlphaSetting* setting, float width); + }; + + class PathSettingNode : public GeodeSettingNode<PathSetting>, public TextInputDelegate { + protected: + MenuInputNode* m_input; + + bool init(PathSetting* setting); + void textChanged(CCTextInputNode* input) override; + void onSelectFile(CCObject*); + void updateState() override; + + PathSettingNode(float width, float height) : GeodeSettingNode<PathSetting>(width, height) {} + + public: + static PathSettingNode* create(PathSetting* setting, float width); + }; + + class StringSelectSettingNode : public GeodeSettingNode<StringSelectSetting> { + protected: + CCLabelBMFont* m_selectedLabel; + + bool init(StringSelectSetting* setting); + void onChange(CCObject* pSender); + void updateState() override; + + StringSelectSettingNode(float width, float height) : GeodeSettingNode<StringSelectSetting>(width, height) {} + + public: + static StringSelectSettingNode* create(StringSelectSetting* setting, float width); + }; + + class CustomSettingPlaceHolderNode : public SettingNode { + protected: + bool init(CustomSettingPlaceHolder* setting, bool isLoaded); + + bool hasUnsavedChanges() const override { + return false; + } + void commitChanges() override {} + void resetToDefault() override {} + + CustomSettingPlaceHolderNode(float width, float height) : SettingNode(width, height) {} + + public: + static CustomSettingPlaceHolderNode* create(CustomSettingPlaceHolder* setting, bool isLoaded, float width); + }; + + class ColorPickPopup : + public Popup<ColorPickPopup, ColorSettingNode*, ColorAlphaSettingNode*>, + public ColorPickerDelegate, + public TextInputDelegate + { + protected: + ColorSettingNode* m_colorNode = nullptr; + ColorAlphaSettingNode* m_colorAlphaNode = nullptr; + MenuInputNode* m_alphaInput = nullptr; + Slider* m_alphaSlider = nullptr; + + void setup(ColorSettingNode* colorNode, ColorAlphaSettingNode* alphaNode) override; + void colorValueChanged(ccColor3B) override; + void textChanged(CCTextInputNode* input) override; + void onSlider(CCObject*); + + public: + static ColorPickPopup* create(ColorSettingNode* colorNode, ColorAlphaSettingNode* colorAlphaNode); + static ColorPickPopup* create(ColorSettingNode* colorNode); + static ColorPickPopup* create(ColorAlphaSettingNode* colorNode); + }; +} + +#pragma warning(default: 4067)*/ diff --git a/loader/src/ui/internal/settings/ModSettingsLayer.cpp b/loader/src/ui/internal/settings/ModSettingsLayer.cpp new file mode 100644 index 00000000..d7a31032 --- /dev/null +++ b/loader/src/ui/internal/settings/ModSettingsLayer.cpp @@ -0,0 +1,194 @@ +/*#include "ModSettingsLayer.hpp" +#include "../settings/ModSettingsList.hpp" + +bool ModSettingsLayer::init(Mod* mod) { + m_noElasticity = true; + m_mod = mod; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + const CCSize size { 440.f, 290.f }; + + if (!this->initWithColor({ 0, 0, 0, 105 })) return false; + m_mainLayer = CCLayer::create(); + this->addChild(m_mainLayer); + + auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f }); + bg->setContentSize(size); + bg->setPosition(winSize.width / 2, winSize.height / 2); + m_mainLayer->addChild(bg); + + m_buttonMenu = CCMenu::create(); + m_buttonMenu->setZOrder(10); + m_mainLayer->addChild(m_buttonMenu); + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + auto nameStr = m_mod->getName() + " Settings"; + auto nameLabel = CCLabelBMFont::create( + nameStr.c_str(), "bigFont.fnt" + ); + nameLabel->setPosition(winSize.width / 2, winSize.height / 2 + 120.f); + nameLabel->setScale(.7f); + m_mainLayer->addChild(nameLabel, 2); + + const CCSize listSize { 350.f, 200.f }; + + auto bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bgSprite->setScale(.5f); + bgSprite->setColor({ 0, 0, 0 }); + bgSprite->setOpacity(75); + bgSprite->setContentSize(listSize * 2); + bgSprite->setPosition(winSize.width / 2, winSize.height / 2); + m_mainLayer->addChild(bgSprite); + + m_list = ModSettingsList::create(mod, this, listSize.width, listSize.height); + m_list->setPosition(winSize.width / 2 - listSize.width / 2, winSize.height / 2 - listSize.height / 2); + m_mainLayer->addChild(m_list); + + { + auto topSprite = CCSprite::createWithSpriteFrameName("GJ_commentTop_001.png"); + topSprite->setPosition({ winSize.width / 2, winSize.height / 2 + listSize.height / 2 - 5.f }); + m_mainLayer->addChild(topSprite); + + auto bottomSprite = CCSprite::createWithSpriteFrameName("GJ_commentTop_001.png"); + bottomSprite->setFlipY(true); + bottomSprite->setPosition({ winSize.width / 2, winSize.height / 2 - listSize.height / 2 + 5.f }); + m_mainLayer->addChild(bottomSprite); + + auto leftSprite = CCSprite::createWithSpriteFrameName("GJ_commentSide_001.png"); + leftSprite->setPosition({ winSize.width / 2 - listSize.width / 2 + 1.5f, winSize.height / 2 }); + leftSprite->setScaleY(5.7f); + m_mainLayer->addChild(leftSprite); + + auto rightSprite = CCSprite::createWithSpriteFrameName("GJ_commentSide_001.png"); + rightSprite->setFlipX(true); + rightSprite->setPosition({ winSize.width / 2 + listSize.width / 2 - 1.5f, winSize.height / 2 }); + rightSprite->setScaleY(5.7f); + m_mainLayer->addChild(rightSprite); + } + + m_applyBtnSpr = ButtonSprite::create("Apply", "goldFont.fnt", "GJ_button_01.png", .8f); + m_applyBtnSpr->setScale(.8f); + + auto applyBtn = CCMenuItemSpriteExtra::create( + m_applyBtnSpr, + this, + menu_selector(ModSettingsLayer::onApply) + ); + applyBtn->setPosition(size.width / 2 - 80.f, -size.height / 2 + 25.f); + m_buttonMenu->addChild(applyBtn); + + + auto resetBtnSpr = ButtonSprite::create("Reset to\nDefault", "bigFont.fnt", "GJ_button_05.png", .8f); + resetBtnSpr->setScale(.4f); + + auto resetBtn = CCMenuItemSpriteExtra::create( + resetBtnSpr, + this, + menu_selector(ModSettingsLayer::onResetAllToDefault) + ); + resetBtn->setPosition(size.width / 2 - 150.f, -size.height / 2 + 25.f); + m_buttonMenu->addChild(resetBtn); + + + auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); + closeSpr->setScale(.8f); + + auto closeBtn = CCMenuItemSpriteExtra::create( + closeSpr, + this, + menu_selector(ModSettingsLayer::onClose) + ); + closeBtn->setPosition(-size.width / 2 + 3.f, size.height / 2 - 3.f); + m_buttonMenu->addChild(closeBtn); + + this->setKeypadEnabled(true); + this->setTouchEnabled(true); + + this->updateState(); + + return true; +} + +void ModSettingsLayer::updateState() { + if (m_list->hasUnsavedModifiedSettings()) { + m_applyBtnSpr->setOpacity(255); + m_applyBtnSpr->setColor({ 255, 255, 255 }); + } else { + m_applyBtnSpr->setOpacity(155); + m_applyBtnSpr->setColor({ 155, 155, 155 }); + } +} + +void ModSettingsLayer::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) { + if (btn2) { + switch (layer->getTag()) { + case 0: this->close(); break; + case 1: m_list->resetAllToDefault(); break; + } + } +} + +void ModSettingsLayer::onResetAllToDefault(CCObject*) { + auto layer = FLAlertLayer::create( + this, "Reset Settings", + "Are you sure you want to <co>reset</c> ALL settings?", + "Cancel", "Reset", 400.f + ); + layer->setTag(1); + layer->show(); +} + +void ModSettingsLayer::onApply(CCObject*) { + if (!m_list->hasUnsavedModifiedSettings()) { + FLAlertLayer::create( + "No Changes", + "No changes were made.", + "OK" + )->show(); + } + m_list->applyChanges(); + this->updateState(); +} + +void ModSettingsLayer::keyDown(enumKeyCodes key) { + if (key == KEY_Escape) + return this->onClose(nullptr); + if (key == KEY_Space) + return; + return FLAlertLayer::keyDown(key); +} + +void ModSettingsLayer::onClose(CCObject*) { + if (m_list->hasUnsavedModifiedSettings()) { + auto layer = FLAlertLayer::create( + this, "Unsaved Changes", + "You have <cy>unsaved</c> settings! Are you sure " + "you want to <cr>discard</c> your changes?", + "Cancel", "Discard", 400.f + ); + layer->setTag(0); + layer->show(); + } else { + this->close(); + } +}; + +void ModSettingsLayer::close() { + this->setKeyboardEnabled(false); + this->removeFromParentAndCleanup(true); +} + +ModSettingsLayer* ModSettingsLayer::create(Mod* mod) { + auto ret = new ModSettingsLayer(); + if (ret && ret->init(mod)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +}*/ + diff --git a/loader/src/ui/internal/settings/ModSettingsLayer.hpp b/loader/src/ui/internal/settings/ModSettingsLayer.hpp new file mode 100644 index 00000000..bc8afcb8 --- /dev/null +++ b/loader/src/ui/internal/settings/ModSettingsLayer.hpp @@ -0,0 +1,31 @@ +/*#pragma once + +#include <Geode.hpp> + +USE_GEODE_NAMESPACE(); + +class ModSettingsList; + +class ModSettingsLayer : public FLAlertLayer, public FLAlertLayerProtocol { + protected: + Mod* m_mod; + ModSettingsList* m_list; + ButtonSprite* m_applyBtnSpr; + + void FLAlert_Clicked(FLAlertLayer*, bool) override; + + bool init(Mod* mod); + + void onApply(CCObject*); + void onResetAllToDefault(CCObject*); + void keyDown(enumKeyCodes) override; + void onClose(CCObject*); + void close(); + + public: + static ModSettingsLayer* create(Mod* Mod); + + void updateState(); +}; + +*/ \ No newline at end of file diff --git a/loader/src/ui/internal/settings/ModSettingsList.cpp b/loader/src/ui/internal/settings/ModSettingsList.cpp new file mode 100644 index 00000000..228a19ec --- /dev/null +++ b/loader/src/ui/internal/settings/ModSettingsList.cpp @@ -0,0 +1,109 @@ +/*#include "ModSettingsList.hpp" +#include <settings/SettingNodeManager.hpp> +#include <settings/SettingNode.hpp> +#include <utils/WackyGeodeMacros.hpp> +#include "ModSettingsLayer.hpp" + +bool ModSettingsList::init(Mod* mod, ModSettingsLayer* layer, float width, float height) { + m_mod = mod; + m_settingsLayer = layer; + + m_scrollLayer = ScrollLayer::create({ width, height }); + this->addChild(m_scrollLayer); + + if (mod->getSettings().size()) { + float offset = 0.f; + bool coloredBG = false; + std::vector<CCNode*> gen; + for (auto const& sett : mod->getSettings()) { + auto node = SettingNodeManager::get()->generateNode(mod, sett, width); + if (node) { + m_settingNodes.push_back(node); + node->m_list = this; + if (coloredBG) { + node->m_backgroundLayer->setColor({ 0, 0, 0 }); + node->m_backgroundLayer->setOpacity(50); + } + node->setPosition( + 0.f, offset - node->getScaledContentSize().height + ); + m_scrollLayer->m_contentLayer->addChild(node); + + auto separator = CCLayerColor::create({ 0, 0, 0, 50 }, width, 1.f); + separator->setPosition(0.f, offset - .5f); + m_scrollLayer->m_contentLayer->addChild(separator); + gen.push_back(separator); + + offset -= node->m_height; + coloredBG = !coloredBG; + gen.push_back(node); + } + } + auto separator = CCLayerColor::create({ 0, 0, 0, 50 }, width, 1.f); + separator->setPosition(0.f, offset); + m_scrollLayer->m_contentLayer->addChild(separator); + gen.push_back(separator); + + offset = -offset; + // to avoid needing to do moveToTopWithOffset, + // just set the content size to the viewport + // size if its less + if (offset < height) offset = height; + for (auto& node : gen) { + node->setPositionY(node->getPositionY() + offset); + } + m_scrollLayer->m_contentLayer->setContentSize({ width, offset }); + m_scrollLayer->moveToTop(); + } else { + auto label = CCLabelBMFont::create("This mod has no settings", "bigFont.fnt"); + label->setPosition(width / 2, height / 2); + label->setScale(.5f); + m_scrollLayer->m_contentLayer->addChild(label); + m_scrollLayer->setContentSize({ width, height }); + m_scrollLayer->moveToTop(); + } + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + m_scrollLayer->registerWithTouchDispatcher(); + + return true; +} + +bool ModSettingsList::hasUnsavedModifiedSettings() const { + for (auto& setting : m_settingNodes) { + if (setting->hasUnsavedChanges()) { + return true; + } + } + return false; +} + +void ModSettingsList::applyChanges() { + for (auto& setting : m_settingNodes) { + if (setting->hasUnsavedChanges()) { + setting->commitChanges(); + } + } +} + +void ModSettingsList::updateList() { + m_settingsLayer->updateState(); +} + +void ModSettingsList::resetAllToDefault() { + for (auto& setting : m_settingNodes) { + setting->resetToDefault(); + } +} + +ModSettingsList* ModSettingsList::create( + Mod* mod, ModSettingsLayer* layer, float width, float height +) { + auto ret = new ModSettingsList(); + if (ret && ret->init(mod, layer, width, height)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +}*/ diff --git a/loader/src/ui/internal/settings/ModSettingsList.hpp b/loader/src/ui/internal/settings/ModSettingsList.hpp new file mode 100644 index 00000000..a7840453 --- /dev/null +++ b/loader/src/ui/internal/settings/ModSettingsList.hpp @@ -0,0 +1,29 @@ +/*#pragma once + +#include <Geode.hpp> +#include <nodes/ScrollLayer.hpp> +#include <settings/SettingNode.hpp> + +USE_GEODE_NAMESPACE(); + +class ModSettingsLayer; + +class ModSettingsList : public CCLayer { +protected: + Mod* m_mod; + ModSettingsLayer* m_settingsLayer; + ScrollLayer* m_scrollLayer; + std::vector<SettingNode*> m_settingNodes; + + bool init(Mod* mod, ModSettingsLayer* layer, float width, float height); + +public: + static ModSettingsList* create( + Mod* mod, ModSettingsLayer* layer, float width, float height + ); + + void updateList(); + void resetAllToDefault(); + bool hasUnsavedModifiedSettings() const; + void applyChanges(); +};*/ diff --git a/loader/src/ui/internal/settings/Setting.cpp b/loader/src/ui/internal/settings/Setting.cpp new file mode 100644 index 00000000..f62bb899 --- /dev/null +++ b/loader/src/ui/internal/settings/Setting.cpp @@ -0,0 +1,86 @@ +// #include <settings/Setting.hpp> +// #include <unordered_map> + +// USE_GEODE_NAMESPACE(); + +// Setting* Setting::from(std::string const& id, nlohmann::json const& json) { +// if (json.is_object()) { + +// } else { + +// } + +// auto ctrl = json["control"]; +// if (!ctrl.is_string()) { +// FLAlertLayer::create( +// "Failed to load settings", +// "JSON error: 'control' key is not a string (or doesn't exist)!", +// "OK" +// )->show(); +// return nullptr; +// } +// std::string control = ctrl; + +// Setting* out = nullptr; +// /*EventCenter::get()->broadcast(Event( +// events::getSetting(id), +// &out, +// Mod::get() +// ));*/ +// #pragma message("Event") + +// if (out == nullptr) { +// FLAlertLayer::create( +// "Failed to load settings", +// std::string("No known setting control '") + control + "'", +// "OK" +// )->show(); +// } + +// return out; +// } + +// bool SettingManager::hasSettings() { +// return !this->m_settings.empty(); +// } + +// SettingManager* SettingManager::with(Mod* m) { +// static std::unordered_map<Mod*, SettingManager*> managers; +// if (!managers.count(m)) { +// managers[m] = new SettingManager(m); +// } +// return managers[m]; +// } + +// SettingManager::SettingManager(Mod* m) { +// m_mod = m; +// auto root = m_mod->getDataStore()["settings"]; + +// if (!root.is_object()) { +// return; +// } + +// for (auto [id, setting] : root.items()) { +// m_settings[id] = Setting::from(id, setting); +// } +// } + +// Setting* SettingManager::getSetting(std::string id) { +// return m_settings[id]; +// } + +// void SettingManager::updateSetting(std::string id) { +// // m_mod->getDataStore()["settings"][id] = this->getSetting(id)->saveJSON(); +// } + +// CCNode* Setting::createControl() const { +// return nullptr; +// } + +// std::vector<CCNode*> SettingManager::generateSettingNodes() { +// std::vector<CCNode*> out; +// for (auto [k, v] : this->m_settings) { +// out.push_back(v->createControl()); +// } +// return out; +// } diff --git a/loader/src/ui/nodes/BasedButton.cpp b/loader/src/ui/nodes/BasedButton.cpp new file mode 100644 index 00000000..44a159e5 --- /dev/null +++ b/loader/src/ui/nodes/BasedButton.cpp @@ -0,0 +1,33 @@ +#include <Geode/ui/BasedButton.hpp> + +USE_GEODE_NAMESPACE(); + +TabButton* TabButton::create( + TabBaseColor unselected, + TabBaseColor selected, + const char* text, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler callback +) { + auto ret = new TabButton(); + auto sprOff = TabButtonSprite::create(text, unselected); + auto sprOn = TabButtonSprite::create(text, selected); + if (ret && ret->init(sprOff, sprOn, target, callback)) { + ret->m_offButton->m_colorDip = .3f; + ret->m_offButton->m_colorEnabled = true; + ret->m_offButton->m_scaleMultiplier = 1.f; + ret->m_onButton->setEnabled(false); + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +TabButton* TabButton::create(const char* text, CCObject* target, SEL_MenuHandler callback) { + return TabButton::create( + TabBaseColor::Unselected, + TabBaseColor::Selected, + text, target, callback + ); +} diff --git a/loader/src/ui/nodes/BasedButtonSprite.cpp b/loader/src/ui/nodes/BasedButtonSprite.cpp new file mode 100644 index 00000000..9612c1b6 --- /dev/null +++ b/loader/src/ui/nodes/BasedButtonSprite.cpp @@ -0,0 +1,167 @@ +#include <Geode/ui/BasedButtonSprite.hpp> + +USE_GEODE_NAMESPACE(); + +bool BasedButtonSprite::init(CCNode* ontop, int type, int size, int color) { + if (!CCSprite::initWithSpriteFrameName( + Mod::get()->expandSpriteName( + CCString::createWithFormat("GEODE_blank%02d_%02d_%02d.png", type, size, color)->getCString() + ) + )) return false; + + this->m_type = type; + this->m_size = size; + this->m_color = color; + + if (ontop) { + this->m_onTop = ontop; + this->m_onTop->retain(); + this->addChild(this->m_onTop); + this->m_onTop->setPosition(this->getContentSize() / 2 + this->getTopOffset()); + } + + return true; +} + +CCPoint BasedButtonSprite::getTopOffset() const { + return { 0, 0 }; +} + +bool BasedButtonSprite::initWithSprite(const char* sprName, float sprScale, int type, int size, int color) { + auto spr = CCSprite::create(sprName); + if (!spr) return false; + spr->setScale(sprScale); + return this->init(spr, type, size, color); +} + +bool BasedButtonSprite::initWithSpriteFrameName(const char* sprName, float sprScale, int type, int size, int color) { + auto spr = CCSprite::createWithSpriteFrameName(sprName); + if (!spr) return false; + spr->setScale(sprScale); + return this->init(spr, type, size, color); +} + +BasedButtonSprite::~BasedButtonSprite() { + CC_SAFE_RELEASE(this->m_onTop); +} + +BasedButtonSprite* BasedButtonSprite::create(CCNode* ontop, int type, int size, int color) { + auto ret = new BasedButtonSprite(); + if (ret && ret->init(ontop, type, size, color)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +EditorButtonSprite* EditorButtonSprite::create(cocos2d::CCNode* top, EditorBaseColor color) { + auto ret = new EditorButtonSprite(); + if (ret && ret->init(top, static_cast<int>(BaseType::Editor), 0, static_cast<int>(color))) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +EditorButtonSprite* EditorButtonSprite::createWithSprite( + const char* sprName, + float sprScale, + EditorBaseColor color +) { + auto ret = new EditorButtonSprite(); + if (ret && ret->initWithSprite( + sprName, sprScale, + static_cast<int>(BaseType::Editor), 0, static_cast<int>(color)) + ) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +EditorButtonSprite* EditorButtonSprite::createWithSpriteFrameName( + const char* sprName, + float sprScale, + EditorBaseColor color +) { + auto ret = new EditorButtonSprite(); + if (ret && ret->initWithSpriteFrameName( + sprName, sprScale, + static_cast<int>(BaseType::Editor), 0, static_cast<int>(color)) + ) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CircleButtonSprite* CircleButtonSprite::create( + cocos2d::CCNode* top, + CircleBaseColor color, + CircleBaseSize size +) { + auto ret = new CircleButtonSprite(); + if (ret && ret->init( + top, + static_cast<int>(BaseType::Circle), static_cast<int>(size), static_cast<int>(color) + )) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CircleButtonSprite* CircleButtonSprite::createWithSprite( + const char* sprName, + float sprScale, + CircleBaseColor color, + CircleBaseSize size +) { + auto ret = new CircleButtonSprite(); + if (ret && ret->initWithSprite( + sprName, sprScale, + static_cast<int>(BaseType::Circle), static_cast<int>(size), static_cast<int>(color) + )) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CircleButtonSprite* CircleButtonSprite::createWithSpriteFrameName( + const char* sprName, + float sprScale, + CircleBaseColor color, + CircleBaseSize size +) { + auto ret = new CircleButtonSprite(); + if (ret && ret->initWithSpriteFrameName( + sprName, sprScale, + static_cast<int>(BaseType::Circle), static_cast<int>(size), static_cast<int>(color) + )) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +TabButtonSprite* TabButtonSprite::create(const char* text, TabBaseColor color) { + auto ret = new TabButtonSprite(); + auto label = CCLabelBMFont::create(text, "bigFont.fnt"); + label->limitLabelWidth(75.f, .6f, .1f); + if (ret && ret->init( + label, static_cast<int>(BaseType::Tab), 0, static_cast<int>(color) + )) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/nodes/IconButtonSprite.cpp b/loader/src/ui/nodes/IconButtonSprite.cpp new file mode 100644 index 00000000..70f4407b --- /dev/null +++ b/loader/src/ui/nodes/IconButtonSprite.cpp @@ -0,0 +1,135 @@ +#include <Geode/ui/IconButtonSprite.hpp> + +USE_GEODE_NAMESPACE(); + +bool IconButtonSprite::init( + const char* bg, + bool bgIsFrame, + cocos2d::CCNode* icon, + const char* text, + const char* font +) { + if (!CCSprite::init()) + return false; + + if (bgIsFrame) { + m_bg = CCScale9Sprite::createWithSpriteFrameName(bg); + } else { + m_bg = CCScale9Sprite::create(bg); + } + this->addChild(m_bg); + + m_label = CCLabelBMFont::create(text, font); + m_label->setAnchorPoint({ .0f, .5f }); + m_label->setZOrder(1); + this->addChild(m_label); + + if (icon) { + m_icon = icon; + icon->setZOrder(1); + this->addChild(icon); + } + + this->updateLayout(); + + return true; +} + +void IconButtonSprite::updateLayout() { + static constexpr const float PAD = 7.5f; + + auto size = CCSize { 20.f, 20.f }; + if (m_label->getString() && strlen(m_label->getString())) { + m_label->limitLabelWidth(100.f, .6f, .1f); + size.width += m_label->getScaledContentSize().width; + } + if (m_icon) { + limitNodeSize(m_icon, size, 1.f, .1f); + } + size.height += 15.f; + + if (m_icon) { + size.width += m_icon->getScaledContentSize().width + PAD; + } + + this->setContentSize(size); + m_bg->setContentSize(size / m_bg->getScale()); + m_bg->setPosition(m_obContentSize / 2); + + if (m_icon) { + m_label->setPosition( + size.height / 2 + m_icon->getScaledContentSize().width / 2 + PAD, + size.height / 2 + 1.f + ); + m_icon->setPosition(size.height / 2, size.height / 2); + } else { + m_label->setPosition(size.height / 2, size.height / 2); + } +} + +IconButtonSprite* IconButtonSprite::create( + const char* bg, + cocos2d::CCNode* icon, + const char* text, + const char* font +) { + auto ret = new IconButtonSprite(); + if (ret && ret->init(bg, false, icon, text, font)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +IconButtonSprite* IconButtonSprite::createWithSpriteFrameName( + const char* bg, + cocos2d::CCNode* icon, + const char* text, + const char* font +) { + auto ret = new IconButtonSprite(); + if (ret && ret->init(bg, true, icon, text, font)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void IconButtonSprite::setBG(const char* bg, bool isFrame) { + if (m_bg) { + m_bg->removeFromParent(); + } + if (isFrame) { + m_bg = CCScale9Sprite::createWithSpriteFrameName(bg); + } else { + m_bg = CCScale9Sprite::create(bg); + } + this->addChild(m_bg); + this->updateLayout(); +} + +void IconButtonSprite::setIcon(cocos2d::CCNode* icon) { + if (m_icon) { + m_icon->removeFromParent(); + } + m_icon = icon; + m_icon->setZOrder(1); + this->addChild(icon); + this->updateLayout(); +} + +cocos2d::CCNode* IconButtonSprite::getIcon() const { + return m_icon; +} + +void IconButtonSprite::setString(const char* label) { + m_label->setString(label); + this->updateLayout(); +} + +const char* IconButtonSprite::getString() { + return m_label->getString(); +} + diff --git a/loader/src/ui/nodes/InputNode.cpp b/loader/src/ui/nodes/InputNode.cpp new file mode 100644 index 00000000..f3bd38a2 --- /dev/null +++ b/loader/src/ui/nodes/InputNode.cpp @@ -0,0 +1,85 @@ +#include <Geode/ui/InputNode.hpp> + +USE_GEODE_NAMESPACE(); + +const char* InputNode::getString() { + return m_input->getTextField()->getString(); +} + +void InputNode::setString(const char* _str) { + m_input->getTextField()->setString(_str); + m_input->refreshLabel(); +} + +CCTextInputNode* InputNode::getInputNode() const { + return m_input; +} + +CCScale9Sprite* InputNode::getBGSprite() const { + return m_bgSprite; +} + +void InputNode::setEnabled(bool enabled) { + m_input->setMouseEnabled(enabled); + m_input->setTouchEnabled(enabled); +} + +bool InputNode::init(float _w, float _h, const char* _phtxt, const char* _fnt, const std::string & _awc, int _cc) { + m_bgSprite = cocos2d::extension::CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + + m_bgSprite->setScale(.5f); + m_bgSprite->setColor({ 0, 0, 0 }); + m_bgSprite->setOpacity(75); + m_bgSprite->setContentSize({ _w * 2, _h * 2 }); + + this->addChild(m_bgSprite); + + m_input = CCTextInputNode::create( + _w - 10.0f, 60.0f, _phtxt, _fnt + ); + + m_input->setLabelPlaceholderColor({ 150, 150, 150 }); + m_input->setLabelPlaceholderScale(.75f); + m_input->setMaxLabelScale(.8f); + m_input->setMaxLabelWidth(_cc); + if (_awc.length()) + m_input->setAllowedChars(_awc); + + this->addChild(m_input); + + return true; +} + +bool InputNode::init(float _w, const char* _phtxt, const char* _fnt, const std::string & _awc, int _cc) { + return init(_w, 30.0f, _phtxt, _fnt, _awc, _cc); +} + +InputNode* InputNode::create(float _w, const char* _phtxt, const char* _fnt, const std::string & _awc, int _cc) { + auto pRet = new InputNode(); + + if (pRet && pRet->init(_w, _phtxt, _fnt, _awc, _cc)) { + pRet->autorelease(); + return pRet; + } + + CC_SAFE_DELETE(pRet); + return nullptr; +} + +InputNode* InputNode::create(float _w, const char* _phtxt, const std::string & _awc) { + return create(_w, _phtxt, "bigFont.fnt", _awc, 69); +} + +InputNode* InputNode::create(float _w, const char* _phtxt, const std::string & _awc, int _cc) { + return create(_w, _phtxt, "bigFont.fnt", _awc, _cc); +} + +InputNode* InputNode::create(float _w, const char* _phtxt, const char* _fnt) { + return create(_w, _phtxt, _fnt, "", 69); +} + +InputNode* InputNode::create(float _w, const char* _phtxt) { + return create(_w, _phtxt, "bigFont.fnt"); +} diff --git a/loader/src/ui/nodes/ListView.cpp b/loader/src/ui/nodes/ListView.cpp new file mode 100644 index 00000000..7f335c4b --- /dev/null +++ b/loader/src/ui/nodes/ListView.cpp @@ -0,0 +1,78 @@ +#include <Geode/ui/ListView.hpp> + +USE_GEODE_NAMESPACE(); + +GenericListCell::GenericListCell(const char* name, CCSize size) : + TableViewCell(name, size.width, size.height) {} + +void GenericListCell::draw() { + reinterpret_cast<StatsCell*>(this)->StatsCell::draw(); +} + +GenericListCell* GenericListCell::create(const char* key, CCSize size) { + auto pRet = new GenericListCell(key, size); + if (pRet) { + return pRet; + } + CC_SAFE_DELETE(pRet); + return nullptr; +} + +void GenericListCell::updateBGColor(int index) { + if (index & 1) m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e)); + else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c)); + m_backgroundLayer->setOpacity(0xff); +} + + +void ListView::setupList() { + if (!m_entries->count()) return; + m_tableView->reloadData(); + + // fix content layer content size so the + // list is properly aligned to the top + auto coverage = calculateChildCoverage(m_tableView->m_contentLayer); + m_tableView->m_contentLayer->setContentSize({ + -coverage.origin.x + coverage.size.width, + -coverage.origin.y + coverage.size.height + }); + + if (m_entries->count() == 1) { + m_tableView->moveToTopWithOffset(m_itemSeparation); + } else { + m_tableView->moveToTop(); + } +} + +TableViewCell* ListView::getListCell(const char* key) { + return GenericListCell::create(key, { this->m_width, this->m_itemSeparation }); +} + +void ListView::loadCell(TableViewCell* cell, unsigned int index) { + auto node = dynamic_cast<CCNode*>(m_entries->objectAtIndex(index)); + if (node) { + auto lcell = as<GenericListCell*>(cell); + node->setContentSize(lcell->getScaledContentSize()); + node->setPosition(0, 0); + lcell->addChild(node); + lcell->updateBGColor(index); + } +} + +ListView* ListView::create( + CCArray* items, + float itemHeight, + float width, + float height +) { + auto ret = new ListView(); + if (ret) { + ret->m_itemSeparation = itemHeight; + if (ret->init(items, BoomListType::Default, width, height)) { + ret->autorelease(); + return ret; + } + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/nodes/MDTextArea.cpp b/loader/src/ui/nodes/MDTextArea.cpp new file mode 100644 index 00000000..08b5699b --- /dev/null +++ b/loader/src/ui/nodes/MDTextArea.cpp @@ -0,0 +1,630 @@ +#include <Geode/ui/MDTextArea.hpp> +#include <Geode/utils/WackyGeodeMacros.hpp> +#include <md4c.h> + +USE_GEODE_NAMESPACE(); + +static constexpr float g_fontScale = .5f; +static constexpr float g_paragraphPadding = 7.f; +static constexpr float g_indent = 7.f; +static constexpr float g_codeBlockIndent = 8.f; +static constexpr ccColor3B g_linkColor = cc3x(0x7ff4f4); + +TextRenderer::Font g_mdFont = [](int style) -> TextRenderer::Label { + if ((style & TextStyleBold) && (style & TextStyleItalic)) { + return CCLabelBMFont::create("", "mdFontBI.fnt"_spr); + } + if ((style & TextStyleBold)) { + return CCLabelBMFont::create("", "mdFontB.fnt"_spr); + } + if ((style & TextStyleItalic)) { + return CCLabelBMFont::create("", "mdFontI.fnt"_spr); + } + return CCLabelBMFont::create("", "mdFont.fnt"_spr); +}; + +TextRenderer::Font g_mdMonoFont = [](int style) -> TextRenderer::Label { + return CCLabelBMFont::create("", "mdFontMono.fnt"_spr); +}; + + +class MDContentLayer : public CCContentLayer { +protected: + CCMenu* m_content; + +public: + static MDContentLayer* create(CCMenu* content, float width, float height) { + auto ret = new MDContentLayer(); + if (ret && ret->initWithColor({ 0, 255, 0, 0 }, width, height)) { + ret->m_content = content; + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; + } + + void setPosition(CCPoint const& pos) override { + // cringe CCContentLayer expect its children to + // all be TableViewCells + CCLayerColor::setPosition(pos); + + // so that's why based MDContentLayer expects itself + // to have a CCMenu :-) + if (m_content) { + CCARRAY_FOREACH_B_TYPE(m_content->getChildren(), child, CCNode) { + auto y = this->getPositionY() + child->getPositionY(); + child->setVisible(!( + (m_content->getContentSize().height < y) || + (y < -child->getContentSize().height) + )); + } + } + } +}; + + +Result<ccColor3B> colorForIdentifier(std::string const& tag) { + if (string_utils::contains(tag, ' ')) { + auto hexStr = string_utils::split( + string_utils::normalize(tag), " " + ).at(1); + try { + auto hex = std::stoi(hexStr, nullptr, 16); + return Ok(cc3x(hex)); + } catch(...) { + return Err("Invalid hex"); + } + } else { + auto colorText = tag.substr(1); + if (!colorText.size()) { + return Err("No color specified"); + } else if (colorText.size() > 1) { + return Err("Color tag " + tag + " unexpectedly long, either do <cx> or <c hex>"); + } else { + switch (colorText.front()) { + case 'b': return Ok(cc3x(0x4a52e1)); break; + case 'g': return Ok(cc3x(0x40e348)); break; + case 'l': return Ok(cc3x(0x60abef)); break; + case 'j': return Ok(cc3x(0x32c8ff)); break; + case 'y': return Ok(cc3x(0xffff00)); break; + case 'o': return Ok(cc3x(0xffa54b)); break; + case 'r': return Ok(cc3x(0xff5a5a)); break; + case 'p': return Ok(cc3x(0xff00ff)); break; + default: + return Err("Unknown color " + colorText); + } + } + } + return Err("Unknown error"); +} + + +bool MDTextArea::init( + std::string const& str, + CCSize const& size +) { + if (!CCLayer::init()) + return false; + + m_text = str; + m_size = size; + this->setContentSize(size); + m_renderer = TextRenderer::create(); + CC_SAFE_RETAIN(m_renderer); + + m_bgSprite = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + m_bgSprite->setScale(.5f); + m_bgSprite->setColor({ 0, 0, 0 }); + m_bgSprite->setOpacity(75); + m_bgSprite->setContentSize(size * 2 + CCSize { 25.f, 25.f }); + m_bgSprite->setPosition(size / 2); + this->addChild(m_bgSprite); + + m_scrollLayer = ScrollLayer::create({ 0, 0, m_size.width, m_size.height }, true); + + m_content = CCMenu::create(); + m_content->setZOrder(2); + m_scrollLayer->m_contentLayer->addChild(m_content); + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + m_scrollLayer->registerWithTouchDispatcher(); + + this->addChild(m_scrollLayer); + + this->updateLabel(); + + return true; +} + +MDTextArea::~MDTextArea() { + CC_SAFE_RELEASE(m_renderer); +} + +class BreakLine : public CCNode { +protected: + void draw() override { + // some nodes sometimes set the blend func to + // something else without resetting it back + ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + ccDrawSolidRect({ 0, 0 }, this->getContentSize(), { 1.f, 1.f, 1.f, .2f }); + CCNode::draw(); + } + +public: + static BreakLine* create(float width) { + auto ret = new BreakLine; + if (ret && ret->init()) { + ret->autorelease(); + ret->setContentSize({ width, 1.f }); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; + } +}; + +void MDTextArea::onLink(CCObject* pSender) { + auto href = as<CCString*>(as<CCNode*>(pSender)->getUserObject()); + auto layer = FLAlertLayer::create( + this, + "Hold Up!", + "Links are spooky! Are you sure you want to go to <cy>" + + std::string(href->getCString()) + "</c>?", + "Cancel", "Yes", + 360.f + ); + layer->setUserObject(href); + layer->show(); +} + +void MDTextArea::onGDProfile(CCObject* pSender) { + auto href = as<CCString*>(as<CCNode*>(pSender)->getUserObject()); + auto profile = std::string(href->getCString()); + profile = profile.substr(profile.find(":") + 1); + try { + ProfilePage::create(std::stoi(profile), false)->show(); + } catch(...) { + FLAlertLayer::create( + "Error", + "Invalid profile ID: <cr>" + profile + "</c>. This is " + "probably the modder's fault, report the bug to them.", + "OK" + )->show(); + } +} + +void MDTextArea::FLAlert_Clicked(FLAlertLayer* layer, bool btn) { + if (btn) { + web::openLinkInBrowser(as<CCString*>(layer->getUserObject())->getCString()); + } +} + +struct MDParser { + static std::string s_lastLink; + static std::string s_lastImage; + static bool s_isOrderedList; + static bool s_isCodeBlock; + static float s_codeStart; + static size_t s_orderedListNum; + static std::vector<TextRenderer::Label> s_codeSpans; + + static int parseText(MD_TEXTTYPE type, MD_CHAR const* rawText, MD_SIZE size, void* mdtextarea) { + auto textarea = reinterpret_cast<MDTextArea*>(mdtextarea); + auto renderer = textarea->m_renderer; + auto text = std::string(rawText, size); + switch (type) { + case MD_TEXTTYPE::MD_TEXT_CODE: { + auto rendered = renderer->renderString(text); + if (!s_isCodeBlock) { + // code span BGs need to be rendered after all + // rendering is done since the position of the + // rendered labels may change after alignments + // are adjusted + vector_utils::push(s_codeSpans, rendered); + } + } break; + + case MD_TEXTTYPE::MD_TEXT_BR: { + renderer->breakLine(); + } break; + + case MD_TEXTTYPE::MD_TEXT_SOFTBR: { + renderer->breakLine(); + } break; + + case MD_TEXTTYPE::MD_TEXT_NORMAL: { + if (s_lastLink.size()) { + renderer->pushColor(g_linkColor); + renderer->pushDecoFlags(TextDecorationUnderline); + auto rendered = renderer->renderStringInteractive( + text, + textarea, + string_utils::startsWith(s_lastLink, "user:") ? + menu_selector(MDTextArea::onGDProfile) : + menu_selector(MDTextArea::onLink) + ); + for (auto const& label : rendered) { + label.m_node->setUserObject(CCString::create(s_lastLink)); + } + renderer->popDecoFlags(); + renderer->popColor(); + } else if (s_lastImage.size()) { + bool isFrame = false; + if (string_utils::startsWith(s_lastImage, "frame:")) { + s_lastImage = s_lastImage.substr(s_lastImage.find(":") + 1); + isFrame = true; + } + CCSprite* spr = nullptr; + if (isFrame) { + spr = CCSprite::createWithSpriteFrameName(s_lastImage.c_str()); + } else { + spr = CCSprite::create(s_lastImage.c_str()); + } + if (spr) { + renderer->renderNode(spr); + } else { + renderer->renderString(text); + } + s_lastImage = ""; + } else { + renderer->renderString(text); + } + } break; + + case MD_TEXTTYPE::MD_TEXT_HTML: { + if (text.size() > 2) { + auto tag = string_utils::trim(text.substr(1, text.size() - 2)); + auto isClosing = tag.front() == '/'; + if (isClosing) tag = tag.substr(1); + if (tag.front() != 'c') { + Log::get() << Severity::Warning << "Unknown tag " << text; + renderer->renderString(text); + } else { + if (isClosing) { + renderer->popColor(); + } else { + auto color = colorForIdentifier(tag); + if (color) { + renderer->pushColor(color.value()); + } else { + Log::get() << Severity::Warning + << "Error parsing color: " << color.error(); + } + } + } + } else { + Log::get() << Severity::Warning + << "Too short tag " << text; + renderer->renderString(text); + } + } break; + + default: { + Log::get() << Severity::Warning << "Unhandled text type " << type; + } break; + } + return 0; + } + + static int enterBlock(MD_BLOCKTYPE type, void* detail, void* mdtextarea) { + auto textarea = reinterpret_cast<MDTextArea*>(mdtextarea); + auto renderer = textarea->m_renderer; + switch (type) { + case MD_BLOCKTYPE::MD_BLOCK_DOC: {} break; + + case MD_BLOCKTYPE::MD_BLOCK_H: { + auto hdetail = reinterpret_cast<MD_BLOCK_H_DETAIL*>(detail); + renderer->pushStyleFlags(TextStyleBold); + switch (hdetail->level) { + case 1: renderer->pushScale(g_fontScale * 2.f); break; + case 2: renderer->pushScale(g_fontScale * 1.5f); break; + case 3: renderer->pushScale(g_fontScale * 1.17f); break; + case 4: renderer->pushScale(g_fontScale); break; + case 5: renderer->pushScale(g_fontScale * .83f); break; + default: + case 6: renderer->pushScale(g_fontScale * .67f); break; + } + // switch (hdetail->level) { + // case 3: renderer->pushCaps(TextCapitalization::AllUpper); break; + // } + } break; + + case MD_BLOCKTYPE::MD_BLOCK_P: {} break; + + case MD_BLOCKTYPE::MD_BLOCK_UL: + case MD_BLOCKTYPE::MD_BLOCK_OL: { + renderer->pushIndent(g_indent); + s_isOrderedList = type == MD_BLOCKTYPE::MD_BLOCK_OL; + s_orderedListNum = 0; + } break; + + case MD_BLOCKTYPE::MD_BLOCK_HR: { + renderer->breakLine(g_paragraphPadding / 2); + renderer->renderNode(BreakLine::create(textarea->m_size.width)); + renderer->breakLine(g_paragraphPadding); + } break; + + case MD_BLOCKTYPE::MD_BLOCK_LI: { + renderer->pushOpacity(renderer->getCurrentOpacity() / 2); + auto lidetail = reinterpret_cast<MD_BLOCK_LI_DETAIL*>(detail); + if (s_isOrderedList) { + s_orderedListNum++; + renderer->renderString(std::to_string(s_orderedListNum) + ". "); + } else { + renderer->renderString("• "); + } + renderer->popOpacity(); + } break; + + case MD_BLOCKTYPE::MD_BLOCK_CODE: { + s_isCodeBlock = true; + s_codeStart = renderer->getCursorPos().y; + renderer->pushFont(g_mdMonoFont); + renderer->pushIndent(g_codeBlockIndent); + renderer->pushWrapOffset(g_codeBlockIndent); + } break; + + default: { + Log::get() << Severity::Warning << "Unhandled block enter type " << type; + } break; + } + return 0; + } + + static int leaveBlock(MD_BLOCKTYPE type, void* detail, void* mdtextarea) { + auto textarea = reinterpret_cast<MDTextArea*>(mdtextarea); + auto renderer = textarea->m_renderer; + switch (type) { + case MD_BLOCKTYPE::MD_BLOCK_DOC: {} break; + + case MD_BLOCKTYPE::MD_BLOCK_H: { + auto hdetail = reinterpret_cast<MD_BLOCK_H_DETAIL*>(detail); + renderer->breakLine(); + if (hdetail->level == 1) { + renderer->breakLine(g_paragraphPadding / 2); + renderer->renderNode(BreakLine::create(textarea->m_size.width)); + } + renderer->breakLine(g_paragraphPadding); + renderer->popScale(); + renderer->popStyleFlags(); + // switch (hdetail->level) { + // case 3: renderer->popCaps(); break; + // } + } break; + + case MD_BLOCKTYPE::MD_BLOCK_P: { + renderer->breakLine(); + renderer->breakLine(g_paragraphPadding); + } break; + + case MD_BLOCKTYPE::MD_BLOCK_OL: + case MD_BLOCKTYPE::MD_BLOCK_UL: { + renderer->popIndent(); + } break; + + case MD_BLOCKTYPE::MD_BLOCK_CODE: { + auto codeEnd = renderer->getCursorPos().y; + + auto pad = g_codeBlockIndent / 1.5f; + + CCSize size { + textarea->m_size.width + - renderer->getCurrentIndent() + - renderer->getCurrentWrapOffset() + + pad * 2, + s_codeStart - codeEnd + pad * 2 + }; + + auto bg = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bg->setScale(.25f); + bg->setColor({ 0, 0, 0 }); + bg->setOpacity(75); + bg->setContentSize(size * 4); + bg->setPosition( + size.width / 2 + renderer->getCurrentIndent() - pad, + // mmm i love magic numbers + // the -2.f is to offset the the box + // to fit the Ubuntu font very neatly. + // idk if it works the same for other + // fonts + s_codeStart - 2.f + pad - size.height / 2 + ); + bg->setAnchorPoint({ .5f, .5f }); + bg->setZOrder(-1); + textarea->m_content->addChild(bg); + + renderer->popWrapOffset(); + renderer->popIndent(); + renderer->popFont(); + + renderer->breakLine(); + } break; + + case MD_BLOCKTYPE::MD_BLOCK_LI: {} break; + + case MD_BLOCKTYPE::MD_BLOCK_HR: {} break; + + default: { + Log::get() << Severity::Warning << "Unhandled block leave type " << type; + } break; + } + return 0; + } + + static int enterSpan(MD_SPANTYPE type, void* detail, void* mdtextarea) { + auto renderer = reinterpret_cast<MDTextArea*>(mdtextarea)->m_renderer; + switch (type) { + case MD_SPANTYPE::MD_SPAN_STRONG: { + renderer->pushStyleFlags(TextStyleBold); + } break; + + case MD_SPANTYPE::MD_SPAN_EM: { + renderer->pushStyleFlags(TextStyleItalic); + } break; + + case MD_SPANTYPE::MD_SPAN_DEL: { + renderer->pushDecoFlags(TextDecorationStrikethrough); + } break; + + case MD_SPANTYPE::MD_SPAN_U: { + renderer->pushDecoFlags(TextDecorationUnderline); + } break; + + case MD_SPANTYPE::MD_SPAN_IMG: { + auto adetail = reinterpret_cast<MD_SPAN_IMG_DETAIL*>(detail); + s_lastImage = std::string(adetail->src.text, adetail->src.size); + } break; + + case MD_SPANTYPE::MD_SPAN_A: { + auto adetail = reinterpret_cast<MD_SPAN_A_DETAIL*>(detail); + s_lastLink = std::string(adetail->href.text, adetail->href.size); + } break; + + case MD_SPANTYPE::MD_SPAN_CODE: { + s_isCodeBlock = false; + renderer->pushFont(g_mdMonoFont); + } break; + + default: { + Log::get() << Severity::Warning << "Unhandled span enter type " << type; + } break; + } + return 0; + } + + static int leaveSpan(MD_SPANTYPE type, void* detail, void* mdtextarea) { + auto renderer = reinterpret_cast<MDTextArea*>(mdtextarea)->m_renderer; + switch (type) { + case MD_SPANTYPE::MD_SPAN_STRONG: { + renderer->popStyleFlags(); + } break; + + case MD_SPANTYPE::MD_SPAN_EM: { + renderer->popStyleFlags(); + } break; + + case MD_SPANTYPE::MD_SPAN_DEL: { + renderer->popDecoFlags(); + } break; + + case MD_SPANTYPE::MD_SPAN_U: { + renderer->popDecoFlags(); + } break; + + case MD_SPANTYPE::MD_SPAN_A: { + s_lastLink = ""; + } break; + + case MD_SPANTYPE::MD_SPAN_IMG: { + s_lastImage = ""; + } break; + + case MD_SPANTYPE::MD_SPAN_CODE: { + renderer->popFont(); + } break; + + default: { + Log::get() << Severity::Warning << "Unhandled span leave type " << type; + } break; + } + return 0; + } +}; +std::string MDParser::s_lastLink = ""; +std::string MDParser::s_lastImage = ""; +bool MDParser::s_isOrderedList = false; +size_t MDParser::s_orderedListNum = 0; +bool MDParser::s_isCodeBlock = false; +float MDParser::s_codeStart = 0; +decltype(MDParser::s_codeSpans) MDParser::s_codeSpans = {}; + +void MDTextArea::updateLabel() { + m_renderer->begin(m_content, CCPointZero, m_size); + + m_renderer->pushFont(g_mdFont); + m_renderer->pushScale(.5f); + m_renderer->pushVerticalAlign(TextAlignment::End); + m_renderer->pushHorizontalAlign(TextAlignment::Begin); + + MD_PARSER parser; + + parser.abi_version = 0; + parser.flags = MD_FLAG_UNDERLINE | + MD_FLAG_STRIKETHROUGH | + MD_FLAG_PERMISSIVEURLAUTOLINKS | + MD_FLAG_PERMISSIVEWWWAUTOLINKS; + + parser.text = &MDParser::parseText; + parser.enter_block = &MDParser::enterBlock; + parser.leave_block = &MDParser::leaveBlock; + parser.enter_span = &MDParser::enterSpan; + parser.leave_span = &MDParser::leaveSpan; + parser.debug_log = nullptr; + parser.syntax = nullptr; + + MDParser::s_codeSpans = {}; + + if (md_parse(m_text.c_str(), m_text.size(), &parser, this)) { + m_renderer->renderString("Error parsing Markdown"); + } + + for (auto& render : MDParser::s_codeSpans) { + auto bg = CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + bg->setScale(.125f); + bg->setColor({ 0, 0, 0 }); + bg->setOpacity(75); + bg->setContentSize(render.m_node->getScaledContentSize() * 8 + CCSize { 20.f, .0f }); + bg->setPosition( + render.m_node->getPositionX() - 2.5f * (.5f - render.m_node->getAnchorPoint().x), + render.m_node->getPositionY() - .5f + ); + bg->setAnchorPoint(render.m_node->getAnchorPoint()); + bg->setZOrder(-1); + m_content->addChild(bg); + // i know what you're thinking. + // my brother in christ, what the hell is this? + // where did this magical + 1.5f come from? + // the reason is that if you remove them, code + // spans are slightly offset and it triggers my + // OCD. + render.m_node->setPositionY(render.m_node->getPositionY() + 1.5f); + } + + m_renderer->end(); + + m_scrollLayer->m_contentLayer->setContentSize(m_content->getContentSize()); + m_scrollLayer->moveToTop(); +} + +CCScrollLayerExt* MDTextArea::getScrollLayer() const { + return m_scrollLayer; +} + +void MDTextArea::setString(const char* text) { + this->m_text = text; + this->updateLabel(); +} + +const char* MDTextArea::getString() { + return this->m_text.c_str(); +} + +MDTextArea* MDTextArea::create( + std::string const& str, + CCSize const& size +) { + auto ret = new MDTextArea; + if (ret && ret->init(str, size)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/nodes/MenuInputNode.cpp b/loader/src/ui/nodes/MenuInputNode.cpp new file mode 100644 index 00000000..5ef260e1 --- /dev/null +++ b/loader/src/ui/nodes/MenuInputNode.cpp @@ -0,0 +1,51 @@ +#include <Geode/ui/MenuInputNode.hpp> + +USE_GEODE_NAMESPACE(); + +bool MenuInputNode::init( + float width, float height, const char* placeholder, const char* fontPath, bool bg +) { + if (!CCMenuItem::init()) + return false; + + if (bg) { + m_bgSprite = cocos2d::extension::CCScale9Sprite::create( + "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } + ); + m_bgSprite->setScale(.5f); + m_bgSprite->setColor({ 0, 0, 0 }); + m_bgSprite->setOpacity(75); + m_bgSprite->setContentSize({ width * 2, height * 2 }); + this->addChild(m_bgSprite); + } + + this->setContentSize({ width, height }); + this->setAnchorPoint({ .5f, .5f }); + m_input = CCTextInputNode::create(width, height, placeholder, fontPath); + m_input->setPosition(width / 2, height / 2); + this->addChild(m_input); + + this->setEnabled(true); + + return true; +} + +MenuInputNode* MenuInputNode::create( + float width, float height, const char* placeholder, const char* fontPath, bool bg +) { + auto ret = new MenuInputNode; + if (ret && ret->init(width, height, placeholder, fontPath, bg)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void MenuInputNode::selected() { + m_input->onClickTrackNode(true); +} + +CCTextInputNode* MenuInputNode::getInput() const { + return m_input; +} diff --git a/loader/src/ui/nodes/Notification.cpp b/loader/src/ui/nodes/Notification.cpp new file mode 100644 index 00000000..55c38bab --- /dev/null +++ b/loader/src/ui/nodes/Notification.cpp @@ -0,0 +1,474 @@ +#include <Geode/ui/Notification.hpp> +#include <Geode/ui/TextRenderer.hpp> +#include <Geode/utils/WackyGeodeMacros.hpp> + +USE_GEODE_NAMESPACE(); + +// todo: make sure notifications dont disappear +// off the screen if the user happens to switch +// scenes or smth that causes actions from being +// run / completed + +Notification::Notification() {} + +Notification::~Notification() { + CCDirector::sharedDirector()->getTouchDispatcher()->decrementForcePrio(2); +} + +void Notification::registerWithTouchDispatcher() { + CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate( + this, + 0, + true + ); +} + +static bool isHovered(CCNode* node, CCTouch* touch) { + auto csize = node->getScaledContentSize(); + if ( + CCRect { + node->getPositionX() - csize.width / 2, + node->getPositionY() - csize.height / 2, + csize.width, + csize.height + }.containsPoint(touch->getLocation()) + ) { + return true; + } + return false; +} + +static bool shouldHideNotification( + CCTouch* touch, NotificationLocation const& location +) { + static constexpr const float HIDE_THRESHOLD = 20.f; + auto dist = touch->getLocation() - touch->getStartLocation(); + switch (location) { + case NotificationLocation::BottomLeft: + case NotificationLocation::TopLeft: + return dist.x < -HIDE_THRESHOLD; + + case NotificationLocation::BottomRight: + case NotificationLocation::TopRight: + return dist.x > HIDE_THRESHOLD; + + case NotificationLocation::BottomCenter: + return dist.y < -HIDE_THRESHOLD; + + case NotificationLocation::TopCenter: + return dist.y > HIDE_THRESHOLD; + } + return false; +} + +void Notification::ccTouchMoved(CCTouch* touch, CCEvent* event) { + auto dist = touch->getLocation() - touch->getStartLocation(); + switch (m_location) { + case NotificationLocation::BottomLeft: + case NotificationLocation::TopLeft: + this->setPositionX(m_posAtTouchStart.x + dist.x); + if (this->getPositionX() > m_showDest.x) { + this->setPositionX(m_showDest.x); + } + break; + + case NotificationLocation::BottomRight: + case NotificationLocation::TopRight: + this->setPositionX(m_posAtTouchStart.x + dist.x); + if (this->getPositionX() < m_showDest.x) { + this->setPositionX(m_showDest.x); + } + break; + + case NotificationLocation::BottomCenter: + this->setPositionY(m_posAtTouchStart.y + dist.y); + if (this->getPositionY() > m_showDest.y) { + this->setPositionY(m_showDest.y); + } + break; + + case NotificationLocation::TopCenter: + this->setPositionY(m_posAtTouchStart.y + dist.y); + if (this->getPositionY() < m_showDest.y) { + this->setPositionY(m_showDest.y); + } + break; + } + auto clicking = !shouldHideNotification(touch, m_location); + if (m_clicking != clicking) { + m_clicking = clicking; + this->animateClicking(); + } + auto hovered = isHovered(this, touch); + if (m_hovered != hovered) { + m_hovered = hovered; + if (hovered) { + m_bg->setColor({ 150, 150, 150 }); + } else { + m_bg->setColor({ 255, 255, 255 }); + } + this->animateClicking(); + } +} + +bool Notification::ccTouchBegan(CCTouch* touch, CCEvent* event) { + if (!isHovered(this, touch)) { + return false; + } + m_bg->setColor({ 150, 150, 150 }); + this->stopAllActions(); + m_posAtTouchStart = this->getPosition(); + m_clicking = true; + m_hovered = true; + this->animateClicking(); + return true; +} + +void Notification::ccTouchEnded(CCTouch* touch, CCEvent* event) { + m_clicking = false; + m_hovered = false; + this->animateClicking(); + m_bg->setColor({ 255, 255, 255 }); + if (shouldHideNotification(touch, m_location)) { + return this->hide(); + } + if (isHovered(this, touch)) { + this->animateIn(); + this->clicked(); + } +} + +void Notification::clicked() { + if (m_callback) { + m_callback(this); + if (m_hideOnClicked) { + this->animateOutClicked(); + } + } +} + +bool Notification::init( + Mod* owner, + std::string const& title, + std::string const& text, + CCNode* icon, + const char* bg, + std::function<void(Notification*)> callback, + bool hideOnClick +) { + if (!CCLayer::init()) + return false; + + m_owner = owner; + m_callback = callback; + m_hideOnClicked = hideOnClick; + + // m_labels is Ref so no need to call + // retain manually + m_labels = CCArray::create(); + + m_bg = CCScale9Sprite::create(bg); + m_bg->setScale(.6f); + + // using TextRenderer to create the text + // so it automatically wraps the lines + auto renderer = TextRenderer::create(); + renderer->begin(this, CCPointZero, { 120.f, 20.f }); + + renderer->pushBMFont("chatFont.fnt"); + renderer->pushScale(.4f); + for (auto& label : renderer->renderString( + text + "\n(from " + owner->getName() + ")" + )) { + m_labels->addObject(label.m_node); + } + + renderer->end(); + renderer->release(); + + // add icon + float iconSpace = .0f; + if (icon) { + m_icon = icon; + iconSpace = 20.f; + m_icon->setPosition({ + -m_obContentSize.width / 2 + iconSpace / 2, + .0f + }); + limitNodeSize(m_icon, + { iconSpace - 8.f, m_obContentSize.height - 8.f }, + 1.f, .1f + ); + this->addChild(m_icon); + } + + // add title + if (title.size()) { + m_title = CCLabelBMFont::create(title.c_str(), "goldFont.fnt"); + m_title->limitLabelWidth(m_obContentSize.width - iconSpace, .4f, .01f); + m_obContentSize.height += 14; + m_title->setPosition( + -m_obContentSize.width / 2 + iconSpace, + m_obContentSize.height / 2 - 6.f + ); + m_title->setAnchorPoint({ .0f, .5f }); + this->addChild(m_title); + } + + // move text content if an icon is present + m_obContentSize.width += iconSpace; + m_icon->setPositionX(m_icon->getPositionX() - iconSpace / 2); + m_title->setPositionX(m_title->getPositionX() - iconSpace / 2); + CCARRAY_FOREACH_B_TYPE(m_labels, label, CCNode) { + label->setPosition( + label->getPositionX() + iconSpace - m_obContentSize.width / 2, + label->getPositionY() - m_obContentSize.height / 2 + 2.f + ); + } + + // fit bg to content + m_bg->setContentSize( + m_obContentSize / m_bg->getScale() + CCSize { 6.f, 6.f } + ); + m_bg->setPosition(0, 0); + m_bg->setZOrder(-1); + this->addChild(m_bg); + + // set anchor point to middle so the + // notification properly scales + this->setAnchorPoint({ .0f, .0f }); + this->setVisible(false); + + // make sure ~CCLayer properly removes + // the notification from touch dispatcher + this->setTouchEnabled(true); + + // make this notification the most important + // touch fella on the screen + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + return true; +} + +Notification* Notification::create( + Mod* owner, + std::string const& title, + std::string const& text, + CCNode* icon, + const char* bg, + std::function<void(Notification*)> callback, + bool hideOnClick +) { + auto ret = new Notification(); + if (ret && ret->init( + owner, title, text, icon, bg, callback, hideOnClick + )) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void Notification::showForReal() { + if (!m_pParent) { + CCDirector::sharedDirector()->getRunningScene()->addChild(this); + } + SceneManager::get()->keepAcrossScenes(this); + // haha i am incredibly mature + this->setZOrder(0xB00B1E5); + this->setVisible(true); + + static constexpr const float pad = 15.f; + + float xMovement = .0f, yMovement = .0f; + float xStart = .0f, yStart = .0f; + switch (m_location) { + case NotificationLocation::TopLeft: { + xMovement = this->getScaledContentSize().width + pad * 2; + xStart = -this->getScaledContentSize().width / 2 - pad; + yStart = m_pParent->getContentSize().height + - pad - this->getScaledContentSize().height / 2; + } break; + + case NotificationLocation::BottomLeft: { + xMovement = this->getScaledContentSize().width + pad * 2; + xStart = -this->getScaledContentSize().width / 2 - pad; + yStart = pad + this->getScaledContentSize().height / 2; + } break; + + case NotificationLocation::TopRight: { + xMovement = -this->getScaledContentSize().width - pad * 2; + xStart = m_pParent->getContentSize().width + + this->getScaledContentSize().width / 2 + pad; + yStart = m_pParent->getContentSize().height + - pad - this->getScaledContentSize().height / 2; + } break; + + case NotificationLocation::BottomRight: { + xMovement = -this->getScaledContentSize().width - pad * 2; + xStart = m_pParent->getContentSize().width + + this->getScaledContentSize().width / 2 + pad; + yStart = pad + this->getScaledContentSize().height / 2; + } break; + + case NotificationLocation::BottomCenter: { + yMovement = pad * 2 + this->getScaledContentSize().height; + xStart = m_pParent->getContentSize().width / 2; + yStart = -pad - this->getScaledContentSize().height / 2; + } break; + + case NotificationLocation::TopCenter: { + yMovement = -pad * 2 - this->getScaledContentSize().height; + xStart = m_pParent->getContentSize().width / 2; + yStart = m_pParent->getContentSize().height + pad + + this->getScaledContentSize().height / 2; + } break; + } + + m_hideDest = CCPoint { xStart, yStart }; + m_showDest = CCPoint { xStart + xMovement, yStart + yMovement }; + + GameSoundManager::sharedManager()->playEffect( + "newNotif03.ogg"_spr, 1.f, 1.f, 1.f + ); + + this->setPosition(xStart, yStart); + this->animateIn(); +} + +void Notification::hide() { + // if this notification has already been hidden, + // don't do anything + if (m_hiding || !NotificationManager::get()->isInQueue(this)) { + return; + } + GameSoundManager::sharedManager()->playEffect( + "byeNotif00.ogg"_spr, 1.f, 1.f, 1.f + ); + m_hiding = true; + this->animateOut(); +} + +void Notification::animateIn() { + this->runAction(CCEaseInOut::create( + CCMoveTo::create(.3f, m_showDest), + 6.f + )); + if (m_time) { + this->runAction(CCSequence::create( + CCDelayTime::create(m_time), + CCCallFunc::create(this, callfunc_selector(Notification::hide)), + nullptr + )); + } +} + +void Notification::animateOut() { + this->runAction(CCSequence::create( + CCEaseInOut::create( + CCMoveTo::create(.3f, { m_hideDest }), + 6.f + ), + CCCallFunc::create(this, callfunc_selector(Notification::hidden)), + nullptr + )); +} + +void Notification::animateOutClicked() { + this->runAction(CCSequence::create( + CCEaseBackIn::create( + CCScaleTo::create(.2f, .0f) + ), + CCCallFunc::create(this, callfunc_selector(Notification::hidden)), + nullptr + )); +} + +void Notification::animateClicking() { + this->runAction(CCEaseInOut::create( + CCScaleTo::create(.1f, ( + (m_clicking && m_hovered) ? m_targetScale * .9f : m_targetScale + )), 2.f + )); +} + +void Notification::show(NotificationLocation location, float time) { + if (location == NotificationLocation::TopCenter) { + // the notification is larger at top center to + // be more easily readable on mobile + this->setScale(1.5f); + } else { + this->setScale(1.2f); + } + m_targetScale = m_fScaleX; + + m_time = time; + m_location = location; + NotificationManager::get()->push(this); +} + +void Notification::hidden() { + NotificationManager::get()->pop(this); + this->removeFromParent(); + SceneManager::get()->forget(this); +} + +NotificationBuilder Notification::build() { + return std::move(NotificationBuilder()); +} + +Notification* NotificationBuilder::show() { + auto icon = m_iconNode; + if (!icon && m_icon.size()) { + icon = CCSprite::create(m_icon.c_str()); + if (!icon) icon = CCSprite::createWithSpriteFrameName(m_icon.c_str()); + } + auto notif = Notification::create( + m_owner, m_title, m_text, icon, + m_bg.c_str(), m_callback, m_hideOnClick + ); + notif->show(m_location, m_time); + return notif; +} + + +bool NotificationManager::isInQueue(Notification* notification) { + auto location = notification->m_location; + if (m_notifications.count(location)) { + return vector_utils::contains( + m_notifications.at(location), Ref(notification) + ); + } + return false; +} + +void NotificationManager::push(Notification* notification) { + auto location = notification->m_location; + if (!m_notifications.count(location)) { + m_notifications[location] = { notification }; + notification->showForReal(); + } else { + m_notifications[location].push_back(notification); + } +} + +void NotificationManager::pop(Notification* notification) { + auto location = notification->m_location; + if (m_notifications.count(location)) { + vector_utils::erase( + m_notifications.at(location), Ref(notification) + ); + if (!m_notifications.at(location).size()) { + m_notifications.erase(location); + } else { + m_notifications.at(location).front()->showForReal(); + } + } +} + +NotificationManager* NotificationManager::get() { + static auto inst = new NotificationManager; + return inst; +} diff --git a/loader/src/ui/nodes/Popup.cpp b/loader/src/ui/nodes/Popup.cpp new file mode 100644 index 00000000..236f23a8 --- /dev/null +++ b/loader/src/ui/nodes/Popup.cpp @@ -0,0 +1,50 @@ +#include <Geode/ui/Popup.hpp> + +USE_GEODE_NAMESPACE(); + +class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol { +protected: + std::function<void(FLAlertLayer*, bool)> m_selected; + + void FLAlert_Clicked(FLAlertLayer* layer, bool btn2) override { + if (m_selected) { + m_selected(layer, btn2); + } + } + +public: + static QuickPopup* create( + const char* title, + std::string const& content, + const char* btn1, + const char* btn2, + std::function<void(FLAlertLayer*, bool)> selected + ) { + auto inst = new QuickPopup; + inst->m_selected = selected; + if (inst && inst->init( + inst, title, content, + btn1, btn2, 350.f, false, .0f + )) { + inst->autorelease(); + return inst; + } + CC_SAFE_DELETE(inst); + return nullptr; + } +}; + +void geode::createQuickPopup( + const char* title, + std::string const& content, + const char* btn1, + const char* btn2, + std::function<void(FLAlertLayer*, bool)> selected +) { + QuickPopup::create( + title, + content, + btn1, btn2, + selected + )->show(); +} diff --git a/loader/src/ui/nodes/SceneManager.cpp b/loader/src/ui/nodes/SceneManager.cpp new file mode 100644 index 00000000..91b746e0 --- /dev/null +++ b/loader/src/ui/nodes/SceneManager.cpp @@ -0,0 +1,39 @@ +#include <Geode/ui/SceneManager.hpp> +#include <Geode/utils/WackyGeodeMacros.hpp> + +USE_GEODE_NAMESPACE(); + +bool SceneManager::setup() { + m_persistedNodes = CCArray::create(); + m_persistedNodes->retain(); + return true; +} + +SceneManager* SceneManager::get() { + static SceneManager* inst = nullptr; + if (!inst) { + inst = new SceneManager(); + inst->setup(); + } + return inst; +} + +SceneManager::~SceneManager() { + m_persistedNodes->release(); +} + +void SceneManager::keepAcrossScenes(CCNode* node) { + m_persistedNodes->addObject(node); +} + +void SceneManager::forget(CCNode* node) { + m_persistedNodes->removeObject(node); +} + +void SceneManager::willSwitchToScene(CCScene* scene) { + CCARRAY_FOREACH_B_TYPE(m_persistedNodes, node, CCNode) { + // no cleanup in order to keep actions running + node->removeFromParentAndCleanup(false); + scene->addChild(node); + } +} diff --git a/loader/src/ui/nodes/ScrollLayer.cpp b/loader/src/ui/nodes/ScrollLayer.cpp new file mode 100644 index 00000000..4fa549b3 --- /dev/null +++ b/loader/src/ui/nodes/ScrollLayer.cpp @@ -0,0 +1,75 @@ +#include <Geode/ui/ScrollLayer.hpp> +#include <Geode/utils/WackyGeodeMacros.hpp> + +USE_GEODE_NAMESPACE(); + +GenericContentLayer* GenericContentLayer::create(float width, float height) { + auto ret = new GenericContentLayer(); + if (ret && ret->initWithColor({ 0, 0, 0, 0 }, width, height)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void GenericContentLayer::setPosition(CCPoint const& pos) { + // CCContentLayer expect its children to + // all be TableViewCells + CCLayerColor::setPosition(pos); + + CCARRAY_FOREACH_B_TYPE(m_pChildren, child, CCNode) { + auto y = this->getPositionY() + child->getPositionY(); + child->setVisible(!( + (m_obContentSize.height < y) || + (y < -child->getContentSize().height) + )); + } +} + + +void ScrollLayer::scrollWheel(float y, float) { + if (m_scrollWheelEnabled) { + this->scrollLayer(y); + } +} + +void ScrollLayer::enableScrollWheel(bool enable) { + m_scrollWheelEnabled = enable; +} + +ScrollLayer::ScrollLayer( + CCRect const& rect, + bool scrollWheelEnabled, + bool vertical +) : CCScrollLayerExt(rect) { + m_scrollWheelEnabled = scrollWheelEnabled; + + m_disableVertical = !vertical; + m_disableHorizontal = vertical; + m_cutContent = true; + + m_contentLayer->removeFromParent(); + m_contentLayer = GenericContentLayer::create( + rect.size.width, rect.size.height + ); + m_contentLayer->setAnchorPoint({ 0, 0 }); + this->addChild(m_contentLayer); + + this->setMouseEnabled(true); + this->setTouchEnabled(true); +} + +ScrollLayer* ScrollLayer::create(CCRect const& rect, bool scroll, bool vertical) { + auto ret = new ScrollLayer(rect, scroll, vertical); + if (ret) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +ScrollLayer* ScrollLayer::create(CCSize const& size, bool scroll, bool vertical) { + return ScrollLayer::create({ 0, 0, size.width, size.height }, scroll, vertical); +} diff --git a/loader/src/ui/nodes/Scrollbar.cpp b/loader/src/ui/nodes/Scrollbar.cpp new file mode 100644 index 00000000..c7e3dfd1 --- /dev/null +++ b/loader/src/ui/nodes/Scrollbar.cpp @@ -0,0 +1,201 @@ +#include <Geode/ui/Scrollbar.hpp> + +// TODO: die +#undef min +#undef max + +USE_GEODE_NAMESPACE(); + +// bool Scrollbar::mouseDownExt(MouseEvent, cocos2d::CCPoint const& mpos) { +// if (!m_target) return false; + +// ExtMouseDispatcher::get()->attainCapture(this); + +// auto pos = this->convertToNodeSpace(mpos); + +// auto contentHeight = m_target->m_contentLayer->getScaledContentSize().height; +// auto targetHeight = m_target->getScaledContentSize().height; + +// auto h = contentHeight - targetHeight + m_target->m_scrollLimitTop; +// auto p = targetHeight / contentHeight; + +// auto thumbHeight = m_resizeThumb ? std::min(p, 1.f) * targetHeight / .4f : 0; + +// auto posY = h * ( +// (-pos.y - targetHeight / 2 + thumbHeight / 4 - 5) / +// (targetHeight - thumbHeight / 2 + 10) +// ); + +// if (posY > 0.0f) posY = 0.0f; +// if (posY < -h) posY = -h; + +// auto offsetY = m_target->m_contentLayer->getPositionY() - posY; + +// return true; +// } + +// bool Scrollbar::mouseUpExt(MouseEvent, cocos2d::CCPoint const&) { +// ExtMouseDispatcher::get()->releaseCapture(this); +// return true; +// } + +// void Scrollbar::mouseMoveExt(cocos2d::CCPoint const& mpos) { +// if (!m_target || !ExtMouseDispatcher::get()->isCapturing(this)) return; + +// if (this->m_extMouseDown.size()) { +// auto pos = this->convertToNodeSpace(mpos); + +// auto contentHeight = m_target->m_contentLayer->getScaledContentSize().height; +// auto targetHeight = m_target->getScaledContentSize().height; + +// auto h = contentHeight - targetHeight + m_target->m_scrollLimitTop; +// auto p = targetHeight / contentHeight; + +// auto thumbHeight = m_resizeThumb ? std::min(p, 1.f) * targetHeight / .4f : 0; + +// auto posY = h * ( +// (-pos.y - targetHeight / 2 + thumbHeight / 4 - 5) / +// (targetHeight - thumbHeight / 2 + 10) +// ); + +// if (posY > 0.0f) posY = 0.0f; +// if (posY < -h) posY = -h; + +// m_target->m_contentLayer->setPositionY(posY); +// } +// } + +void Scrollbar::scrollWheel(float x, float y) { + if (!m_target) return; + m_target->scrollWheel(x, y); +} + +void Scrollbar::draw() { + CCLayer::draw(); + + if (!m_target) return; + + auto contentHeight = m_target->m_contentLayer->getScaledContentSize().height; + auto targetHeight = m_target->getScaledContentSize().height; + + if (m_trackIsRotated) { + m_track->setContentSize({ + targetHeight / m_track->getScale(), + m_width / m_track->getScale() + }); + } else { + m_track->setContentSize({ + m_width / m_track->getScale(), + targetHeight / m_track->getScale() + }); + } + m_track->setPosition(.0f, .0f); + + this->setContentSize({ m_width, targetHeight }); + + auto h = contentHeight - targetHeight + m_target->m_scrollLimitTop; + auto p = targetHeight / contentHeight; + + GLubyte o; + if (m_hoverHighlight) { + o = 100; + // if (m_extMouseHovered) { + // o = 160; + // } + // if (m_extMouseDown.size()) { + // o = 255; + // } + } else { + o = 255; + // if (m_extMouseDown.size()) { + // o = 125; + // } + } + m_thumb->setColor({ o, o, o }); + + auto y = m_target->m_contentLayer->getPositionY(); + + auto thumbHeight = m_resizeThumb ? std::min(p, 1.f) * targetHeight / .4f : 0; + auto thumbPosY = - targetHeight / 2 + thumbHeight / 4 - 5.0f + + (h ? (-y) / h : 1.f) * (targetHeight - thumbHeight / 2 + 10.0f); + + auto fHeightTop = [&]() -> float { + return thumbPosY - targetHeight / 2 + thumbHeight * .4f / 2 + 3.0f; + }; + auto fHeightBottom = [&]() -> float { + return thumbPosY + targetHeight / 2 - thumbHeight * .4f / 2 - 3.0f; + }; + + if (fHeightTop() > 0.0f) { + thumbHeight -= fHeightTop(); + thumbPosY -= fHeightTop(); + } + + if (fHeightBottom() < 0.f) { + thumbHeight += fHeightBottom(); + thumbPosY -= fHeightBottom(); + } + + m_thumb->setPosition(0.f, thumbPosY); + if (m_resizeThumb) { + m_thumb->setContentSize({ m_width, thumbHeight }); + } +} + +void Scrollbar::setTarget(CCScrollLayerExt* target) { + m_target = target; +} + +bool Scrollbar::init(CCScrollLayerExt* target) { + if (!this->CCLayer::init()) + return false; + + m_target = target; + + if (cocos::fileExistsInSearchPaths("scrollbar.png"_spr)) { + m_track = CCScale9Sprite::create("scrollbar.png"_spr); + m_track->setColor({ 0, 0, 0 }); + m_track->setOpacity(150); + m_track->setScale(.8f); + + m_thumb = CCScale9Sprite::create("scrollbar.png"_spr); + m_thumb->setScale(.4f); + + m_width = 8.f; + m_resizeThumb = true; + m_trackIsRotated = false; + m_hoverHighlight = true; + } else { + m_track = CCScale9Sprite::create("slidergroove.png"); + m_track->setRotation(90); + m_track->setScale(.8f); + + m_thumb = CCScale9Sprite::create("sliderthumb.png"); + m_thumb->setScale(.6f); + + m_width = 12.f; + m_resizeThumb = false; + m_trackIsRotated = true; + m_hoverHighlight = false; + } + + this->addChild(m_track); + this->addChild(m_thumb); + + // this->registerWithMouseDispatcher(); + + return true; +} + +Scrollbar* Scrollbar::create(CCScrollLayerExt* target) { + auto ret = new Scrollbar; + + if (ret && ret->init(target)) { + ret->autorelease(); + return ret; + } + + CC_SAFE_DELETE(ret); + return nullptr; +} + diff --git a/loader/src/ui/nodes/TextRenderer.cpp b/loader/src/ui/nodes/TextRenderer.cpp new file mode 100644 index 00000000..ab9a7f07 --- /dev/null +++ b/loader/src/ui/nodes/TextRenderer.cpp @@ -0,0 +1,779 @@ +#include <Geode/ui/TextRenderer.hpp> +#include <Geode/utils/WackyGeodeMacros.hpp> +#undef max +#undef min + +USE_GEODE_NAMESPACE(); +using namespace std::string_literals; + +bool TextDecorationWrapper::init( + TextRenderer::Label const& label, + int deco, + ccColor3B const& color, + GLubyte opacity +) { + if (!CCNodeRGBA::init()) + return false; + + label.m_node->removeFromParent(); + this->addChild(label.m_node); + + m_label = label; + m_deco = deco; + this->setColor(color); + this->setOpacity(opacity); + + return true; +} + +void TextDecorationWrapper::draw() { + // some nodes sometimes set the blend func to + // something else without resetting it back + ccGLBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + if (m_deco & TextDecorationUnderline) { + ccDrawSolidRect( + { 0, 0 }, + { m_obContentSize.width, 1.f }, + { + _realColor.r / 255.f, + _realColor.g / 255.f, + _realColor.b / 255.f, + _realOpacity / 255.f + } + ); + } + if (m_deco & TextDecorationStrikethrough) { + ccDrawSolidRect( + { 0, m_obContentSize.height * .4f - .75f }, + { m_obContentSize.width, m_obContentSize.height * .4f + .75f }, + { + _realColor.r / 255.f, + _realColor.g / 255.f, + _realColor.b / 255.f, + _realOpacity / 255.f + } + ); + } + CCNode::draw(); +} + +void TextDecorationWrapper::setString(const char* text) { + m_label.m_labelProtocol->setString(text); + this->setContentSize(m_label.m_node->getScaledContentSize()); + m_label.m_node->setPosition(m_obContentSize / 2); +} + +const char* TextDecorationWrapper::getString() { + return m_label.m_labelProtocol->getString(); +} + +void TextDecorationWrapper::setColor(cocos2d::ccColor3B const& color) { + m_label.m_rgbaProtocol->setColor(color); + return CCNodeRGBA::setColor(color); +} + +void TextDecorationWrapper::setOpacity(GLubyte opacity) { + m_label.m_rgbaProtocol->setOpacity(opacity); + return CCNodeRGBA::setOpacity(opacity); +} + +void TextDecorationWrapper::updateDisplayedColor(ccColor3B const& color) { + m_label.m_rgbaProtocol->updateDisplayedColor(color); + return CCNodeRGBA::updateDisplayedColor(color); +} + +void TextDecorationWrapper::updateDisplayedOpacity(GLubyte opacity) { + m_label.m_rgbaProtocol->updateDisplayedOpacity(opacity); + return CCNodeRGBA::updateDisplayedOpacity(opacity); +} + +TextDecorationWrapper* TextDecorationWrapper::create( + TextRenderer::Label const& label, + int deco, + ccColor3B const& color, + GLubyte opacity +) { + auto ret = new TextDecorationWrapper; + if (ret && ret->init(label, deco, color, opacity)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +TextDecorationWrapper* TextDecorationWrapper::wrap( + TextRenderer::Label const& label, + int deco, + ccColor3B const& color, + GLubyte opacity +) { + auto pos = label.m_node->getPosition(); + auto wrapper = TextDecorationWrapper::create(label, deco, color, opacity); + wrapper->setPosition(pos); + return wrapper; +} + + +bool TextLinkedButtonWrapper::init( + TextRenderer::Label const& label, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler handler +) { + if (!CCMenuItemSprite::initWithNormalSprite(label.m_node, nullptr, nullptr, target, handler)) + return false; + + m_label = label; + + label.m_node->removeFromParent(); + this->addChild(label.m_node); + + this->setCascadeColorEnabled(true); + this->setCascadeOpacityEnabled(true); + + return true; +} + +void TextLinkedButtonWrapper::link(TextLinkedButtonWrapper* other) { + if (this != other) { + m_linked.push_back(other); + } +} + +void TextLinkedButtonWrapper::setString(const char* text) { + m_label.m_labelProtocol->setString(text); + this->setContentSize(m_label.m_node->getScaledContentSize()); + m_label.m_node->setPosition(m_obContentSize * m_label.m_node->getAnchorPoint()); +} + +const char* TextLinkedButtonWrapper::getString() { + return m_label.m_labelProtocol->getString(); +} + +void TextLinkedButtonWrapper::setColor(cocos2d::ccColor3B const& color) { + m_label.m_rgbaProtocol->setColor(color); + return CCMenuItemSprite::setColor(color); +} + +void TextLinkedButtonWrapper::setOpacity(GLubyte opacity) { + m_label.m_rgbaProtocol->setOpacity(opacity); + return CCMenuItemSprite::setOpacity(opacity); +} + +void TextLinkedButtonWrapper::updateDisplayedColor(ccColor3B const& color) { + m_label.m_rgbaProtocol->updateDisplayedColor(color); + return CCMenuItemSprite::updateDisplayedColor(color); +} + +void TextLinkedButtonWrapper::updateDisplayedOpacity(GLubyte opacity) { + m_label.m_rgbaProtocol->updateDisplayedOpacity(opacity); + return CCMenuItemSprite::updateDisplayedOpacity(opacity); +} + +void TextLinkedButtonWrapper::selectedWithoutPropagation(bool selected) { + if (selected) { + m_opacity = this->getOpacity(); + m_color = this->getColor(); + this->setOpacity(150); + this->setColor({ 255, 255, 255 }); + } else { + this->setOpacity(m_opacity); + this->setColor(m_color); + } +} + +void TextLinkedButtonWrapper::selected() { + this->selectedWithoutPropagation(true); + for (auto& node : m_linked) { + node->selectedWithoutPropagation(true); + } +} + +void TextLinkedButtonWrapper::unselected() { + this->selectedWithoutPropagation(false); + for (auto& node : m_linked) { + node->selectedWithoutPropagation(false); + } +} + +TextLinkedButtonWrapper* TextLinkedButtonWrapper::create( + TextRenderer::Label const& label, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler handler +) { + auto ret = new TextLinkedButtonWrapper; + if (ret && ret->init(label, target, handler)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +TextLinkedButtonWrapper* TextLinkedButtonWrapper::wrap( + TextRenderer::Label const& label, + cocos2d::CCObject* target, + cocos2d::SEL_MenuHandler handler +) { + auto pos = label.m_node->getPosition(); + auto anchor = label.m_node->getAnchorPoint(); + auto wrapper = TextLinkedButtonWrapper::create(label, target, handler); + wrapper->setPosition(pos - wrapper->getScaledContentSize() * anchor); + return wrapper; +} + + +bool TextRenderer::init() { + return true; +} + +void TextRenderer::begin(CCNode* target, CCPoint const& pos, CCSize const& size) { + m_target = target ? target : CCNode::create(); + m_target->setContentSize(size); + m_target->setPosition(pos); + m_target->removeAllChildren(); + m_cursor = CCPointZero; + m_origin = pos; + m_size = size; +} + +CCNode* TextRenderer::end( + bool fitToContent, + TextAlignment horizontalAlign, + TextAlignment verticalAlign +) { + // adjust vertical alignments of last line + this->adjustLineAlignment(); + // resize target + if (fitToContent && m_target) { + // figure out area covered by target children + auto coverage = calculateChildCoverage(m_target); + // convert area from rect to size + auto renderedWidth = -coverage.origin.x + coverage.size.width; + auto renderedHeight = -coverage.origin.y + coverage.size.height; + // target size is always at least requested size + m_target->setContentSize({ + std::max(renderedWidth, m_size.width), + std::max(renderedHeight, m_size.height) + }); + // calculate paddings + float padX; + float padY; + switch (horizontalAlign) { + default: + case TextAlignment::Begin: + padX = .0f; break; + case TextAlignment::Center: + padX = std::max(m_size.width - renderedWidth, 0.f) / 2; break; + case TextAlignment::End: + padX = std::max(m_size.width - renderedWidth, 0.f); break; + } + switch (verticalAlign) { + default: + case TextAlignment::Begin: + padY = .0f; break; + case TextAlignment::Center: + padY = std::max(m_size.height - renderedHeight, 0.f) / 2; break; + case TextAlignment::End: + padY = std::max(m_size.height - renderedHeight, 0.f); break; + } + // adjust child positions + CCARRAY_FOREACH_B_TYPE(m_target->getChildren(), child, CCNode) { + child->setPosition( + child->getPositionX() + padX, + child->getPositionY() + + m_target->getContentSize().height - + coverage.size.height - padY + ); + } + } + + // clear stacks + m_fontStack.clear(); + m_scaleStack.clear(); + m_styleStack.clear(); + m_colorStack.clear(); + m_opacityStack.clear(); + m_decorationStack.clear(); + m_capsStack.clear(); + m_lastRendered.clear(); + m_indentationStack.clear(); + m_wrapOffsetStack.clear(); + m_hAlignmentStack.clear(); + m_vAlignmentStack.clear(); + + // reset + m_cursor = CCPointZero; + m_size = CCSizeZero; + auto ret = m_target; + m_target = nullptr; + m_lastRendered = {}; + CC_SAFE_RELEASE_NULL(m_lastRenderedNode); + m_renderedLine.clear(); + + return ret; +} + +void TextRenderer::moveCursor(CCPoint const& pos) { + m_cursor = pos; +} + +CCPoint const& TextRenderer::getCursorPos() { + return m_cursor; +} + +bool TextRenderer::render(std::string const& word, CCNode* to, CCLabelProtocol* label) { + auto origLabelStr = label->getString(); + auto str = ((origLabelStr && strlen(origLabelStr)) ? + origLabelStr : "" + ) + word; + if (m_size.width) { + std::string orig = origLabelStr; + label->setString(str.c_str()); + if ( + m_cursor.x + to->getScaledContentSize().width > + m_size.width - this->getCurrentWrapOffset() + ) { + label->setString(orig.c_str()); + return false; + } + return true; + } else { + label->setString(str.c_str()); + return true; + } +} + +TextRenderer::Label TextRenderer::addWrappers( + Label const& label, + bool isButton, + CCObject* target, + SEL_MenuHandler callback +) { + Label ret = label; + if (this->getCurrentDeco() != TextDecorationNone) { + auto wrapper = TextDecorationWrapper::wrap( + ret, this->getCurrentDeco(), this->getCurrentColor(), + this->getCurrentOpacity() + ); + ret = Label(wrapper); + } + if (isButton) { + auto wrapper = TextLinkedButtonWrapper::wrap( + ret, target, callback + ); + ret = Label(wrapper); + } + ret.m_lineHeight = label.m_lineHeight; + return ret; +} + +std::vector<TextRenderer::Label> TextRenderer::renderStringEx( + std::string const& str, + Font font, + float scale, + ccColor3B color, + GLubyte opacity, + int style, + int deco, + TextCapitalization caps, + bool addToTarget, + bool isButton, + CCObject* target, + SEL_MenuHandler callback +) { + std::vector<Label> res; + + if (!target) target = m_target; + + Label label; + bool newLine = true; + + auto lastIndent = m_indentationStack.size() > 1 ? + m_indentationStack.at(m_indentationStack.size() - 1) : .0f; + + if (m_cursor.x == m_origin.x + lastIndent && this->getCurrentIndent() > .0f) { + m_cursor.x += this->getCurrentIndent(); + } + + auto createLabel = [&]() -> bool { + // create label through font and add + // decorations (underline, strikethrough) + + // buttonize (new word just dropped) + label = this->addWrappers( + font(style), isButton, target, callback + ); + + label.m_node->setScale(scale); + label.m_node->setPosition(m_cursor); + label.m_node->setAnchorPoint({ .0f, label.m_node->getAnchorPoint().y }); + label.m_rgbaProtocol->setColor(color); + label.m_rgbaProtocol->setOpacity(opacity); + + res.push_back(label); + m_renderedLine.push_back(label.m_node); + if (addToTarget) { + m_target->addChild(label.m_node); + } + + return true; + }; + + auto nextLine = [&]() -> bool { + this->breakLine(label.m_lineHeight * scale); + if (!createLabel()) return false; + newLine = true; + return true; + }; + + // create initial label + if (!createLabel()) return {}; + + bool firstLine = true; + for (auto line : string_utils::split(str, "\n")) { + if (!firstLine && !nextLine()) { + return {}; + } + firstLine = false; + for (auto word : string_utils::split(line, " ")) { + // add extra space in front of word if not on + // new line + if (!newLine) word = " " + word; + newLine = false; + + // update capitalization + switch (caps) { + case TextCapitalization::AllUpper: string_utils::toUpperIP(word); break; + case TextCapitalization::AllLower: string_utils::toLowerIP(word); break; + default: break; + } + + // try to render at the end of current line + if (this->render(word, label.m_node, label.m_labelProtocol)) continue; + + // try to create a new line + if (!nextLine()) return {}; + + if (string_utils::startsWith(word, " ")) word = word.substr(1); + newLine = false; + + // try to render on new line + if (this->render(word, label.m_node, label.m_labelProtocol)) continue; + + // no need to create a new line as we know + // the current one has no content and is + // supposed to receive this one + + // render character by character + for (auto& c : word) { + if ( + !this->render(std::string(1, c), label.m_node, label.m_labelProtocol) + ) { + if (!nextLine()) return {}; + + if (string_utils::startsWith(word, " ")) word = word.substr(1); + newLine = false; + } + } + } + // increment cursor position + m_cursor.x += label.m_node->getScaledContentSize().width; + } + + if (isButton) { + for (auto& btn : res) for (auto& b : res) { + if (btn.m_node != b.m_node) { + as<TextLinkedButtonWrapper*>(btn.m_node)->link( + as<TextLinkedButtonWrapper*>(b.m_node) + ); + } + } + } + + CC_SAFE_RELEASE_NULL(m_lastRenderedNode); + m_lastRendered = res; + + return res; +} + +std::vector<TextRenderer::Label> TextRenderer::renderString(std::string const& str) { + return this->renderStringEx( + str, + this->getCurrentFont(), + this->getCurrentScale(), + this->getCurrentColor(), + this->getCurrentOpacity(), + this->getCurrentStyle(), + this->getCurrentDeco(), + this->getCurrentCaps(), + true, false, + nullptr, nullptr + ); +} + +std::vector<TextRenderer::Label> TextRenderer::renderStringInteractive( + std::string const& str, + CCObject* target, + SEL_MenuHandler callback +) { + return this->renderStringEx( + str, + this->getCurrentFont(), + this->getCurrentScale(), + this->getCurrentColor(), + this->getCurrentOpacity(), + this->getCurrentStyle(), + this->getCurrentDeco(), + this->getCurrentCaps(), + true, true, + target, callback + ); +} + +CCNode* TextRenderer::renderNode(CCNode* node) { + m_cursor.x += node->getScaledContentSize().width * node->getAnchorPoint().x; + node->setPosition(m_cursor); + m_target->addChild(node); + m_cursor.x += node->getScaledContentSize().width * (1.f - node->getAnchorPoint().x); + m_lastRendered.clear(); + CC_SAFE_RELEASE(m_lastRenderedNode); + m_lastRenderedNode = node; + CC_SAFE_RETAIN(m_lastRenderedNode); + m_renderedLine.push_back(node); + return node; +} + +void TextRenderer::breakLine(float incY) { + auto h = this->adjustLineAlignment(); + m_renderedLine.clear(); + float y = incY; + if (!y && m_fontStack.size()) { + y = m_fontStack.back()(this->getCurrentStyle()).m_lineHeight * this->getCurrentScale(); + if (!y && m_lastRendered.size()) { + y = as<CCNode*>(m_lastRendered.back().m_node)->getScaledContentSize().height; + } + if (!y && m_lastRenderedNode) { + y = m_lastRenderedNode->getScaledContentSize().height; + } + } + if (h > y) y = h; + m_cursor.y -= y; + m_cursor.x = m_origin.x + getCurrentIndent(); +} + +float TextRenderer::adjustLineAlignment() { + auto coverage = calculateNodeCoverage(m_renderedLine); + auto maxWidth = -coverage.origin.x + coverage.size.width; + auto maxHeight = .0f; + for (auto& node : m_renderedLine) { + if (node->getScaledContentSize().height > maxHeight) { + maxHeight = node->getScaledContentSize().height; + } + } + for (auto& node : m_renderedLine) { + auto height = node->getScaledContentSize().height; + auto anchor = node->getAnchorPoint().y; + switch (this->getCurrentVerticalAlign()) { + case TextAlignment::Begin: default: { + node->setPositionY(m_cursor.y - height * (1.f - anchor)); + } break; + + case TextAlignment::Center: { + node->setPositionY(m_cursor.y - maxHeight / 2 + height * (.5f - anchor)); + } break; + + case TextAlignment::End: { + node->setPositionY(m_cursor.y - maxHeight + height * anchor); + } break; + } + switch (this->getCurrentHorizontalAlign()) { + case TextAlignment::Begin: default: { + // already correct + } break; + + case TextAlignment::Center: { + node->setPositionX( + node->getPositionX() + (m_size.width - maxWidth) / 2 + ); + } break; + + case TextAlignment::End: { + node->setPositionX( + node->getPositionX() + m_size.width - maxWidth - this->getCurrentIndent() + ); + } break; + } + } + return maxHeight; +} + +void TextRenderer::pushBMFont(const char* bmFont) { + m_fontStack.push_back([bmFont](int) -> Label { + return CCLabelBMFont::create("", bmFont); + }); +} + +void TextRenderer::pushFont(Font const& font) { + m_fontStack.push_back(font); +} + +void TextRenderer::popFont() { + if (m_fontStack.size()) m_fontStack.pop_back(); +} + +TextRenderer::Font TextRenderer::getCurrentFont() const { + if (!m_fontStack.size()) { + return [](int) -> Label { + return CCLabelBMFont::create("", "bigFont.fnt"); + }; + } + return m_fontStack.back(); +} + +void TextRenderer::pushScale(float scale) { + m_scaleStack.push_back(scale); +} + +void TextRenderer::popScale() { + if (m_scaleStack.size()) m_scaleStack.pop_back(); +} + +float TextRenderer::getCurrentScale() const { + return m_scaleStack.size() ? m_scaleStack.back() : 1.f; +} + +void TextRenderer::pushStyleFlags(int style) { + int oldStyle = TextStyleRegular; + if (m_styleStack.size()) oldStyle = m_styleStack.back(); + m_styleStack.push_back(oldStyle | style); +} + +void TextRenderer::popStyleFlags() { + if (m_styleStack.size()) m_styleStack.pop_back(); +} + +int TextRenderer::getCurrentStyle() const { + return m_styleStack.size() ? m_styleStack.back() : TextStyleRegular; +} + +void TextRenderer::pushColor(ccColor3B const& color) { + m_colorStack.push_back(color); +} + +void TextRenderer::popColor() { + if (m_colorStack.size()) m_colorStack.pop_back(); +} + +ccColor3B TextRenderer::getCurrentColor() const { + return m_colorStack.size() ? m_colorStack.back() : ccColor3B { 255, 255, 255 }; +} + +void TextRenderer::pushOpacity(GLubyte opacity) { + m_opacityStack.push_back(opacity); +} + +void TextRenderer::popOpacity() { + if (m_opacityStack.size()) m_opacityStack.pop_back(); +} + +GLubyte TextRenderer::getCurrentOpacity() const { + return m_opacityStack.size() ? m_opacityStack.back() : 255; +} + +void TextRenderer::pushDecoFlags(int deco) { + int oldDeco = TextDecorationNone; + if (m_decorationStack.size()) oldDeco = m_decorationStack.back(); + m_decorationStack.push_back(oldDeco | deco); +} + +void TextRenderer::popDecoFlags() { + if (m_decorationStack.size()) m_decorationStack.pop_back(); +} + +int TextRenderer::getCurrentDeco() const { + return m_decorationStack.size() ? m_decorationStack.back() : TextDecorationNone; +} + +void TextRenderer::pushCaps(TextCapitalization caps) { + m_capsStack.push_back(caps); +} + +void TextRenderer::popCaps() { + if (m_capsStack.size()) m_capsStack.pop_back(); +} + +TextCapitalization TextRenderer::getCurrentCaps() const { + return m_capsStack.size() ? m_capsStack.back() : TextCapitalization::Normal; +} + +void TextRenderer::pushIndent(float indent) { + m_indentationStack.push_back(indent); +} + +void TextRenderer::popIndent() { + if (m_indentationStack.size()) m_indentationStack.pop_back(); +} + +float TextRenderer::getCurrentIndent() const { + float res = .0f; + for (auto& indent : m_indentationStack) { + res += indent; + } + return res; +} + +void TextRenderer::pushWrapOffset(float wrapOffset) { + m_wrapOffsetStack.push_back(wrapOffset); +} + +void TextRenderer::popWrapOffset() { + if (m_wrapOffsetStack.size()) m_wrapOffsetStack.pop_back(); +} + +float TextRenderer::getCurrentWrapOffset() const { + float res = .0f; + for (auto& offset : m_wrapOffsetStack) { + res += offset; + } + return res; +} + +void TextRenderer::pushVerticalAlign(TextAlignment align) { + m_vAlignmentStack.push_back(align); +} + +void TextRenderer::popVerticalAlign() { + if (m_vAlignmentStack.size()) m_vAlignmentStack.pop_back(); +} + +TextAlignment TextRenderer::getCurrentVerticalAlign() const { + return m_vAlignmentStack.size() ? m_vAlignmentStack.back() : TextAlignment::Center; +} + +void TextRenderer::pushHorizontalAlign(TextAlignment align) { + m_hAlignmentStack.push_back(align); +} + +void TextRenderer::popHorizontalAlign() { + if (m_hAlignmentStack.size()) m_hAlignmentStack.pop_back(); +} + +TextAlignment TextRenderer::getCurrentHorizontalAlign() const { + return m_hAlignmentStack.size() ? m_hAlignmentStack.back() : TextAlignment::Begin; +} + + +TextRenderer::~TextRenderer() { + this->end(); +} + +TextRenderer* TextRenderer::create() { + auto ret = new TextRenderer(); + if (ret && ret->init()) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} +