From bf841514a1daeec2d77b0b8fca18aa500ee94d15 Mon Sep 17 00:00:00 2001 From: NSGolova Date: Fri, 2 Feb 2024 18:02:37 +0000 Subject: [PATCH] Added Clan captor badge and clan ranking --- include/API/PlayerController.hpp | 2 + include/Models/Clan.hpp | 13 ++ include/Models/ClanScore.hpp | 24 +++ include/Models/Difficulty.hpp | 2 + include/UI/CaptorClanUI.hpp | 17 ++ include/UI/UIUtils.hpp | 1 + include/Utils/FormatUtils.hpp | 41 ++-- include/Utils/ModConfig.hpp | 3 +- src/API/PlayerController.cpp | 10 + src/Models/Clan.cpp | 29 +++ src/Models/ClanScore.cpp | 14 ++ src/Models/Difficulty.cpp | 6 + src/Models/Player.cpp | 12 +- src/UI/CaptorClanUI.cpp | 229 +++++++++++++++++++++++ src/UI/LeaderboardUI.cpp | 249 +++++++++++++++++++------ src/UI/LevelInfoUI.cpp | 11 +- src/UI/ScoreDetails/ScoreDetailsUI.cpp | 2 +- src/UI/UIUtils.cpp | 14 +- 18 files changed, 592 insertions(+), 87 deletions(-) create mode 100644 include/Models/ClanScore.hpp create mode 100644 include/UI/CaptorClanUI.hpp create mode 100644 src/Models/ClanScore.cpp create mode 100644 src/UI/CaptorClanUI.cpp diff --git a/include/API/PlayerController.hpp b/include/API/PlayerController.hpp index 43f2b4f..36caac7 100644 --- a/include/API/PlayerController.hpp +++ b/include/API/PlayerController.hpp @@ -21,6 +21,8 @@ class PlayerController static bool IsIncognito(Player anotherPlayer); static void SetIsIncognito(Player anotherPlayer, bool value); + static bool InClan(string tag); + static optional currentPlayer; static string lastErrorDescription; }; diff --git a/include/Models/Clan.hpp b/include/Models/Clan.hpp index 56a10d3..df03b8a 100644 --- a/include/Models/Clan.hpp +++ b/include/Models/Clan.hpp @@ -12,6 +12,19 @@ struct Clan string icon; string color; + int rank; + float pp; + Clan(rapidjson::Value const& document); Clan() = default; }; + +struct ClanRankingStatus +{ + std::optional clan; + bool clanRankingContested; + bool applicable; + + ClanRankingStatus(rapidjson::Value const& document); + ClanRankingStatus() = default; +}; diff --git a/include/Models/ClanScore.hpp b/include/Models/ClanScore.hpp new file mode 100644 index 0000000..9ad124b --- /dev/null +++ b/include/Models/ClanScore.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "beatsaber-hook/shared/config/rapidjson-utils.hpp" +#include "include/Models/Clan.hpp" + +#include +using namespace std; + +struct ClanScore +{ + int id; + // int baseScore; + int modifiedScore; + float accuracy; + int clanId; + float pp; + // float Weight; + int rank; + string timeset; + + Clan clan; + ClanScore(); + ClanScore(rapidjson::Value const& document); +}; \ No newline at end of file diff --git a/include/Models/Difficulty.hpp b/include/Models/Difficulty.hpp index a6d3ec3..95f75e6 100644 --- a/include/Models/Difficulty.hpp +++ b/include/Models/Difficulty.hpp @@ -2,6 +2,7 @@ #include "beatsaber-hook/shared/config/rapidjson-utils.hpp" #include "include/Models/TriangleRating.hpp" +#include "include/Models/Clan.hpp" #include #include @@ -17,6 +18,7 @@ struct Difficulty unordered_map modifierValues; unordered_map modifiersRating; TriangleRating rating; + ClanRankingStatus clanStatus; Difficulty(rapidjson::Value const& document); Difficulty(int statusGiven, int typeGiven, vector votesGive, unordered_map modifierValuesGiven, unordered_map modifiersRatingGiven, TriangleRating ratingGiven); diff --git a/include/UI/CaptorClanUI.hpp b/include/UI/CaptorClanUI.hpp new file mode 100644 index 0000000..f8a17d8 --- /dev/null +++ b/include/UI/CaptorClanUI.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "include/Models/Clan.hpp" + +#include "questui/shared/BeatSaberUI.hpp" +#include "questui/shared/QuestUI.hpp" + +#include "TMPro/TextMeshProUGUI.hpp" + +namespace CaptorClanUI { + extern bool showClanRanking; + extern function showClanRankingCallback; + + void initCaptorClan(UnityEngine::GameObject* header, TMPro::TextMeshProUGUI* headerPanelText); + void setClan(ClanRankingStatus clanStatus); + void setActive(bool active); +} \ No newline at end of file diff --git a/include/UI/UIUtils.hpp b/include/UI/UIUtils.hpp index fd1c8e0..6d09838 100644 --- a/include/UI/UIUtils.hpp +++ b/include/UI/UIUtils.hpp @@ -60,6 +60,7 @@ namespace UIUtils { extern SafePtrUnity roundRectSprite; HMUI::ImageView* getRoundRectSprite(); + void AddRoundRect(HMUI::ImageView* background); HMUI::ImageView* GetCopyOf(HMUI::ImageView* comp, HMUI::ImageView* other); void OpenSettings(); HMUI::ImageView* CreateRoundRectImage(UnityEngine::Transform* parent, UnityEngine::Vector2 anchoredPosition, UnityEngine::Vector2 sizeDelta); diff --git a/include/Utils/FormatUtils.hpp b/include/Utils/FormatUtils.hpp index b519435..feac027 100644 --- a/include/Utils/FormatUtils.hpp +++ b/include/Utils/FormatUtils.hpp @@ -13,6 +13,7 @@ #include "include/Models/Player.hpp" #include "include/Models/Clan.hpp" #include "include/Models/Score.hpp" +#include "include/Models/ClanScore.hpp" #include "include/API/PlayerController.hpp" #include using namespace std; @@ -104,23 +105,24 @@ namespace FormatUtils { return "." + tag + "."; } - inline string FormatNameWithClans(Player const& player, int limit) { - string clansLabel = ""; + inline string FormatNameWithClans(Player const& player, int limit, bool withClans) { + string clansLabel = withClans ? "" : ""; int clanCount = player.clans.size(); - - if (clanCount == 2) { - clansLabel = ""; - } - else if (clanCount == 3) { - clansLabel = ""; + if (withClans) { + if (clanCount == 2) { + clansLabel = ""; + } + else if (clanCount == 3) { + clansLabel = ""; + } + + for (size_t i = 0; i < clanCount; i++) { + Clan clan = player.clans[i]; + clansLabel += " " + clan.tag + ""; + } + clansLabel += ""; } - for (size_t i = 0; i < clanCount; i++) { - Clan clan = player.clans[i]; - clansLabel += " " + clan.tag + ""; - } - clansLabel += ""; - string name = ""; if (!player.name.empty() && player.name != "") { name = player.name; @@ -136,7 +138,7 @@ namespace FormatUtils { string name = ""; if (!PlayerController::IsIncognito(score.player)) { - name = getModConfig().ClansActive.GetValue() ? FormatNameWithClans(score.player, 24) : "" + truncate(score.player.name, 24) + ""; + name = getModConfig().ClansActive.GetValue() ? FormatNameWithClans(score.player, 24, true) : "" + truncate(score.player.name, 24) + ""; } else { name = "[REDACTED]"; @@ -147,6 +149,15 @@ namespace FormatUtils { return name + "" + FormatPP(score.pp) + " " + formatAcc(score.accuracy) + " " + fcLabel + time; } + inline string FormatClanScore(ClanScore const& score) { + string name = ""; + + name = "" + truncate(score.clan.name, 24) + " " + score.clan.tag + ""; + + string time = getModConfig().TimesetActive.GetValue() ? " " + GetRelativeTimeString(score.timeset) + "" : ""; + + return name + "" + FormatPP(score.pp) + " " + formatAcc(score.accuracy) + " " + time; + } inline string GetFullPlatformName(string serverPlatform) { diff --git a/include/Utils/ModConfig.hpp b/include/Utils/ModConfig.hpp index 149f79c..8f75edd 100644 --- a/include/Utils/ModConfig.hpp +++ b/include/Utils/ModConfig.hpp @@ -14,7 +14,8 @@ DECLARE_CONFIG(ModConfig, CONFIG_VALUE(ShowReplaySettings, bool, "Show replay settings", true); CONFIG_VALUE(ShowBeatleader, bool, "Show BeatLeader", true, "Priority for BeatLeader or SS"); CONFIG_VALUE(StarValueToShow, int, "Display Stars", 0); - CONFIG_VALUE(Context, int, "Selected Context", static_cast(LeaderboardUI::Context::Standard)) + CONFIG_VALUE(Context, int, "Selected Context", static_cast(LeaderboardUI::Context::Standard)); + CONFIG_VALUE(CaptureActive, bool, "Show Leaderboard clan capture status", true); ) inline bool UploadEnabled() { diff --git a/src/API/PlayerController.cpp b/src/API/PlayerController.cpp index 385c01e..804ce50 100644 --- a/src/API/PlayerController.cpp +++ b/src/API/PlayerController.cpp @@ -1,6 +1,7 @@ #include "include/API/PlayerController.hpp" #include "include/Utils/WebUtils.hpp" #include "include/Utils/ModConfig.hpp" +#include "include/Utils/StringUtils.hpp" #include "include/main.hpp" #include "include/UI/LeaderboardUI.hpp" @@ -122,6 +123,15 @@ bool PlayerController::IsFriend(Player anotherPlayer) { return std::find(currentPlayer->friends.begin(), currentPlayer->friends.end(), anotherPlayer.id) != currentPlayer->friends.end(); } +bool PlayerController::InClan(string tag) { + if (currentPlayer == nullopt) return false; + + auto it = std::find_if(currentPlayer->clans.begin(), currentPlayer->clans.end(), + [&tag](const auto& clan) { return toLower(clan.tag) == toLower(tag); }); + + return it != currentPlayer->clans.end(); +} + bool PlayerController::IsIncognito(Player anotherPlayer) { Document incognitoList; incognitoList.Parse(getModConfig().IncognitoList.GetValue().c_str()); diff --git a/src/Models/Clan.cpp b/src/Models/Clan.cpp index 0c60f7e..292ca14 100644 --- a/src/Models/Clan.cpp +++ b/src/Models/Clan.cpp @@ -3,4 +3,33 @@ Clan::Clan(rapidjson::Value const& document) { tag = document["tag"].GetString(); color = document["color"].GetString(); + + if (document.HasMember("name") && !document["name"].IsNull()) { + name = document["name"].GetString(); + } else { + name = "No name"; + } + + if (document.HasMember("avatar")) { + icon = document["avatar"].GetString(); + } else { + icon = ""; + } + if (document.HasMember("rank")) { + rank = document["rank"].GetInt(); + } + if (document.HasMember("pp")) { + pp = document["pp"].GetFloat(); + } +} + +ClanRankingStatus::ClanRankingStatus(rapidjson::Value const& document) { + if (document["clan"].IsNull()) { + clan = nullopt; + } else { + clan = Clan(document["clan"].GetObject()); + } + + applicable = document["applicable"].GetBool(); + clanRankingContested = document["clanRankingContested"].GetBool(); } \ No newline at end of file diff --git a/src/Models/ClanScore.cpp b/src/Models/ClanScore.cpp new file mode 100644 index 0000000..9b88258 --- /dev/null +++ b/src/Models/ClanScore.cpp @@ -0,0 +1,14 @@ +#include "include/Models/ClanScore.hpp" + +ClanScore::ClanScore() {} + +ClanScore::ClanScore(rapidjson::Value const& document) { + auto const& clanObject = document["clan"]; + clan = Clan(clanObject); + + pp = document["pp"].GetFloat(); + rank = document["rank"].GetInt(); + modifiedScore = document["modifiedScore"].GetInt(); + timeset = document["timepost"].GetString(); + accuracy = document["accuracy"].GetFloat(); +} \ No newline at end of file diff --git a/src/Models/Difficulty.cpp b/src/Models/Difficulty.cpp index ec0a146..868ad60 100644 --- a/src/Models/Difficulty.cpp +++ b/src/Models/Difficulty.cpp @@ -57,6 +57,12 @@ Difficulty::Difficulty(rapidjson::Value const& document) { rating.passRating = document["passRating"].GetFloat(); rating.accRating = document["accRating"].GetFloat(); rating.techRating = document["techRating"].GetFloat(); + + if (document.HasMember("clanStatus") && !document["clanStatus"].IsNull()) { + clanStatus = ClanRankingStatus(document["clanStatus"].GetObject()); + } else { + clanStatus = ClanRankingStatus(); + } } Difficulty::Difficulty(int statusGiven, int typeGiven, vector votesGiven, unordered_map modifierValuesGiven,unordered_map modifiersRatingGiven, TriangleRating ratingGiven) { diff --git a/src/Models/Player.cpp b/src/Models/Player.cpp index 600be24..c3341c2 100644 --- a/src/Models/Player.cpp +++ b/src/Models/Player.cpp @@ -13,12 +13,18 @@ Player::Player(rapidjson::Value const& userModInterface) { // For standard context and players from v3/scores (where contextExtension is null cause we request for one context) we use the main player int currentContext = getModConfig().Context.GetValue(); - std::optional> contextExtensions = userModInterface.HasMember("contextExtensions") && !userModInterface["contextExtensions"].IsNull() ? userModInterface["contextExtensions"].GetArray() : std::optional>(); + std::optional> contextExtensions = + userModInterface.HasMember("contextExtensions") && !userModInterface["contextExtensions"].IsNull() + ? userModInterface["contextExtensions"].GetArray() + : std::optional>(); // If we are Standard Context or we have no contexts or our selected context is not in contextextensions we use the normal rank. Else we use the correct context extension rank - rapidjson::Value const& contextRank = currentContext == 0 || !contextExtensions || currentContext - 1 >= contextExtensions.value().Size() ? userModInterface : contextExtensions.value()[currentContext - 1]; + rapidjson::Value const& contextRank = + currentContext == 0 || + !contextExtensions || + currentContext - 1 >= contextExtensions.value().Size() ? userModInterface : contextExtensions.value()[currentContext - 1]; rank = contextRank["rank"].GetInt(); - countryRank = contextRank["countryRank"].GetInt(); + countryRank = contextRank["countryRank"].GetInt(); pp = contextRank["pp"].GetFloat(); auto clansList = userModInterface["clans"].GetArray(); diff --git a/src/UI/CaptorClanUI.cpp b/src/UI/CaptorClanUI.cpp new file mode 100644 index 0000000..7eda8fd --- /dev/null +++ b/src/UI/CaptorClanUI.cpp @@ -0,0 +1,229 @@ +#include "GlobalNamespace/SinglePlayerLevelSelectionFlowCoordinator.hpp" +#include "GlobalNamespace/LevelCollectionNavigationController.hpp" +#include "GlobalNamespace/LevelSelectionNavigationController.hpp" +#include "GlobalNamespace/StandardLevelDetailViewController.hpp" +#include "GlobalNamespace/StandardLevelDetailView.hpp" +#include "GlobalNamespace/IPreviewBeatmapLevel.hpp" +#include "GlobalNamespace/IDifficultyBeatmap.hpp" +#include "GlobalNamespace/BeatmapCharacteristicSO.hpp" +#include "GlobalNamespace/IDifficultyBeatmapSet.hpp" +#include "GlobalNamespace/BeatmapDifficulty.hpp" +#include "GlobalNamespace/IBeatmapLevel.hpp" +#include "GlobalNamespace/BeatmapCharacteristicSegmentedControlController.hpp" +#include "GlobalNamespace/LevelParamsPanel.hpp" +#include "GlobalNamespace/ColorExtensions.hpp" + +#include "beatsaber-hook/shared/utils/hooking.hpp" +#include "beatsaber-hook/shared/utils/logging.hpp" +#include "beatsaber-hook/shared/config/rapidjson-utils.hpp" + +#include "include/Assets/Sprites.hpp" +#include "include/Assets/BundleLoader.hpp" +#include "include/Models/Song.hpp" +#include "include/Models/Difficulty.hpp" +#include "include/Models/TriangleRating.hpp" +#include "include/UI/CaptorClanUI.hpp" +#include "include/UI/ModifiersUI.hpp" +#include "include/UI/UIUtils.hpp" +#include "include/UI/EmojiSupport.hpp" +#include "include/Utils/FormatUtils.hpp" +#include "include/Utils/WebUtils.hpp" +#include "include/Utils/ModConfig.hpp" +#include "include/Utils/StringUtils.hpp" +#include "include/Enhancers/MapEnhancer.hpp" +#include "main.hpp" + +#include "TMPro/TMP_Text.hpp" + +#include "questui/shared/QuestUI.hpp" +#include "questui/shared/ArrayUtil.hpp" +#include "questui/shared/BeatSaberUI.hpp" +#include "questui/shared/CustomTypes/Components/MainThreadScheduler.hpp" + +#include +#include +#include +#include + +using namespace GlobalNamespace; +using namespace std; +using namespace QuestUI; +using namespace BeatSaberUI; + +namespace CaptorClanUI { + bool showClanRanking = false; + function showClanRankingCallback; + + ClanRankingStatus lastStatus = ClanRankingStatus(); + bool isActive = true; + + TMPro::TextMeshProUGUI* headerText; + + UnityEngine::UI::HorizontalLayoutGroup* mainPanel; + TMPro::TextMeshProUGUI* clanTag; + TMPro::TextMeshProUGUI* captorClanStatus; + + QuestUI::ClickableImage* backgroundImage; + HMUI::ImageView* clanImage; + + HMUI::HoverHint* clanHint; + + UnityEngine::Color color; + float alpha = 1.0; + const float WidthPerCharacter = 2.3f; + + UnityEngine::Color hexToRGB(const std::string& hex) { + // Assuming hex is in the format "#RRGGBB" + if (hex.length() != 7 || hex[0] != '#') { + throw std::invalid_argument("Invalid hex color format"); + } + + int r, g, b; + std::stringstream ss; + ss << std::hex << hex.substr(1, 2); + ss >> r; + ss.clear(); + ss << std::hex << hex.substr(3, 2); + ss >> g; + ss.clear(); + ss << std::hex << hex.substr(5, 2); + ss >> b; + + return UnityEngine::Color(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); + } + + UnityEngine::Color BackgroundColor() { + return showClanRanking ? hexToRGB("#822cb8") : UnityEngine::Color::get_black(); + } + + UnityEngine::Color BackgroundHoverColor() { + return hexToRGB("#1e1a69"); + } + + void initCaptorClan(UnityEngine::GameObject* headerPanel, TMPro::TextMeshProUGUI* headerPanelText) { + headerText = headerPanelText; + + backgroundImage = CreateClickableImage(headerPanel->get_transform(), Sprites::get_TransparentPixel(), {140, 36}, {17, 6}, []() { + showClanRanking = !showClanRanking; + showClanRankingCallback(); + }); + backgroundImage->set_material(BundleLoader::bundle->clanTagBackgroundMaterial); + + backgroundImage->set_color(BackgroundColor()); + backgroundImage->get_onPointerEnterEvent() += [](auto _){ + backgroundImage->set_color(BackgroundHoverColor()); + }; + + backgroundImage->get_onPointerExitEvent() += [](auto _){ + backgroundImage->set_color(BackgroundColor()); + }; + + mainPanel = BeatSaberUI::CreateHorizontalLayoutGroup(backgroundImage->get_transform()); + mainPanel->set_padding(RectOffset::New_ctor(1, 1, 0, 0)); + mainPanel->set_spacing(3); + mainPanel->GetComponentInChildren()->set_horizontalFit(UnityEngine::UI::ContentSizeFitter::FitMode::PreferredSize); + mainPanel->GetComponentInChildren()->set_preferredHeight(5.0f); + clanHint = AddHoverHint(mainPanel, ""); + + captorClanStatus = BeatSaberUI::CreateText(mainPanel->get_transform(), ""); + captorClanStatus->set_fontSize(3.0f); + captorClanStatus->set_alignment(TMPro::TextAlignmentOptions::Midline); + EmojiSupport::AddSupport(captorClanStatus); + + clanImage = UIUtils::CreateRoundRectImage(mainPanel->get_transform(), {0, 0}, {8, 2.5}); + clanImage->set_material(BundleLoader::bundle->clanTagBackgroundMaterial); + + clanTag = CreateText(clanImage->get_transform(), "", UnityEngine::Vector2(0.0, 0.0)); + clanTag->set_enableAutoSizing(true); + clanTag->set_fontSizeMin(0.1); + clanTag->set_fontSizeMax(3.0); + clanTag->set_alignment(TMPro::TextAlignmentOptions::Midline); + + setClan(lastStatus); + } + + void setClan(ClanRankingStatus clanStatus) { + lastStatus = clanStatus; + + if (mainPanel == NULL || !isActive) return; + + clanImage->get_gameObject()->SetActive(false); + + if (!clanStatus.applicable) { + if (showClanRanking) { + showClanRanking = false; + showClanRankingCallback(); + } + backgroundImage->get_gameObject()->SetActive(false); + mainPanel->get_gameObject()->SetActive(false); + if (headerText != NULL) { + headerText->get_gameObject()->SetActive(true); + } + return; + } else { + headerText->get_gameObject()->SetActive(false); + backgroundImage->get_gameObject()->SetActive(true); + mainPanel->get_gameObject()->SetActive(true); + } + + float backgroundWidth = 0; + float backgroundHeight = 5; + + if (clanStatus.clan != nullopt) { + clanImage->get_gameObject()->SetActive(true); + + auto clan = clanStatus.clan; + string clanText = "." + clan->tag + "."; + clanTag->set_text(clanText); + bool useDarkFont = (color.r * 0.299f + color.g * 0.687f + color.b * 0.114f) > 0.73f; + clanTag->set_color(useDarkFont ? UnityEngine::Color::get_black() : UnityEngine::Color::get_white()); + clanImage->set_color(ColorExtensions::ColorWithAlpha(color, alpha)); + + clanImage->GetComponentInChildren()->set_preferredWidth(WidthPerCharacter * clan->tag.length()); + + color = hexToRGB(clan->color); + + string text = "👑 "; + captorClanStatus->set_text(text); + captorClanStatus->set_faceColor(UnityEngine::Color32(255, 215, 0, 255)); + + clanHint->set_text("Map is captured by \r\n" + clan->name + "\r\n They have the highest weighted PP on this leaderboard"); + backgroundWidth = WidthPerCharacter * clan->tag.length() + WidthPerCharacter * text.length() / 2; + backgroundHeight = 7; + } else if (clanStatus.clanRankingContested) { + string text = "⚔ Contested "; + captorClanStatus->set_text(text); + captorClanStatus->set_faceColor(UnityEngine::Color32(192, 192, 192, 255)); + + clanHint->set_text("Several clans claim equal rights to capture this map! Set a score to break the tie"); + backgroundWidth = WidthPerCharacter * text.length() / 2; + + } else { + string text = "👑 Uncaptured "; + captorClanStatus->set_text(text); + captorClanStatus->set_faceColor(UnityEngine::Color32(255, 255, 255, 255)); + + clanHint->set_text("Map is not captured! Set a score to capture it for your clan"); + backgroundWidth = WidthPerCharacter * text.length() / 2; + } + + auto rectTransform = backgroundImage->get_rectTransform(); + rectTransform->set_sizeDelta({backgroundWidth, backgroundHeight}); + } + + void setActive(bool active) { + isActive = active; + if (!active) { + showClanRanking = false; + if (backgroundImage) { + backgroundImage->get_gameObject()->SetActive(false); + mainPanel->get_gameObject()->SetActive(false); + if (headerText != NULL) { + headerText->get_gameObject()->SetActive(true); + } + } + } else { + setClan(lastStatus); + } + } +} \ No newline at end of file diff --git a/src/UI/LeaderboardUI.cpp b/src/UI/LeaderboardUI.cpp index a73916d..cb5dff5 100644 --- a/src/UI/LeaderboardUI.cpp +++ b/src/UI/LeaderboardUI.cpp @@ -2,6 +2,7 @@ #include "include/Models/Replay.hpp" #include "include/Models/Score.hpp" +#include "include/Models/ClanScore.hpp" #include "include/API/PlayerController.hpp" #include "include/Assets/Sprites.hpp" #include "include/Assets/BundleLoader.hpp" @@ -21,6 +22,7 @@ #include "include/UI/LevelInfoUI.hpp" #include "include/UI/LeaderboardUI.hpp" #include "include/UI/ModifiersUI.hpp" +#include "include/UI/CaptorClanUI.hpp" #include "include/Utils/WebUtils.hpp" #include "include/Utils/StringUtils.hpp" @@ -154,6 +156,7 @@ namespace LeaderboardUI { bool showRetryButton = false; int selectedScore = 11; static vector scoreVector = vector(11); + static vector clanScoreVector = vector(11); map avatars; map cellBackgrounds; @@ -242,7 +245,7 @@ namespace LeaderboardUI { updatePlayerRank(); playerName->set_alignment(TMPro::TextAlignmentOptions::Center); - playerName->SetText(FormatUtils::FormatNameWithClans(PlayerController::currentPlayer.value(), 25)); + playerName->SetText(FormatUtils::FormatNameWithClans(PlayerController::currentPlayer.value(), 25, false)); auto params = GetAvatarParams(player.value(), false); playerAvatar->SetPlayer(player->avatar, params.baseMaterial, params.hueShift, params.saturation); @@ -365,7 +368,7 @@ namespace LeaderboardUI { return make_tuple(hash, difficulty, mode); } - void refreshFromTheServer() { + void refreshFromTheServerScores() { auto [hash, difficulty, mode] = getLevelDetails(reinterpret_cast(plvc->difficultyBeatmap->get_level())); string url = WebUtils::API_URL + "v3/scores/" + hash + "/" + difficulty + "/" + mode + "/" + contextToUrlString[static_cast(getModConfig().Context.GetValue())]; @@ -498,6 +501,82 @@ namespace LeaderboardUI { plvc->loadingControl->ShowText("Loading", true); } + void refreshFromTheServerClans() { + auto [hash, difficulty, mode] = getLevelDetails(reinterpret_cast(plvc->difficultyBeatmap->get_level())); + string url = WebUtils::API_URL + "v1/clanScores/" + hash + "/" + difficulty + "/" + mode + "/page"; + + url += "?page=" + to_string(page); + + lastUrl = url; + + WebUtils::GetAsync(url, [url](long status, string stringResult){ + if (url != lastUrl) return; + if (!showBeatLeader) return; + + if (status != 200) { + return; + } + + QuestUI::MainThreadScheduler::Schedule([status, stringResult] { + rapidjson::Document result; + result.Parse(stringResult.c_str()); + if (result.HasParseError() || !result.HasMember("data")) return; + + auto scores = result["data"].GetArray(); + + plvc->scores->Clear(); + if ((int)scores.Size() == 0) { + plvc->loadingControl->Hide(); + plvc->hasScoresData = false; + plvc->loadingControl->ShowText("No clan rankings were found!", true); + + plvc->leaderboardTableView->tableView->SetDataSource((HMUI::TableView::IDataSource *)plvc->leaderboardTableView, true); + return; + } + + auto metadata = result["metadata"].GetObject(); + int perPage = metadata["itemsPerPage"].GetInt(); + int pageNum = metadata["page"].GetInt(); + int total = metadata["total"].GetInt(); + + for (int index = 0; index < 10; ++index) + { + if (index < (int)scores.Size()) + { + auto const& score = scores[index]; + + ClanScore currentScore = ClanScore(score); + clanScoreVector[index] = currentScore; + + getLogger().info("ClanScore"); + LeaderboardTableView::ScoreData* scoreData = LeaderboardTableView::ScoreData::New_ctor( + currentScore.modifiedScore, + FormatUtils::FormatClanScore(currentScore), + currentScore.rank, + false); + plvc->scores->Add(scoreData); + } + } + + plvc->leaderboardTableView->rowHeight = 6; + + plvc->leaderboardTableView->scores = plvc->scores; + plvc->leaderboardTableView->specialScorePos = 12; + + if (upPageButton != NULL) { + upPageButton->get_gameObject()->SetActive(pageNum != 1); + downPageButton->get_gameObject()->SetActive(pageNum * perPage < total); + } + + plvc->loadingControl->Hide(); + plvc->hasScoresData = true; + plvc->leaderboardTableView->tableView->SetDataSource((HMUI::TableView::IDataSource *)plvc->leaderboardTableView, true); + }); + }); + + plvc->loadingControl->ShowText("Loading", true); + } + void updateModifiersButton() { contextsButtonHover->set_text("Currently selected leaderboard - " + contextToDisplayString[static_cast(getModConfig().Context.GetValue())]); @@ -571,18 +650,26 @@ namespace LeaderboardUI { } } + void refreshFromTheServerCurrent() { + if (CaptorClanUI::showClanRanking) { + refreshFromTheServerClans(); + } else { + refreshFromTheServerScores(); + } + } + void PageDown() { page++; clearTable(); - refreshFromTheServer(); + refreshFromTheServerCurrent(); } void PageUp() { page--; clearTable(); - refreshFromTheServer(); + refreshFromTheServerCurrent(); } void updateLeaderboard(PlatformLeaderboardViewController* self) { @@ -764,6 +851,12 @@ namespace LeaderboardUI { settingsButton->set_material(BundleLoader::bundle->UIAdditiveGlowMaterial); settingsButton->set_defaultColor(FadedColor); settingsButton->set_highlightColor(SelectedColor); + + CaptorClanUI::initCaptorClan(parentScreen, plvc->get_gameObject()->get_transform()->Find("HeaderPanel")->get_gameObject()->GetComponentInChildren()); + CaptorClanUI::showClanRankingCallback = []() { + clearTable(); + refreshFromTheServerCurrent(); + }; } if (ssInstalled && !sspageUpButton) { @@ -792,7 +885,7 @@ namespace LeaderboardUI { } IPreviewBeatmapLevel* levelData = reinterpret_cast(self->difficultyBeatmap->get_level()); - refreshFromTheServer(); + refreshFromTheServerCurrent(); } Score detailsTextWorkaround; @@ -809,6 +902,7 @@ namespace LeaderboardUI { LevelInfoUI::SetLevelInfoActive(showBeatLeader); ModifiersUI::SetModifiersActive(showBeatLeader); + CaptorClanUI::setActive(showBeatLeader && getModConfig().CaptureActive.GetValue()); for (size_t i = 0; i < ssElements.size(); i++) { @@ -978,54 +1072,81 @@ namespace LeaderboardUI { if (!isLocal && showBeatLeader) { if (cellBackgrounds.count(result)) { - auto player = scoreVector[row].player; - cellBackgrounds[result]->get_gameObject()->set_active(true); - result->playerNameText->GetComponent()->set_anchoredPosition({ - getModConfig().AvatarsActive.GetValue() ? 10.5f : 6.5f, - result->playerNameText->GetComponent()->get_anchoredPosition().y - }); - avatars[result]->get_gameObject()->set_active(getModConfig().AvatarsActive.GetValue()); - result->scoreText->get_gameObject()->set_active(getModConfig().ScoresActive.GetValue()); - - if (row == selectedScore) { - cellBackgrounds[result]->set_color(ownScoreColor); - } else { - cellBackgrounds[result]->set_color(someoneElseScoreColor); - } - cellScores[result] = scoreVector[row]; + if (!CaptorClanUI::showClanRanking) { + auto player = scoreVector[row].player; + cellBackgrounds[result]->get_gameObject()->set_active(true); - if(getModConfig().AvatarsActive.GetValue()){ - avatars[result]->set_sprite(plvc->aroundPlayerLeaderboardIcon); - - if (!PlayerController::IsIncognito(player)) { - Sprites::get_Icon(player.avatar, [result](UnityEngine::Sprite* sprite) { - if (sprite != NULL && avatars[result] != NULL && sprite->get_texture() != NULL) { - avatars[result]->set_sprite(sprite); + result->playerNameText->GetComponent()->set_anchoredPosition({ + getModConfig().AvatarsActive.GetValue() ? 10.5f : 6.5f, + result->playerNameText->GetComponent()->get_anchoredPosition().y + }); + avatars[result]->get_gameObject()->set_active(getModConfig().AvatarsActive.GetValue()); + result->scoreText->get_gameObject()->set_active(getModConfig().ScoresActive.GetValue()); + + if (row == selectedScore) { + cellBackgrounds[result]->set_color(ownScoreColor); + } else { + cellBackgrounds[result]->set_color(someoneElseScoreColor); + } + cellScores[result] = scoreVector[row]; + + if (getModConfig().AvatarsActive.GetValue()){ + avatars[result]->set_sprite(plvc->aroundPlayerLeaderboardIcon); + + if (!PlayerController::IsIncognito(player)) { + Sprites::get_Icon(player.avatar, [result](UnityEngine::Sprite* sprite) { + if (sprite != NULL && avatars[result] != NULL && sprite->get_texture() != NULL) { + avatars[result]->set_sprite(sprite); + } + }); } + } + + auto scoreSelector = cellHighlights[result]; + scoreSelector->get_gameObject()->set_active(true); + + + float hg = idleHighlight(player.role); + scoreSelector->set_defaultColor(UnityEngine::Color(hg, 0.0, 0.0, 1.0)); + scoreSelector->set_highlightColor(underlineHoverColor); + schemeForRole(player.role, false).Apply(scoreSelector->get_material()); + } else { + auto clan = clanScoreVector[row].clan; + cellBackgrounds[result]->get_gameObject()->set_active(true); + + result->playerNameText->GetComponent()->set_anchoredPosition({ + getModConfig().AvatarsActive.GetValue() ? 10.5f : 6.5f, + result->playerNameText->GetComponent()->get_anchoredPosition().y }); - } - } + avatars[result]->get_gameObject()->set_active(getModConfig().AvatarsActive.GetValue()); + result->scoreText->get_gameObject()->set_active(getModConfig().ScoresActive.GetValue()); + + if (PlayerController::InClan(clan.tag)) { + cellBackgrounds[result]->set_color(ownScoreColor); + } else { + cellBackgrounds[result]->set_color(someoneElseScoreColor); + } - // TODO - // auto tagList = clanGroups[result]; - // for (int i = 0; i < tagList->get_transform()->get_childCount(); i++) - // UnityEngine::GameObject::Destroy(tagList->get_transform()->GetChild(i)->get_gameObject()); - // for (size_t i = 0; i < player.clans.size(); i++) { - // getLogger().info("%s", player.clans[i].tag.c_str()); - // auto text = ::QuestUI::BeatSaberUI::CreateText(tagList->get_transform(), player.clans[i].tag, false); - // text->set_alignment(TMPro::TextAlignmentOptions::Center); - // auto background = text->get_gameObject()->AddComponent(); - - // background->set_material(BundleLoader::clanTagBackgroundMaterial); - // background->set_color(FormatUtils::hex2rgb(player.clans[i].color)); - // } - - auto scoreSelector = cellHighlights[result]; - scoreSelector->get_gameObject()->set_active(true); - float hg = idleHighlight(player.role); - scoreSelector->set_defaultColor(UnityEngine::Color(hg, 0.0, 0.0, 1.0)); - scoreSelector->set_highlightColor(underlineHoverColor); - schemeForRole(player.role, false).Apply(scoreSelector->get_material()); + if (getModConfig().AvatarsActive.GetValue()){ + avatars[result]->set_sprite(plvc->aroundPlayerLeaderboardIcon); + + + Sprites::get_Icon(clan.icon, [result](UnityEngine::Sprite* sprite) { + if (sprite != NULL && avatars[result] != NULL && sprite->get_texture() != NULL) { + avatars[result]->set_sprite(sprite); + } + }); + } + + auto scoreSelector = cellHighlights[result]; + scoreSelector->get_gameObject()->set_active(false); + + + float hg = idleHighlight(""); + scoreSelector->set_defaultColor(UnityEngine::Color(hg, 0.0, 0.0, 1.0)); + scoreSelector->set_highlightColor(underlineHoverColor); + schemeForRole("", false).Apply(scoreSelector->get_material()); + } } } else { if (cellBackgrounds.count(result) && cellBackgrounds[result]) { @@ -1093,7 +1214,7 @@ namespace LeaderboardUI { // Refresh the context button icon updateModifiersButton(); // Fill the leaderboard - refreshFromTheServer(); + refreshFromTheServerCurrent(); // Refresh the player rank PlayerController::Refresh(0, [](auto player, auto str){ QuestUI::MainThreadScheduler::Schedule([]{ @@ -1107,38 +1228,46 @@ namespace LeaderboardUI { } void initSettingsModal(UnityEngine::Transform* parent){ - auto container = QuestUI::BeatSaberUI::CreateModal(parent, {40,50}, nullptr, true); + auto container = QuestUI::BeatSaberUI::CreateModal(parent, {40,60}, nullptr, true); - QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Leaderboard Settings", {16, 19}); + QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Leaderboard Settings", {16, 24}); - QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Avatar", {12, 9}); + QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Avatar", {12, 14}); - CreateToggle(container->get_transform(), getModConfig().AvatarsActive.GetValue(), {-3, 11}, [](bool value){ + CreateToggle(container->get_transform(), getModConfig().AvatarsActive.GetValue(), {-3, 16}, [](bool value){ getModConfig().AvatarsActive.SetValue(value); plvc->Refresh(true, true); }); - QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Clans", {12, -1}); + QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Clans", {12, 4}); - CreateToggle(container->get_transform(), getModConfig().ClansActive.GetValue(), {-3, 1}, [](bool value){ + CreateToggle(container->get_transform(), getModConfig().ClansActive.GetValue(), {-3, 6}, [](bool value){ getModConfig().ClansActive.SetValue(value); plvc->Refresh(true, true); }); - QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Score", {12, -11}); + QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Score", {12, -6}); - CreateToggle(container->get_transform(), getModConfig().ScoresActive.GetValue(), {-3, -9}, [](bool value){ + CreateToggle(container->get_transform(), getModConfig().ScoresActive.GetValue(), {-3, -4}, [](bool value){ getModConfig().ScoresActive.SetValue(value); plvc->Refresh(true, true); }); - QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Time", {12, -21}); + QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Time", {12, -16}); - CreateToggle(container->get_transform(), getModConfig().TimesetActive.GetValue(), {-3, -19}, [](bool value){ + CreateToggle(container->get_transform(), getModConfig().TimesetActive.GetValue(), {-3, -14}, [](bool value){ getModConfig().TimesetActive.SetValue(value); plvc->Refresh(true, true); }); + QuestUI::BeatSaberUI::CreateText(container->get_transform(), "Capture", {12, -26}); + + CreateToggle(container->get_transform(), getModConfig().CaptureActive.GetValue(), {-3, -24}, [](bool value){ + getModConfig().CaptureActive.SetValue(value); + CaptorClanUI::setActive(value); + plvc->Refresh(true, true); + }); + settingsContainer = container; } diff --git a/src/UI/LevelInfoUI.cpp b/src/UI/LevelInfoUI.cpp index 3119231..086797d 100644 --- a/src/UI/LevelInfoUI.cpp +++ b/src/UI/LevelInfoUI.cpp @@ -24,6 +24,7 @@ #include "include/UI/LevelInfoUI.hpp" #include "include/UI/ModifiersUI.hpp" #include "include/UI/UIUtils.hpp" +#include "include/UI/CaptorClanUI.hpp" #include "include/Utils/WebUtils.hpp" #include "include/Utils/ModConfig.hpp" #include "include/Utils/StringUtils.hpp" @@ -187,8 +188,6 @@ namespace LevelInfoUI { noSubmissionLabel->set_alignment(TMPro::TextAlignmentOptions::Center); } - if (bslInstalled) return; - // Why not just substr str.substr("custom_level_".size())? // Because not every level is a custom level. string hash = regex_replace((string)reinterpret_cast(self->level)->get_levelID(), basic_regex("custom_level_"), ""); @@ -203,9 +202,13 @@ namespace LevelInfoUI { lastKey = key; if (_mapInfos.contains(key.first)) { setLabels(_mapInfos[key.first].difficulties[key.second]); + CaptorClanUI::setClan(_mapInfos[key.first].difficulties[key.second].clanStatus); } else { string url = WebUtils::API_URL + "map/modinterface/" + key.first; + setLabels(Difficulty()); + CaptorClanUI::setClan(ClanRankingStatus()); + WebUtils::GetAsync(url, [key](long status, string stringResult){ // If the map was already switched again, the response is irrelevant if(lastKey != key) return; @@ -238,6 +241,8 @@ namespace LevelInfoUI { } setLabels(selectedDifficulty); + + CaptorClanUI::setClan(selectedDifficulty.clanStatus); }); }); } @@ -305,6 +310,8 @@ namespace LevelInfoUI { void setLabels(Difficulty selectedDifficulty) { + if (!starsLabel) return; + // The difficulty may have changed therefor we need to tell the ModifiersUI the new Values and Ratings ModifiersUI::songModifiers = selectedDifficulty.modifierValues; ModifiersUI::songModifierRatings = selectedDifficulty.modifiersRating; diff --git a/src/UI/ScoreDetails/ScoreDetailsUI.cpp b/src/UI/ScoreDetails/ScoreDetailsUI.cpp index 958bfcf..edc0c6c 100644 --- a/src/UI/ScoreDetails/ScoreDetailsUI.cpp +++ b/src/UI/ScoreDetails/ScoreDetailsUI.cpp @@ -135,7 +135,7 @@ void BeatLeader::initScoreDetailsPopup( void BeatLeader::ScoreDetailsPopup::updatePlayerDetails(Player player) { if (!PlayerController::IsIncognito(player)) { - name->SetText(FormatUtils::FormatNameWithClans(player, 20)); + name->SetText(FormatUtils::FormatNameWithClans(player, 20, true)); auto params = GetAvatarParams(player, false); playerAvatar->SetPlayer(player.avatar, params.baseMaterial, params.hueShift, params.saturation); } else { diff --git a/src/UI/UIUtils.cpp b/src/UI/UIUtils.cpp index 653f122..3a6a25e 100644 --- a/src/UI/UIUtils.cpp +++ b/src/UI/UIUtils.cpp @@ -96,11 +96,7 @@ namespace UIUtils { } } - // Copied from BSML - HMUI::ImageView* CreateRoundRectImage(UnityEngine::Transform* parent, UnityEngine::Vector2 anchoredPosition, UnityEngine::Vector2 sizeDelta) { - static ConstString name("QuestUIImage"); - UnityEngine::GameObject* gameObj = UnityEngine::GameObject::New_ctor(name); - HMUI::ImageView* background = gameObj->AddComponent();// GetCopyOf(, getRoundRectSprite()); + void AddRoundRect(HMUI::ImageView* background) { auto bgTemplate = getRoundRectSprite(); background->set_alphaHitTestMinimumThreshold(bgTemplate->get_alphaHitTestMinimumThreshold()); background->set_color(bgTemplate->get_color()); @@ -130,6 +126,14 @@ namespace UIUtils { background->set_useGUILayout(bgTemplate->get_useGUILayout()); background->set_useLegacyMeshGeneration(bgTemplate->get_useLegacyMeshGeneration()); background->set_useSpriteMesh(bgTemplate->get_useSpriteMesh()); + } + + // Copied from BSML + HMUI::ImageView* CreateRoundRectImage(UnityEngine::Transform* parent, UnityEngine::Vector2 anchoredPosition, UnityEngine::Vector2 sizeDelta) { + static ConstString name("QuestUIImage"); + UnityEngine::GameObject* gameObj = UnityEngine::GameObject::New_ctor(name); + HMUI::ImageView* background = gameObj->AddComponent();// GetCopyOf(, getRoundRectSprite()); + AddRoundRect(background); background->get_transform()->SetParent(parent, false); background->set_enabled(true);