diff --git a/.github/workflows/multi-platform.yml b/.github/workflows/multi-platform.yml new file mode 100644 index 0000000..1cbfbca --- /dev/null +++ b/.github/workflows/multi-platform.yml @@ -0,0 +1,55 @@ +name: Build Geode Mod + +on: + workflow_dispatch: + push: + branches: + - "**" + +jobs: + build: + strategy: + fail-fast: false + matrix: + config: + - name: Windows + os: windows-latest + + - name: macOS + os: macos-latest + + - name: Android32 + os: ubuntu-latest + target: Android32 + + - name: Android64 + os: ubuntu-latest + target: Android64 + + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Build the mod + uses: geode-sdk/build-geode-mod@main + with: + bindings: geode-sdk/bindings + bindings-ref: main + combine: true + target: ${{ matrix.config.target }} + + package: + name: Package builds + runs-on: ubuntu-latest + needs: ['build'] + + steps: + - uses: geode-sdk/build-geode-mod/combine@main + id: build + + - uses: actions/upload-artifact@v4 + with: + name: Build Output + path: ${{ steps.build.outputs.build-output }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..beb171c --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Macos be like +**/.DS_Store + +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Ignore build folders +**/build +# Ignore platform specific build folders +build-*/ + +# Workspace files are user-specific +*.sublime-workspace + +# ILY vscode +**/.vscode +.idea/ + +# clangd +.cache/ + +# Visual Studio +.vs/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a472cfa --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "json"] + path = json + url = https://github.com/nlohmann/json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..72c8187 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.21) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_OSX_ARCHITECTURES "x86_64") +set(CMAKE_CXX_VISIBILITY_PRESET hidden) + +project(LevelHistory VERSION 1.0.0) + +# Set up the mod binary +add_library(${PROJECT_NAME} SHARED + src/main.cpp src/GDHistoryProvider.cpp + src/ProviderPopup.cpp src/LevelProvider.cpp + src/LoadingCircleLayer.cpp src/ProviderPopupWait.cpp + # Add your cpp files here +) + +target_include_directories(${PROJECT_NAME} PRIVATE json/single_include) + +if (NOT DEFINED ENV{GEODE_SDK}) + message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") +else() + message(STATUS "Found Geode: $ENV{GEODE_SDK}") +endif() + +add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) + +# Set up dependencies, resources, link Geode +setup_geode_mod(${PROJECT_NAME}) diff --git a/README.md b/README.md new file mode 100644 index 0000000..26ada77 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# LevelHistory + +This is where she makes a mod. \ No newline at end of file diff --git a/about.md b/about.md new file mode 100644 index 0000000..c434987 --- /dev/null +++ b/about.md @@ -0,0 +1,12 @@ +# Level History + +Look through deleted and outdated levels using GDHistory or other sources! + +## As of `v1.0.0` GDHistory is supported only. + +# How To Use + +- Go to the Online menu +- Find folder icon in the up right corner +- Select level provider you want to use for this session +- GG diff --git a/json b/json new file mode 160000 index 0000000..8c391e0 --- /dev/null +++ b/json @@ -0,0 +1 @@ +Subproject commit 8c391e04fe4195d8be862c97f38cfe10e2a3472e diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..6b53e87 Binary files /dev/null and b/logo.png differ diff --git a/logo.xcf b/logo.xcf new file mode 100644 index 0000000..7d90bc9 Binary files /dev/null and b/logo.xcf differ diff --git a/mod.json b/mod.json new file mode 100644 index 0000000..e08eaf0 --- /dev/null +++ b/mod.json @@ -0,0 +1,18 @@ +{ + "geode": "2.0.0-beta.22", + "gd": "2.204", + "version": "v1.0.0", + "id": "dogotrigger.level_history", + "name": "Level History", + "developer": "dogotrigger", + "description": "Look through deleted and outdated levels using GDHistory or other sources!", + "dependencies": [ + {"id": "hjfod.gmd-api", "importance": "required", "version": "*"}, + {"id": "geode.node-ids", "importance": "required", "version": "*"} + ], + "repository": "https://github.com/SergeyMC9730/levelhistory", + "issues": { + "url": "https://github.com/SergeyMC9730/levelhistory/issues", + "info": "Any bugs with Level History should be reported here." + } +} \ No newline at end of file diff --git a/src/GDHistoryProvider.cpp b/src/GDHistoryProvider.cpp new file mode 100644 index 0000000..2dde36c --- /dev/null +++ b/src/GDHistoryProvider.cpp @@ -0,0 +1,390 @@ +#include +#include +#include +#include + +using namespace geode::prelude; + +#include "GDHistoryProvider.hpp" + +std::string GDHistoryProvider::getName() { + return "GDHistory"; +} +void GDHistoryProvider::downloadLevel(std::function onComplete) { + cleanupLevels(false); + + if (_params.count(LPFeatures::QueryID)) { + web::AsyncWebRequest() + .fetch(fmt::format("{}/api/v1/level/{}", _baseUrl, std::get(_params[LPFeatures::QueryID]))) + .text() + .then([this, onComplete](std::string const& catgirl) { + nlohmann::json data = nlohmann::json::parse(catgirl); + + if (!data.contains("records")) { + onComplete(this, nullptr); + + return; + } + + nlohmann::json records = data.at("records"); + if (!records.is_array()) { + onComplete(this, nullptr); + + return; + } + + int levels = records.size(); + + if (levels == 0) { + onComplete(this, nullptr); + + return; + } + + for (int i = 0; i < levels; i++) { + nlohmann::json leveljson = records[i]; + GJGameLevel *level = GJGameLevel::create(); + +#define PARSE_STRING(lvalue, rvalue) if (!rvalue.is_null()) lvalue = rvalue.get().c_str() +#define PARSE_INT(lvalue, rvalue) if (!rvalue.is_null()) lvalue = rvalue.get() +#define PARSE_BOOL(lvalue, rvalue) if (!rvalue.is_null()) lvalue = (int)(rvalue.get()) + + PARSE_STRING(level->m_levelName, data["cache_level_name"]); + PARSE_STRING(level->m_levelDesc, leveljson["level_description"]); + PARSE_STRING(level->m_uploadDate, leveljson["real_date"]); + PARSE_STRING(level->m_creatorName, data["cache_username"]); + PARSE_STRING(level->m_songIDs, data["song_ids"]); + PARSE_STRING(level->m_sfxIDs, data["sfx_ids"]); + PARSE_INT(level->m_audioTrack, leveljson["official_song"]); + PARSE_INT(level->m_gameVersion, leveljson["game_version"]); + PARSE_INT(level->m_ratings, leveljson["raiting"]); + PARSE_INT(level->m_ratingsSum, leveljson["raiting_sum"]); + PARSE_INT(level->m_downloads, leveljson["downloads"]); + PARSE_INT(level->m_likes, leveljson["likes"]); + PARSE_INT(level->m_levelLength, leveljson["length"]); + PARSE_INT(level->m_userID, data["cache_user_id"]); + PARSE_INT(level->m_coins, leveljson["coins"]); + PARSE_INT(level->m_coinsVerified, leveljson["coins_verified"]); + PARSE_INT(level->m_rateStars, leveljson["stars"]); + PARSE_INT(level->m_accountID, leveljson["account_id"]); + PARSE_INT(level->m_levelID, data["online_id"]); + PARSE_BOOL(level->m_demon, leveljson["demon"]); + PARSE_BOOL(level->m_autoLevel, leveljson["auto"]); + PARSE_BOOL(level->m_isEditable, leveljson["level_string_available"]); + PARSE_INT(level->m_demonDifficulty, leveljson["demon_type"]); + PARSE_INT(level->m_demonVotes, leveljson["id"]); + if (!leveljson["feature_score"].is_null() && leveljson["feature_score"].get() > 0) { + level->m_featured = 1; + PARSE_INT(level->m_rateFeature, leveljson["feature_score"]); + } + if (!leveljson["song"].is_null()) { + PARSE_INT(level->m_songID, leveljson["song"]["online_id"]); + } + PARSE_INT(level->m_isEpic, leveljson["epic"]); + + level->retain(); + + this->_serverResponseParsed.push_back(level); + } + + if (_params.count(LPFeatures::LimitLevelArray)) { + int max = std::get(_params[LPFeatures::LimitLevelArray]); + + if (max > 0) { + std::vector new_vec; + + for (int i = 0; i < max && i < _serverResponseParsed.size(); i++) { + new_vec.push_back(this->_serverResponseParsed.at(i)); + } + + this->_serverResponseParsed = new_vec; + } + } + + onComplete(this, this->_serverResponseParsed.at(0)); + }) + .expect([this, onComplete](std::string const& error) { + onComplete(this, nullptr); + }); + } + + if (_params.count(LPFeatures::QueryLevelName)) { + std::string a = std::get(_params[LPFeatures::QueryLevelName]); + int b = 0; + int c = 0; + + if (_params.count(LPFeatures::LimitLevelArray)) { + b = std::get(_params[LPFeatures::LimitLevelArray]); + } + if (_params.count(LPFeatures::SetLevelArrayPage)) { + c = std::get(_params[LPFeatures::SetLevelArrayPage]); + } + + if (b == 0) { + b = 100; + } + + web::AsyncWebRequest() + .fetch(fmt::format("{}/api/v1/search/level/advanced/?query={}&limit={}&offset={}", _baseUrl, a, b, b * c)) + .text() + .then([this, onComplete](std::string const& catgirl) { + nlohmann::json data = nlohmann::json::parse(catgirl); + + if (!data.contains("hits")) { + onComplete(this, nullptr); + + return; + } + + nlohmann::json records = data.at("hits"); + if (!records.is_array()) { + onComplete(this, nullptr); + + return; + } + + int levels = records.size(); + + if (levels == 0) { + onComplete(this, nullptr); + + return; + } + + for (int i = 0; i < levels; i++) { + nlohmann::json leveljson = records[i]; + GJGameLevel *level = GJGameLevel::create(); + + PARSE_STRING(level->m_levelName, leveljson["cache_level_name"]); + // PARSE_STRING(level->m_levelDesc, leveljson["level_description"]); + PARSE_STRING(level->m_uploadDate, leveljson["cache_submitted"]); + PARSE_STRING(level->m_creatorName, leveljson["cache_username"]); + // PARSE_INT(level->m_audioTrack, leveljson["official_song"]); + level->m_gameVersion = 22; + PARSE_INT(level->m_ratings, leveljson["cache_rating"]); + PARSE_INT(level->m_ratingsSum, leveljson["cache_rating_sum"]); + PARSE_BOOL(level->m_demon, leveljson["cache_demon"]); + PARSE_BOOL(level->m_autoLevel, leveljson["cache_auto"]); + PARSE_INT(level->m_demonDifficulty, leveljson["cache_demon_type"]); + PARSE_INT(level->m_downloads, leveljson["cache_downloads"]); + // PARSE_INT(level->m_difficulty, leveljson["cache_main_difficulty"]); + PARSE_INT(level->m_likes, leveljson["cache_likes"]); + PARSE_INT(level->m_levelLength, leveljson["cache_length"]); + PARSE_INT(level->m_userID, data["cache_user_id"]); + PARSE_INT(level->m_rateStars, leveljson["cache_stars"]); + PARSE_INT(level->m_levelID, leveljson["online_id"]); + PARSE_INT(level->m_demonVotes, leveljson["id"]); + if (!leveljson["cache_featured"].is_null() && leveljson["cache_featured"].get() > 0) { + level->m_featured = 1; + PARSE_INT(level->m_rateFeature, leveljson["cache_featured"]); + } + PARSE_INT(level->m_isEpic, leveljson["cache_epic"]); + + level->retain(); + + this->_serverResponseParsed.push_back(level); + } + + onComplete(this, this->_serverResponseParsed.at(0)); + }) + .expect([this, onComplete](std::string const& error) { + onComplete(this, nullptr); + }); + } + + _params.clear(); + + return; +} + +GDHistoryProvider::GDHistoryProvider() { + _baseUrl = "https://history.geometrydash.eu"; +} + +std::unordered_map GDHistoryProvider::getFeatures() { + std::unordered_map features; + + features[LPFeatures::QueryID] = true; + features[LPFeatures::QueryLevelName] = true; + features[LPFeatures::LimitLevelArray] = true; + features[LPFeatures::SetLevelArrayPage] = true; + features[LPFeatures::ReturnMultipleLevels] = true; + + return features; +} + +std::vector GDHistoryProvider::askMultipleLevels() { + return _serverResponseParsed; +} +void GDHistoryProvider::cleanupLevels(bool withRelease) { + if (!withRelease) { + _serverResponseParsed.clear(); + + return; + } + + for (auto level : _serverResponseParsed) { + if (level != nullptr) { + level->release(); + } + } + + _serverResponseParsed.clear(); +} + +#include +#include + +void GDHistoryProvider::getLevelData(int id, std::function onComplete) { + int recid = 0; + + log::info("(GDHistoryProvider) preparing to download {}", id); + + if (_params.count(LPFeatures::SpecificRecord)) { + recid = std::get(_params[LPFeatures::SpecificRecord]); + } + + if (recid == 0) { + log::info("(GDHistoryProvider) record for {} not found, getting it", id); + + setParameter(LPFeatures::QueryID, id); + + std::vector old_vec = this->_serverResponseParsed; + cleanupLevels(false); + + downloadLevel([this, id, onComplete, old_vec](LevelProvider *, GJGameLevel *level) { + struct LevelProvider::BasicLevelInformation info; + + this->_params.clear(); + + if (!level) { + this->cleanupLevels(true); + this->_serverResponseParsed = old_vec; + + onComplete(this, "-4", info); + + return; + } + + GJGameLevel *propLevel = nullptr; + auto levels = this->askMultipleLevels(); + + for (auto lvl : levels) { + if (lvl->m_isEditable) { + propLevel = lvl; + + break; + } + } + + if (!propLevel) { + this->cleanupLevels(true); + this->_serverResponseParsed = old_vec; + + onComplete(this, "-5", info); + + return; + } + + if (propLevel->m_demonVotes <= 0) { + log::info("votes: {}", propLevel->m_demonVotes); + + this->cleanupLevels(true); + this->_serverResponseParsed = old_vec; + + onComplete(this, "-3", info); + + return; + } + + this->setParameter(LPFeatures::SpecificRecord, propLevel->m_demonVotes); + + this->cleanupLevels(true); + this->_serverResponseParsed = old_vec; + + this->getLevelData(id, onComplete); + }); + + return; + } + + log::info("(GDHistoryProvider) downloading level {}", id); + + web::AsyncWebRequest() + .fetch(fmt::format("{}/level/{}/{}/download", _baseUrl, id, recid)) + .text() + .then([this, onComplete](std::string const& catgirl) { + struct LevelProvider::BasicLevelInformation info; + + if (catgirl.find("This record does not contain any level data.") != std::string::npos) { + onComplete(this, "-5", info); + + return; + } + + std::ofstream gmdfile; + + gmdfile.open ("temp.gmd"); + gmdfile << catgirl; + gmdfile.close(); + + auto file = gmd::ImportGmdFile::from("temp.gmd"); + + // geode::Result intoLevel() const; + + // std::filesystem::remove("temp.gmd"); + + file.inferType(); + + auto result = file.intoLevel(); + + if (result.isErr() || !result.isOk()) { + log::info("error: {}", result.unwrapErr()); + + onComplete(this, "-2", info); + + return; + } + + auto level = result.unwrap(); + level->retain(); + + auto str = level->m_levelString; + int mid = level->m_audioTrack; + int sid = level->m_songID; + + info.musicID = mid; + info.songID = sid; + info._22songs = level->m_songIDs; + info._22sfxs = level->m_sfxIDs; + + level->release(); + + onComplete(this, str, info); + }) + .expect([this, onComplete](std::string const& error) { + struct LevelProvider::BasicLevelInformation info; + + onComplete(this, "-1", info); + }); + + _params.clear(); +} + +std::string GDHistoryProvider::getErrorCodeDescription(std::string err) { + std::string res = "unknown error"; + + std::unordered_map errors = { + {"-1", "http error."}, + {"-2", "gmd api error. (dumped into console)"}, + {"-3", "invalid record id."}, + {"-4", "level not found."}, + {"-5", "level data cannot be downloaded for this level. Note that this issue will be fixed if level would have downloadable link for it in the future."} + }; + + if (errors.count(err)) { + res = errors[err]; + } + + return res; +} \ No newline at end of file diff --git a/src/GDHistoryProvider.hpp b/src/GDHistoryProvider.hpp new file mode 100644 index 0000000..37b2516 --- /dev/null +++ b/src/GDHistoryProvider.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "LevelProvider.hpp" +#include + +class GDHistoryProvider : public LevelProvider { +protected: + std::vector _serverResponseParsed; + + // GJGameLevel *parseFromJson(nlohmann::json j); +public: + GDHistoryProvider(); + + std::string getName() override; + void downloadLevel(std::function onComplete) override; + + std::unordered_map getFeatures() override; + + std::vector askMultipleLevels() override; + void cleanupLevels(bool withRelease) override; + + void getLevelData(int id, std::function onComplete) override; + + virtual std::string getErrorCodeDescription(std::string err) override; +}; \ No newline at end of file diff --git a/src/LevelProvider.cpp b/src/LevelProvider.cpp new file mode 100644 index 0000000..6de2a5f --- /dev/null +++ b/src/LevelProvider.cpp @@ -0,0 +1,12 @@ +#include "LevelProvider.hpp" + +void LevelProvider::setParameter(LevelProvider::LPFeatures parameter, int value) { + _params[parameter] = value; +} +void LevelProvider::setParameter(LevelProvider::LPFeatures parameter, std::string value) { + _params[parameter] = value; +} + +std::vector LevelProvider::askMultipleLevels() { + return {}; +} \ No newline at end of file diff --git a/src/LevelProvider.hpp b/src/LevelProvider.hpp new file mode 100644 index 0000000..72acfe4 --- /dev/null +++ b/src/LevelProvider.hpp @@ -0,0 +1,45 @@ +#pragma once + +class GJGameLevel; + +#include +#include +#include +#include +#include + +class LevelProvider { +public: + enum LPFeatures { + QueryID, QueryLevelName, + LimitLevelArray, SetLevelArrayPage, + ReturnMultipleLevels, SpecificRecord + }; +protected: + std::string _baseUrl; + + std::unordered_map> _params; +public: + struct BasicLevelInformation { + int musicID = 0; + int songID = 0; + std::string _22songs = ""; + std::string _22sfxs = ""; + }; + + virtual std::string getName() = 0; + virtual void downloadLevel(std::function onComplete) = 0; + + virtual std::unordered_map getFeatures() = 0; + + virtual void setParameter(enum LPFeatures parameter, int value); + virtual void setParameter(enum LPFeatures parameter, std::string value); + + virtual std::vector askMultipleLevels(); + + virtual void cleanupLevels(bool withRelease) = 0; + + virtual void getLevelData(int id, std::function onComplete) = 0; + + virtual std::string getErrorCodeDescription(std::string err) = 0; +}; \ No newline at end of file diff --git a/src/LoadingCircleLayer.cpp b/src/LoadingCircleLayer.cpp new file mode 100644 index 0000000..1c2d876 --- /dev/null +++ b/src/LoadingCircleLayer.cpp @@ -0,0 +1,20 @@ +#include "LoadingCircleLayer.hpp" + +bool LoadingCircleLayer::init() { + m_pCircle = cocos2d::CCSprite::create("loadingCircle.png"); + this->addChild(m_pCircle); + + m_pCircle->setBlendFunc({GL_SRC_ALPHA, GL_ONE}); + + scheduleUpdate(); + + return true; +} + +void LoadingCircleLayer::update(float delta) { + if(m_pCircle) { + float rot = m_pCircle->getRotation(); + rot += 3; + m_pCircle->setRotation(rot); + } +} \ No newline at end of file diff --git a/src/LoadingCircleLayer.hpp b/src/LoadingCircleLayer.hpp new file mode 100644 index 0000000..86766a8 --- /dev/null +++ b/src/LoadingCircleLayer.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +class LoadingCircleLayer : public cocos2d::CCLayer { +public: + cocos2d::CCSprite *m_pCircle; + + bool init(); + CREATE_FUNC(LoadingCircleLayer); + + void update(float delta); +}; \ No newline at end of file diff --git a/src/ProviderPopup.cpp b/src/ProviderPopup.cpp new file mode 100644 index 0000000..6f8fc67 --- /dev/null +++ b/src/ProviderPopup.cpp @@ -0,0 +1,1337 @@ +#include "ProviderPopup.hpp" + +#include +#include +#include "ProviderPopupWait.hpp" + +using namespace geode::prelude; + +#include "LevelProvider.hpp" + +void ProviderPopup::onToggler1PressMaybe(CCObject *sender) { + LevelProvider *provider = static_cast(((CCNode *)sender)->getUserData()); + + auto popup = ProviderPopup::get(); + + log::info("clicked provider: {}", provider->getName()); + + CCNode *providerBox = getChildByIDRecursive("provider-box"); + popup->setupProviderBox((CCLayer *)providerBox, provider); +} + +void ProviderPopup::onExitButton(CCObject *sender) { + auto popup = ProviderPopup::get(); + + popup->removeMeAndCleanup(); +} + +bool ProviderPopup::init(std::vector> providers) { + if (!FLAlertLayer::init(0)) return false; + + _providers = providers; + + CCLayer *objectSelector = CCLayer::create(); + CCLayer *scale9layer = CCLayer::create(); + + CCScale9Sprite *spr1 = CCScale9Sprite::create("GJ_square01.png"); + auto winsize = CCDirector::sharedDirector()->getWinSize(); + //spr1->setAnchorPoint({0, 1}); + spr1->setContentSize({380, 250}); + + scale9layer->addChild(spr1); + objectSelector->addChild(scale9layer, 0); + + scale9layer->setPosition({winsize.width / 2, winsize.height / 2}); + + auto bmf = CCLabelBMFont::create("Search From Other Sources", "bigFont.fnt"); + bmf->setScale(0.6f); + bmf->setPositionX(winsize.width / 2); + bmf->setPositionY(winsize.height / 2 + spr1->getContentSize().height / 2 - 30); + + objectSelector->addChild(bmf, 1); + + auto exitBtn = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); + auto btn3 = CCMenuItemSpriteExtra::create( + exitBtn, this, menu_selector(ProviderPopup::onExitButton) + ); + + CCMenu *menu = CCMenu::create(); + + CCLayer *infoLayer = CCLayer::create(); + auto vlayout = ColumnLayout::create(); + + infoLayer->setLayout(vlayout); + + auto layout = RowLayout::create(); + layout->setGrowCrossAxis(true); + + menu->setLayout(layout); + + for (auto provider : providers) { + auto spr4 = ButtonSprite::create(provider->getName().c_str()); + spr4->setScale(0.6f); + + auto btn4 = CCMenuItemSpriteExtra::create( + spr4, this, menu_selector(ProviderPopup::onToggler1PressMaybe) + ); + btn4->setUserData(provider.get()); + + menu->addChild(btn4); + menu->updateLayout(); + } + + // menu->ignoreAnchorPointForPosition(true); + + CCLayer *providerBox = CCLayer::create(); + providerBox->setID("provider-box"); + + CCScale9Sprite *s9s = CCScale9Sprite::create("GJ_square02.png"); + s9s->setContentSize({300, 150}); + s9s->setAnchorPoint({0, 0}); + s9s->setID("s9s"); + + providerBox->setContentSize(s9s->getContentSize()); + providerBox->addChild(s9s); + + setupProviderBox(providerBox, nullptr); + + infoLayer->addChild(providerBox); + infoLayer->addChild(menu); + + infoLayer->updateLayout(); + infoLayer->setID("info-layer"); + infoLayer->setPositionX(winsize.width / 2); + infoLayer->setPositionY(winsize.height / 2 - 15); + + objectSelector->addChild(infoLayer, 2); + + auto base = CCSprite::create("square.png"); + base->setPosition({ 0, 0 }); + base->setScale(500.f); + base->setColor({0, 0, 0}); + base->setOpacity(0); + base->runAction(CCFadeTo::create(0.3f, 125)); + + this->addChild(base, -1); + + objectSelector->setID("object-selector"); + + // selectorlayers.push_back(static_cast(this)); + + scheduleUpdate(); + + menu->setID("providers"); + + auto pos = objectSelector->getPosition(); + // menu->setPositionY(menu->getPositionY() + (objectSelector->getContentSize().height / 1.5f) - 30.f); + + CCMenu *men2 = CCMenu::create(); + + men2->setPosition({ + winsize.width / 2 - spr1->getContentSize().width / 2, + winsize.height / 2 + spr1->getContentSize().height / 2 + }); + men2->addChild(btn3); + + objectSelector->addChild(men2, 2); + + m_mainLayer->addChild(objectSelector); + + show(); + + setID("provider-popup"); + + // _levelPage = new LevelPageInfo(); + + return true; +} + +void ProviderPopup::update(float delta) { + // m_pSelectedObject->m_targetColorID = atoi(m_pInputPGID->getString()); + // if(m_pSelectedObject->m_targetColorID > MAX_PLAYER_GROUP_SIZE + 1 || m_pSelectedObject->m_targetColorID < 0) { + // m_pSelectedObject->m_targetColorID = MAX_PLAYER_GROUP_SIZE; + // } + // m_pSelectedObject->m_tintTrigger = m_pFollowPlayer->isToggled(); + + // in->setID("level-array-input"); in->setID("level-page-input"); + + // auto laiNd = getChildByIDRecursive("level-array-input"); + // auto lpiNd = getChildByIDRecursive("level-page-input"); + + // if (laiNd) { + // auto lai = dynamic_cast(laiNd); + // _levelArraySize = lai->getString(); + // log::info("las: {}", _levelArraySize); + // } + // if (lpiNd) { + // auto lpi = dynamic_cast(lpiNd); + // _levelPageStr = lpi->getString(); + // log::info("lps: {}", _levelPageStr); + // } +} + +ProviderPopup* ProviderPopup::create(std::vector> providers) { + ProviderPopup* pRet = new ProviderPopup(); + if (pRet && pRet->init(providers)) { + pRet->autorelease(); + return pRet; + } else { + delete pRet; + pRet = 0; + return 0; + } +} + +void ProviderPopup::registerWithTouchDispatcher() { + CCTouchDispatcher *dispatcher = cocos2d::CCDirector::sharedDirector()->getTouchDispatcher(); + + dispatcher->addTargetedDelegate(this, -502, true); +} + +void ProviderPopup::setupProviderBox(CCLayer *providerBox, LevelProvider *provider) { + _selectedProvider = provider; + + auto s9s = (CCScale9Sprite *)providerBox->getChildByID("s9s"); + auto csz = s9s->getContentSize(); + + auto popup = ProviderPopup::get(); + if (!popup) popup = this; + + providerBox->removeAllChildrenWithCleanup(true); + popup->_levelPage._cells.clear(); + + s9s = CCScale9Sprite::create("GJ_square02.png"); + s9s->setContentSize(csz); + s9s->setAnchorPoint({0, 0}); + s9s->setID("s9s"); + + providerBox->addChild(s9s); + + if (!provider) { + CCLabelBMFont *l = CCLabelBMFont::create("Nothing is currently selected.", "chatFont.fnt"); + l->setPosition(csz.width / 2, csz.height / 2); + + providerBox->addChild(l); + + return; + } + + ProviderPopupInfo *info = new ProviderPopupInfo(); + info->provider = provider; + + providerBox->setUserData(info); + + auto features = provider->getFeatures(); + + if (features[LevelProvider::LPFeatures::QueryID] && features[LevelProvider::LPFeatures::QueryLevelName]) { + info->_pagesMax = 2; + } + + bool has_settings = features[LevelProvider::LPFeatures::LimitLevelArray] || features[LevelProvider::LPFeatures::SetLevelArrayPage]; + + if (has_settings) { + info->_pagesMax++; + } + + if (info->_pagesMax >= 2) { + CCMenu *menu = CCMenu::create(); + menu->setID("page-control"); + + auto prev_page_spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + prev_page_spr->setColor({64, 64, 64}); + prev_page_spr->setScale(0.8f); + prev_page_spr->setID("prev-page-spr"); + + auto prev_page = CCMenuItemSpriteExtra::create( + prev_page_spr, + providerBox, + menu_selector(ProviderPopup::onPrevPage) + ); + prev_page->setID("prev-page"); + prev_page->setPosition(prev_page->getContentSize().width - 7, csz.height / 2); + prev_page->setEnabled(false); + + auto next_page_spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + next_page_spr->setFlipX(true); + next_page_spr->setScale(0.8f); + next_page_spr->setID("next-page-spr"); + + auto next_page = CCMenuItemSpriteExtra::create( + next_page_spr, + providerBox, + menu_selector(ProviderPopup::onNextPage) + ); + next_page->setID("next-page"); + next_page->setPosition(csz.width - next_page->getContentSize().width + 7, csz.height / 2); + + menu->addChild(prev_page); + menu->addChild(next_page); + + providerBox->addChild(menu, 100); + + menu->setPosition({0, 0}); + + CCLabelBMFont *l = CCLabelBMFont::create("Page 0", "chatFont.fnt"); + l->setAnchorPoint({0, 0}); + l->setScale(0.5f); + l->setPosition(9, 9); + l->setID("page-string"); + l->setAlignment(CCTextAlignment::kCCTextAlignmentLeft); + + providerBox->addChild(l); + } + + CCLayer *page = CCLayer::create(); + + page->setID("page"); + page->setContentSize(csz); + page->setPosition({0, 0}); + + providerBox->addChild(page); + + setupProviderBoxOnPage(providerBox); + + providerBox->setScale(0.f); + providerBox->runAction(CCEaseElasticOut::create(CCScaleTo::create(0.5f, 1.0f), 0.6f)); +} + +void ProviderPopup::setupProviderBoxOnPage(CCLayer *providerBox) { + ProviderPopupInfo *info = (ProviderPopupInfo *)providerBox->getUserData(); + + if (!info) return; + if ((info->_page > (info->_pagesMax - 1))) return; + + if (info->_pagesMax > 1) { + auto prev_page = dynamic_cast(providerBox->getChildByIDRecursive("prev-page")); + auto next_page = dynamic_cast(providerBox->getChildByIDRecursive("next-page")); + + auto prev_page_spr = dynamic_cast(providerBox->getChildByIDRecursive("prev-page-spr")); + auto next_page_spr = dynamic_cast(providerBox->getChildByIDRecursive("next-page-spr")); + + if (info->_page == 0) { + prev_page->setEnabled(false); + prev_page_spr->setColor({64, 64, 64}); + + next_page_spr->setColor({255, 255, 255}); + next_page->setEnabled(true); + } + if (info->_page >= 1) { + prev_page->setEnabled(true); + next_page->setEnabled(true); + + next_page_spr->setColor({255, 255, 255}); + prev_page_spr->setColor({255, 255, 255}); + } + if (info->_page == (info->_pagesMax - 1)) { + next_page_spr->setColor({64, 64, 64}); + next_page->setEnabled(false); + + prev_page->setEnabled(true); + prev_page_spr->setColor({255, 255, 255}); + } + + CCLabelBMFont *l = dynamic_cast(providerBox->getChildByID("page-string")); + if (l) { + l->setString(fmt::format("Page {}", info->_page + 1).c_str()); + } + } + + CCLayer *page = dynamic_cast(providerBox->getChildByID("page")); + + if (!page) return; + + auto popup = ProviderPopup::get(); + + page->removeAllChildrenWithCleanup(true); + popup->_levelPage._cells.clear(); + + switch(info->_page) { + case 0: { + auto feats = info->provider->getFeatures(); + + if (feats[LevelProvider::LPFeatures::QueryID]) { + popup->setupLevelIDPage(providerBox); + + return; + } + if (feats[LevelProvider::LPFeatures::QueryLevelName]) { + popup->setupGenericSearchPage(providerBox); + + return; + } + if (feats[LevelProvider::LPFeatures::LimitLevelArray] || feats[LevelProvider::LPFeatures::SetLevelArrayPage]) { + popup->setupSettingsPage(providerBox); + + return; + } + + break; + } + case 1: { + auto feats = info->provider->getFeatures(); + + if (feats[LevelProvider::LPFeatures::QueryLevelName]) { + popup->setupGenericSearchPage(providerBox); + + return; + } + + if (feats[LevelProvider::LPFeatures::LimitLevelArray] || feats[LevelProvider::LPFeatures::SetLevelArrayPage]) { + popup->setupSettingsPage(providerBox); + + return; + } + + break; + } + case 2: { + auto feats = info->provider->getFeatures(); + + if (feats[LevelProvider::LPFeatures::LimitLevelArray] || feats[LevelProvider::LPFeatures::SetLevelArrayPage]) { + popup->setupSettingsPage(providerBox); + + return; + } + + break; + } + } +} + +void ProviderPopup::onPrevPage(CCObject *sender) { + CCNode *orig = static_cast(sender); + CCLayer *box = dynamic_cast(orig->getParent()->getParent()); + auto popup = ProviderPopup::get(); + + if (!box) return; + + ProviderPopupInfo *info = static_cast(box->getUserData()); + + if (!info) return; + + info->_page--; + if (info->_page < 0) info->_page = 0; + + // for (auto _cell : _levelPage._cells) { + // _cell->removeMeAndCleanup(); + // } + popup-> _levelPage._cells.clear(); + info->provider->cleanupLevels(false); + + setupProviderBoxOnPage(box); +} +void ProviderPopup::onNextPage(CCObject *sender) { + CCNode *orig = static_cast(sender); + CCLayer *box = dynamic_cast(orig->getParent()->getParent()); + auto popup = ProviderPopup::get(); + + if (!box) return; + + ProviderPopupInfo *info = static_cast(box->getUserData()); + if (!info) return; + + info->_page++; + if (info->_page >= (info->_pagesMax - 1)) info->_page = info->_pagesMax - 1; + + // for (auto _cell : _levelPage._cells) { + // _cell->removeMeAndCleanup(); + // } + popup->_levelPage._cells.clear(); + info->provider->cleanupLevels(false); + + setupProviderBoxOnPage(box); +} + +class SearchInstance { +public: + TextInput *_input; + CCLayer *_page; + ProviderPopupInfo *_info;; +}; + +#include "LoadingCircleLayer.hpp" + +void ProviderPopup::setupLevelIDPage(CCLayer *providerBox) { + CCLayer *page = dynamic_cast(providerBox->getChildByID("page")); + ProviderPopupInfo *info = (ProviderPopupInfo *)providerBox->getUserData(); + + TextInput *in = TextInput::create(100, "Enter level ID...", "chatFont.fnt"); + in->setPosition(0, 0); + in->setAnchorPoint({0.5f, 0.5f}); + + int sz = 0; + auto prev_page = static_cast(providerBox->getChildByIDRecursive("prev-page")); + auto next_page = static_cast(providerBox->getChildByIDRecursive("next-page")); + + if (prev_page) { + sz += prev_page->getContentSize().width + 10; + } + if (next_page) { + sz += next_page->getContentSize().width + 10; + } + + CCLayer *levelInputLayer = CCLayer::create(); + auto layout = RowLayout::create(); + + layout->setGrowCrossAxis(true); + + levelInputLayer->setID("level-input-layer"); + levelInputLayer->setLayout(layout); + levelInputLayer->setContentSize(page->getContentSize()); + + SearchInstance *si = new SearchInstance(); + si->_input = in; + si->_page = page; + si->_info = info; + + auto spr4 = ButtonSprite::create("Search"); + spr4->setScale(0.7f); + + auto btn4 = CCMenuItemSpriteExtra::create( + spr4, this, menu_selector(ProviderPopup::onLevelIDSearch) + ); + btn4->setAnchorPoint({0, 0}); + btn4->setUserData(si); + + CCMenu *buttonMenu = CCMenu::create(); + buttonMenu->setContentSize(btn4->getContentSize()); + + buttonMenu->addChild(btn4); + + levelInputLayer->addChild(buttonMenu); + levelInputLayer->addChild(in); + + levelInputLayer->updateLayout(); + levelInputLayer->setPosition(page->getContentSize().width / 2, page->getContentSize().height - levelInputLayer->getContentSize().height + 5); + + page->addChild(levelInputLayer); + + TextInput *in2 = TextInput::create(page->getContentSize().width - sz, "", "chatFont.fnt"); + in2->setEnabled(false); + in2->setPosition(page->getContentSize().width / 2, page->getContentSize().height / 2); + in2->setAnchorPoint({0.5f, 0.5f}); + in2->setScaleY(2.f); + + page->addChild(in2); + + LoadingCircleLayer *circle = LoadingCircleLayer::create(); + auto csz = page->getContentSize(); + + circle->setContentSize(csz); + circle->setPosition(csz.width / 2, csz.height / 2); + circle->setID("circle"); + circle->m_pCircle->setScale(0.7f); + circle->m_pCircle->setOpacity(0); + + page->addChild(circle); + + auto popup = ProviderPopup::get(); + popup->_levelPage._cells.clear(); +} + +void ProviderPopup::onLevelIDSearch(CCObject *sender) { + auto popup = ProviderPopup::get(); + + if (popup->_locked) return; + + SearchInstance *si = (SearchInstance *)(((CCNode *)sender)->getUserData()); + + CCNode *existingCirclePtr = si->_page->getChildByID("circle"); + if (existingCirclePtr == nullptr) { + return; + } + + LoadingCircleLayer *existingCircle = dynamic_cast(existingCirclePtr); + + if (existingCircle->m_pCircle->getOpacity() > 0) { + return; + } + + if (si->_input->getString().empty()) { + FLAlertLayer::create("Error", "Level ID input should not be empty!", "OK")->show(); + return; + + } + + bool d = si->_input->getString().find_first_not_of("0123456789") == std::string::npos; + + if (!d) { + FLAlertLayer::create("Error", "Level ID input should contain numbers only!", "OK")->show(); + return; + } + + int levels_limit = 00; + + std::string levelArraySize = ProviderPopup::get()->_levelArraySize; + + d = levelArraySize .find_first_not_of("0123456789") == std::string::npos; + + log::info("levelArraySize ={}",levelArraySize ); + + if (!d && !levelArraySize .empty()) { + FLAlertLayer::create("Error", "Level array size should contain numbers only!", "OK")->show(); + return; + } + + if (!levelArraySize.empty()) { + levels_limit = std::stoi(levelArraySize ); + } + + int id = std::stoi(si->_input->getString()); + + si->_info->provider->setParameter(LevelProvider::QueryID, id); + si->_info->provider->setParameter(LevelProvider::LimitLevelArray, levels_limit); + + for (auto _cell : popup->_levelPage._cells) { + _cell->removeMeAndCleanup(); + } + popup->_levelPage._cells.clear(); + si->_info->provider->cleanupLevels(false); + + existingCircle->m_pCircle->setOpacity(0); + existingCircle->m_pCircle->runAction(cocos2d::CCFadeTo::create(0.35f, 255)); + existingCircle->m_pCircle->setScale(0.7f); + existingCircle->m_pCircle->setRotation(0); + + CCNode *levelControls = si->_page->getChildByID("level-page-control"); + if (levelControls != nullptr) { + levelControls->removeMeAndCleanup(); + } + + // delete this->_levelPage; + // this->_levelPage = new LevelPageInfo(); + + popup->_levelPage._cells.clear(); + popup->_levelPage._cId = id; + + si->_info->provider->downloadLevel([this, si, existingCircle, popup](LevelProvider *prov, GJGameLevel *level) { + popup->lambdaOnDownloadLevel(si, existingCircle, popup, prov, level); + }); +} + +LevelCell *ProviderPopup::createLevelCell(GJGameLevel *level, CCLayer *page) { + auto csz = page->getContentSize(); + log::info("1"); + + LevelCell *cell = LevelCell::create(0.f, 0.f); + log::info("2"); + cell->loadFromLevel(level); + log::info("3"); + cell->setPosition(0, csz.height / 2); + log::info("4"); + + CCLayer *base = dynamic_cast(cell->getChildByID("main-layer")); + base->setAnchorPoint({0, 0}); + base->setPositionX(csz.width / 3); + base->setScale(0.65f); + + std::vector to_lower; + + to_lower.push_back(dynamic_cast(cell->getChildByIDRecursive("length-label"))); + to_lower.push_back(dynamic_cast(cell->getChildByIDRecursive("downloads-label"))); + to_lower.push_back(dynamic_cast(cell->getChildByIDRecursive("likes-label"))); + to_lower.push_back(dynamic_cast(cell->getChildByIDRecursive("length-icon"))); + to_lower.push_back(dynamic_cast(cell->getChildByIDRecursive("downloads-icon"))); + to_lower.push_back(dynamic_cast(cell->getChildByIDRecursive("likes-icon"))); + + for (auto node : to_lower) { + node->setPositionY(node->getPositionY() - 4); + } + + CCLabelBMFont *song_name = dynamic_cast(base->getChildByID("song-name")); + CCMenuItemSpriteExtra *creator_name = dynamic_cast(base->getChildByIDRecursive("creator-name")); + song_name->setPositionY(song_name->getPositionY() - 15); + creator_name->setPositionY(creator_name->getPositionY() - 15); + + page->addChild(cell); + + CCLabelBMFont *dateLabel = CCLabelBMFont::create(level->m_uploadDate.c_str(), "chatFont.fnt"); + dateLabel->setScale(0.5f); + dateLabel->setPosition(csz.width / 2, -37); + + cell->addChild(dateLabel); + + return cell; +} + +void ProviderPopup::onLevelPage(CCObject *sender) { + auto button = dynamic_cast(sender); + + if (!button) return; + + CCScene *currentScene = CCScene::get(); + auto popup_ = currentScene->getChildByID("provider-popup"); + auto popup = dynamic_cast(popup_); + + popup->_levelPage._currentLevelsIndex += button->getTag(); + + log::info("tag={}", button->getTag()); + + if (popup->_levelPage._currentLevelsIndex > (popup->_levelPage._currentLevels.size() - 1)) { + popup->_levelPage._currentLevelsIndex = popup->_levelPage._currentLevels.size() - 1; + } + if (popup->_levelPage._currentLevelsIndex < 0) { + popup->_levelPage._currentLevelsIndex = 0; + } + + // CCNode *menu = _levelPage->page->getChildByID("level-page-control"); + // if (!menu) { + // FLAlertLayer::create("Error", "1!", "OK")->show(); + // return; + // } + + auto prev_page = dynamic_cast(getChildByIDRecursive("level-prev-page")); + auto next_page = dynamic_cast(getChildByIDRecursive("level-next-page")); + + auto prev_page_spr = dynamic_cast(getChildByIDRecursive("level-prev-page-spr")); + auto next_page_spr = dynamic_cast(getChildByIDRecursive("level-next-page-spr")); + + if (popup->_levelPage._currentLevelsIndex == 0) { + prev_page->setEnabled(false); + prev_page_spr->setColor({64, 64, 64}); + + next_page_spr->setColor({255, 255, 255}); + next_page->setEnabled(true); + } + if (popup->_levelPage._currentLevelsIndex >= 1) { + prev_page->setEnabled(true); + next_page->setEnabled(true); + + next_page_spr->setColor({255, 255, 255}); + prev_page_spr->setColor({255, 255, 255}); + } + if (popup->_levelPage._currentLevelsIndex == (popup->_levelPage._currentLevels.size() - 1)) { + next_page_spr->setColor({64, 64, 64}); + next_page->setEnabled(false); + + prev_page->setEnabled(true); + prev_page_spr->setColor({255, 255, 255}); + } + + auto level = popup->_levelPage._currentLevels.at(popup->_levelPage._currentLevelsIndex); + + for (auto cell : popup->_levelPage._cells) { + cell->setVisible(false); + } + + // int id = level->m_levelID.value(); + // if (id == 0) { + // id = popup->_levelPage._cId; + // } + + // std::string cellname = fmt::format("LevelCell_{}_{}", id, popup->_levelPage._currentLevelsIndex); + // CCNode *_cell = popup->_levelPage.page->getChildByID(cellname); + + popup->_levelPage._cells[popup->_levelPage._currentLevelsIndex]->setVisible(true); +} + +void ProviderPopup::lambdaOnDownloadLevel(SearchInstance *si, LoadingCircleLayer *existingCircle, ProviderPopup *popup, LevelProvider *prov, GJGameLevel *level) { + if (!popup->isPageHasFeature(si->_info, LevelProvider::QueryID)) { + delete si; + + return; + } + + CCScene *currentScene = CCScene::get(); + auto popup_ = currentScene->getChildByID("provider-popup"); + + if (!popup_) { + delete si; + + return; + } + + CCNode *existingCirclePtr = si->_page->getChildByID("circle"); + if (existingCirclePtr == nullptr) { + delete si; + + return; + } + + existingCircle->m_pCircle->setOpacity(0); + + if (level == nullptr) { + FLAlertLayer::create("Error", "No such level!", "OK")->show(); + return; + } + + auto more = prov->askMultipleLevels(); + + popup->_levelPage._cells.clear(); + + if (more.size() == 1) { + popup->_levelPage._cells.push_back(this->createLevelCell(level, si->_page)); + popup-> _levelPage._cells[0]->setVisible(true); + + return; + } + + int i = 0; + for (auto lvl : more) { + auto cell = popup->createLevelCell(lvl, si->_page); + cell->setID(fmt::format("LevelCell_{}_{}", lvl->m_levelID.value(), i)); + cell->setVisible(false); + + popup->_levelPage._cells.push_back(cell); + + i++; + } + + popup->_levelPage._cells[0]->setVisible(true); + + popup->_levelPage._currentLevels = more; + popup->_levelPage._currentLevelsIndex = 0; + popup->_levelPage.page = si->_page; + + auto csz = si->_page->getContentSize(); + + CCMenu *menu = CCMenu::create(); + menu->setID("level-page-control"); + + int delta = 40; + + auto prev_page_spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + prev_page_spr->setColor({64, 64, 64}); + prev_page_spr->setScale(0.5f); + prev_page_spr->setID("level-prev-page-spr"); + + auto prev_page = CCMenuItemSpriteExtra::create( + prev_page_spr, + this, + menu_selector(ProviderPopup::onLevelPage) + ); + prev_page->setID("level-prev-page"); + prev_page->setPosition(delta, 0); + prev_page->setEnabled(false); + prev_page->setTag(-1); + + auto next_page_spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + next_page_spr->setFlipX(true); + next_page_spr->setScale(0.5f); + next_page_spr->setID("level-next-page-spr"); + + auto next_page = CCMenuItemSpriteExtra::create( + next_page_spr, + this, + menu_selector(ProviderPopup::onLevelPage) + ); + next_page->setID("level-next-page"); + next_page->setPosition(csz.width - delta, 0); + next_page->setTag(1); + + menu->addChild(prev_page); + menu->addChild(next_page); + menu->setPosition(0, next_page->getContentSize().height / 2 + 10); + + si->_page->addChild(menu); + + applyBottomButtons(si->_page); +} + +void ProviderPopup::setupSettingsPage(CCLayer *providerBox) { + CCLayer *page = dynamic_cast(providerBox->getChildByID("page")); + ProviderPopupInfo *info = (ProviderPopupInfo *)providerBox->getUserData(); + + auto *layout = RowLayout::create(); + CCLayer *settings = CCLayer::create(); + auto csz = page->getContentSize(); + + settings->setLayout(layout); + settings->setContentSize(csz); + settings->setPosition(0, 0); + + page->addChild(settings); + + auto feats = info->provider->getFeatures(); + + CCSprite *checkOn = CCSprite::createWithSpriteFrameName("GJ_checkOn_001.png"); + CCSprite *checkOff = CCSprite::createWithSpriteFrameName("GJ_checkOff_001.png"); + + // feats[LevelProvider::LPFeatures::LimitLevelArray] || feats[LevelProvider::LPFeatures::SetLevelArrayPage] + if (feats[LevelProvider::LPFeatures::LimitLevelArray]) { + // m_pFollowPlayer = CCMenuItemToggler::create(checkOff, checkOn, this, menu_selector(ProviderPopup::onToggler1PressMaybe)); + // CCLabelBMFont *men1_info = CCLabelBMFont::create("Follow Player", "bigFont.fnt"); + // men1_info->setAnchorPoint({0.5f, 0.5f}); + // men1_info->setScale(0.5f); + // men1_info->setPositionX(100.f); + // std::string _final = "Set level array size..."; + // if (!this->_levelArraySize.empty()) { + // _final = this->_levelArraySize; + // } + + TextInput *in = TextInput::create(100, "Set level array size...", "chatFont.fnt"); + in->setID("level-array-input"); + + if (!ProviderPopup::get()->_levelArraySize.empty()) { + in->setString(ProviderPopup::get()->_levelArraySize, false); + } + + in->setCallback([this](const std::string &value) { + ProviderPopup::get()->_levelArraySize = value; + }); + + settings->addChild(in); + settings->updateLayout(); + } + + if (feats[LevelProvider::LPFeatures::SetLevelArrayPage]) { + TextInput *in = TextInput::create(100, "Set level page...", "chatFont.fnt"); + in->setID("level-page-input"); + + if (!ProviderPopup::get()->_levelPageStr.empty()) { + in->setString(ProviderPopup::get()->_levelPageStr, false); + } + in->setCallback([this](const std::string &value) { + ProviderPopup::get()->_levelPageStr = value; + }); + + settings->addChild(in); + settings->updateLayout(); + } + + settings->setPosition(csz.width / 2, (csz.height - settings->getContentSize().height) / 2 + 15); +} + +ProviderPopup *ProviderPopup::get() { + CCScene *currentScene = CCScene::get(); + auto popup_ = currentScene->getChildByID("provider-popup"); + if (!popup_) return nullptr; + auto popup = dynamic_cast(popup_); + return popup; +} + +void ProviderPopup::setupGenericSearchPage(CCLayer *providerBox) { + CCLayer *page = dynamic_cast(providerBox->getChildByID("page")); + ProviderPopupInfo *info = (ProviderPopupInfo *)providerBox->getUserData(); + + TextInput *in = TextInput::create(150, "Enter level ID/level name/etc...", "chatFont.fnt"); + in->setPosition(0, 0); + in->setAnchorPoint({0.5f, 0.5f}); + + int sz = 0; + auto prev_page = static_cast(providerBox->getChildByIDRecursive("prev-page")); + auto next_page = static_cast(providerBox->getChildByIDRecursive("next-page")); + + if (prev_page) { + sz += prev_page->getContentSize().width + 10; + } + if (next_page) { + sz += next_page->getContentSize().width + 10; + } + + CCLayer *levelInputLayer = CCLayer::create(); + auto layout = RowLayout::create(); + + layout->setGrowCrossAxis(true); + + levelInputLayer->setID("level-input-layer"); + levelInputLayer->setLayout(layout); + levelInputLayer->setContentSize(page->getContentSize()); + + SearchInstance *si = new SearchInstance(); + si->_input = in; + si->_page = page; + si->_info = info; + + auto spr4 = ButtonSprite::create("Search"); + spr4->setScale(0.7f); + + auto btn4 = CCMenuItemSpriteExtra::create( + spr4, this, menu_selector(ProviderPopup::onGenericSearch) + ); + btn4->setAnchorPoint({0, 0}); + btn4->setUserData(si); + + CCMenu *buttonMenu = CCMenu::create(); + buttonMenu->setContentSize(btn4->getContentSize()); + + buttonMenu->addChild(btn4); + + levelInputLayer->addChild(buttonMenu); + levelInputLayer->addChild(in); + + levelInputLayer->updateLayout(); + levelInputLayer->setPosition(page->getContentSize().width / 2, page->getContentSize().height - levelInputLayer->getContentSize().height + 5); + + page->addChild(levelInputLayer); + + TextInput *in2 = TextInput::create(page->getContentSize().width - sz, "", "chatFont.fnt"); + in2->setEnabled(false); + in2->setPosition(page->getContentSize().width / 2, page->getContentSize().height / 2); + in2->setAnchorPoint({0.5f, 0.5f}); + in2->setScaleY(2.f); + + page->addChild(in2); + + LoadingCircleLayer *circle = LoadingCircleLayer::create(); + auto csz = page->getContentSize(); + + circle->setContentSize(csz); + circle->setPosition(csz.width / 2, csz.height / 2); + circle->setID("circle"); + circle->m_pCircle->setScale(0.7f); + circle->m_pCircle->setOpacity(0); + + page->addChild(circle); + + auto popup = ProviderPopup::get(); + popup->_levelPage._cells.clear(); +} + +bool ProviderPopup::isPageHasFeature(ProviderPopupInfo *info, int feature) { + auto feats = info->provider->getFeatures(); + + bool settings = feats[LevelProvider::LPFeatures::LimitLevelArray] || feats[LevelProvider::LPFeatures::SetLevelArrayPage]; + + switch(info->_page) { + case 0: { + if (feats[LevelProvider::LPFeatures::QueryID] && feature == LevelProvider::LPFeatures::QueryID) { + return true; + } + if (feats[LevelProvider::LPFeatures::QueryLevelName] && feature == LevelProvider::LPFeatures::QueryLevelName) { + return true; + } + if (settings && + ((feature == LevelProvider::LPFeatures::LimitLevelArray) || + (feature == LevelProvider::LPFeatures::SetLevelArrayPage) + ) + ) { + return true; + } + + break; + } + case 1: { + if (feats[LevelProvider::LPFeatures::QueryLevelName] && feature == LevelProvider::LPFeatures::QueryLevelName) { + return true; + } + + if (settings && + ((feature == LevelProvider::LPFeatures::LimitLevelArray) || + (feature == LevelProvider::LPFeatures::SetLevelArrayPage) + ) + ) { + return true; + } + + break; + } + case 2: { + if (settings && + ((feature == LevelProvider::LPFeatures::LimitLevelArray) || + (feature == LevelProvider::LPFeatures::SetLevelArrayPage) + ) + ) { + return true; + } + + break; + } + } + + return false; +} + +void ProviderPopup::lambdaOnDownloadLevelList(SearchInstance *si, LoadingCircleLayer *existingCircle, ProviderPopup *popup, LevelProvider *prov, GJGameLevel *level) { + if (!popup->isPageHasFeature(si->_info, LevelProvider::QueryLevelName)) { + delete si; + + return; + } + + CCScene *currentScene = CCScene::get(); + auto popup_ = currentScene->getChildByID("provider-popup"); + + if (!popup_) { + delete si; + + return; + } + + CCNode *existingCirclePtr = si->_page->getChildByID("circle"); + if (existingCirclePtr == nullptr) { + delete si; + + return; + } + + existingCircle->m_pCircle->setOpacity(0); + + if (level == nullptr) { + FLAlertLayer::create("Error", "No levels were found!", "OK")->show(); + return; + } + + auto more = prov->askMultipleLevels(); + + popup->_levelPage._cells.clear(); + + if (more.size() == 1) { + popup->_levelPage._cells.push_back(this->createLevelCell(level, si->_page)); + popup-> _levelPage._cells[0]->setVisible(true); + + return; + } + + int i = 0; + for (auto lvl : more) { + auto cell = popup->createLevelCell(lvl, si->_page); + cell->setID(fmt::format("LevelCell_{}_{}", lvl->m_levelID.value(), i)); + cell->setVisible(false); + + popup->_levelPage._cells.push_back(cell); + + i++; + } + + popup->_levelPage._cells[0]->setVisible(true); + + popup->_levelPage._currentLevels = more; + popup->_levelPage._currentLevelsIndex = 0; + popup->_levelPage.page = si->_page; + + auto csz = si->_page->getContentSize(); + + CCMenu *menu = CCMenu::create(); + menu->setID("level-page-control"); + + int delta = 40; + + auto prev_page_spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + prev_page_spr->setColor({64, 64, 64}); + prev_page_spr->setScale(0.5f); + prev_page_spr->setID("level-prev-page-spr"); + + auto prev_page = CCMenuItemSpriteExtra::create( + prev_page_spr, + this, + menu_selector(ProviderPopup::onLevelPage) + ); + prev_page->setID("level-prev-page"); + prev_page->setPosition(delta, 0); + prev_page->setEnabled(false); + prev_page->setTag(-1); + + auto next_page_spr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + next_page_spr->setFlipX(true); + next_page_spr->setScale(0.5f); + next_page_spr->setID("level-next-page-spr"); + + auto next_page = CCMenuItemSpriteExtra::create( + next_page_spr, + this, + menu_selector(ProviderPopup::onLevelPage) + ); + next_page->setID("level-next-page"); + next_page->setPosition(csz.width - delta, 0); + next_page->setTag(1); + + menu->addChild(prev_page); + menu->addChild(next_page); + menu->setPosition(0, next_page->getContentSize().height / 2 + 10); + + si->_page->addChild(menu); + + applyBottomButtons(si->_page); +} + +void ProviderPopup::onGenericSearch(CCObject *sender) { + auto popup = ProviderPopup::get(); + + if (popup->_locked) return; + + SearchInstance *si = (SearchInstance *)(((CCNode *)sender)->getUserData()); + + CCNode *existingCirclePtr = si->_page->getChildByID("circle"); + if (existingCirclePtr == nullptr) { + return; + } + + LoadingCircleLayer *existingCircle = dynamic_cast(existingCirclePtr); + + if (existingCircle->m_pCircle->getOpacity() > 0) { + return; + } + + if (si->_input->getString().empty()) { + FLAlertLayer::create("Error", "Query should not be empty!", "OK")->show(); + return; + + } + + int levels_limit = 00; + int lp = 0; + + std::string levelArraySize = ProviderPopup::get()->_levelArraySize; + std::string levelPageStr = ProviderPopup::get()->_levelPageStr; + + bool d = levelArraySize .find_first_not_of("0123456789") == std::string::npos; + + log::info("levelArraySize ={}",levelArraySize ); + + if (!d && !levelArraySize .empty()) { + FLAlertLayer::create("Error", "Level array size should contain numbers only!", "OK")->show(); + return; + } + + d = levelPageStr .find_first_not_of("0123456789") == std::string::npos; + + if (!d && !levelPageStr .empty()) { + FLAlertLayer::create("Error", "Page number should contain numbers only!", "OK")->show(); + return; + } + + if (!levelArraySize.empty()) { + levels_limit = std::stoi(levelArraySize ); + } + if (!levelPageStr.empty()) { + lp = std::stoi(levelPageStr); + } + + si->_info->provider->setParameter(LevelProvider::QueryLevelName, si->_input->getString()); + si->_info->provider->setParameter(LevelProvider::LimitLevelArray, levels_limit); + si->_info->provider->setParameter(LevelProvider::SetLevelArrayPage, lp); + + for (auto _cell : popup->_levelPage._cells) { + _cell->removeMeAndCleanup(); + } + popup->_levelPage._cells.clear(); + si->_info->provider->cleanupLevels(false); + + existingCircle->m_pCircle->setOpacity(0); + existingCircle->m_pCircle->runAction(cocos2d::CCFadeTo::create(0.35f, 255)); + existingCircle->m_pCircle->setScale(0.7f); + existingCircle->m_pCircle->setRotation(0); + + CCNode *levelControls = si->_page->getChildByID("level-page-control"); + if (levelControls != nullptr) { + levelControls->removeMeAndCleanup(); + } + + // delete this->_levelPage; + // this->_levelPage = new LevelPageInfo(); + + popup->_levelPage._cells.clear(); + popup->_levelPage._cId = 0; + + si->_info->provider->downloadLevel([this, si, existingCircle, popup](LevelProvider *prov, GJGameLevel *level) { + popup->lambdaOnDownloadLevelList(si, existingCircle, popup, prov, level); + }); +} + +void ProviderPopup::onCopyID(CCObject *sender) { + auto popup = ProviderPopup::get(); + + auto level = popup->_levelPage._currentLevels[popup->_levelPage._currentLevelsIndex]; + + if (level->m_levelID.value() <= 0) { + auto notification = Notification::create("Level has an invalid ID.", NotificationIcon::Error, 0.5f); + notification->show(); + + return; + } + + clipboard::write(fmt::format("{}", level->m_levelID.value())); + + auto notification = Notification::create("Level ID has been copied.", NotificationIcon::Success, 0.5f); + notification->show(); +} + +#include + +void ProviderPopup::onPlayLevelDownload(CCObject *sender) { + auto popup = ProviderPopup::get(); + + if (popup->_locked) return; + + popup->_locked = true; + + CCScene *sc = CCScene::get(); + + ProviderPopupWait *wait = ProviderPopupWait::create(); + + popup->addChild(wait); + + auto level = popup->_levelPage._currentLevels[popup->_levelPage._currentLevelsIndex]; + + popup->_selectedProvider->setParameter(LevelProvider::SpecificRecord, level->m_demonVotes); + + popup->_selectedProvider->getLevelData(level->m_levelID.value(), [popup, level, wait] (LevelProvider *prov, std::string data, struct LevelProvider::BasicLevelInformation info) { + auto newpopup = ProviderPopup::get(); + + if (!newpopup || newpopup != popup) return; + + wait->cleanup(); + wait->removeMeAndCleanup(); + + newpopup->_locked = false; + + if (data.empty()) { + FLAlertLayer::create("Error", "Level data is empty!", "OK")->show(); + return; + } + + if (data[0] == '-') { + FLAlertLayer::create("Error", fmt::format("Error while getting level data: {}", prov->getErrorCodeDescription(data)), "OK")->show(); + return; + } + + level->m_levelString = data; + level->m_songID = info.songID; + level->m_audioTrack = info.musicID; + level->m_songIDs = info._22songs; + level->m_sfxIDs = info._22sfxs; + + // log::info("level string: {}", data); + + auto scene = PlayLayer::scene(level, false, false); + + auto transition = CCTransitionFade::create(0.5f, scene); + + CCDirector::sharedDirector()->replaceScene(transition); + }); +} + +void ProviderPopup::applyBottomButtons(CCLayer *page) { + CCMenu *levelActionsMenu = CCMenu::create(); + RowLayout *rLayout = RowLayout::create(); + levelActionsMenu->setLayout(rLayout); + + { + auto spr4 = ButtonSprite::create("Copy ID"); + spr4->setScale(0.6f); + + auto btn4 = CCMenuItemSpriteExtra::create( + spr4, this, menu_selector(ProviderPopup::onCopyID) + ); + + levelActionsMenu->addChild(btn4); + levelActionsMenu->updateLayout(); + } + + { + auto spr4 = ButtonSprite::create("Play Level"); + spr4->setScale(0.6f); + + auto btn4 = CCMenuItemSpriteExtra::create( + spr4, this, menu_selector(ProviderPopup::onPlayLevelDownload) + ); + + levelActionsMenu->addChild(btn4); + levelActionsMenu->updateLayout(); + } + + levelActionsMenu->setPosition(0, 0); + + auto csz1 = page->getContentSize(); + auto csz2 = levelActionsMenu->getContentSize(); + + csz2.width = csz1.width; + + levelActionsMenu->setContentSize(csz2); + levelActionsMenu->updateLayout(); + + levelActionsMenu->setPosition(csz1.width / 2, csz2.height); + + page->addChild(levelActionsMenu); +} \ No newline at end of file diff --git a/src/ProviderPopup.hpp b/src/ProviderPopup.hpp new file mode 100644 index 0000000..29725df --- /dev/null +++ b/src/ProviderPopup.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +class LevelProvider; +class SearchInstance; +class LoadingCircleLayer; + +class ProviderPopupInfo { +public: + int _page = 0; + int _pagesMax = 1; + LevelProvider *provider = nullptr; +}; + +class LevelPageInfo { +public: + std::vector _currentLevels = {}; + std::vector _cells = {}; + int _currentLevelsIndex = 0; + cocos2d::CCLayer *page = nullptr; + int _cId = 0; +}; + +class ProviderPopup : public FLAlertLayer { +public: + CCTextInputNode *m_pInputPGID; + CCMenuItemToggler *m_pFollowPlayer; + + LevelProvider *_selectedProvider; + + std::vector> _providers; + + void setupProviderBox(CCLayer *providerBox, LevelProvider *provider); + void setupProviderBoxOnPage(CCLayer *providerBox); + + void setupLevelIDPage(CCLayer *providerBox); + void setupSettingsPage(CCLayer *providerBox); + void setupGenericSearchPage(CCLayer *providerBox); + + bool _locked = false; +public: + static ProviderPopup *get(); + + LevelPageInfo _levelPage; + + std::string _levelArraySize = ""; + std::string _levelPageStr = ""; + + LevelCell *createLevelCell(GJGameLevel *level, CCLayer *page); + + void lambdaOnDownloadLevel(SearchInstance *si, LoadingCircleLayer *existingCircle, ProviderPopup *popup, LevelProvider *prov, GJGameLevel *level); + void lambdaOnDownloadLevelList(SearchInstance *si, LoadingCircleLayer *existingCircle, ProviderPopup *popup, LevelProvider *prov, GJGameLevel *level); + + void onToggler1PressMaybe(cocos2d::CCObject *sender); + void onExitButton(CCObject *sender); + + void update(float delta) override; + + static ProviderPopup* create(std::vector> providers); + bool init(std::vector> providers); + + void registerWithTouchDispatcher() override; + + void onPrevPage(CCObject *sender); + void onNextPage(CCObject *sender); + + void onLevelPage(CCObject *sender); + + void onLevelIDSearch(CCObject *sender); + void onGenericSearch(CCObject *sender); + + bool isPageHasFeature(ProviderPopupInfo *info, int feature); + + void onCopyID(CCObject *sender); + + void onPlayLevelDownload(CCObject *sender); + + void applyBottomButtons(CCLayer *page); + // bool ccTouchBegan(cocos2d::CCTouch *touch, cocos2d::CCEvent *event) override; +}; \ No newline at end of file diff --git a/src/ProviderPopupWait.cpp b/src/ProviderPopupWait.cpp new file mode 100644 index 0000000..4d9468e --- /dev/null +++ b/src/ProviderPopupWait.cpp @@ -0,0 +1,47 @@ +#include "ProviderPopupWait.hpp" +#include "LoadingCircleLayer.hpp" + +bool ProviderPopupWait::init() { + if (!CCLayer::init()) return false; + + CCLayer *l = cocos2d::CCLayer::create(); + l->setID("main-layer"); + + auto winSize = cocos2d::CCDirector::sharedDirector()->getWinSize(); + auto base = cocos2d::CCSprite::create("square.png"); + + base->setPosition({ 0, 0 }); + base->setScale(500.f); + base->setColor({0, 0, 0}); + base->setOpacity(0); + base->runAction(cocos2d::CCFadeTo::create(0.5f, 125)); + + auto loadingCircle = LoadingCircleLayer::create(); + loadingCircle->m_pCircle->setOpacity(0); + loadingCircle->m_pCircle->runAction(cocos2d::CCFadeTo::create(0.5f, 255)); + loadingCircle->setPosition(winSize.width / 2, winSize.height / 2 ); + addChild(base, -1); + + l->addChild(loadingCircle, 1); + + _circle = loadingCircle; + + addChild(l, 1024); + + // setTouchPriority(-1024); + // registerWithTouchDispatcher(); + + // cocos2d::CCTouchDispatcher *dispatcher = cocos2d::CCDirector::sharedDirector()->getTouchDispatcher(); + // dispatcher->registerForcePrio(this, -1024); + // dispatcher->addPrioTargetedDelegate(this, -1024, true); + + return true; +}; + +void ProviderPopupWait::cleanup() { + // cocos2d::CCTouchDispatcher *dispatcher = cocos2d::CCDirector::sharedDirector()->getTouchDispatcher(); + // setTouchPriority(1); + // dispatcher->registerForcePrio(this, 1); + // dispatcher->unregisterForcePrio(this); + // dispatcher->addPrioTargetedDelegate(this, 1, true); +} \ No newline at end of file diff --git a/src/ProviderPopupWait.hpp b/src/ProviderPopupWait.hpp new file mode 100644 index 0000000..2d1b9b7 --- /dev/null +++ b/src/ProviderPopupWait.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +class LoadingCircleLayer; + +class ProviderPopupWait : public cocos2d::CCLayer { +private: + LoadingCircleLayer *_circle; +public: + CREATE_FUNC(ProviderPopupWait); + + bool init(); + void cleanup(); +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4f2ccbf --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,60 @@ +#include + +using namespace geode::prelude; + +#include +#include + +#include "GDHistoryProvider.hpp" + +namespace LHSettings { + std::vector> providers; +}; + +#include "ProviderPopup.hpp" + +#include +class $modify(LHLevelSearchLayer, LevelSearchLayer) { + bool init(int p0) { + if (!LevelSearchLayer::init(p0)) { + return false; + } + + // auto winSize = CCDirectory::sharedInstance() + + auto extrasearch = CCMenuItemSpriteExtra::create( + CCSprite::createWithSpriteFrameName("gj_folderBtn_001.png"), + this, + menu_selector(LHLevelSearchLayer::onMyButton) + ); + + auto menu = (CCMenu *)this->getChildByID("other-filter-menu"); + + extrasearch->setID("provider-button"_spr); + + auto children = menu->getChildren(); + + auto last_child = (CCNode *)children->lastObject(); + auto pos = last_child->getPosition(); + auto csz = last_child->getContentSize(); + + pos.y -= csz.width + 10.f; + + extrasearch->setPosition(pos); + + menu->addChild(extrasearch); + + return true; + } + + void onMyButton(CCObject*) { + // FLAlertLayer::create("Geode", "Hello from my custom mod!", "OK")->show(); + ProviderPopup *popup = ProviderPopup::create(LHSettings::providers); + + // addChild(popup, 500); + } +}; + +$execute { + LHSettings::providers.push_back(std::make_shared()); +} \ No newline at end of file diff --git a/src/test.json b/src/test.json new file mode 100644 index 0000000..c08f983 --- /dev/null +++ b/src/test.json @@ -0,0 +1,47 @@ +{ + "hits": [ + { + "online_id": 10565740, + "comment": null, + "is_deleted": false, + "cache_level_name": "Bloodbath", + "cache_submitted": "2024-04-22 02:00:51.505566+00:00", + "cache_submitted_timestamp": 1713751251, + "cache_downloads": 80138936, + "cache_likes": 3889023, + "cache_rating_sum": 50, + "cache_rating": 10, + "cache_demon": true, + "cache_auto": false, + "cache_demon_type": 6, + "cache_stars": 10, + "cache_username": "Riot", + "cache_level_string_available": true, + "cache_user_id": 503085, + "cache_daily_id": 100079, + "cache_needs_updating": false, + "cache_available_versions": 2, + "cache_search_available": true, + "cache_main_difficulty": 5, + "cache_min_stars": 10, + "cache_max_stars": 10, + "cache_rating_changed": false, + "cache_filter_difficulty": 12, + "cache_length": 3, + "cache_featured": 10330, + "cache_max_featured": 10330, + "cache_epic": 0, + "cache_max_epic": 0, + "cache_two_player": false, + "cache_max_two_player": false, + "cache_original": 7679228, + "cache_max_original": 7679228, + "cache_needs_revalidation": false + } + ], + "query": "Bloodbath", + "processingTimeMs": 344, + "limit": 1, + "offset": 0, + "estimatedTotalHits": 106241 +} \ No newline at end of file