diff --git a/include/utils.hpp b/include/utils.hpp index 7a1fa9b..8ce934e 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -16,6 +16,9 @@ namespace Qounters::Utils { std::tuple GetBeatmapDetails(GlobalNamespace::BeatmapKey beatmap); std::string GetBeatmapIdentifier(GlobalNamespace::BeatmapKey beatmap); std::vector GetSimplifiedRequirements(GlobalNamespace::BeatmapKey beatmap); + std::string FormatNumber(int value, int separator); + double GetScoreRatio(bool includeModifiers = true, int saber = (int) Types::Sabers::Both); + double GetBestScoreRatio(); template U* ptr_cast(T* inst) { @@ -46,4 +49,4 @@ namespace Qounters::Utils { void RebuildWithScrollPosition(UnityEngine::GameObject* scrollView); UnityEngine::RectTransform* GetScrollViewTop(UnityEngine::GameObject* scrollView); -} +} \ No newline at end of file diff --git a/shared/options.hpp b/shared/options.hpp index 92bf615..6a6adad 100644 --- a/shared/options.hpp +++ b/shared/options.hpp @@ -10,6 +10,7 @@ namespace Qounters::Options { extern std::vector const FillStrings; extern std::vector const TypeStrings; extern std::vector const AnchorStrings; + extern std::vector const SeparatorStrings; extern std::vector const BaseGameObjectStrings; DECLARE_JSON_STRUCT(Gradient) { diff --git a/shared/sources.hpp b/shared/sources.hpp index 68f0cdf..02c1e5d 100644 --- a/shared/sources.hpp +++ b/shared/sources.hpp @@ -57,6 +57,7 @@ namespace Qounters::Sources { } extern std::vector const AverageCutPartStrings; + extern std::vector const PBDisplayStrings; extern std::vector const NotesDisplayStrings; extern std::vector const PPLeaderboardStrings; extern std::vector const SaberSpeedModeStrings; @@ -89,6 +90,7 @@ namespace Qounters::Sources { VALUE_DEFAULT(int, Saber, (int) Types::Sabers::Both); VALUE_DEFAULT(int, Decimals, 1); VALUE_DEFAULT(bool, Percentage, true); + VALUE_DEFAULT(int, Separator, (int) Types::Separators::Gap); }; DECLARE_JSON_STRUCT(Rank) { VALUE_DEFAULT(int, Saber, (int) Types::Sabers::Both); @@ -96,10 +98,17 @@ namespace Qounters::Sources { VALUE_DEFAULT(bool, NegativeModifiers, true); }; DECLARE_JSON_STRUCT(PersonalBest) { + enum class Displays { + PersonalBest, + PBGap, + }; VALUE_DEFAULT(int, Decimals, 1); VALUE_DEFAULT(bool, Percentage, true); VALUE_DEFAULT(bool, HideFirstScore, true); VALUE_DEFAULT(bool, Label, true); + VALUE_DEFAULT(int, Separator, (int) Types::Separators::None); + VALUE_DEFAULT(int, Display, (int) Displays::PersonalBest); + VALUE_DEFAULT(bool, Sign, true); }; DECLARE_JSON_STRUCT(Combo) { VALUE_DEFAULT(int, Saber, (int) Types::Sabers::Both); diff --git a/shared/types.hpp b/shared/types.hpp index 2142d51..5aa3ff1 100644 --- a/shared/types.hpp +++ b/shared/types.hpp @@ -18,6 +18,13 @@ namespace Qounters::Types { Enable, }; + enum class Separators { + None, + Gap, + Comma, + Period, + }; + template using SourceFn = std::function; using SourceUIFn = std::function; diff --git a/src/config.cpp b/src/config.cpp index 0b09052..d6a8721 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -58,6 +58,12 @@ std::vector const Options::AnchorStrings = { "Bottom", "Center", }; +std::vector const Options::SeparatorStrings = { + "None", + "Gap", + "Comma", + "Period", +}; std::vector const Options::BaseGameObjectStrings = { "Multiplier Ring", "Song Time Panel", // "Health Bar", diff --git a/src/events.cpp b/src/events.cpp index dc03968..5fce9c5 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -19,6 +19,7 @@ static std::map>> eventS {Types::Sources::Shape, Sources::Shape::ScoreName}, {Types::Sources::Color, Sources::Color::RankName}, {Types::Sources::Color, Sources::Color::PersonalBestName}, + {Types::Sources::Text, Sources::Text::PersonalBestName}, {Types::Sources::Enable, Sources::Enable::PercentageName}, }}, {(int) MetaCore::Events::NoteCut, diff --git a/src/sources.cpp b/src/sources.cpp index 3e94e64..fcac9e1 100644 --- a/src/sources.cpp +++ b/src/sources.cpp @@ -91,7 +91,11 @@ Sources::PremadeInfo* Sources::GetPremadeInfo(std::string const& mod, std::strin } return nullptr; } +std::vector const Sources::PBDisplayStrings = { + "Best Score", + "PB Gap", +}; std::vector const Sources::AverageCutPartStrings = { "Preswing", "Postswing", @@ -183,21 +187,13 @@ std::string Sources::Text::GetScore(UnparsedJSON unparsed) { int score = Stats::GetScore(opts.Saber); if (opts.Percentage) { - int max = Stats::GetMaxScore(opts.Saber); - double ratio = max > 0 ? score / (double) max : 1; + double ratio = Utils::GetScoreRatio(false, opts.Saber); ratio *= 100; return Strings::FormatDecimals(ratio, opts.Decimals) + "%"; } else { - // spaces between every three digits, and pad zeroes if below 100 - auto number = fmt::format("{:03}", score); if (score < 1000) - return number; - size_t len = number.size(); - for (int i = 1; i <= (len - 1) / 3; i++) { - size_t split = len - 3 * i; - number = number.substr(0, split) + " " + number.substr(split); - } - return number; + return fmt::format("{:03}", score); + return Utils::FormatNumber(score, opts.Separator); } } std::string Sources::Text::GetRank(UnparsedJSON unparsed) { @@ -235,26 +231,47 @@ std::string Sources::Text::GetRank(UnparsedJSON unparsed) { } std::string Sources::Text::GetPersonalBest(UnparsedJSON unparsed) { auto opts = unparsed.Parse(); - + bool pbGap = opts.Display == (int) PersonalBest::Displays::PBGap; int best = Stats::GetBestScore(); if (best == -1) { - if (opts.HideFirstScore) + if (opts.HideFirstScore && !pbGap) return opts.Label ? "PB: --" : "--"; else best = 0; } - int max = Stats::GetSongMaxScore(); + int songMax = Stats::GetSongMaxScore(); std::string text; - if (opts.Percentage) { - double ratio; - if (Environment::InSettings()) - ratio = Playtest::GetOverridePBRatio(); - else - ratio = max > 0 ? best / (Stats::GetModifierMultiplier(true, true) * max) : 1; - text = Strings::FormatDecimals(ratio * 100, opts.Decimals) + "%"; - } else - text = Environment::InSettings() && max == 1 ? "0" : std::to_string(best); - return opts.Label ? "PB: " + text : text; + int maxScore = Stats::GetMaxScore((int) Types::Sabers::Both); + double ratio = Utils::GetScoreRatio(); + double bestRatio; + if (Environment::InSettings()) + bestRatio = Playtest::GetOverridePBRatio(); + else + bestRatio = songMax > 0 ? (!pbGap ? (double) best / (Stats::GetModifierMultiplier(true, true) * songMax) : Utils::GetBestScoreRatio()) : 1; + if (pbGap) { + if (opts.Percentage) { + double percentDiff = (maxScore > 0) ? ((ratio - bestRatio) * 100.0) : 0.0; + if (opts.Sign) { + text = (percentDiff >= 0 ? "+" : "") + Strings::FormatDecimals(percentDiff, opts.Decimals) + "%"; + } else { + text = Strings::FormatDecimals(std::abs(percentDiff), opts.Decimals) + "%"; + } + } else { + int difference = static_cast(std::round((ratio - bestRatio) * maxScore)); + if (opts.Sign) { + text = (difference >= 0 ? "+" : "") + Utils::FormatNumber(difference, opts.Separator); + } else { + text = Utils::FormatNumber(std::abs(difference), opts.Separator); + } + } + } else { + if (opts.Percentage) { + text = Strings::FormatDecimals(bestRatio * 100, opts.Decimals) + "%"; + } else { + text = Environment::InSettings() && songMax == 1 ? "0" : Utils::FormatNumber(best, opts.Separator); + } + } + return opts.Label ? (pbGap ? "PB Gap: " : "PB: ") + text : text; } std::string Sources::Text::GetCombo(UnparsedJSON unparsed) { auto opts = unparsed.Parse(); @@ -387,9 +404,7 @@ float Sources::Shape::GetStatic(UnparsedJSON unparsed) { float Sources::Shape::GetScore(UnparsedJSON unparsed) { auto opts = unparsed.Parse(); - int score = Stats::GetScore(opts.Saber); - int max = Stats::GetMaxScore(opts.Saber); - return max > 0 ? score / (double) max : 1; + return Utils::GetScoreRatio(false, opts.Saber); } float Sources::Shape::GetMultiplier(UnparsedJSON unparsed) { auto opts = unparsed.Parse(); @@ -471,15 +486,8 @@ UnityEngine::Color Sources::Color::GetRank(UnparsedJSON unparsed) { } UnityEngine::Color Sources::Color::GetPersonalBest(UnparsedJSON unparsed) { auto opts = unparsed.Parse(); - - double best = Stats::GetBestScore(); - int songMax = Stats::GetSongMaxScore(); - double current = Stats::GetScore((int) Types::Sabers::Both); - // PB modifiers would be applied to best score - current *= Stats::GetModifierMultiplier(true, true); - int max = Stats::GetMaxScore((int) Types::Sabers::Both); - double bestRatio = songMax > 0 ? best / songMax : 1; - double ratio = max > 0 ? current / max : 1; + double bestRatio = Utils::GetBestScoreRatio(); + double ratio = Utils::GetScoreRatio(); return ratio >= bestRatio ? opts.Better : opts.Worse; } @@ -530,10 +538,7 @@ bool Sources::Enable::GetFullCombo(UnparsedJSON unparsed) { } bool Sources::Enable::GetPercentage(UnparsedJSON unparsed) { auto opts = unparsed.Parse(); - - int score = Stats::GetScore(opts.Saber); - int max = Stats::GetMaxScore(opts.Saber); - float percent = max > 0 ? score / (double) max : 1; + float percent = Utils::GetScoreRatio(false, opts.Saber); return percent * 100 >= opts.Percent; } bool Sources::Enable::GetFailed(UnparsedJSON unparsed) { diff --git a/src/sourceui.cpp b/src/sourceui.cpp index 2a1ad76..7982ddd 100644 --- a/src/sourceui.cpp +++ b/src/sourceui.cpp @@ -1,5 +1,7 @@ #include "sourceui.hpp" +#include "UnityEngine/Events/UnityAction_1.hpp" +#include "UnityEngine/UI/Toggle.hpp" #include "bsml/shared/BSML-Lite.hpp" #include "editor.hpp" #include "metacore/shared/ui.hpp" @@ -34,6 +36,24 @@ void Sources::Text::ScoreUI(GameObject* parent, UnparsedJSON unparsed) { }); BSML::Lite::AddHoverHint(saber, "The saber to show the score for"); + auto percentage = BSML::Lite::CreateToggle(parent, "Percentage", opts.Percentage, [parent](bool val) { + static int id = Editor::GetActionId(); + opts.Percentage = val; + // logic to enable/disable decimals + auto decimalGO = parent->Find("decimals"); + auto decimals = decimalGO->GetComponent(); + decimals->set_interactable(val); + auto decimalCanvasGroup = decimals->get_gameObject()->GetComponent(); + if (val) { + decimalCanvasGroup->set_alpha(1); + } else { + decimalCanvasGroup->set_alpha(0.5f); // dimmed + } + Editor::SetSourceOptions(id, opts); + Editor::FinalizeAction(); + }); + BSML::Lite::AddHoverHint(percentage, "Show the score as a percentage instead of absolute value"); + auto inc = BSML::Lite::CreateIncrementSetting(parent, "Decimals", 0, 1, opts.Decimals, 0, 10, [](float val) { static int id = Editor::GetActionId(); opts.Decimals = val; @@ -42,13 +62,23 @@ void Sources::Text::ScoreUI(GameObject* parent, UnparsedJSON unparsed) { }); BSML::Lite::AddHoverHint(inc, "The number of decimals in the score, if a percentage"); - auto percentage = BSML::Lite::CreateToggle(parent, "Percentage", opts.Percentage, [](bool val) { + auto separator = MUI::CreateDropdownEnum(parent, "Separator", opts.Separator, Options::SeparatorStrings, [](int val) { static int id = Editor::GetActionId(); - opts.Percentage = val; + opts.Separator = val; Editor::SetSourceOptions(id, opts); Editor::FinalizeAction(); }); - BSML::Lite::AddHoverHint(percentage, "Show the score as a percentage instead of absolute value"); + BSML::Lite::AddHoverHint(separator, "The thousands separator style to use if absolute value"); + + // logic to set intractability of decimals on initialization + inc->set_name("decimals"); + inc->set_interactable(opts.Percentage); + auto decimalCanvasGroup = inc->get_gameObject()->AddComponent(); + if (opts.Percentage) { + decimalCanvasGroup->set_alpha(1); + } else { + decimalCanvasGroup->set_alpha(0.5f); // dimmed + } } void Sources::Text::RankUI(GameObject* parent, UnparsedJSON unparsed) { static Rank opts; @@ -82,6 +112,23 @@ void Sources::Text::PersonalBestUI(GameObject* parent, UnparsedJSON unparsed) { static PersonalBest opts; opts = unparsed.Parse(); + auto percentage = BSML::Lite::CreateToggle(parent, "Percentage", opts.Percentage, [parent](bool val) { + static int id = Editor::GetActionId(); + opts.Percentage = val; + auto decimalGO = parent->Find("decimals"); + auto decimals = decimalGO->GetComponent(); + decimals->set_interactable(val); + auto decimalCanvasGroup = decimals->get_gameObject()->GetComponent(); + if (val) { + decimalCanvasGroup->set_alpha(1); + } else { + decimalCanvasGroup->set_alpha(0.5f); // dimmed + } + Editor::SetSourceOptions(id, opts); + Editor::FinalizeAction(); + }); + BSML::Lite::AddHoverHint(percentage, "Display the difference from personal best as a percentage instead of absolute value"); + auto decimals = BSML::Lite::CreateIncrementSetting(parent, "Decimals", 0, 1, opts.Decimals, 0, 10, [](float val) { static int id = Editor::GetActionId(); opts.Decimals = val; @@ -90,29 +137,90 @@ void Sources::Text::PersonalBestUI(GameObject* parent, UnparsedJSON unparsed) { }); BSML::Lite::AddHoverHint(decimals, "The number of decimals to show, if a percentage"); - auto percentage = BSML::Lite::CreateToggle(parent, "Percentage", opts.Percentage, [](bool val) { + auto label = BSML::Lite::CreateToggle(parent, "Label Text", opts.Label, [](bool val) { static int id = Editor::GetActionId(); - opts.Percentage = val; + opts.Label = val; Editor::SetSourceOptions(id, opts); Editor::FinalizeAction(); }); - BSML::Lite::AddHoverHint(percentage, "Display the personal best as a percentage instead of absolute value"); + BSML::Lite::AddHoverHint(label, "Labels the text with \"PB: \" or \"PB Gap: \""); - auto showZero = BSML::Lite::CreateToggle(parent, "Show 0 On First Score", !opts.HideFirstScore, [](bool val) { + auto separator = MUI::CreateDropdownEnum(parent, "Separator", opts.Separator, Options::SeparatorStrings, [](int val) { static int id = Editor::GetActionId(); - opts.HideFirstScore = !val; + opts.Separator = val; Editor::SetSourceOptions(id, opts); Editor::FinalizeAction(); }); - BSML::Lite::AddHoverHint(showZero, "Shows 0 if you have no personal best instead of \"--\""); + BSML::Lite::AddHoverHint(separator, "The thousands separator style to use if absolute value"); - auto label = BSML::Lite::CreateToggle(parent, "Label Text", opts.Label, [](bool val) { + auto display = MUI::CreateDropdownEnum(parent, "Display", opts.Display, PBDisplayStrings, [parent](int val) { static int id = Editor::GetActionId(); - opts.Label = val; + opts.Display = val; + // logic to enable/disable sign and showZero + auto signGO = parent->get_transform()->Find("sign"); + auto sign = signGO->GetComponent(); + auto showZeroGO = parent->get_transform()->Find("showZero"); + auto showZero = showZeroGO->GetComponent(); + sign->set_interactable(val == (int) PersonalBest::Displays::PBGap); + showZero->set_interactable(val == (int) PersonalBest::Displays::PersonalBest); + auto signCanvasGroup = sign->get_gameObject()->GetComponent(); + auto zeroCanvasGroup = showZero->get_gameObject()->GetComponent(); + if (val) { + signCanvasGroup->set_alpha(1); + zeroCanvasGroup->set_alpha(0.5f); // dimmed + } else { + signCanvasGroup->set_alpha(0.5f); // dimmed + zeroCanvasGroup->set_alpha(1); + } + Editor::SetSourceOptions(id, opts); + Editor::FinalizeAction(); + }); + BSML::Lite::AddHoverHint(display, "The personal best related value to show"); + + auto sign = BSML::Lite::CreateToggle(parent, "Sign", opts.Sign, [](bool val) { + static int id = Editor::GetActionId(); + opts.Sign = val; + Editor::SetSourceOptions(id, opts); + Editor::FinalizeAction(); + }); + BSML::Lite::AddHoverHint(sign, "Display a positive or negative sign next to the difference if PB Gap is enabled"); + + auto showZero = BSML::Lite::CreateToggle(parent, "Show 0 On First Score", !opts.HideFirstScore, [](bool val) { + static int id = Editor::GetActionId(); + opts.HideFirstScore = !val; Editor::SetSourceOptions(id, opts); Editor::FinalizeAction(); }); - BSML::Lite::AddHoverHint(label, "Labels the text with \"PB: \""); + BSML::Lite::AddHoverHint(showZero, "Shows 0 if you have no personal best instead of \"--\" if PB Gap is disabled"); + + // logic to set intractability of the settings on initialization + // decimals + decimals->set_name("decimals"); + decimals->set_interactable(opts.Percentage); + auto decimalCanvasGroup = decimals->get_gameObject()->AddComponent(); + if (opts.Percentage) { + decimalCanvasGroup->set_alpha(1); + } else { + decimalCanvasGroup->set_alpha(0.5f); // dimmed + } + // Sign + sign->set_name("sign"); + sign->set_interactable(opts.Display == (int) PersonalBest::Displays::PBGap); + auto signCanvasGroup = sign->get_gameObject()->AddComponent(); + if (opts.Display == (int) PersonalBest::Displays::PBGap) { + signCanvasGroup->set_alpha(1); + } else { + signCanvasGroup->set_alpha(0.5f); // dimmed + } + // showZero + showZero->set_name("showZero"); + showZero->set_interactable(opts.Display == (int) PersonalBest::Displays::PersonalBest); + auto zeroCanvasGroup = showZero->get_gameObject()->AddComponent(); + if (opts.Display == (int) PersonalBest::Displays::PersonalBest) { + zeroCanvasGroup->set_alpha(1); + } else { + zeroCanvasGroup->set_alpha(0.5f); // dimmed + } } void Sources::Text::ComboUI(GameObject* parent, UnparsedJSON unparsed) { static Combo opts; diff --git a/src/utils.cpp b/src/utils.cpp index d07903e..d28a17b 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "GlobalNamespace/BeatmapCharacteristicSO.hpp" #include "GlobalNamespace/BeatmapDifficulty.hpp" @@ -23,9 +24,11 @@ #include "customtypes/settings.hpp" #include "main.hpp" #include "metacore/shared/delegates.hpp" +#include "metacore/shared/stats.hpp" #include "metacore/shared/ui.hpp" #include "songcore/shared/SongCore.hpp" #include "songcore/shared/SongLoader/CustomBeatmapLevel.hpp" +#include "types.hpp" using namespace Qounters; using namespace MetaCore; @@ -90,6 +93,51 @@ std::vector Utils::GetSimplifiedRequirements(BeatmapKey beatmap) { return ret; } +std::string Utils::FormatNumber(int value, int separator) { + std::string seperatorString; + switch ((Qounters::Types::Separators) separator) { + case Qounters::Types::Separators::None: + return std::to_string(value); + case Qounters::Types::Separators::Gap: + seperatorString = " "; + break; + case Qounters::Types::Separators::Comma: + seperatorString = ","; + break; + case Qounters::Types::Separators::Period: + seperatorString = "."; + break; + } + + std::string num = std::to_string(std::abs(value)); + int insertPosition = static_cast(num.length()) - 3; + while (insertPosition > 0) { + num.insert(insertPosition, seperatorString); + insertPosition -= 3; + } + return (value < 0 ? "-" + num : num); +} + +double Utils::GetScoreRatio(bool includeModifiers, int saber) { + double current = Stats::GetScore((int) saber); + if (includeModifiers) { + current *= Stats::GetModifierMultiplier(true, true); + } + int maxScore = Stats::GetMaxScore((int) saber); + double ratio = maxScore > 0 ? current / maxScore : 1.0; + return ratio; +} + +double Utils::GetBestScoreRatio() { + int songMax = Stats::GetSongMaxScore(); + double best = Stats::GetBestScore(); + if (best == -1) { + best = 0; + } + double ratio = songMax > 0 ? best / songMax : 1; + return ratio; +} + BSML::ColorSetting* Utils::CreateColorPicker( UnityEngine::GameObject* parent, std::string name,