From 5d5858cba223b0435b7dde871488d34108c87266 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Mon, 30 Jun 2025 10:10:40 -0500 Subject: [PATCH 01/14] link solos to main --- CMakeLists.txt | 2 +- changelog.md | 14 +++++++++---- incl/Avalanche.hpp | 27 +++++++++++++++++++++++-- incl/src/Avalanche.cpp | 18 +++++++++++++++-- mod.json | 2 +- src/{headers => }/ParticleHelper.hpp | 2 +- src/headers/src/ProjectInfoPopup.cpp | 30 +++++++++++++++++++++++----- src/main.cpp | 2 +- 8 files changed, 80 insertions(+), 17 deletions(-) rename src/{headers => }/ParticleHelper.hpp (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 507f3eb..5268e31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ else() set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") endif() -project(AvalancheIndex VERSION 1.1.2) +project(AvalancheIndex VERSION 1.1.3) file(GLOB_RECURSE SOURCES src/*.cpp diff --git a/changelog.md b/changelog.md index 90baa2d..5fff020 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ +### v1.1.3 +##### Patches + +#### Changes +- **Fixed** thumbnail error for Team levels in Team Project information pop-up + +###### Latest +--- +###### Older + ### v1.1.2 ##### Patches @@ -7,10 +17,6 @@ - **Added** toggle setting `Collaboration Level Pages` - Minor tweaks -###### Latest ---- -###### Older - ### v1.1.1 ##### Patches diff --git a/incl/Avalanche.hpp b/incl/Avalanche.hpp index e05014e..cebbc70 100644 --- a/incl/Avalanche.hpp +++ b/incl/Avalanche.hpp @@ -29,6 +29,7 @@ namespace avalanche { // Avalanche Index mod namespace constexpr const char* und = "undefined"; constexpr const char* err = "404: Not Found"; + // Profile class class Profile { public: enum class Badge { @@ -45,9 +46,13 @@ namespace avalanche { // Avalanche Index mod namespace std::string name; // Official pseudonym of the member Badge badge; // ID of the member's badge - Profile(std::string n = "Name", Badge b = Badge::NONE) : name(n), badge(b) {}; + Profile( + std::string n = "Name", + Badge b = Badge::NONE + ) : name(n), badge(b) {}; }; + // Avalanche project class class Project { public: enum class Type { @@ -58,6 +63,15 @@ namespace avalanche { // Avalanche Index mod namespace EVENT, // A project that resulted from a public or private event hosted by Avalanche }; + // Link to the main team project + class LinkToMain { + public: + bool enabled; // If the link is enabled + std::string name; // Name of the linked team project + std::string url; // URL of the showcase of the team project + int level_id; // ID of the in-game level for the linked project + }; + static std::map projectTypeEnum; // Convert a string to a Type enum std::string name; // Official name of the level @@ -66,7 +80,16 @@ namespace avalanche { // Avalanche Index mod namespace Type type; // Type of project the level is featured as bool fame; // If the level will be highlighted on lists - Project(std::string n = "Name", std::string h = "Host", std::string su = URL_AVALANCHE, Type t = Type::NONE, bool f = false) : name(n), host(h), showcase_url(su), type(t), fame(f) {}; + LinkToMain link_to_main; // Optional link to the main team project + + Project( + std::string n = "Name", + std::string h = "Host", + std::string su = URL_AVALANCHE, + Type t = Type::NONE, + bool f = false, + LinkToMain ltm = LinkToMain() + ) : name(n), host(h), showcase_url(su), type(t), fame(f), link_to_main(ltm) {}; }; class Handler { diff --git a/incl/src/Avalanche.cpp b/incl/src/Avalanche.cpp index 5a8dfca..982fbb4 100644 --- a/incl/src/Avalanche.cpp +++ b/incl/src/Avalanche.cpp @@ -190,12 +190,26 @@ namespace avalanche { auto c_type = (lType != avalanche::Project::projectTypeEnum.end()) ? lType->second : avalanche::Project::Type::NONE; auto c_fame = cacheStd["fame"].asBool().unwrapOr(false); - avalanche::Project res(c_name, c_host, c_showcase, c_type, c_fame); + auto c_link = cacheStd["link_to_main"]; + avalanche::Project::LinkToMain ltm; + if (c_link.isObject()) { + ltm.enabled = c_link["enabled"].asBool().unwrapOr(false); + ltm.name = c_link["name"].asString().unwrapOr(und); + ltm.url = c_link["url"].asString().unwrapOr(und); + ltm.level_id = c_link["level_id"].asInt().unwrapOr(0); + } else { + ltm.enabled = false; + ltm.name = und; + ltm.url = und; + ltm.level_id = 0; + }; + + avalanche::Project res(c_name, c_host, c_showcase, c_type, c_fame, ltm); return res; } else { log::error("Project ID is invalid"); - avalanche::Project res("Name", "Host", URL_AVALANCHE, avalanche::Project::Type::NONE, false); + avalanche::Project res("Name", "Host", URL_AVALANCHE, avalanche::Project::Type::NONE, false, avalanche::Project::LinkToMain()); return res; }; }; diff --git a/mod.json b/mod.json index 0beb37b..989d4be 100644 --- a/mod.json +++ b/mod.json @@ -8,7 +8,7 @@ }, "id": "cubicstudios.avalancheindex", "name": "Avalanche Index", - "version": "1.1.2", + "version": "1.1.3", "developers": [ "Cheeseworks" ], diff --git a/src/headers/ParticleHelper.hpp b/src/ParticleHelper.hpp similarity index 99% rename from src/headers/ParticleHelper.hpp rename to src/ParticleHelper.hpp index 495983a..0c4c5b9 100644 --- a/src/headers/ParticleHelper.hpp +++ b/src/ParticleHelper.hpp @@ -1,7 +1,7 @@ #ifndef PARTICLEHELPER_H #define PARTICLEHELPER_H -#include "../Debugger.hpp" +#include "./Debugger.hpp" #include diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index d05a733..b235aa0 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -378,11 +378,31 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { projThumb->setPosition({ m_clippingNode->getScaledContentWidth() / 2, m_clippingNode->getScaledContentHeight() / 2 }); // extract video id from url - std::string videoId = m_avalProject.showcase_url; - - std::string prefix = "https://youtu.be/"; - - if (videoId.find(prefix) == 0) videoId = videoId.substr(prefix.length()); + std::string videoId = "coCcCJYLVRk"; + size_t idSize = 11; // default length for yt video ids + + // list of possible yt url prefixes + const std::pair prefixes[] = { + {"https://youtu.be/", idSize}, + {"https://www.youtube.com/watch?v=", idSize}, + {"https://www.youtube.com/embed/", idSize}, + {"https://www.youtube.com/v/", idSize}, + {"https://youtube.com/watch?v=", idSize}, + {"https://youtube.com/embed/", idSize}, + {"https://youtube.com/v/", idSize} + }; + + for (const auto& [prefix, idLen] : prefixes) { + auto url = m_avalProject.showcase_url; + + if (url.find(prefix) == 0) { + AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); + videoId = url.substr(prefix.length(), idLen); + break; + } else { + AVAL_LOG_DEBUG("Skipped URL format '{}'", url); + }; + }; // format url auto projThumbURL = fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); diff --git a/src/main.cpp b/src/main.cpp index b85c7e2..7a47716 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,8 @@ #include "./Debugger.hpp" +#include "./ParticleHelper.hpp" #include "../incl/Avalanche.hpp" -#include "./headers/ParticleHelper.hpp" #include "./headers/AvalancheFeatured.hpp" #include "./headers/ProjectInfoPopup.hpp" From 6663aaf51eea107c903f72acbb931f2c917ea004 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 1 Jul 2025 19:51:48 -0500 Subject: [PATCH 02/14] custom thumbnail --- changelog.md | 2 ++ incl/Avalanche.hpp | 46 +++++++++++++++------------- incl/src/Avalanche.cpp | 6 ++-- mod.json | 2 +- src/headers/src/ProjectInfoPopup.cpp | 33 ++++++++++++-------- 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/changelog.md b/changelog.md index 5fff020..c3c3ae7 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,9 @@ ##### Patches #### Changes +- **Updated** Geode compatibility to version `4.6.3` - **Fixed** thumbnail error for Team levels in Team Project information pop-up +- Team Projects may now show a customized thumbnail on the Team Project information pop-up ###### Latest --- diff --git a/incl/Avalanche.hpp b/incl/Avalanche.hpp index cebbc70..8185c52 100644 --- a/incl/Avalanche.hpp +++ b/incl/Avalanche.hpp @@ -20,7 +20,7 @@ namespace avalanche { // Avalanche Index mod namespace extern int ACC_PUBLISHER; // Account ID of Avalanche's level publisher account - constexpr const char* URL_CUBIC = "https://www.cubicstudios.xyz/"; // URL to Cubic Studios's official website + constexpr const char* URL_CUBIC = "https://www.cubicstudios.xyz/"; // URL to Cubic Studios's official website constexpr const char* URL_AVALANCHE = "https://avalanche.cubicstudios.xyz/"; // URL to Avalanche's official website constexpr const char* URL_API_BADGES = "https://gh.cubicstudios.xyz/WebLPS/data/avalProfiles.json"; // URL to remote JSON file containing all data on profiles @@ -33,18 +33,18 @@ namespace avalanche { // Avalanche Index mod namespace class Profile { public: enum class Badge { - NONE, // No badge - CUBIC, // Staff of Cubic Studios - DIRECTOR, // Leads the whole team - MANAGER, // Helps keep things in check - MEMBER, // Participates in projects + NONE, // No badge + CUBIC, // Staff of Cubic Studios + DIRECTOR, // Leads the whole team + MANAGER, // Helps keep things in check + MEMBER, // Participates in projects COLLABORATOR, // Non-members of the team who also worked on a project }; static std::map profileBadgeEnum; // Convert a string to a Badge enum std::string name; // Official pseudonym of the member - Badge badge; // ID of the member's badge + Badge badge; // ID of the member's badge Profile( std::string n = "Name", @@ -56,11 +56,11 @@ namespace avalanche { // Avalanche Index mod namespace class Project { public: enum class Type { - NONE, // Not a project - SOLO, // A project that a member worked on by themself - TEAM, // A project that members of the team worked on + NONE, // Not a project + SOLO, // A project that a member worked on by themself + TEAM, // A project that members of the team worked on COLLAB, // A project that involves the work of Collaborators - EVENT, // A project that resulted from a public or private event hosted by Avalanche + EVENT, // A project that resulted from a public or private event hosted by Avalanche }; // Link to the main team project @@ -68,17 +68,18 @@ namespace avalanche { // Avalanche Index mod namespace public: bool enabled; // If the link is enabled std::string name; // Name of the linked team project - std::string url; // URL of the showcase of the team project + std::string url; // Tiny YouTube URL of the showcase video of the team project int level_id; // ID of the in-game level for the linked project }; static std::map projectTypeEnum; // Convert a string to a Type enum - std::string name; // Official name of the level - std::string host; // Team member that hosted the level + std::string name; // Official name of the level + std::string host; // Team member that hosted the level std::string showcase_url; // Tiny YouTube video URL of the full showcase of the level - Type type; // Type of project the level is featured as - bool fame; // If the level will be highlighted on lists + std::string custom_thumbnail; // URL for a custom thumbnail for the level + Type type; // Type of project the level is featured as + bool fame; // If the level will be highlighted on lists LinkToMain link_to_main; // Optional link to the main team project @@ -86,10 +87,11 @@ namespace avalanche { // Avalanche Index mod namespace std::string n = "Name", std::string h = "Host", std::string su = URL_AVALANCHE, + std::string ct = "", Type t = Type::NONE, bool f = false, LinkToMain ltm = LinkToMain() - ) : name(n), host(h), showcase_url(su), type(t), fame(f), link_to_main(ltm) {}; + ) : name(n), host(h), showcase_url(su), custom_thumbnail(ct), type(t), fame(f), link_to_main(ltm) {}; }; class Handler { @@ -104,10 +106,10 @@ namespace avalanche { // Avalanche Index mod namespace void scanAll(); static std::map badgeStringID; // Convert a Badge enum to a string ID - static std::map badgeSpriteName; // Get the sprite from the string badge ID - static std::map badgeColor; // Get the color from the string badge ID - static std::map badgeToAPI; // Convert badge node ID to API ID - static std::map apiToString; // Convert API-provided string to string ID + static std::map badgeSpriteName; // Get the sprite from the string badge ID + static std::map badgeColor; // Get the color from the string badge ID + static std::map badgeToAPI; // Convert badge node ID to API ID + static std::map apiToString; // Convert API-provided string to string ID // Get profile data on a player Profile GetProfile(int id); @@ -126,7 +128,7 @@ namespace avalanche { // Avalanche Index mod namespace log::debug("Creating badge for {}...", profile.name); std::string idString = avalanche::Handler::badgeStringID[profile.badge]; // gets the string equivalent - bool idFailTest = idString.empty(); // checks the map for this value to see if its invalid + bool idFailTest = idString.empty(); // checks the map for this value to see if its invalid if (idFailTest) { log::error("Badge is invalid."); diff --git a/incl/src/Avalanche.cpp b/incl/src/Avalanche.cpp index 982fbb4..21bcfa9 100644 --- a/incl/src/Avalanche.cpp +++ b/incl/src/Avalanche.cpp @@ -187,11 +187,13 @@ namespace avalanche { auto c_name = cacheStd["name"].asString().unwrapOr(und); auto c_host = cacheStd["host"].asString().unwrapOr(und); auto c_showcase = cacheStd["showcase"].asString().unwrapOr(und); + auto c_custom_thumbnail = cacheStd["custom_thumbnail"].asString().unwrapOr(""); auto c_type = (lType != avalanche::Project::projectTypeEnum.end()) ? lType->second : avalanche::Project::Type::NONE; auto c_fame = cacheStd["fame"].asBool().unwrapOr(false); auto c_link = cacheStd["link_to_main"]; avalanche::Project::LinkToMain ltm; + if (c_link.isObject()) { ltm.enabled = c_link["enabled"].asBool().unwrapOr(false); ltm.name = c_link["name"].asString().unwrapOr(und); @@ -204,12 +206,12 @@ namespace avalanche { ltm.level_id = 0; }; - avalanche::Project res(c_name, c_host, c_showcase, c_type, c_fame, ltm); + avalanche::Project res(c_name, c_host, c_showcase, c_custom_thumbnail, c_type, c_fame, ltm); return res; } else { log::error("Project ID is invalid"); - avalanche::Project res("Name", "Host", URL_AVALANCHE, avalanche::Project::Type::NONE, false, avalanche::Project::LinkToMain()); + avalanche::Project res("Name", "Host", URL_AVALANCHE, "", avalanche::Project::Type::NONE, false, avalanche::Project::LinkToMain()); return res; }; }; diff --git a/mod.json b/mod.json index 989d4be..2bf53fe 100644 --- a/mod.json +++ b/mod.json @@ -1,5 +1,5 @@ { - "geode": "4.6.2", + "geode": "4.6.3", "gd": { "win": "2.2074", "android": "2.2074", diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index b235aa0..505fa1b 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -381,8 +381,11 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { std::string videoId = "coCcCJYLVRk"; size_t idSize = 11; // default length for yt video ids - // list of possible yt url prefixes - const std::pair prefixes[] = { + bool isCustomThumbnail = false; + + if (m_avalProject.custom_thumbnail.empty()) { + // list of possible yt url prefixes + const std::pair prefixes[] = { {"https://youtu.be/", idSize}, {"https://www.youtube.com/watch?v=", idSize}, {"https://www.youtube.com/embed/", idSize}, @@ -390,22 +393,26 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { {"https://youtube.com/watch?v=", idSize}, {"https://youtube.com/embed/", idSize}, {"https://youtube.com/v/", idSize} - }; + }; - for (const auto& [prefix, idLen] : prefixes) { - auto url = m_avalProject.showcase_url; + for (const auto& [prefix, idLen] : prefixes) { + auto url = m_avalProject.showcase_url; - if (url.find(prefix) == 0) { - AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); - videoId = url.substr(prefix.length(), idLen); - break; - } else { - AVAL_LOG_DEBUG("Skipped URL format '{}'", url); + if (url.find(prefix) == 0) { + AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); + videoId = url.substr(prefix.length(), idLen); + break; + } else { + AVAL_LOG_DEBUG("Skipped URL format '{}'", url); + }; }; + } else { + AVAL_LOG_INFO("Using custom thumbnail URL: {}", m_avalProject.custom_thumbnail); + isCustomThumbnail = true; }; - // format url - auto projThumbURL = fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); + // custom thumbnail or formatted yt url + std::string projThumbURL = isCustomThumbnail ? m_avalProject.custom_thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); AVAL_LOG_DEBUG("Getting thumbnail at {}...", projThumbURL); From a4f36753f0b4a3f4da34b8d165e50143c6a6b9c7 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 1 Jul 2025 20:18:20 -0500 Subject: [PATCH 03/14] fix api handling --- incl/Avalanche.hpp | 8 +++----- incl/src/Avalanche.cpp | 16 ++++++---------- src/headers/src/ProjectInfoPopup.cpp | 16 ++++++++-------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/incl/Avalanche.hpp b/incl/Avalanche.hpp index 8185c52..c8b0233 100644 --- a/incl/Avalanche.hpp +++ b/incl/Avalanche.hpp @@ -67,8 +67,6 @@ namespace avalanche { // Avalanche Index mod namespace class LinkToMain { public: bool enabled; // If the link is enabled - std::string name; // Name of the linked team project - std::string url; // Tiny YouTube URL of the showcase video of the team project int level_id; // ID of the in-game level for the linked project }; @@ -76,8 +74,8 @@ namespace avalanche { // Avalanche Index mod namespace std::string name; // Official name of the level std::string host; // Team member that hosted the level - std::string showcase_url; // Tiny YouTube video URL of the full showcase of the level - std::string custom_thumbnail; // URL for a custom thumbnail for the level + std::string showcase; // Tiny YouTube video URL of the full showcase of the level + std::string thumbnail; // Imgur URL for a custom thumbnail for the level Type type; // Type of project the level is featured as bool fame; // If the level will be highlighted on lists @@ -91,7 +89,7 @@ namespace avalanche { // Avalanche Index mod namespace Type t = Type::NONE, bool f = false, LinkToMain ltm = LinkToMain() - ) : name(n), host(h), showcase_url(su), custom_thumbnail(ct), type(t), fame(f), link_to_main(ltm) {}; + ) : name(n), host(h), showcase(su), thumbnail(ct), type(t), fame(f), link_to_main(ltm) {}; }; class Handler { diff --git a/incl/src/Avalanche.cpp b/incl/src/Avalanche.cpp index 21bcfa9..834a0d2 100644 --- a/incl/src/Avalanche.cpp +++ b/incl/src/Avalanche.cpp @@ -187,26 +187,22 @@ namespace avalanche { auto c_name = cacheStd["name"].asString().unwrapOr(und); auto c_host = cacheStd["host"].asString().unwrapOr(und); auto c_showcase = cacheStd["showcase"].asString().unwrapOr(und); - auto c_custom_thumbnail = cacheStd["custom_thumbnail"].asString().unwrapOr(""); + auto c_thumbnail = cacheStd["thumbnail"].asString().unwrapOr(""); auto c_type = (lType != avalanche::Project::projectTypeEnum.end()) ? lType->second : avalanche::Project::Type::NONE; auto c_fame = cacheStd["fame"].asBool().unwrapOr(false); - auto c_link = cacheStd["link_to_main"]; + auto c_linked = cacheStd["project"]; avalanche::Project::LinkToMain ltm; - if (c_link.isObject()) { - ltm.enabled = c_link["enabled"].asBool().unwrapOr(false); - ltm.name = c_link["name"].asString().unwrapOr(und); - ltm.url = c_link["url"].asString().unwrapOr(und); - ltm.level_id = c_link["level_id"].asInt().unwrapOr(0); + if (c_linked.isObject()) { + ltm.enabled = c_linked["enabled"].asBool().unwrapOr(false); + ltm.level_id = c_linked["id"].asInt().unwrapOr(0); } else { ltm.enabled = false; - ltm.name = und; - ltm.url = und; ltm.level_id = 0; }; - avalanche::Project res(c_name, c_host, c_showcase, c_custom_thumbnail, c_type, c_fame, ltm); + avalanche::Project res(c_name, c_host, c_showcase, c_thumbnail, c_type, c_fame, ltm); return res; } else { log::error("Project ID is invalid"); diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 505fa1b..42f561e 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -68,8 +68,8 @@ void ProjectInfoPopup::infoPopup(CCObject*) { "OK", "Watch", [this](auto, bool btn2) { if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase_url); - web::openLinkInBrowser(this->m_avalProject.showcase_url); + AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase); + web::openLinkInBrowser(this->m_avalProject.showcase); } else { AVAL_LOG_DEBUG("User clicked OK"); }; }, @@ -108,8 +108,8 @@ void ProjectInfoPopup::onPlayShowcase(CCObject*) { "Cancel", "Watch", [this](auto, bool btn2) { if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase_url); - web::openLinkInBrowser(this->m_avalProject.showcase_url); + AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase); + web::openLinkInBrowser(this->m_avalProject.showcase); } else { AVAL_LOG_DEBUG("User clicked Cancel"); }; }, @@ -383,7 +383,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { bool isCustomThumbnail = false; - if (m_avalProject.custom_thumbnail.empty()) { + if (m_avalProject.thumbnail.empty()) { // list of possible yt url prefixes const std::pair prefixes[] = { {"https://youtu.be/", idSize}, @@ -396,7 +396,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { }; for (const auto& [prefix, idLen] : prefixes) { - auto url = m_avalProject.showcase_url; + auto url = m_avalProject.showcase; if (url.find(prefix) == 0) { AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); @@ -407,12 +407,12 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { }; }; } else { - AVAL_LOG_INFO("Using custom thumbnail URL: {}", m_avalProject.custom_thumbnail); + AVAL_LOG_INFO("Using custom thumbnail URL: {}", m_avalProject.thumbnail); isCustomThumbnail = true; }; // custom thumbnail or formatted yt url - std::string projThumbURL = isCustomThumbnail ? m_avalProject.custom_thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); + std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); AVAL_LOG_DEBUG("Getting thumbnail at {}...", projThumbURL); From 923bee3999c84442212444663671e80f753f29a5 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 1 Jul 2025 21:41:49 -0500 Subject: [PATCH 04/14] linked project ui --- src/headers/src/AvalancheFeatured.cpp | 8 +- src/headers/src/ProjectInfoPopup.cpp | 261 ++++++++++++++++++++++---- 2 files changed, 228 insertions(+), 41 deletions(-) diff --git a/src/headers/src/AvalancheFeatured.cpp b/src/headers/src/AvalancheFeatured.cpp index 1bd1c18..4dd6f3e 100644 --- a/src/headers/src/AvalancheFeatured.cpp +++ b/src/headers/src/AvalancheFeatured.cpp @@ -116,7 +116,7 @@ bool AvalancheFeatured::setup() { art_bottomLeft->setID("bottom-left-corner"); art_bottomLeft->setAnchorPoint({ 0, 0 }); art_bottomLeft->setPosition({ 0, 0 }); - art_bottomLeft->setScale(1.250); + art_bottomLeft->setScale(1.250f); art_bottomLeft->setFlipX(false); art_bottomLeft->setFlipY(false); art_bottomLeft->setZOrder(0); @@ -127,7 +127,7 @@ bool AvalancheFeatured::setup() { art_bottomRight->setID("bottom-right-corner"); art_bottomRight->setAnchorPoint({ 1, 0 }); art_bottomRight->setPosition({ m_overlayMenu->getScaledContentWidth(), 0 }); - art_bottomRight->setScale(1.250); + art_bottomRight->setScale(1.250f); art_bottomRight->setFlipX(true); art_bottomRight->setFlipY(false); art_bottomLeft->setZOrder(0); @@ -138,7 +138,7 @@ bool AvalancheFeatured::setup() { art_topLeft->setID("top-left-corner"); art_topLeft->setAnchorPoint({ 0, 1 }); art_topLeft->setPosition({ 0, m_overlayMenu->getScaledContentHeight() }); - art_topLeft->setScale(1.250); + art_topLeft->setScale(1.250f); art_topLeft->setFlipX(false); art_topLeft->setFlipY(true); art_topLeft->setZOrder(0); @@ -149,7 +149,7 @@ bool AvalancheFeatured::setup() { art_topRight->setID("top-right-corner"); art_topRight->setAnchorPoint({ 1, 1 }); art_topRight->setPosition({ m_overlayMenu->getScaledContentWidth(), m_overlayMenu->getScaledContentHeight() }); - art_topRight->setScale(1.250); + art_topRight->setScale(1.250f); art_topRight->setFlipX(true); art_topRight->setFlipY(true); art_topRight->setZOrder(0); diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 42f561e..b39053b 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -21,6 +21,36 @@ using namespace geode::prelude; using namespace avalanche; +std::string findYouTubeID(std::string const& url, Project const& avalProject) { + std::string videoId = "coCcCJYLVRk"; // extract from url + constexpr size_t idSize = 11; // default length for yt video ids + + // list of possible yt url prefixes + const std::pair prefixes[] = { + {"https://youtu.be/", idSize}, + {"https://www.youtube.com/watch?v=", idSize}, + {"https://www.youtube.com/embed/", idSize}, + {"https://www.youtube.com/v/", idSize}, + {"https://youtube.com/watch?v=", idSize}, + {"https://youtube.com/embed/", idSize}, + {"https://youtube.com/v/", idSize} + }; + + for (const auto& [prefix, idLen] : prefixes) { + auto url = avalProject.showcase; + + if (url.find(prefix) == 0) { + AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); + videoId = url.substr(prefix.length(), idLen); + break; + } else { + AVAL_LOG_DEBUG("Skipped URL format '{}'", url); + }; + }; + + return videoId; +}; + ProjectInfoPopup* ProjectInfoPopup::create() { auto ret = new ProjectInfoPopup; if (ret->initAnchored(440, 290)) { @@ -78,7 +108,7 @@ void ProjectInfoPopup::infoPopup(CCObject*) { void ProjectInfoPopup::onFameInfo(CCObject*) { std::ostringstream body; - body << "This level, '" << m_avalProject.name << "', is featured in Avalanche's Hall of Fame. It is a special list of levels that are considered to be the best of the best from the team."; + body << "'" << m_avalProject.name << "' by '" << m_avalPublisher << "' is featured in Avalanche's Hall of Fame. It is a special list of levels that are considered to be the best of the best from the team."; std::string resultBody = body.str(); @@ -265,7 +295,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { art_bottomLeft->setID("bottom-left-corner"); art_bottomLeft->setAnchorPoint({ 0, 0 }); art_bottomLeft->setPosition({ 0, 0 }); - art_bottomLeft->setScale(1.250); + art_bottomLeft->setScale(1.250f); art_bottomLeft->setFlipX(false); art_bottomLeft->setFlipY(false); art_bottomLeft->setZOrder(0); @@ -276,7 +306,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { art_bottomRight->setID("bottom-right-corner"); art_bottomRight->setAnchorPoint({ 1, 0 }); art_bottomRight->setPosition({ m_overlayMenu->getScaledContentWidth(), 0 }); - art_bottomRight->setScale(1.250); + art_bottomRight->setScale(1.250f); art_bottomRight->setFlipX(true); art_bottomRight->setFlipY(false); art_bottomLeft->setZOrder(0); @@ -287,7 +317,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { art_topLeft->setID("top-left-corner"); art_topLeft->setAnchorPoint({ 0, 1 }); art_topLeft->setPosition({ 0, m_overlayMenu->getScaledContentHeight() }); - art_topLeft->setScale(1.250); + art_topLeft->setScale(1.250f); art_topLeft->setFlipX(false); art_topLeft->setFlipY(true); art_topLeft->setZOrder(0); @@ -298,7 +328,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { art_topRight->setID("top-right-corner"); art_topRight->setAnchorPoint({ 1, 1 }); art_topRight->setPosition({ m_overlayMenu->getScaledContentWidth(), m_overlayMenu->getScaledContentHeight() }); - art_topRight->setScale(1.250); + art_topRight->setScale(1.250f); art_topRight->setFlipX(true); art_topRight->setFlipY(true); art_topRight->setZOrder(0); @@ -377,42 +407,18 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { projThumb->ignoreAnchorPointForPosition(false); projThumb->setPosition({ m_clippingNode->getScaledContentWidth() / 2, m_clippingNode->getScaledContentHeight() / 2 }); - // extract video id from url - std::string videoId = "coCcCJYLVRk"; - size_t idSize = 11; // default length for yt video ids - - bool isCustomThumbnail = false; + bool isCustomThumbnail = false; // whether the thumbnail is a custom one or not if (m_avalProject.thumbnail.empty()) { - // list of possible yt url prefixes - const std::pair prefixes[] = { - {"https://youtu.be/", idSize}, - {"https://www.youtube.com/watch?v=", idSize}, - {"https://www.youtube.com/embed/", idSize}, - {"https://www.youtube.com/v/", idSize}, - {"https://youtube.com/watch?v=", idSize}, - {"https://youtube.com/embed/", idSize}, - {"https://youtube.com/v/", idSize} - }; - - for (const auto& [prefix, idLen] : prefixes) { - auto url = m_avalProject.showcase; - - if (url.find(prefix) == 0) { - AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); - videoId = url.substr(prefix.length(), idLen); - break; - } else { - AVAL_LOG_DEBUG("Skipped URL format '{}'", url); - }; - }; + AVAL_LOG_INFO("Using default thumbnail for project '{}'", m_avalProject.name); + isCustomThumbnail = false; } else { - AVAL_LOG_INFO("Using custom thumbnail URL: {}", m_avalProject.thumbnail); + AVAL_LOG_INFO("Using custom thumbnail URL {}", m_avalProject.thumbnail); isCustomThumbnail = true; }; - // custom thumbnail or formatted yt url - std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); + std::string videoId = isCustomThumbnail ? nullptr : findYouTubeID(m_avalProject.showcase, m_avalProject); // extracted video id from showcase url + std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); // custom thumbnail or formatted yt url AVAL_LOG_DEBUG("Getting thumbnail at {}...", projThumbURL); @@ -424,11 +430,16 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { float scale = bgSize.height / projThumb->getContentHeight(); projThumb->setScale(scale); - projThumb->setAnchorPoint({ 0, 0 }); projThumb->ignoreAnchorPointForPosition(false); projThumb->setColor({ 125, 125, 125 }); - projThumb->setPosition({ 0, 0 }); projThumb->setOpacity(125); + + if (m_avalProject.thumbnail.empty()) { + projThumb->setPosition({ 0, 0 }); + projThumb->setAnchorPoint({ 0, 0 }); + } else { + AVAL_LOG_DEBUG("Custom thumbnail loaded, keeping position to center"); + }; } else { AVAL_LOG_ERROR("{}", res.unwrapErr()); projThumb->removeMeAndCleanup(); @@ -441,6 +452,182 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { AVAL_LOG_DEBUG("Project thumbnail setting is disabled, not adding thumbnail to project info popup"); }; + if (m_avalProject.link_to_main.enabled) { + AVAL_LOG_DEBUG("Project '{}' has a link to the main project", m_avalProject.name); + auto linkedProj = Handler::get()->GetProject(m_avalProject.link_to_main.level_id); + + if (linkedProj.type == Project::Type::NONE) { + AVAL_LOG_ERROR("Failed to get linked project with ID {}", m_avalProject.link_to_main.level_id); + } else { + AVAL_LOG_INFO("Adding link to main project '{}'", linkedProj.name); + + // create linked project button + AVAL_LOG_DEBUG("Creating linked project button"); + auto linkedProjMenu = CCMenu::create(); + linkedProjMenu->setID("linked-project-menu"); + linkedProjMenu->setAnchorPoint({ 1, 1 }); + linkedProjMenu->ignoreAnchorPointForPosition(false); + linkedProjMenu->setPosition({ m_mainLayer->getScaledContentWidth() - 25.f, m_mainLayer->getScaledContentHeight() - 50.f }); + linkedProjMenu->setScaledContentSize({ 150.f, 81.f }); + + m_clippingNode->addChild(linkedProjMenu); + + // create clipping node for linked project + AVAL_LOG_DEBUG("Creating clipping node for linked project"); + auto linkedProjClippingNode = CCClippingNode::create(); + linkedProjClippingNode->setID("clipping-node"); + linkedProjClippingNode->setContentSize(linkedProjMenu->getScaledContentSize()); + linkedProjClippingNode->ignoreAnchorPointForPosition(false); + linkedProjClippingNode->setAnchorPoint({ 0.5f, 0.5f }); + linkedProjClippingNode->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() / 2.f }); + linkedProjClippingNode->setStencil(CCLayerColor::create({ 255, 255, 255 }, linkedProjMenu->getScaledContentWidth(), linkedProjMenu->getScaledContentHeight()));; + linkedProjClippingNode->setZOrder(-1); + + linkedProjMenu->addChild(linkedProjClippingNode); + + auto linkedProjClippingNodeBg = CCScale9Sprite::create("GJ_square01.png"); + linkedProjClippingNodeBg->setID("background"); + linkedProjClippingNodeBg->setContentSize(linkedProjClippingNode->getContentSize()); + linkedProjClippingNodeBg->setPosition(linkedProjClippingNode->getPosition()); + linkedProjClippingNodeBg->setAnchorPoint(linkedProjClippingNode->getAnchorPoint()); + linkedProjClippingNodeBg->setZOrder(-1); + + linkedProjClippingNode->addChild(linkedProjClippingNodeBg); + + auto art_bottomLeft_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + art_bottomLeft_linkedProj->setID("bottom-left-corner"); + art_bottomLeft_linkedProj->setAnchorPoint({ 0, 0 }); + art_bottomLeft_linkedProj->setPosition({ 0, 0 }); + art_bottomLeft_linkedProj->setScale(0.5f); + art_bottomLeft_linkedProj->setFlipX(false); + art_bottomLeft_linkedProj->setFlipY(false); + art_bottomLeft_linkedProj->setZOrder(3); + + linkedProjClippingNode->addChild(art_bottomLeft_linkedProj); + + auto art_bottomRight_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + art_bottomRight_linkedProj->setID("bottom-right-corner"); + art_bottomRight_linkedProj->setAnchorPoint({ 1, 0 }); + art_bottomRight_linkedProj->setPosition({ linkedProjClippingNode->getScaledContentWidth(), 0 }); + art_bottomRight_linkedProj->setScale(0.5f); + art_bottomRight_linkedProj->setFlipX(true); + art_bottomRight_linkedProj->setFlipY(false); + art_bottomRight_linkedProj->setZOrder(3); + + linkedProjClippingNode->addChild(art_bottomRight_linkedProj); + + auto art_topLeft_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + art_topLeft_linkedProj->setID("top-left-corner"); + art_topLeft_linkedProj->setAnchorPoint({ 0, 1 }); + art_topLeft_linkedProj->setPosition({ 0, linkedProjClippingNode->getScaledContentHeight() }); + art_topLeft_linkedProj->setScale(0.5f); + art_topLeft_linkedProj->setFlipX(false); + art_topLeft_linkedProj->setFlipY(true); + art_topLeft_linkedProj->setZOrder(3); + + linkedProjClippingNode->addChild(art_topLeft_linkedProj); + + auto art_topRight_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + art_topRight_linkedProj->setID("top-right-corner"); + art_topRight_linkedProj->setAnchorPoint({ 1, 1 }); + art_topRight_linkedProj->setPosition({ linkedProjClippingNode->getScaledContentWidth(), linkedProjClippingNode->getScaledContentHeight() }); + art_topRight_linkedProj->setScale(0.5f); + art_topRight_linkedProj->setFlipX(true); + art_topRight_linkedProj->setFlipY(true); + art_topRight_linkedProj->setZOrder(3); + + linkedProjClippingNode->addChild(art_topRight_linkedProj); + + // create thumbnail lazy sprite for linked project + AVAL_LOG_DEBUG("Creating thumbnail lazy sprite for linked project '{}'", linkedProj.name); + LazySprite* linkedProjThumb = LazySprite::create(linkedProjMenu->getScaledContentSize(), true); + linkedProjThumb->setID("thumbnail"); + linkedProjThumb->setAnchorPoint({ 0.5, 0.5 }); + linkedProjThumb->ignoreAnchorPointForPosition(false); + linkedProjThumb->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() / 2.f }); + + linkedProjThumb->setLoadCallback([linkedProjThumb, linkedProjClippingNode](Result<> res) { + if (res.isOk()) { + AVAL_LOG_INFO("Linked project thumbnail loaded successfully"); + } else { + AVAL_LOG_ERROR("Failed to load linked project thumbnail: {}", res.unwrapErr()); + linkedProjThumb->initWithSpriteFrameName("unavailable.png"_spr); + }; + + linkedProjThumb->setScale(linkedProjClippingNode->getScaledContentHeight() / linkedProjThumb->getScaledContentHeight()); + linkedProjThumb->setPosition(linkedProjClippingNode->getPosition()); + linkedProjThumb->ignoreAnchorPointForPosition(false); + linkedProjThumb->setColor({ 250, 250, 250 }); + linkedProjThumb->setOpacity(250); + }); + + bool isCustomThumbnail = false; // whether the thumbnail is a custom one or not + + // check if linked project has a custom thumbnail + if (linkedProj.thumbnail.empty()) { + AVAL_LOG_INFO("Using default thumbnail for project '{}'", linkedProj.name); + isCustomThumbnail = false; + } else { + AVAL_LOG_INFO("Using custom thumbnail URL {}", linkedProj.thumbnail); + isCustomThumbnail = true; + }; + + std::string linkedVideoId = findYouTubeID(linkedProj.showcase, m_avalProject); // extracted video id from showcase url + std::string linkedProjThumbURL = isCustomThumbnail ? linkedProj.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)linkedVideoId); // custom thumbnail or formatted yt url + + AVAL_LOG_DEBUG("Getting linked project thumbnail at {}...", linkedProjThumbURL); + linkedProjThumb->loadFromUrl(linkedProjThumbURL, LazySprite::Format::kFmtUnKnown, false); + linkedProjClippingNode->addChild(linkedProjThumb); + + // set border + auto linkedProjBorder = CCScale9Sprite::create("GJ_square07.png"); + linkedProjBorder->setID("border"); + linkedProjBorder->setPosition(linkedProjClippingNode->getPosition()); + linkedProjBorder->setContentSize(linkedProjClippingNode->getScaledContentSize()); + linkedProjBorder->ignoreAnchorPointForPosition(false); + linkedProjBorder->setAnchorPoint({ 0.5f, 0.5f }); + linkedProjBorder->setZOrder(2); + + // add border to clipping node + linkedProjClippingNode->addChild(linkedProjBorder); + + // create sprite for linked project showcase button + auto linkedProjShowcase_sprite = CCSprite::createWithSpriteFrameName("GJ_playBtn2_001.png"); + linkedProjShowcase_sprite->setScale(0.375f); + + // create button to play linked project showcase + AVAL_LOG_DEBUG("Creating button to play linked project showcase"); + auto linkedProjShowcase = CCMenuItemSpriteExtra::create( + linkedProjShowcase_sprite, + this, + menu_selector(ProjectInfoPopup::onPlayShowcase)); + linkedProjShowcase->setID("button"); + linkedProjShowcase->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, 25.f }); + + linkedProjMenu->addChild(linkedProjShowcase); + + // create label for linked project showcase + AVAL_LOG_DEBUG("Creating label for linked project showcase"); + auto linkedProjLabel = CCLabelBMFont::create("Play Now!", "bigFont.fnt"); + linkedProjLabel->setID("label"); + linkedProjLabel->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() - 10.f }); + linkedProjLabel->setScale(0.25f); + + linkedProjMenu->addChild(linkedProjLabel); + + // create name text label for linked project name + AVAL_LOG_DEBUG("Creating label for linked project name"); + auto linkedProjName = CCLabelBMFont::create(linkedProj.name.c_str(), "goldFont.fnt"); + linkedProjName->setID("name"); + linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjShowcase->getPositionY() + 37.5f }); + linkedProjName->setScale(0.625f); + + linkedProjMenu->addChild(linkedProjName); + }; + } else { + AVAL_LOG_DEBUG("Project '{}' does not have a link to the main project", m_avalProject.name); + }; + // geode settings popup button auto settingsBtnSprite = CCSprite::createWithSpriteFrameName("GJ_optionsBtn_001.png"); settingsBtnSprite->setScale(0.75f); From a181553ad659fa3d7047d392c14fa0b91fe593e0 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 1 Jul 2025 21:58:41 -0500 Subject: [PATCH 05/14] linked proj info btn --- src/headers/src/ProjectInfoPopup.cpp | 30 ++++++++++++++++++++-------- src/main.cpp | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index b39053b..d5f9340 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -362,15 +362,15 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { auto hostName_label = CCLabelBMFont::create(hostLabelTxt, "bigFont.fnt"); hostName_label->setID("host-name-label"); hostName_label->ignoreAnchorPointForPosition(false); - hostName_label->setAnchorPoint({ 0, 0.5 }); - hostName_label->setPosition({ 10.f, (m_mainLayer->getScaledContentHeight() / 2.f) + 70.f }); + hostName_label->setAnchorPoint({ 0, 1 }); + hostName_label->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 50.f }); hostName_label->setScale(0.25f); auto hostName = CCLabelBMFont::create(m_avalProject.host.c_str(), "goldFont.fnt"); hostName->setID("host-name"); hostName->ignoreAnchorPointForPosition(false); - hostName->setAnchorPoint({ 0, 0.5 }); - hostName->setPosition({ 10.f, (m_mainLayer->getScaledContentHeight() / 2.f) + 55.f }); + hostName->setAnchorPoint({ 0, 1 }); + hostName->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 70.f }); hostName->setScale(0.75f); m_overlayMenu->addChild(hostName_label); @@ -417,7 +417,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { isCustomThumbnail = true; }; - std::string videoId = isCustomThumbnail ? nullptr : findYouTubeID(m_avalProject.showcase, m_avalProject); // extracted video id from showcase url + std::string videoId = isCustomThumbnail ? "" : findYouTubeID(m_avalProject.showcase, m_avalProject); // extracted video id from showcase url std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); // custom thumbnail or formatted yt url AVAL_LOG_DEBUG("Getting thumbnail at {}...", projThumbURL); @@ -472,6 +472,20 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { m_clippingNode->addChild(linkedProjMenu); + // info button + auto linkedProjInfoBtnSprite = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + linkedProjInfoBtnSprite->setScale(0.375f); + + auto linkedProjInfoBtn = CCMenuItemSpriteExtra::create( + linkedProjInfoBtnSprite, + this, + menu_selector(ProjectInfoPopup::infoPopup)); + linkedProjInfoBtn->setID("info-button"); + linkedProjInfoBtn->setPosition({ linkedProjMenu->getScaledContentWidth() - 5.f, linkedProjMenu->getScaledContentHeight() - 5.f }); + linkedProjInfoBtn->setZOrder(126); + + linkedProjMenu->addChild(linkedProjInfoBtn); + // create clipping node for linked project AVAL_LOG_DEBUG("Creating clipping node for linked project"); auto linkedProjClippingNode = CCClippingNode::create(); @@ -480,7 +494,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjClippingNode->ignoreAnchorPointForPosition(false); linkedProjClippingNode->setAnchorPoint({ 0.5f, 0.5f }); linkedProjClippingNode->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() / 2.f }); - linkedProjClippingNode->setStencil(CCLayerColor::create({ 255, 255, 255 }, linkedProjMenu->getScaledContentWidth(), linkedProjMenu->getScaledContentHeight()));; + linkedProjClippingNode->setStencil(CCLayerColor::create({ 250, 250, 250 }, linkedProjMenu->getScaledContentWidth(), linkedProjMenu->getScaledContentHeight()));; linkedProjClippingNode->setZOrder(-1); linkedProjMenu->addChild(linkedProjClippingNode); @@ -572,7 +586,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { isCustomThumbnail = true; }; - std::string linkedVideoId = findYouTubeID(linkedProj.showcase, m_avalProject); // extracted video id from showcase url + std::string linkedVideoId = isCustomThumbnail ? "" : findYouTubeID(linkedProj.showcase, m_avalProject); // extracted video id from showcase url std::string linkedProjThumbURL = isCustomThumbnail ? linkedProj.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)linkedVideoId); // custom thumbnail or formatted yt url AVAL_LOG_DEBUG("Getting linked project thumbnail at {}...", linkedProjThumbURL); @@ -619,7 +633,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { AVAL_LOG_DEBUG("Creating label for linked project name"); auto linkedProjName = CCLabelBMFont::create(linkedProj.name.c_str(), "goldFont.fnt"); linkedProjName->setID("name"); - linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjShowcase->getPositionY() + 37.5f }); + linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjShowcase->getScaledContentHeight() - 15.f }); linkedProjName->setScale(0.625f); linkedProjMenu->addChild(linkedProjName); diff --git a/src/main.cpp b/src/main.cpp index 7a47716..1dfe359 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -399,7 +399,7 @@ class $modify(LevelInfo, LevelInfoLayer) { if (showFame) { if (auto bgThumbnail = CCSprite::createWithSpriteFrameName("fame-bg.png"_spr)) { - bgThumbnail->setOpacity(75); + bgThumbnail->setOpacity(125); bgThumbnail->setAnchorPoint({ 0.5, 0 }); bgThumbnail->ignoreAnchorPointForPosition(false); bgThumbnail->setPosition({ this->getContentWidth() / 2, 0 }); From 8d417a272ca3450d4f89b6908a62dc8da5830a75 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 1 Jul 2025 22:05:36 -0500 Subject: [PATCH 06/14] adjustments --- src/headers/src/ProjectInfoPopup.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index d5f9340..212786e 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -363,14 +363,14 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { hostName_label->setID("host-name-label"); hostName_label->ignoreAnchorPointForPosition(false); hostName_label->setAnchorPoint({ 0, 1 }); - hostName_label->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 50.f }); + hostName_label->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 55.f }); hostName_label->setScale(0.25f); auto hostName = CCLabelBMFont::create(m_avalProject.host.c_str(), "goldFont.fnt"); hostName->setID("host-name"); hostName->ignoreAnchorPointForPosition(false); hostName->setAnchorPoint({ 0, 1 }); - hostName->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 70.f }); + hostName->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 65.f }); hostName->setScale(0.75f); m_overlayMenu->addChild(hostName_label); @@ -467,7 +467,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjMenu->setID("linked-project-menu"); linkedProjMenu->setAnchorPoint({ 1, 1 }); linkedProjMenu->ignoreAnchorPointForPosition(false); - linkedProjMenu->setPosition({ m_mainLayer->getScaledContentWidth() - 25.f, m_mainLayer->getScaledContentHeight() - 50.f }); + linkedProjMenu->setPosition({ m_mainLayer->getScaledContentWidth() - 25.f, m_mainLayer->getScaledContentHeight() - 55.f }); linkedProjMenu->setScaledContentSize({ 150.f, 81.f }); m_clippingNode->addChild(linkedProjMenu); @@ -481,7 +481,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { this, menu_selector(ProjectInfoPopup::infoPopup)); linkedProjInfoBtn->setID("info-button"); - linkedProjInfoBtn->setPosition({ linkedProjMenu->getScaledContentWidth() - 5.f, linkedProjMenu->getScaledContentHeight() - 5.f }); + linkedProjInfoBtn->setPosition({ linkedProjMenu->getScaledContentWidth() - 6.25f, linkedProjMenu->getScaledContentHeight() - 6.25f }); linkedProjInfoBtn->setZOrder(126); linkedProjMenu->addChild(linkedProjInfoBtn); @@ -559,6 +559,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjThumb->setAnchorPoint({ 0.5, 0.5 }); linkedProjThumb->ignoreAnchorPointForPosition(false); linkedProjThumb->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() / 2.f }); + linkedProjThumb->setScale(0.5f); linkedProjThumb->setLoadCallback([linkedProjThumb, linkedProjClippingNode](Result<> res) { if (res.isOk()) { @@ -633,7 +634,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { AVAL_LOG_DEBUG("Creating label for linked project name"); auto linkedProjName = CCLabelBMFont::create(linkedProj.name.c_str(), "goldFont.fnt"); linkedProjName->setID("name"); - linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjShowcase->getScaledContentHeight() - 15.f }); + linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() - 20.f }); linkedProjName->setScale(0.625f); linkedProjMenu->addChild(linkedProjName); From 1cb588140f8184c2b4a02074579827c1956133b4 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 1 Jul 2025 22:11:58 -0500 Subject: [PATCH 07/14] fixes --- src/headers/src/ProjectInfoPopup.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 212786e..a18033b 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -407,7 +407,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { projThumb->ignoreAnchorPointForPosition(false); projThumb->setPosition({ m_clippingNode->getScaledContentWidth() / 2, m_clippingNode->getScaledContentHeight() / 2 }); - bool isCustomThumbnail = false; // whether the thumbnail is a custom one or not + bool isCustomThumbnail = false; // whether the thumbnail is custom if (m_avalProject.thumbnail.empty()) { AVAL_LOG_INFO("Using default thumbnail for project '{}'", m_avalProject.name); @@ -508,7 +508,10 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjClippingNode->addChild(linkedProjClippingNodeBg); - auto art_bottomLeft_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + // corner art deco for linked project + auto corner = "rewardCorner_001.png"; + + auto art_bottomLeft_linkedProj = CCSprite::createWithSpriteFrameName(corner); art_bottomLeft_linkedProj->setID("bottom-left-corner"); art_bottomLeft_linkedProj->setAnchorPoint({ 0, 0 }); art_bottomLeft_linkedProj->setPosition({ 0, 0 }); @@ -519,7 +522,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjClippingNode->addChild(art_bottomLeft_linkedProj); - auto art_bottomRight_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + auto art_bottomRight_linkedProj = CCSprite::createWithSpriteFrameName(corner); art_bottomRight_linkedProj->setID("bottom-right-corner"); art_bottomRight_linkedProj->setAnchorPoint({ 1, 0 }); art_bottomRight_linkedProj->setPosition({ linkedProjClippingNode->getScaledContentWidth(), 0 }); @@ -530,7 +533,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjClippingNode->addChild(art_bottomRight_linkedProj); - auto art_topLeft_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + auto art_topLeft_linkedProj = CCSprite::createWithSpriteFrameName(corner); art_topLeft_linkedProj->setID("top-left-corner"); art_topLeft_linkedProj->setAnchorPoint({ 0, 1 }); art_topLeft_linkedProj->setPosition({ 0, linkedProjClippingNode->getScaledContentHeight() }); @@ -541,7 +544,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjClippingNode->addChild(art_topLeft_linkedProj); - auto art_topRight_linkedProj = CCSprite::createWithSpriteFrameName(m_cornerArtType.c_str()); + auto art_topRight_linkedProj = CCSprite::createWithSpriteFrameName(corner); art_topRight_linkedProj->setID("top-right-corner"); art_topRight_linkedProj->setAnchorPoint({ 1, 1 }); art_topRight_linkedProj->setPosition({ linkedProjClippingNode->getScaledContentWidth(), linkedProjClippingNode->getScaledContentHeight() }); @@ -569,14 +572,16 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjThumb->initWithSpriteFrameName("unavailable.png"_spr); }; + linkedProjThumb->setScale(1.f); linkedProjThumb->setScale(linkedProjClippingNode->getScaledContentHeight() / linkedProjThumb->getScaledContentHeight()); + linkedProjThumb->setPosition(linkedProjClippingNode->getPosition()); linkedProjThumb->ignoreAnchorPointForPosition(false); linkedProjThumb->setColor({ 250, 250, 250 }); linkedProjThumb->setOpacity(250); }); - bool isCustomThumbnail = false; // whether the thumbnail is a custom one or not + bool isCustomThumbnail = false; // whether the thumbnail is custom // check if linked project has a custom thumbnail if (linkedProj.thumbnail.empty()) { From 2b6aab5f0121a6f302feeacb066cb98baa02138a Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Wed, 2 Jul 2025 15:42:36 -0500 Subject: [PATCH 08/14] new api --- CMakeLists.txt | 2 +- changelog.md | 8 ++- incl/Avalanche.hpp | 4 +- mod.json | 2 +- src/headers/src/AvalancheFeatured.cpp | 2 +- src/headers/src/ProjectInfoPopup.cpp | 72 +++++++++++---------------- src/main.cpp | 2 +- 7 files changed, 41 insertions(+), 51 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5268e31..5642f6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ else() set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") endif() -project(AvalancheIndex VERSION 1.1.3) +project(AvalancheIndex VERSION 1.2.0) file(GLOB_RECURSE SOURCES src/*.cpp diff --git a/changelog.md b/changelog.md index c3c3ae7..aeb04ae 100644 --- a/changelog.md +++ b/changelog.md @@ -1,11 +1,15 @@ -### v1.1.3 -##### Patches +## v1.2.0 +#### Team Project pop-up #### Changes - **Updated** Geode compatibility to version `4.6.3` +- **Added** promotional container for parent Team Project, if any, in information pop-up for independent levels - **Fixed** thumbnail error for Team levels in Team Project information pop-up - Team Projects may now show a customized thumbnail on the Team Project information pop-up +#### Developers +- **Updated** URLs to use Cubic Studios API endpoints for all web requests + ###### Latest --- ###### Older diff --git a/incl/Avalanche.hpp b/incl/Avalanche.hpp index c8b0233..26175f0 100644 --- a/incl/Avalanche.hpp +++ b/incl/Avalanche.hpp @@ -23,8 +23,8 @@ namespace avalanche { // Avalanche Index mod namespace constexpr const char* URL_CUBIC = "https://www.cubicstudios.xyz/"; // URL to Cubic Studios's official website constexpr const char* URL_AVALANCHE = "https://avalanche.cubicstudios.xyz/"; // URL to Avalanche's official website - constexpr const char* URL_API_BADGES = "https://gh.cubicstudios.xyz/WebLPS/data/avalProfiles.json"; // URL to remote JSON file containing all data on profiles - constexpr const char* URL_API_LEVELS = "https://gh.cubicstudios.xyz/WebLPS/data/avalProjects.json"; // URL to remote JSON file containing all data on projects + constexpr const char* URL_API_BADGES = "https://api.cubicstudios.xyz/avalanche/v1/profiles"; // URL to remote JSON file containing all data on profiles + constexpr const char* URL_API_LEVELS = "https://api.cubicstudios.xyz/avalanche/v1/projects"; // URL to remote JSON file containing all data on projects constexpr const char* und = "undefined"; constexpr const char* err = "404: Not Found"; diff --git a/mod.json b/mod.json index 2bf53fe..8fc0977 100644 --- a/mod.json +++ b/mod.json @@ -8,7 +8,7 @@ }, "id": "cubicstudios.avalancheindex", "name": "Avalanche Index", - "version": "1.1.3", + "version": "1.2.0", "developers": [ "Cheeseworks" ], diff --git a/src/headers/src/AvalancheFeatured.cpp b/src/headers/src/AvalancheFeatured.cpp index 4dd6f3e..44c58ff 100644 --- a/src/headers/src/AvalancheFeatured.cpp +++ b/src/headers/src/AvalancheFeatured.cpp @@ -223,7 +223,7 @@ bool AvalancheFeatured::setup() { projThumb->setScale(scale); }); - projThumb->loadFromUrl("https://gh.cubicstudios.xyz/WebLPS/aval-project/thumbnail.png", LazySprite::Format::kFmtUnKnown, false); + projThumb->loadFromUrl("https://api.cubicstudios.xyz/avalanche/v1/featured/thumbnail", LazySprite::Format::kFmtUnKnown, false); m_clippingNode->addChild(projThumb); if (AVAL_GEODE_MOD->getSettingValue("dev-mode")) { diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index a18033b..878bf00 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -4,6 +4,7 @@ #include "../../incl/Avalanche.hpp" +#include #include #include @@ -21,35 +22,21 @@ using namespace geode::prelude; using namespace avalanche; -std::string findYouTubeID(std::string const& url, Project const& avalProject) { - std::string videoId = "coCcCJYLVRk"; // extract from url - constexpr size_t idSize = 11; // default length for yt video ids - - // list of possible yt url prefixes - const std::pair prefixes[] = { - {"https://youtu.be/", idSize}, - {"https://www.youtube.com/watch?v=", idSize}, - {"https://www.youtube.com/embed/", idSize}, - {"https://www.youtube.com/v/", idSize}, - {"https://youtube.com/watch?v=", idSize}, - {"https://youtube.com/embed/", idSize}, - {"https://youtube.com/v/", idSize} - }; - - for (const auto& [prefix, idLen] : prefixes) { - auto url = avalProject.showcase; +Handler* avalHandler = Handler::get(); - if (url.find(prefix) == 0) { - AVAL_LOG_INFO("Found YouTube URL prefix '{}'", prefix); - videoId = url.substr(prefix.length(), idLen); - break; +inline std::string url_encode(const std::string& value) { + std::ostringstream escaped; + escaped.fill('0'); + escaped << std::hex; + for (char c : value) { + if (isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + escaped << c; } else { - AVAL_LOG_DEBUG("Skipped URL format '{}'", url); - }; - }; - - return videoId; -}; + escaped << '%' << std::setw(2) << int((unsigned char)c); + } + } + return escaped.str(); +} ProjectInfoPopup* ProjectInfoPopup::create() { auto ret = new ProjectInfoPopup; @@ -194,7 +181,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { setZOrder(10); m_level = level; - m_avalProject = Handler::get()->GetProject(m_level->m_levelID.value()); + m_avalProject = avalHandler->GetProject(m_level->m_levelID.value()); if (m_avalProject.type == Project::Type::NONE) { AVAL_LOG_ERROR("Avalanche project type is NONE"); @@ -417,8 +404,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { isCustomThumbnail = true; }; - std::string videoId = isCustomThumbnail ? "" : findYouTubeID(m_avalProject.showcase, m_avalProject); // extracted video id from showcase url - std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)videoId); // custom thumbnail or formatted yt url + std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/thumbnails?id={}", (int)m_level->m_levelID.value()); // custom thumbnail or default AVAL_LOG_DEBUG("Getting thumbnail at {}...", projThumbURL); @@ -454,7 +440,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { if (m_avalProject.link_to_main.enabled) { AVAL_LOG_DEBUG("Project '{}' has a link to the main project", m_avalProject.name); - auto linkedProj = Handler::get()->GetProject(m_avalProject.link_to_main.level_id); + auto linkedProj = avalHandler->GetProject(m_avalProject.link_to_main.level_id); if (linkedProj.type == Project::Type::NONE) { AVAL_LOG_ERROR("Failed to get linked project with ID {}", m_avalProject.link_to_main.level_id); @@ -567,18 +553,18 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjThumb->setLoadCallback([linkedProjThumb, linkedProjClippingNode](Result<> res) { if (res.isOk()) { AVAL_LOG_INFO("Linked project thumbnail loaded successfully"); + + linkedProjThumb->setScale(1.f); + linkedProjThumb->setScale(linkedProjClippingNode->getScaledContentHeight() / linkedProjThumb->getScaledContentHeight()); + + linkedProjThumb->setPosition(linkedProjClippingNode->getPosition()); + linkedProjThumb->ignoreAnchorPointForPosition(false); + linkedProjThumb->setColor({ 250, 250, 250 }); + linkedProjThumb->setOpacity(250); } else { AVAL_LOG_ERROR("Failed to load linked project thumbnail: {}", res.unwrapErr()); - linkedProjThumb->initWithSpriteFrameName("unavailable.png"_spr); + linkedProjThumb->removeMeAndCleanup(); }; - - linkedProjThumb->setScale(1.f); - linkedProjThumb->setScale(linkedProjClippingNode->getScaledContentHeight() / linkedProjThumb->getScaledContentHeight()); - - linkedProjThumb->setPosition(linkedProjClippingNode->getPosition()); - linkedProjThumb->ignoreAnchorPointForPosition(false); - linkedProjThumb->setColor({ 250, 250, 250 }); - linkedProjThumb->setOpacity(250); }); bool isCustomThumbnail = false; // whether the thumbnail is custom @@ -592,12 +578,12 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { isCustomThumbnail = true; }; - std::string linkedVideoId = isCustomThumbnail ? "" : findYouTubeID(linkedProj.showcase, m_avalProject); // extracted video id from showcase url - std::string linkedProjThumbURL = isCustomThumbnail ? linkedProj.thumbnail : fmt::format("https://img.youtube.com/vi/{}/maxresdefault.jpg", (std::string)linkedVideoId); // custom thumbnail or formatted yt url + std::string encodedShowcaseUrl = url_encode(linkedProj.showcase); // encode the showcase url for use in the thumbnail url + std::string linkedProjThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/thumbnails?id={}", (int)m_avalProject.link_to_main.level_id); // custom thumbnail or default AVAL_LOG_DEBUG("Getting linked project thumbnail at {}...", linkedProjThumbURL); linkedProjThumb->loadFromUrl(linkedProjThumbURL, LazySprite::Format::kFmtUnKnown, false); - linkedProjClippingNode->addChild(linkedProjThumb); + if (linkedProjThumb) linkedProjClippingNode->addChild(linkedProjThumb); // set border auto linkedProjBorder = CCScale9Sprite::create("GJ_square07.png"); diff --git a/src/main.cpp b/src/main.cpp index 1dfe359..873ec30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -977,7 +977,7 @@ class $modify(Menu, MenuLayer) { }; }); auto avalReq = web::WebRequest(); - m_fields->avalWebListener.setFilter(avalReq.get("https://gh.cubicstudios.xyz/WebLPS/aval-project/code.txt")); + m_fields->avalWebListener.setFilter(avalReq.get("https://api.cubicstudios.xyz/avalanche/v1/featured/code")); } else { AVAL_LOG_ERROR("Avalanche featured project button disabled"); }; From ac415836dd34fe28fff07ca4e9ff96276bbd439c Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Wed, 2 Jul 2025 16:17:57 -0500 Subject: [PATCH 09/14] tweaks --- src/headers/src/ProjectInfoPopup.cpp | 90 +++++++++++----------------- 1 file changed, 36 insertions(+), 54 deletions(-) diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 878bf00..6eb72c0 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -394,20 +394,6 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { projThumb->ignoreAnchorPointForPosition(false); projThumb->setPosition({ m_clippingNode->getScaledContentWidth() / 2, m_clippingNode->getScaledContentHeight() / 2 }); - bool isCustomThumbnail = false; // whether the thumbnail is custom - - if (m_avalProject.thumbnail.empty()) { - AVAL_LOG_INFO("Using default thumbnail for project '{}'", m_avalProject.name); - isCustomThumbnail = false; - } else { - AVAL_LOG_INFO("Using custom thumbnail URL {}", m_avalProject.thumbnail); - isCustomThumbnail = true; - }; - - std::string projThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/thumbnails?id={}", (int)m_level->m_levelID.value()); // custom thumbnail or default - - AVAL_LOG_DEBUG("Getting thumbnail at {}...", projThumbURL); - projThumb->setLoadCallback([this, projThumb, bgSize](Result<> res) { if (res.isOk()) { AVAL_LOG_INFO("Sprite loaded successfully"); @@ -432,6 +418,9 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { }; }); + std::string projThumbURL = fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/thumbnails?id={}", (int)m_level->m_levelID.value()); // custom thumbnail + + AVAL_LOG_DEBUG("Getting thumbnail at {}...", (std::string)projThumbURL); projThumb->loadFromUrl(projThumbURL, LazySprite::Format::kFmtUnKnown, false); if (projThumb) m_clippingNode->addChild(projThumb); } else { @@ -542,49 +531,42 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjClippingNode->addChild(art_topRight_linkedProj); // create thumbnail lazy sprite for linked project - AVAL_LOG_DEBUG("Creating thumbnail lazy sprite for linked project '{}'", linkedProj.name); - LazySprite* linkedProjThumb = LazySprite::create(linkedProjMenu->getScaledContentSize(), true); - linkedProjThumb->setID("thumbnail"); - linkedProjThumb->setAnchorPoint({ 0.5, 0.5 }); - linkedProjThumb->ignoreAnchorPointForPosition(false); - linkedProjThumb->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() / 2.f }); - linkedProjThumb->setScale(0.5f); - - linkedProjThumb->setLoadCallback([linkedProjThumb, linkedProjClippingNode](Result<> res) { - if (res.isOk()) { - AVAL_LOG_INFO("Linked project thumbnail loaded successfully"); - - linkedProjThumb->setScale(1.f); - linkedProjThumb->setScale(linkedProjClippingNode->getScaledContentHeight() / linkedProjThumb->getScaledContentHeight()); - - linkedProjThumb->setPosition(linkedProjClippingNode->getPosition()); - linkedProjThumb->ignoreAnchorPointForPosition(false); - linkedProjThumb->setColor({ 250, 250, 250 }); - linkedProjThumb->setOpacity(250); - } else { - AVAL_LOG_ERROR("Failed to load linked project thumbnail: {}", res.unwrapErr()); - linkedProjThumb->removeMeAndCleanup(); - }; - }); - - bool isCustomThumbnail = false; // whether the thumbnail is custom - - // check if linked project has a custom thumbnail - if (linkedProj.thumbnail.empty()) { - AVAL_LOG_INFO("Using default thumbnail for project '{}'", linkedProj.name); - isCustomThumbnail = false; + if (AVAL_GEODE_MOD->getSettingValue("show-proj-thumb")) { + AVAL_LOG_DEBUG("Creating thumbnail lazy sprite for linked project '{}'", linkedProj.name); + LazySprite* linkedProjThumb = LazySprite::create(linkedProjMenu->getScaledContentSize(), true); + linkedProjThumb->setID("thumbnail"); + linkedProjThumb->setAnchorPoint({ 0.5, 0.5 }); + linkedProjThumb->ignoreAnchorPointForPosition(false); + linkedProjThumb->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() / 2.f }); + linkedProjThumb->setScale(0.5f); + + linkedProjThumb->setLoadCallback([linkedProjThumb, linkedProjClippingNode](Result<> res) { + if (res.isOk()) { + AVAL_LOG_INFO("Linked project thumbnail loaded successfully"); + + linkedProjThumb->setScale(1.f); + linkedProjThumb->setScale(linkedProjClippingNode->getScaledContentHeight() / linkedProjThumb->getScaledContentHeight()); + + linkedProjThumb->setPosition(linkedProjClippingNode->getPosition()); + linkedProjThumb->ignoreAnchorPointForPosition(false); + linkedProjThumb->setColor({ 250, 250, 250 }); + linkedProjThumb->setOpacity(250); + } else { + AVAL_LOG_ERROR("Failed to load linked project thumbnail: {}", res.unwrapErr()); + linkedProjThumb->removeMeAndCleanup(); + }; + }); + + std::string encodedShowcaseUrl = url_encode(linkedProj.showcase); // encode the showcase url for use in the thumbnail url + std::string linkedProjThumbURL = fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/yt-thumbnails?url={}", (std::string)encodedShowcaseUrl); // custom thumbnail + + AVAL_LOG_DEBUG("Getting linked project thumbnail at {}...", (std::string)linkedProjThumbURL); + linkedProjThumb->loadFromUrl(linkedProjThumbURL, LazySprite::Format::kFmtUnKnown, false); + if (linkedProjThumb) linkedProjClippingNode->addChild(linkedProjThumb); } else { - AVAL_LOG_INFO("Using custom thumbnail URL {}", linkedProj.thumbnail); - isCustomThumbnail = true; + AVAL_LOG_DEBUG("Linked project thumbnail setting is disabled, not adding thumbnail to linked project container"); }; - std::string encodedShowcaseUrl = url_encode(linkedProj.showcase); // encode the showcase url for use in the thumbnail url - std::string linkedProjThumbURL = isCustomThumbnail ? m_avalProject.thumbnail : fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/thumbnails?id={}", (int)m_avalProject.link_to_main.level_id); // custom thumbnail or default - - AVAL_LOG_DEBUG("Getting linked project thumbnail at {}...", linkedProjThumbURL); - linkedProjThumb->loadFromUrl(linkedProjThumbURL, LazySprite::Format::kFmtUnKnown, false); - if (linkedProjThumb) linkedProjClippingNode->addChild(linkedProjThumb); - // set border auto linkedProjBorder = CCScale9Sprite::create("GJ_square07.png"); linkedProjBorder->setID("border"); From e97a116ec10c8938619db0f189aca1230c99ae79 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Wed, 2 Jul 2025 16:46:29 -0500 Subject: [PATCH 10/14] adjustments --- src/headers/src/AvalancheFeatured.cpp | 2 +- src/headers/src/ProjectInfoPopup.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/headers/src/AvalancheFeatured.cpp b/src/headers/src/AvalancheFeatured.cpp index 44c58ff..80d425f 100644 --- a/src/headers/src/AvalancheFeatured.cpp +++ b/src/headers/src/AvalancheFeatured.cpp @@ -209,7 +209,7 @@ bool AvalancheFeatured::setup() { projThumb->setPosition({ m_mainLayer->getContentWidth() / 2.f, m_mainLayer->getContentHeight() / 2.f }); projThumb->setLoadCallback([this, projThumb](Result<> res) { - if (res) { + if (res.isOk()) { // Success: scale and position the sprite AVAL_LOG_INFO("Sprite loaded successfully"); } else { diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 6eb72c0..3e8ef42 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -413,7 +413,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { AVAL_LOG_DEBUG("Custom thumbnail loaded, keeping position to center"); }; } else { - AVAL_LOG_ERROR("{}", res.unwrapErr()); + AVAL_LOG_ERROR("Failed to get project thumbnail, {}", res.unwrapErr()); projThumb->removeMeAndCleanup(); }; }); From be9c4d4ad4dfdcc7c26a26cddbf8ca083f28df2c Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Wed, 2 Jul 2025 19:49:47 -0500 Subject: [PATCH 11/14] rework showcase button --- changelog.md | 1 + src/headers/ProjectInfoPopup.hpp | 5 + src/headers/src/ProjectInfoPopup.cpp | 276 +++++++++++++++++++++++---- 3 files changed, 243 insertions(+), 39 deletions(-) diff --git a/changelog.md b/changelog.md index aeb04ae..08038d1 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ #### Changes - **Updated** Geode compatibility to version `4.6.3` - **Added** promotional container for parent Team Project, if any, in information pop-up for independent levels +- **Reworked** showcase menu in Team Project information pop-up - **Fixed** thumbnail error for Team levels in Team Project information pop-up - Team Projects may now show a customized thumbnail on the Team Project information pop-up diff --git a/src/headers/ProjectInfoPopup.hpp b/src/headers/ProjectInfoPopup.hpp index 5ebaf07..ed04f03 100644 --- a/src/headers/ProjectInfoPopup.hpp +++ b/src/headers/ProjectInfoPopup.hpp @@ -18,9 +18,11 @@ class ProjectInfoPopup : public geode::Popup<> { protected: std::string m_avalPublisher = "Avalanche"; + std::string m_linkedPublisher = "Avalanche"; std::string m_cornerArtType = "rewardCorner_001.png"; Project m_avalProject; + Project m_linkedProject; GJGameLevel* m_level; @@ -35,5 +37,8 @@ class ProjectInfoPopup : public geode::Popup<> { void onPlayShowcase(CCObject*); + void infoPopupLinked(CCObject*); + void onPlayShowcaseLinked(CCObject*); + bool setup() override; }; \ No newline at end of file diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 3e8ef42..805df7f 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -93,46 +93,110 @@ void ProjectInfoPopup::infoPopup(CCObject*) { true); }; -void ProjectInfoPopup::onFameInfo(CCObject*) { +void ProjectInfoPopup::onPlayShowcase(CCObject*) { std::ostringstream body; - body << "'" << m_avalProject.name << "' by '" << m_avalPublisher << "' is featured in Avalanche's Hall of Fame. It is a special list of levels that are considered to be the best of the best from the team."; + body << "Watch the full showcase video for " << m_linkedPublisher << " - '" << m_avalProject.name << "'?"; std::string resultBody = body.str(); createQuickPopup( - "Hall of Fame", + m_avalProject.name.c_str(), resultBody.c_str(), - "OK", "Learn More", - [](auto, bool btn2) { + "Cancel", "Watch", + [this](auto, bool btn2) { if (btn2) { - AVAL_LOG_INFO("Opening Hall of Fame link in browser"); - web::openLinkInBrowser(URL_AVALANCHE); + AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase); + web::openLinkInBrowser(this->m_avalProject.showcase); + } else { + AVAL_LOG_DEBUG("User clicked Cancel"); + }; }, + true); +}; + +void ProjectInfoPopup::infoPopupLinked(CCObject*) { + std::ostringstream typeOfProj; + + switch (m_linkedProject.type) { + case Project::Type::TEAM: + typeOfProj << "a team project hosted by " << m_linkedProject.host << ""; + break; + + case Project::Type::COLLAB: + typeOfProj << "a collaboration project hosted by Avalanche. One or more guest creators partook in the creation of this level"; + break; + + case Project::Type::EVENT: + typeOfProj << "an event level. It is the winner of a public or private event hosted by Avalanche"; + break; + + case Project::Type::SOLO: + typeOfProj << "a featured solo level. A member of Avalanche created this level on their own"; + break; + + default: + typeOfProj << "an official Avalanche project"; + break; + }; + + std::ostringstream body; + body << "" << m_linkedPublisher << " - '" << m_linkedProject.name << "' is " << typeOfProj.str() << ". You can watch its showcase here."; + + std::string resultBody = body.str(); + + createQuickPopup( + m_linkedProject.name.c_str(), + resultBody.c_str(), + "OK", "Watch", + [this](auto, bool btn2) { + if (btn2) { + AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_linkedProject.showcase); + web::openLinkInBrowser(this->m_linkedProject.showcase); } else { AVAL_LOG_DEBUG("User clicked OK"); }; }, true); }; -void ProjectInfoPopup::onPlayShowcase(CCObject*) { +void ProjectInfoPopup::onPlayShowcaseLinked(CCObject*) { std::ostringstream body; - body << "Watch the full showcase video for " << m_avalPublisher << " - '" << m_avalProject.name << "'?"; + body << "Watch the full showcase video for " << m_linkedPublisher << " - '" << m_linkedProject.name << "'?"; std::string resultBody = body.str(); createQuickPopup( - m_avalProject.name.c_str(), + m_linkedProject.name.c_str(), resultBody.c_str(), "Cancel", "Watch", [this](auto, bool btn2) { if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase); - web::openLinkInBrowser(this->m_avalProject.showcase); + AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_linkedProject.showcase); + web::openLinkInBrowser(this->m_linkedProject.showcase); } else { AVAL_LOG_DEBUG("User clicked Cancel"); }; }, true); }; +void ProjectInfoPopup::onFameInfo(CCObject*) { + std::ostringstream body; + body << "The level '" << m_avalProject.name << "' by " << m_avalPublisher << " is featured in Avalanche's Hall of Fame. It is a special list of levels that are considered to be the best of the best from the team."; + + std::string resultBody = body.str(); + + createQuickPopup( + "Hall of Fame", + resultBody.c_str(), + "OK", "Learn More", + [](auto, bool btn2) { + if (btn2) { + AVAL_LOG_INFO("Opening Hall of Fame link in browser"); + web::openLinkInBrowser(URL_AVALANCHE); + } else { + AVAL_LOG_DEBUG("User clicked OK"); + }; }, + true); +}; + void ProjectInfoPopup::settingsPopup(CCObject*) { AVAL_LOG_INFO("Opening settings popup"); openSettingsPopup(getMod()); @@ -227,6 +291,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { fameIcon->setScale(0.25f); auto fameLabel = CCLabelBMFont::create("Avalanche Hall of Fame", "goldFont.fnt"); + fameLabel->setAlignment(CCTextAlignment::kCCTextAlignmentCenter); fameLabel->ignoreAnchorPointForPosition(false); fameLabel->setAnchorPoint({ 0.5, 0.5 }); fameLabel->setScale(0.375f); @@ -348,27 +413,163 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { auto hostName_label = CCLabelBMFont::create(hostLabelTxt, "bigFont.fnt"); hostName_label->setID("host-name-label"); - hostName_label->ignoreAnchorPointForPosition(false); hostName_label->setAnchorPoint({ 0, 1 }); + hostName_label->ignoreAnchorPointForPosition(false); + hostName_label->setAlignment(CCTextAlignment::kCCTextAlignmentCenter); hostName_label->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 55.f }); hostName_label->setScale(0.25f); auto hostName = CCLabelBMFont::create(m_avalProject.host.c_str(), "goldFont.fnt"); hostName->setID("host-name"); - hostName->ignoreAnchorPointForPosition(false); hostName->setAnchorPoint({ 0, 1 }); + hostName->ignoreAnchorPointForPosition(false); + hostName->setAlignment(CCTextAlignment::kCCTextAlignmentCenter); hostName->setPosition({ 10.f, m_mainLayer->getScaledContentHeight() - 65.f }); hostName->setScale(0.75f); m_overlayMenu->addChild(hostName_label); m_overlayMenu->addChild(hostName); - auto playShowcase_label = CCLabelBMFont::create("Watch the Showcase", "chatFont.fnt"); + AVAL_LOG_INFO("Adding showcase container to main project '{}'", m_avalProject.name); + + // create showcase project button + AVAL_LOG_DEBUG("Creating showcase project button"); + auto showcaseProjMenu = CCMenu::create(); + showcaseProjMenu->setID("showcase-project-menu"); + showcaseProjMenu->setAnchorPoint({ 0.5, 0 }); + showcaseProjMenu->ignoreAnchorPointForPosition(false); + showcaseProjMenu->setPosition({ m_mainLayer->getScaledContentWidth() / 2.f, 12.5f }); + showcaseProjMenu->setScaledContentSize({ 192.f, 108.f }); + showcaseProjMenu->setZOrder(2); + + m_clippingNode->addChild(showcaseProjMenu); + + // create clipping node for showcase project + AVAL_LOG_DEBUG("Creating clipping node for showcase project"); + auto showcaseProjClippingNode = CCClippingNode::create(); + showcaseProjClippingNode->setID("clipping-node"); + showcaseProjClippingNode->setContentSize(showcaseProjMenu->getScaledContentSize()); + showcaseProjClippingNode->ignoreAnchorPointForPosition(false); + showcaseProjClippingNode->setAnchorPoint({ 0.5f, 0.5f }); + showcaseProjClippingNode->setPosition({ showcaseProjMenu->getScaledContentWidth() / 2.f, showcaseProjMenu->getScaledContentHeight() / 2.f }); + showcaseProjClippingNode->setStencil(CCLayerColor::create({ 250, 250, 250 }, showcaseProjMenu->getScaledContentWidth(), showcaseProjMenu->getScaledContentHeight()));; + showcaseProjClippingNode->setZOrder(-1); + + showcaseProjMenu->addChild(showcaseProjClippingNode); + + auto showcaseProjClippingNodeBg = CCScale9Sprite::create("GJ_square04.png"); + showcaseProjClippingNodeBg->setID("background"); + showcaseProjClippingNodeBg->setContentSize(showcaseProjClippingNode->getContentSize()); + showcaseProjClippingNodeBg->setPosition(showcaseProjClippingNode->getPosition()); + showcaseProjClippingNodeBg->setAnchorPoint(showcaseProjClippingNode->getAnchorPoint()); + showcaseProjClippingNodeBg->setZOrder(-1); + + showcaseProjClippingNode->addChild(showcaseProjClippingNodeBg); + + // corner art deco for showcase project + auto corner = "rewardCorner_001.png"; + + auto art_bottomLeft_showcaseProj = CCSprite::createWithSpriteFrameName(corner); + art_bottomLeft_showcaseProj->setID("bottom-left-corner"); + art_bottomLeft_showcaseProj->setAnchorPoint({ 0, 0 }); + art_bottomLeft_showcaseProj->setPosition({ 0, 0 }); + art_bottomLeft_showcaseProj->setScale(0.75f); + art_bottomLeft_showcaseProj->setFlipX(false); + art_bottomLeft_showcaseProj->setFlipY(false); + art_bottomLeft_showcaseProj->setZOrder(4); + + showcaseProjClippingNode->addChild(art_bottomLeft_showcaseProj); + + auto art_bottomRight_showcaseProj = CCSprite::createWithSpriteFrameName(corner); + art_bottomRight_showcaseProj->setID("bottom-right-corner"); + art_bottomRight_showcaseProj->setAnchorPoint({ 1, 0 }); + art_bottomRight_showcaseProj->setPosition({ showcaseProjClippingNode->getScaledContentWidth(), 0 }); + art_bottomRight_showcaseProj->setScale(0.75f); + art_bottomRight_showcaseProj->setFlipX(true); + art_bottomRight_showcaseProj->setFlipY(false); + art_bottomRight_showcaseProj->setZOrder(4); + + showcaseProjClippingNode->addChild(art_bottomRight_showcaseProj); + + auto art_topLeft_showcaseProj = CCSprite::createWithSpriteFrameName(corner); + art_topLeft_showcaseProj->setID("top-left-corner"); + art_topLeft_showcaseProj->setAnchorPoint({ 0, 1 }); + art_topLeft_showcaseProj->setPosition({ 0, showcaseProjClippingNode->getScaledContentHeight() }); + art_topLeft_showcaseProj->setScale(0.75f); + art_topLeft_showcaseProj->setFlipX(false); + art_topLeft_showcaseProj->setFlipY(true); + art_topLeft_showcaseProj->setZOrder(4); + + showcaseProjClippingNode->addChild(art_topLeft_showcaseProj); + + auto art_topRight_showcaseProj = CCSprite::createWithSpriteFrameName(corner); + art_topRight_showcaseProj->setID("top-right-corner"); + art_topRight_showcaseProj->setAnchorPoint({ 1, 1 }); + art_topRight_showcaseProj->setPosition({ showcaseProjClippingNode->getScaledContentWidth(), showcaseProjClippingNode->getScaledContentHeight() }); + art_topRight_showcaseProj->setScale(0.75f); + art_topRight_showcaseProj->setFlipX(true); + art_topRight_showcaseProj->setFlipY(true); + art_topRight_showcaseProj->setZOrder(4); + + showcaseProjClippingNode->addChild(art_topRight_showcaseProj); + + // create thumbnail lazy sprite for showcase project + if (AVAL_GEODE_MOD->getSettingValue("show-proj-thumb")) { + AVAL_LOG_DEBUG("Creating thumbnail lazy sprite for showcase project '{}'", m_avalProject.name); + LazySprite* showcaseProjThumb = LazySprite::create(showcaseProjMenu->getScaledContentSize(), true); + showcaseProjThumb->setID("thumbnail"); + showcaseProjThumb->setAnchorPoint({ 0.5, 0.5 }); + showcaseProjThumb->ignoreAnchorPointForPosition(false); + showcaseProjThumb->setPosition({ showcaseProjMenu->getScaledContentWidth() / 2.f, showcaseProjMenu->getScaledContentHeight() / 2.f }); + showcaseProjThumb->setScale(0.5f); + + showcaseProjThumb->setLoadCallback([showcaseProjThumb, showcaseProjClippingNode](Result<> res) { + if (res.isOk()) { + AVAL_LOG_INFO("Showcase project thumbnail loaded successfully"); + + showcaseProjThumb->setScale(1.f); + showcaseProjThumb->setScale(showcaseProjClippingNode->getScaledContentHeight() / showcaseProjThumb->getScaledContentHeight()); + + showcaseProjThumb->setPosition(showcaseProjClippingNode->getPosition()); + showcaseProjThumb->ignoreAnchorPointForPosition(false); + showcaseProjThumb->setColor({ 125, 125, 125 }); + showcaseProjThumb->setOpacity(125); + } else { + AVAL_LOG_ERROR("Failed to load showcase project thumbnail: {}", res.unwrapErr()); + showcaseProjThumb->removeMeAndCleanup(); + }; + }); + + std::string encodedShowcaseUrl = url_encode(m_avalProject.showcase); // encode the showcase url for use in the thumbnail url + std::string showcaseProjThumbURL = fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/yt-thumbnails?url={}", (std::string)encodedShowcaseUrl); // custom thumbnail + + AVAL_LOG_DEBUG("Getting showcase project thumbnail at {}...", (std::string)showcaseProjThumbURL); + showcaseProjThumb->loadFromUrl(showcaseProjThumbURL, LazySprite::Format::kFmtUnKnown, false); + if (showcaseProjThumb) showcaseProjClippingNode->addChild(showcaseProjThumb); + } else { + AVAL_LOG_DEBUG("Showcase project thumbnail setting is disabled, not adding thumbnail to showcase containered project container"); + }; + + // set border + auto showcaseProjBorder = CCScale9Sprite::create("GJ_square07.png"); + showcaseProjBorder->setID("border"); + showcaseProjBorder->setPosition(showcaseProjClippingNode->getPosition()); + showcaseProjBorder->setContentSize(showcaseProjClippingNode->getScaledContentSize()); + showcaseProjBorder->ignoreAnchorPointForPosition(false); + showcaseProjBorder->setAnchorPoint({ 0.5f, 0.5f }); + showcaseProjBorder->setZOrder(3); + + // add border to clipping node + showcaseProjClippingNode->addChild(showcaseProjBorder); + + auto playShowcase_label = CCLabelBMFont::create("Watch the\nShowcase", "bigFont.fnt"); playShowcase_label->setID("play-showcase-label"); - playShowcase_label->ignoreAnchorPointForPosition(false); + playShowcase_label->setScale(0.5f); playShowcase_label->setAnchorPoint({ 0.5, 0.5 }); - playShowcase_label->setPosition({ m_mainLayer->getScaledContentWidth() / 2.f, 60.f }); - playShowcase_label->setScale(1.f); + playShowcase_label->ignoreAnchorPointForPosition(false); + playShowcase_label->setAlignment(CCTextAlignment::kCCTextAlignmentCenter); + playShowcase_label->setPosition({ showcaseProjMenu->getScaledContentWidth() / 2.f, 75.f }); + playShowcase_label->setZOrder(3); auto playShowcase_sprite = CCSprite::createWithSpriteFrameName("GJ_playBtn2_001.png"); playShowcase_sprite->setScale(0.5f); @@ -377,11 +578,12 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { playShowcase_sprite, this, menu_selector(ProjectInfoPopup::onPlayShowcase)); - playShowcase->setID("play-showcase-button"); - playShowcase->setPosition({ m_mainLayer->getScaledContentWidth() / 2.f, 30.f }); + playShowcase->setID("play-showcase"); + playShowcase->setPosition({ showcaseProjMenu->getScaledContentWidth() / 2.f, 37.5f }); + playShowcase->setZOrder(4); - m_overlayMenu->addChild(playShowcase_label); - m_overlayMenu->addChild(playShowcase); + showcaseProjMenu->addChild(playShowcase_label); + showcaseProjMenu->addChild(playShowcase); // project thumbnail if (AVAL_GEODE_MOD->getSettingValue("show-proj-thumb")) { @@ -405,13 +607,6 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { projThumb->ignoreAnchorPointForPosition(false); projThumb->setColor({ 125, 125, 125 }); projThumb->setOpacity(125); - - if (m_avalProject.thumbnail.empty()) { - projThumb->setPosition({ 0, 0 }); - projThumb->setAnchorPoint({ 0, 0 }); - } else { - AVAL_LOG_DEBUG("Custom thumbnail loaded, keeping position to center"); - }; } else { AVAL_LOG_ERROR("Failed to get project thumbnail, {}", res.unwrapErr()); projThumb->removeMeAndCleanup(); @@ -429,12 +624,12 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { if (m_avalProject.link_to_main.enabled) { AVAL_LOG_DEBUG("Project '{}' has a link to the main project", m_avalProject.name); - auto linkedProj = avalHandler->GetProject(m_avalProject.link_to_main.level_id); + m_linkedProject = avalHandler->GetProject(m_avalProject.link_to_main.level_id); - if (linkedProj.type == Project::Type::NONE) { + if (m_linkedProject.type == Project::Type::NONE) { AVAL_LOG_ERROR("Failed to get linked project with ID {}", m_avalProject.link_to_main.level_id); } else { - AVAL_LOG_INFO("Adding link to main project '{}'", linkedProj.name); + AVAL_LOG_INFO("Adding link to main project '{}'", m_linkedProject.name); // create linked project button AVAL_LOG_DEBUG("Creating linked project button"); @@ -444,6 +639,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjMenu->ignoreAnchorPointForPosition(false); linkedProjMenu->setPosition({ m_mainLayer->getScaledContentWidth() - 25.f, m_mainLayer->getScaledContentHeight() - 55.f }); linkedProjMenu->setScaledContentSize({ 150.f, 81.f }); + linkedProjMenu->setZOrder(2); m_clippingNode->addChild(linkedProjMenu); @@ -454,7 +650,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { auto linkedProjInfoBtn = CCMenuItemSpriteExtra::create( linkedProjInfoBtnSprite, this, - menu_selector(ProjectInfoPopup::infoPopup)); + menu_selector(ProjectInfoPopup::infoPopupLinked)); linkedProjInfoBtn->setID("info-button"); linkedProjInfoBtn->setPosition({ linkedProjMenu->getScaledContentWidth() - 6.25f, linkedProjMenu->getScaledContentHeight() - 6.25f }); linkedProjInfoBtn->setZOrder(126); @@ -474,7 +670,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { linkedProjMenu->addChild(linkedProjClippingNode); - auto linkedProjClippingNodeBg = CCScale9Sprite::create("GJ_square01.png"); + auto linkedProjClippingNodeBg = CCScale9Sprite::create("GJ_square02.png"); linkedProjClippingNodeBg->setID("background"); linkedProjClippingNodeBg->setContentSize(linkedProjClippingNode->getContentSize()); linkedProjClippingNodeBg->setPosition(linkedProjClippingNode->getPosition()); @@ -532,7 +728,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { // create thumbnail lazy sprite for linked project if (AVAL_GEODE_MOD->getSettingValue("show-proj-thumb")) { - AVAL_LOG_DEBUG("Creating thumbnail lazy sprite for linked project '{}'", linkedProj.name); + AVAL_LOG_DEBUG("Creating thumbnail lazy sprite for linked project '{}'", m_linkedProject.name); LazySprite* linkedProjThumb = LazySprite::create(linkedProjMenu->getScaledContentSize(), true); linkedProjThumb->setID("thumbnail"); linkedProjThumb->setAnchorPoint({ 0.5, 0.5 }); @@ -557,7 +753,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { }; }); - std::string encodedShowcaseUrl = url_encode(linkedProj.showcase); // encode the showcase url for use in the thumbnail url + std::string encodedShowcaseUrl = url_encode(m_linkedProject.showcase); // encode the showcase url for use in the thumbnail url std::string linkedProjThumbURL = fmt::format("https://api.cubicstudios.xyz/avalanche/v1/fetch/yt-thumbnails?url={}", (std::string)encodedShowcaseUrl); // custom thumbnail AVAL_LOG_DEBUG("Getting linked project thumbnail at {}...", (std::string)linkedProjThumbURL); @@ -588,7 +784,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { auto linkedProjShowcase = CCMenuItemSpriteExtra::create( linkedProjShowcase_sprite, this, - menu_selector(ProjectInfoPopup::onPlayShowcase)); + menu_selector(ProjectInfoPopup::onPlayShowcaseLinked)); linkedProjShowcase->setID("button"); linkedProjShowcase->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, 25.f }); @@ -598,6 +794,7 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { AVAL_LOG_DEBUG("Creating label for linked project showcase"); auto linkedProjLabel = CCLabelBMFont::create("Play Now!", "bigFont.fnt"); linkedProjLabel->setID("label"); + linkedProjLabel->setAlignment(CCTextAlignment::kCCTextAlignmentCenter); linkedProjLabel->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() - 10.f }); linkedProjLabel->setScale(0.25f); @@ -605,9 +802,10 @@ ProjectInfoPopup* ProjectInfoPopup::setProject(GJGameLevel* level) { // create name text label for linked project name AVAL_LOG_DEBUG("Creating label for linked project name"); - auto linkedProjName = CCLabelBMFont::create(linkedProj.name.c_str(), "goldFont.fnt"); + auto linkedProjName = CCLabelBMFont::create(m_linkedProject.name.c_str(), "goldFont.fnt"); linkedProjName->setID("name"); - linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() - 20.f }); + linkedProjName->setAlignment(CCTextAlignment::kCCTextAlignmentCenter); + linkedProjName->setPosition({ linkedProjMenu->getScaledContentWidth() / 2.f, linkedProjMenu->getScaledContentHeight() - 25.f }); linkedProjName->setScale(0.625f); linkedProjMenu->addChild(linkedProjName); From 51965406016842187ea31012686fac98ed2be9aa Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Thu, 3 Jul 2025 08:16:50 -0500 Subject: [PATCH 12/14] optimize btns --- README.md | 2 +- SECURITY.md | 6 +- about.md | 2 +- changelog.md | 4 +- src/headers/ProjectInfoPopup.hpp | 4 +- src/headers/src/ProjectInfoPopup.cpp | 112 ++++++++------------------- 6 files changed, 43 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index c88c26c..7880799 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ When viewing level cells or pages, you may notice the changes in the background Level cells with a glow emerging from the side represent levels in Avalanche's hall of fame. These levels stand out the most among the team's creations. By default, unrated or unlisted levels will not be highlighted. -If a level is featured on Avalanche's servers, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its showcase video thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. +If a level is featured on Avalanche's servers, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its level thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. --- diff --git a/SECURITY.md b/SECURITY.md index fe15e26..474f8cb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,10 @@ List of versions of this mod that are currently being supported with security updates. | Version | Supported | -| ------- | --------- | -| 1.0.x | ✅ | +| ------- | :-------: | +| 1.2.x | ✅ | +| 1.1.x | ✅ | +| 1.0.x | ❌ | ## Reporting a Vulnerability If you discover a security vulnerability, please follow the steps below: diff --git a/about.md b/about.md index 419c599..cc10e5a 100644 --- a/about.md +++ b/about.md @@ -43,7 +43,7 @@ When viewing level cells or pages, you may notice the changes in the background Level cells with a glow emerging from the side represent levels in Avalanche's hall of fame. These levels stand out the most among the team's creations. By default, unrated or unlisted levels will not be highlighted. -If a level is featured on Avalanche's servers, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its showcase video thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. +If a level is featured on Avalanche's servers, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its level thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. --- diff --git a/changelog.md b/changelog.md index 08038d1..1083c80 100644 --- a/changelog.md +++ b/changelog.md @@ -4,8 +4,8 @@ #### Changes - **Updated** Geode compatibility to version `4.6.3` - **Added** promotional container for parent Team Project, if any, in information pop-up for independent levels -- **Reworked** showcase menu in Team Project information pop-up -- **Fixed** thumbnail error for Team levels in Team Project information pop-up +- **Reworked** level showcase menu in Team Project information pop-up +- **Fixed** thumbnail loading error for Team levels in Team Project information pop-up - Team Projects may now show a customized thumbnail on the Team Project information pop-up #### Developers diff --git a/src/headers/ProjectInfoPopup.hpp b/src/headers/ProjectInfoPopup.hpp index ed04f03..a0248eb 100644 --- a/src/headers/ProjectInfoPopup.hpp +++ b/src/headers/ProjectInfoPopup.hpp @@ -15,7 +15,6 @@ class ProjectInfoPopup : public geode::Popup<> { ProjectInfoPopup* setProject(GJGameLevel* level); void show() override; - protected: std::string m_avalPublisher = "Avalanche"; std::string m_linkedPublisher = "Avalanche"; @@ -30,6 +29,9 @@ class ProjectInfoPopup : public geode::Popup<> { CCClippingNode* m_clippingNode; + void doInfo(Project proj); + void doShowcase(Project proj); + void infoPopup(CCObject*); void settingsPopup(CCObject*); diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 805df7f..002862d 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -38,23 +38,12 @@ inline std::string url_encode(const std::string& value) { return escaped.str(); } -ProjectInfoPopup* ProjectInfoPopup::create() { - auto ret = new ProjectInfoPopup; - if (ret->initAnchored(440, 290)) { - ret->autorelease(); - return ret; - }; - - CC_SAFE_DELETE(ret); - return nullptr; -}; - -void ProjectInfoPopup::infoPopup(CCObject*) { +void ProjectInfoPopup::doInfo(Project proj) { std::ostringstream typeOfProj; - switch (m_avalProject.type) { + switch (proj.type) { case Project::Type::TEAM: - typeOfProj << "a team project hosted by " << m_avalProject.host << ""; + typeOfProj << "a team project hosted by " << proj.host << ""; break; case Project::Type::COLLAB: @@ -75,106 +64,69 @@ void ProjectInfoPopup::infoPopup(CCObject*) { }; std::ostringstream body; - body << "" << m_avalPublisher << " - '" << m_avalProject.name << "' is " << typeOfProj.str() << ". You can watch its showcase here."; + body << "" << m_avalPublisher << " - '" << proj.name << "' is " << typeOfProj.str() << ". You can watch its showcase here."; std::string resultBody = body.str(); createQuickPopup( - m_avalProject.name.c_str(), + proj.name.c_str(), resultBody.c_str(), "OK", "Watch", - [this](auto, bool btn2) { + [proj](auto, bool btn2) { if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase); - web::openLinkInBrowser(this->m_avalProject.showcase); + AVAL_LOG_INFO("Opening showcase link in browser: {}", proj.showcase); + web::openLinkInBrowser(proj.showcase); } else { AVAL_LOG_DEBUG("User clicked OK"); }; }, true); }; -void ProjectInfoPopup::onPlayShowcase(CCObject*) { +void ProjectInfoPopup::doShowcase(Project proj) { std::ostringstream body; - body << "Watch the full showcase video for " << m_linkedPublisher << " - '" << m_avalProject.name << "'?"; + body << "Watch the full showcase video for " << m_linkedPublisher << " - '" << proj.name << "'?"; std::string resultBody = body.str(); createQuickPopup( - m_avalProject.name.c_str(), + proj.name.c_str(), resultBody.c_str(), "Cancel", "Watch", - [this](auto, bool btn2) { + [proj](auto, bool btn2) { if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_avalProject.showcase); - web::openLinkInBrowser(this->m_avalProject.showcase); + AVAL_LOG_INFO("Opening showcase link in browser: {}", proj.showcase); + web::openLinkInBrowser(proj.showcase); } else { AVAL_LOG_DEBUG("User clicked Cancel"); }; }, true); }; -void ProjectInfoPopup::infoPopupLinked(CCObject*) { - std::ostringstream typeOfProj; - - switch (m_linkedProject.type) { - case Project::Type::TEAM: - typeOfProj << "a team project hosted by " << m_linkedProject.host << ""; - break; - - case Project::Type::COLLAB: - typeOfProj << "a collaboration project hosted by Avalanche. One or more guest creators partook in the creation of this level"; - break; - - case Project::Type::EVENT: - typeOfProj << "an event level. It is the winner of a public or private event hosted by Avalanche"; - break; - - case Project::Type::SOLO: - typeOfProj << "a featured solo level. A member of Avalanche created this level on their own"; - break; - - default: - typeOfProj << "an official Avalanche project"; - break; +ProjectInfoPopup* ProjectInfoPopup::create() { + auto ret = new ProjectInfoPopup; + if (ret->initAnchored(440, 290)) { + ret->autorelease(); + return ret; }; - std::ostringstream body; - body << "" << m_linkedPublisher << " - '" << m_linkedProject.name << "' is " << typeOfProj.str() << ". You can watch its showcase here."; - - std::string resultBody = body.str(); + CC_SAFE_DELETE(ret); + return nullptr; +}; - createQuickPopup( - m_linkedProject.name.c_str(), - resultBody.c_str(), - "OK", "Watch", - [this](auto, bool btn2) { - if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_linkedProject.showcase); - web::openLinkInBrowser(this->m_linkedProject.showcase); - } else { - AVAL_LOG_DEBUG("User clicked OK"); - }; }, - true); +void ProjectInfoPopup::infoPopup(CCObject*) { + doInfo(m_avalProject); }; -void ProjectInfoPopup::onPlayShowcaseLinked(CCObject*) { - std::ostringstream body; - body << "Watch the full showcase video for " << m_linkedPublisher << " - '" << m_linkedProject.name << "'?"; +void ProjectInfoPopup::onPlayShowcase(CCObject*) { + doShowcase(m_avalProject); +}; - std::string resultBody = body.str(); +void ProjectInfoPopup::infoPopupLinked(CCObject*) { + doInfo(m_linkedProject); +}; - createQuickPopup( - m_linkedProject.name.c_str(), - resultBody.c_str(), - "Cancel", "Watch", - [this](auto, bool btn2) { - if (btn2) { - AVAL_LOG_INFO("Opening showcase link in browser: {}", this->m_linkedProject.showcase); - web::openLinkInBrowser(this->m_linkedProject.showcase); - } else { - AVAL_LOG_DEBUG("User clicked Cancel"); - }; }, - true); +void ProjectInfoPopup::onPlayShowcaseLinked(CCObject*) { + doShowcase(m_linkedProject); }; void ProjectInfoPopup::onFameInfo(CCObject*) { From 5b3fbd59627b6f23abe68bfe0e5f728cf16d6d24 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Thu, 3 Jul 2025 08:29:13 -0500 Subject: [PATCH 13/14] fix publishers --- changelog.md | 3 +-- src/headers/ProjectInfoPopup.hpp | 4 ++-- src/headers/src/ProjectInfoPopup.cpp | 16 ++++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/changelog.md b/changelog.md index 1083c80..23679df 100644 --- a/changelog.md +++ b/changelog.md @@ -3,10 +3,9 @@ #### Changes - **Updated** Geode compatibility to version `4.6.3` +- **Added** level thumbnail background to Team Project information pop-up - **Added** promotional container for parent Team Project, if any, in information pop-up for independent levels - **Reworked** level showcase menu in Team Project information pop-up -- **Fixed** thumbnail loading error for Team levels in Team Project information pop-up -- Team Projects may now show a customized thumbnail on the Team Project information pop-up #### Developers - **Updated** URLs to use Cubic Studios API endpoints for all web requests diff --git a/src/headers/ProjectInfoPopup.hpp b/src/headers/ProjectInfoPopup.hpp index a0248eb..5e7fef5 100644 --- a/src/headers/ProjectInfoPopup.hpp +++ b/src/headers/ProjectInfoPopup.hpp @@ -29,8 +29,8 @@ class ProjectInfoPopup : public geode::Popup<> { CCClippingNode* m_clippingNode; - void doInfo(Project proj); - void doShowcase(Project proj); + void doInfo(Project proj, std::string publisher); + void doShowcase(Project proj, std::string publisher); void infoPopup(CCObject*); void settingsPopup(CCObject*); diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index 002862d..ea2fec7 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -38,7 +38,7 @@ inline std::string url_encode(const std::string& value) { return escaped.str(); } -void ProjectInfoPopup::doInfo(Project proj) { +void ProjectInfoPopup::doInfo(Project proj, std::string publisher) { std::ostringstream typeOfProj; switch (proj.type) { @@ -64,7 +64,7 @@ void ProjectInfoPopup::doInfo(Project proj) { }; std::ostringstream body; - body << "" << m_avalPublisher << " - '" << proj.name << "' is " << typeOfProj.str() << ". You can watch its showcase here."; + body << "" << publisher << " - '" << proj.name << "' is " << typeOfProj.str() << ". You can watch its showcase here."; std::string resultBody = body.str(); @@ -82,9 +82,9 @@ void ProjectInfoPopup::doInfo(Project proj) { true); }; -void ProjectInfoPopup::doShowcase(Project proj) { +void ProjectInfoPopup::doShowcase(Project proj, std::string publisher) { std::ostringstream body; - body << "Watch the full showcase video for " << m_linkedPublisher << " - '" << proj.name << "'?"; + body << "Watch the full showcase video for " << publisher << " - '" << proj.name << "'?"; std::string resultBody = body.str(); @@ -114,19 +114,19 @@ ProjectInfoPopup* ProjectInfoPopup::create() { }; void ProjectInfoPopup::infoPopup(CCObject*) { - doInfo(m_avalProject); + ProjectInfoPopup::doInfo(m_avalProject, m_avalPublisher); }; void ProjectInfoPopup::onPlayShowcase(CCObject*) { - doShowcase(m_avalProject); + ProjectInfoPopup::doShowcase(m_avalProject, m_avalPublisher); }; void ProjectInfoPopup::infoPopupLinked(CCObject*) { - doInfo(m_linkedProject); + ProjectInfoPopup::doInfo(m_linkedProject, m_linkedPublisher); }; void ProjectInfoPopup::onPlayShowcaseLinked(CCObject*) { - doShowcase(m_linkedProject); + ProjectInfoPopup::doShowcase(m_linkedProject, m_linkedPublisher); }; void ProjectInfoPopup::onFameInfo(CCObject*) { From 92a12004743ed6ef0503a7c46882e1044debec9c Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Fri, 4 Jul 2025 08:18:35 -0500 Subject: [PATCH 14/14] release itttt --- README.md | 2 +- about.md | 2 +- src/headers/src/ProjectInfoPopup.cpp | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7880799..aec909d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ When viewing level cells or pages, you may notice the changes in the background Level cells with a glow emerging from the side represent levels in Avalanche's hall of fame. These levels stand out the most among the team's creations. By default, unrated or unlisted levels will not be highlighted. -If a level is featured on Avalanche's servers, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its level thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. +If a level is featured on Cubic's servers for Avalanche, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its level thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. --- diff --git a/about.md b/about.md index cc10e5a..8cf4d44 100644 --- a/about.md +++ b/about.md @@ -43,7 +43,7 @@ When viewing level cells or pages, you may notice the changes in the background Level cells with a glow emerging from the side represent levels in Avalanche's hall of fame. These levels stand out the most among the team's creations. By default, unrated or unlisted levels will not be highlighted. -If a level is featured on Avalanche's servers, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its level thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. +If a level is featured on Cubic's servers for Avalanche, you may see a button with the Avalanche logo appear on the left-hand side menu on the level's information page, or the right-hand side menu on the pause menu while playing the level. Pressing this will create a pop-up showing its level thumbnail as the background, and gives more information about the level as a project by the team or its members, the type of project it is, its host, etc. You will also see a button to watch its full showcase video. --- diff --git a/src/headers/src/ProjectInfoPopup.cpp b/src/headers/src/ProjectInfoPopup.cpp index ea2fec7..032d619 100644 --- a/src/headers/src/ProjectInfoPopup.cpp +++ b/src/headers/src/ProjectInfoPopup.cpp @@ -28,15 +28,17 @@ inline std::string url_encode(const std::string& value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; + for (char c : value) { if (isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { escaped << c; } else { escaped << '%' << std::setw(2) << int((unsigned char)c); - } - } + }; + }; + return escaped.str(); -} +}; void ProjectInfoPopup::doInfo(Project proj, std::string publisher) { std::ostringstream typeOfProj;