From 0d16bcaaddcc9e571136f20f2ff8625c8912c722 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:04:14 +1100 Subject: [PATCH 01/92] begin moving scoresaber to leaderboardcore --- ScoreSaber/Core/MainInstaller.cs | 12 +- .../Core/Services/LeaderboardService.cs | 51 ++-- ScoreSaber/Patches/LeaderboardPatches.cs | 231 +----------------- ScoreSaber/Resources/Player.png | Bin 0 -> 1908 bytes ScoreSaber/Resources/carat.png | Bin 0 -> 5492 bytes ScoreSaber/Resources/globe.png | Bin 0 -> 124927 bytes ScoreSaber/ScoreSaber.csproj | 15 +- .../UI/Leaderboard/ScoreSaberLeaderboard.cs | 40 +++ .../ScoreSaberLeaderboardViewController.bsml | 121 +++++---- .../ScoreSaberLeaderboardViewController.cs | 205 +++++++++++++++- 10 files changed, 368 insertions(+), 307 deletions(-) create mode 100644 ScoreSaber/Resources/Player.png create mode 100644 ScoreSaber/Resources/carat.png create mode 100644 ScoreSaber/Resources/globe.png create mode 100644 ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs diff --git a/ScoreSaber/Core/MainInstaller.cs b/ScoreSaber/Core/MainInstaller.cs index a494c2d..f5afab9 100644 --- a/ScoreSaber/Core/MainInstaller.cs +++ b/ScoreSaber/Core/MainInstaller.cs @@ -1,4 +1,5 @@ using ScoreSaber.Core.Daemons; +using ScoreSaber.Core.Data.Models; using ScoreSaber.Core.ReplaySystem; using ScoreSaber.Core.ReplaySystem.UI; using ScoreSaber.Core.Services; @@ -21,6 +22,12 @@ internal class MainInstaller : Installer { public override void InstallBindings() { Container.BindInstance(new object()).WithId("ScoreSaberUIBindings").AsCached(); + + Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.Bind().FromNewComponentAsViewController().AsSingle(); + + Container.BindInterfacesTo().AsSingle(); + Container.Bind().AsSingle().NonLazy(); Container.BindInterfacesTo().AsSingle(); @@ -31,7 +38,7 @@ public override void InstallBindings() { Container.Bind().AsSingle(); - Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); @@ -54,8 +61,7 @@ public override void InstallBindings() { Container.Bind().FromMethodMultiple(context => clickingViews).AsSingle().WhenInjectedInto(); clickingViews.ForEach(y => Container.QueueForInject(y)); - Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); - Container.BindInterfacesTo().AsSingle(); + #if RELEASE Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); diff --git a/ScoreSaber/Core/Services/LeaderboardService.cs b/ScoreSaber/Core/Services/LeaderboardService.cs index 9d5ff2f..8f6bc90 100644 --- a/ScoreSaber/Core/Services/LeaderboardService.cs +++ b/ScoreSaber/Core/Services/LeaderboardService.cs @@ -4,6 +4,7 @@ using ScoreSaber.Core.Data.Models; using System; using System.Linq; +using ScoreSaber.UI.Leaderboard; namespace ScoreSaber.Core.Services { internal class LeaderboardService { @@ -16,7 +17,7 @@ public LeaderboardService() { Plugin.Log.Debug("LeaderboardService Setup"); } - public async Task GetLeaderboardData(int maxMultipliedScore, BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, PlatformLeaderboardsModel.ScoresScope scope, int page, PlayerSpecificSettings playerSpecificSettings, bool filterAroundCountry = false) { + public async Task GetLeaderboardData(int maxMultipliedScore, BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page, PlayerSpecificSettings playerSpecificSettings, bool filterAroundCountry = false) { string leaderboardUrl = GetLeaderboardUrl(beatmapKey, scope, page, filterAroundCountry); string leaderboardRawData = await Plugin.HttpInstance.GetAsync(leaderboardUrl); @@ -29,7 +30,7 @@ public async Task GetLeaderboardData(int maxMultipliedScore, Bea public async Task GetCurrentLeaderboard(BeatmapKey beatmapKey) { - string leaderboardUrl = GetLeaderboardUrl(beatmapKey, PlatformLeaderboardsModel.ScoresScope.Global, 1, false); + string leaderboardUrl = GetLeaderboardUrl(beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Global, 1, false); int attempts = 0; while (attempts < 4) { @@ -45,7 +46,7 @@ public async Task GetCurrentLeaderboard(BeatmapKey beatmapKey) { return null; } - private string GetLeaderboardUrl(BeatmapKey beatmapKey, PlatformLeaderboardsModel.ScoresScope scope, int page, bool filterAroundCountry) { + private string GetLeaderboardUrl(BeatmapKey beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page, bool filterAroundCountry) { string url = "/game/leaderboard"; string leaderboardId = beatmapKey.levelId.Split('_')[2]; @@ -54,29 +55,27 @@ private string GetLeaderboardUrl(BeatmapKey beatmapKey, PlatformLeaderboardsMode bool hasPage = true; - if (!filterAroundCountry) { - switch (scope) { - case PlatformLeaderboardsModel.ScoresScope.Global: - url = $"{url}/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; - break; - case PlatformLeaderboardsModel.ScoresScope.AroundPlayer: - url = $"{url}/around-player/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}"; - hasPage = false; - break; - case PlatformLeaderboardsModel.ScoresScope.Friends: - url = $"{url}/around-friends/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; - break; - } - } else { - if(Plugin.Settings.locationFilterMode.ToLower() == "region") { - url = $"{url}/around-region/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; - } - else if(Plugin.Settings.locationFilterMode.ToLower() == "country") { - url = $"{url}/around-country/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; - } else { - Plugin.Log.Error("Invalid location filter mode, falling back to country"); - url = $"{url}/around-country/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; - } + switch (scope) { + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Global: + url = $"{url}/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; + break; + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.AroundPlayer: + url = $"{url}/around-player/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}"; + hasPage = false; + break; + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Friends: + url = $"{url}/around-friends/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; + break; + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Area: + if (Plugin.Settings.locationFilterMode.ToLower() == "region") { + url = $"{url}/around-region/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; + } else if (Plugin.Settings.locationFilterMode.ToLower() == "country") { + url = $"{url}/around-country/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; + } else { + Plugin.Log.Error("Invalid location filter mode, falling back to country"); + url = $"{url}/around-country/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; + } + break; } if (Plugin.Settings.hideNAScoresFromLeaderboard) { diff --git a/ScoreSaber/Patches/LeaderboardPatches.cs b/ScoreSaber/Patches/LeaderboardPatches.cs index c86f17b..38104c6 100644 --- a/ScoreSaber/Patches/LeaderboardPatches.cs +++ b/ScoreSaber/Patches/LeaderboardPatches.cs @@ -19,238 +19,11 @@ using Zenject; using static HMUI.IconSegmentedControl; using static PlatformLeaderboardsModel; -using static ScoreSaber.Patches.LeaderboardPatches; namespace ScoreSaber.Patches { - internal class LeaderboardPatches : IInitializable, IAffinity { + internal class LeaderboardPatchesREMOVE{ - private readonly ScoreSaberLeaderboardViewController _scoresaberLeaderboardViewController; - private readonly BeatmapLevelsModel _beatmapLevelsModel; - private PlatformLeaderboardViewController _platformLeaderboardViewController; - - private int _lastScopeIndex = -1; - - public LeaderboardPatches(ScoreSaberLeaderboardViewController scoresaberLeaderboardViewController, BeatmapLevelsModel beatmapLevelsModel) { - _scoresaberLeaderboardViewController = scoresaberLeaderboardViewController; - _beatmapLevelsModel = beatmapLevelsModel; - } - - public void Initialize() { } - - [AffinityPatch(typeof(PlatformLeaderboardViewController), nameof(PlatformLeaderboardViewController.Refresh))] - [AffinityPrefix] - bool PatchPlatformLeaderboardsRefresh(ref BeatmapKey ____beatmapKey, ref List ____scores, ref bool ____hasScoresData, ref LeaderboardTableView ____leaderboardTableView, ref int[] ____playerScorePos, ref PlatformLeaderboardsModel.ScoresScope ____scoresScope, ref LoadingControl ____loadingControl) { - // clean up cell clickers - foreach (var holder in _scoresaberLeaderboardViewController._cellClickingHolders) { - CellClicker existingCellClicker = holder?.cellClickerImage?.gameObject?.GetComponent(); - if (existingCellClicker != null) { - GameObject.Destroy(existingCellClicker); - } - } - - if (____beatmapKey.levelId.StartsWith("custom_level_")) { - ____hasScoresData = false; - ____scores.Clear(); - ____leaderboardTableView.SetScores(____scores, ____playerScorePos[(int)____scoresScope]); - ____loadingControl.ShowLoading(); - - _scoresaberLeaderboardViewController.isOST = false; - BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(____beatmapKey.levelId); - _scoresaberLeaderboardViewController.RefreshLeaderboard(beatmapLevel, ____beatmapKey, ____leaderboardTableView, ____scoresScope, ____loadingControl, Guid.NewGuid().ToString()).RunTask(); - return false; - } else { - _scoresaberLeaderboardViewController.isOST = true; - return true; - } - } - - private bool obtainedAnchor = false; - private Vector2 normalAnchor = Vector2.zero; - - [AffinityPatch(typeof(LeaderboardTableView), nameof(LeaderboardTableView.CellForIdx))] - void PatchLeaderboardTableView(ref LeaderboardTableView __instance, TableCell __result, int row) { - if (__instance.transform.parent.transform.parent.name == "PlatformLeaderboardViewController") { - LeaderboardTableCell tableCell = (LeaderboardTableCell)__result; - - if (!_scoresaberLeaderboardViewController.isOST) { - CellClicker cellClicker = _scoresaberLeaderboardViewController._cellClickingHolders[row].cellClickerImage.gameObject.AddComponent(); - cellClicker.onClick = _scoresaberLeaderboardViewController._infoButtons.InfoButtonClicked; - cellClicker.index = row; - cellClicker.seperator = tableCell.GetField("_separatorImage") as ImageView; - } - - TextMeshProUGUI _playerNameText = tableCell.GetField("_playerNameText"); - - if (!obtainedAnchor) { - normalAnchor = _playerNameText.rectTransform.anchoredPosition; - obtainedAnchor = true; - } - - if (_scoresaberLeaderboardViewController.isOST) { - _playerNameText.richText = false; - _playerNameText.rectTransform.anchoredPosition = normalAnchor; - tableCell.showSeparator = row != __instance._scores.Count - 1; - } else { - _playerNameText.richText = true; - Vector2 newPosition = new Vector2(normalAnchor.x + 2.5f, 0f); - _playerNameText.rectTransform.anchoredPosition = newPosition; - tableCell.showSeparator = true; - } - } - } - - - [AffinityPatch(typeof(PlatformLeaderboardViewController), "DidActivate")] - [AffinityPrefix] - bool PatchPlatformLeaderboardDidActivatePrefix(ref PlatformLeaderboardViewController __instance) { - _platformLeaderboardViewController = __instance; - return true; - } - - [AffinityPatch(typeof(PlatformLeaderboardViewController), "DidActivate")] - [AffinityPostfix] - void PatchPlatformLeaderboardDidActivatePostfix(ref bool firstActivation, ref Sprite ____friendsLeaderboardIcon, ref Sprite ____globalLeaderboardIcon, ref Sprite ____aroundPlayerLeaderboardIcon, ref IconSegmentedControl ____scopeSegmentedControl) { - if (firstActivation) { - _platformLeaderboardViewController?.InvokeMethod("Refresh", true, true); - } - if (Plugin.Settings.enableCountryLeaderboards) { - UpdateScopeControl(____friendsLeaderboardIcon, ____globalLeaderboardIcon, ____aroundPlayerLeaderboardIcon, ____scopeSegmentedControl); - ____scopeSegmentedControl.SelectCellWithNumber(_lastScopeIndex); - } - } - - [AffinityPatch(typeof(LeaderboardTableView), nameof(LeaderboardTableView.SetScores))] - [AffinityPostfix] - void PatchLeaderboardTableViewSetScoresPost(ref LeaderboardTableView __instance, List ____scores) { - if (__instance.transform.parent.transform.parent.name == "PlatformLeaderboardViewController") { - for(int i = ____scores.Count; i < 10; i++) { - _scoresaberLeaderboardViewController._ImageHolders[i].ClearSprite(); - } - } - } - - [AffinityPatch(typeof(LeaderboardTableView), nameof(LeaderboardTableView.SetScores))] - [AffinityPrefix] - void PatchLeaderboardTableViewSetScoresPre(ref LeaderboardTableView __instance) { - if (__instance.transform.parent.transform.parent.name == "PlatformLeaderboardViewController") { - _scoresaberLeaderboardViewController.ByeImages(); - } - } - - internal void UpdateScopeControl(Sprite ____friendsLeaderboardIcon, Sprite ____globalLeaderboardIcon, Sprite ____aroundPlayerLeaderboardIcon, IconSegmentedControl ____scopeSegmentedControl) { - - Texture2D countryTexture = new Texture2D(64, 64); - countryTexture.LoadImage(Utilities.GetResource(Assembly.GetExecutingAssembly(), "ScoreSaber.Resources.country.png")); - countryTexture.Apply(); - - Sprite _countryIcon = Sprite.Create(countryTexture, new Rect(0, 0, countryTexture.width, countryTexture.height), Vector2.zero); - ____scopeSegmentedControl.SetData(new DataItem[] { - new DataItem(____globalLeaderboardIcon, "Global"), - new DataItem(____aroundPlayerLeaderboardIcon, "Around You"), - new DataItem(____friendsLeaderboardIcon, "Friends"), - Plugin.Settings.locationFilterMode.ToLower() == "country" ? new DataItem(_countryIcon, "Country") : Plugin.Settings.locationFilterMode.ToLower() == "region" ? new DataItem(_countryIcon, "Region") : new DataItem(_countryIcon, "Country") - }); - - ____scopeSegmentedControl.didSelectCellEvent -= _platformLeaderboardViewController.HandleScopeSegmentedControlDidSelectCell; - ____scopeSegmentedControl.didSelectCellEvent += ScopeSegmentedControl_didSelectCellEvent; - } - - private void ScopeSegmentedControl_didSelectCellEvent(SegmentedControl segmentedControl, int cellNumber) { - - bool filterAroundCountry = false; - - switch (cellNumber) { - case 0: - _platformLeaderboardViewController.SetStaticField("_scoresScope", PlatformLeaderboardsModel.ScoresScope.Global); - break; - case 1: - _platformLeaderboardViewController.SetStaticField("_scoresScope", PlatformLeaderboardsModel.ScoresScope.AroundPlayer); - break; - case 2: - _platformLeaderboardViewController.SetStaticField("_scoresScope", PlatformLeaderboardsModel.ScoresScope.Friends); - break; - case 3: - filterAroundCountry = true; - break; - } - - _lastScopeIndex = cellNumber; - _scoresaberLeaderboardViewController.ChangeScope(filterAroundCountry); - } - - // probably a better place to put this - public class CellClicker : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { - public Action onClick; - public int index; - public ImageView seperator; - private Vector3 originalScale; - private bool isScaled = false; - - private Color origColour = new Color(1, 1, 1, 1); - private Color origColour0 = new Color(1, 1, 1, 0.2509804f); - private Color origColour1 = new Color(1, 1, 1, 0); - - private void Start() { - originalScale = seperator.transform.localScale; - } - - public void OnPointerClick(PointerEventData data) { - BeatSaberUI.BasicUIAudioManager.HandleButtonClickEvent(); - onClick(index); - } - - public void OnPointerEnter(PointerEventData eventData) { - if (!isScaled) { - seperator.transform.localScale = originalScale * 1.8f; - isScaled = true; - } - - Color targetColor = Color.white; - Color targetColor0 = Color.white; - Color targetColor1 = new Color(1, 1, 1, 0); - - float lerpDuration = 0.15f; - - StopAllCoroutines(); - StartCoroutine(LerpColors(seperator, seperator.color, targetColor, seperator.color0, targetColor0, seperator.color1, targetColor1, lerpDuration)); - } - - public void OnPointerExit(PointerEventData eventData) { - if (isScaled) { - seperator.transform.localScale = originalScale; - isScaled = false; - } - - float lerpDuration = 0.05f; - - StopAllCoroutines(); - StartCoroutine(LerpColors(seperator, seperator.color, origColour, seperator.color0, origColour0, seperator.color1, origColour1, lerpDuration)); - } - - - private IEnumerator LerpColors(ImageView target, Color startColor, Color endColor, Color startColor0, Color endColor0, Color startColor1, Color endColor1, float duration) { - float elapsedTime = 0f; - while (elapsedTime < duration) { - float t = elapsedTime / duration; - target.color = Color.Lerp(startColor, endColor, t); - target.color0 = Color.Lerp(startColor0, endColor0, t); - target.color1 = Color.Lerp(startColor1, endColor1, t); - elapsedTime += Time.deltaTime; - yield return null; - } - target.color = endColor; - target.color0 = endColor0; - target.color1 = endColor1; - } - - private void OnDestroy() { - StopAllCoroutines(); - onClick = null; - seperator.color = origColour; - seperator.color0 = origColour0; - seperator.color1 = origColour1; - } - } + } } \ No newline at end of file diff --git a/ScoreSaber/Resources/Player.png b/ScoreSaber/Resources/Player.png new file mode 100644 index 0000000000000000000000000000000000000000..2547b6d9bbab172a5d9b2f30e3e0f3eb066b6e7d GIT binary patch literal 1908 zcmb`I=|7u^9>t&Mk=P=HTH7E9rIskhE+s+|t)T6+Vca&B>a?hJH0`9CaNC=)iw>%` zimEOX2CZcbNo`Rn?J#PWT8gP!TH8=H?!Rz9_r*Eq^F5#6yWflRlP{Hols_mB005Fg zCZ60+)_)}{z3*-KpdJ8_aiS32PBRKW7DaMSf2+wV8IKR|iR?KMC^V0^euIzvGYh%n z8gQKVn%>W;N&LQHBrh9voLK4BUQnSLs{7n%t;>ujn}V;jv39pY=De)491ck6$PBF4 zU2YtU8H&6bc5$h3u5gpxQ#5?HakxO*q*x!=T7uayEvOJmlu5WdYfeAJMkN6kr9O*eDR!B8(4S$CWOUqM zGPaY+)3cwm12whT@tVHC@2h)VBRs z+#;vg3w{%vcX3|+uJ#Vb0qz$gqDt&)>o)Z!q9wMWYR z^VJG}+~M5P)&aeerKWSmXW4}@Vo$zCc}tJ|N2oAUp-J>iv;PD(j^VJP6WeV+xrG#+ zrCKA~`!m_F%CWWx7t7pzC5lR+%#;94C@sFNjJ>No zCIrK2 zGBcNK^b!)$R8Ovbg5KPh{B;klKJ8Sox$pd)j-C^{v;#Bp`Hbmxk@O@@g>ayB-)gBkk-unox68Q!$+Mc%9jq5I#E$R;(>9PcQ)`dIr%IK;d@&aTvg_oeMlt6%i@cF zXl+|`uK-BOV8FufPft8P=zK^6HlIG8-{|6}C1BME% UO*-bksO_&QK=Ggw`2>2>KdkXWod5s; literal 0 HcmV?d00001 diff --git a/ScoreSaber/Resources/carat.png b/ScoreSaber/Resources/carat.png new file mode 100644 index 0000000000000000000000000000000000000000..b3b284f36159a521130486616ee664800f65b91c GIT binary patch literal 5492 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 literal 0 HcmV?d00001 diff --git a/ScoreSaber/Resources/globe.png b/ScoreSaber/Resources/globe.png new file mode 100644 index 0000000000000000000000000000000000000000..ec14d0ed9f613cb9e0f4de47b7e129d1d345e776 GIT binary patch literal 124927 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%0AT!5%>6A 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̖ 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%0D3or71( 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 {844132E4-CDFC-49D3-950D-38F62ED8B513} - net472 + net48 Library 7.3 true @@ -30,6 +30,11 @@ True True + + + + + $(BeatSaberDir)\Beat Saber_Data\Managed\AdditionalContentModel.Interfaces.dll @@ -80,6 +85,11 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\Interactable.dll + + $(BeatSaberDir)\Plugins\LeaderboardCore.dll + False + False + $(BeatSaberDir)\Beat Saber_Data\Managed\Networking.dll False @@ -278,6 +288,9 @@ + + + diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs new file mode 100644 index 0000000..20bb808 --- /dev/null +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs @@ -0,0 +1,40 @@ +using HMUI; +using LeaderboardCore.Managers; +using LeaderboardCore.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.UI.Leaderboard { + internal class ScoreSaberLeaderboard : CustomLeaderboard, IDisposable { + + private readonly CustomLeaderboardManager _manager; + private readonly ScoreSaberLeaderboardViewController _leaderboardView; + + public override bool ShowForLevel(BeatmapKey? selectedLevel) { + if (selectedLevel.HasValue) { + if (!selectedLevel.Value.levelId.Contains("custom_level_")) { + return false; + } + } + return true; + } + protected override string leaderboardId => "ScoreSaber"; + + internal ScoreSaberLeaderboard(CustomLeaderboardManager customLeaderboardManager, PanelView panelView, ScoreSaberLeaderboardViewController leaderboardView) { + panelViewController = panelView; + _leaderboardView = leaderboardView; + _manager = customLeaderboardManager; + _manager.Register(this); + } + + protected override ViewController panelViewController { get; } + protected override ViewController leaderboardViewController => _leaderboardView; + + public void Dispose() { + _manager.Unregister(this); + } + } +} diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml index 8ef9a94..a6a4ccd 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml @@ -1,13 +1,6 @@ - + - - - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index f786e4a..021820b 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -20,10 +20,31 @@ using ScoreSaber.Core.Utils; using ScoreSaber.Core.ReplaySystem.Data; using System.Threading; +using BeatSaberMarkupLanguage.ViewControllers; +using LeaderboardCore.Interfaces; +using static ScoreSaber.Patches.LeaderboardPatchesREMOVE; +using TMPro; +using HMUI; +using SiraUtil.Affinity; +using static HMUI.IconSegmentedControl; +using System.Linq; +using System.Collections; +using UnityEngine.EventSystems; namespace ScoreSaber.UI.Leaderboard { - internal class ScoreSaberLeaderboardViewController : IInitializable, IDisposable { + internal class ScoreSaberLeaderboardViewController : BSMLAutomaticViewController, IInitializable, IDisposable, INotifyLeaderboardSet { + + public enum ScoreSaberScoresScope { + Global, + AroundPlayer, + Friends, + Area + } + + ScoreSaberScoresScope scoreSaberScoresScope; + + internal BeatmapKey currentBeatmapKey; #region BSML Components [UIComponent("root")] @@ -49,6 +70,39 @@ internal class ScoreSaberLeaderboardViewController : IInitializable, IDisposable [UIAction("up-button-click")] private void UpButtonClicked() => DirectionalButtonClicked(false); [UIAction("down-button-click")] private void DownButtonClicked() => DirectionalButtonClicked(true); + + [UIComponent("leaderboardTableView")] + private readonly LeaderboardTableView leaderboardTableView = null; + + [UIComponent("leaderboardTableView")] + private readonly Transform leaderboardTransform = null; + + [UIComponent("errorText")] + private readonly TextMeshProUGUI errorText = null; + + private LoadingControl _loadingControl; + + [UIValue("leaderboardIcons")] + private List leaderboardIcons { + get { +#pragma warning disable CS0618 // Type or member is obsolete + return new IconSegmentedControl.DataItem[] { + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.globe.png"), "Global"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Around You"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Friends"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.country.png"), "Area"), + }.ToList(); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + [UIAction("OnIconSelected")] + private void OnIconSelected(SegmentedControl segmentedControl, int index) { + scoreSaberScoresScope = (ScoreSaberScoresScope)index; + OnLeaderboardSet(currentBeatmapKey); + } + + #if PPV3 [UIAction("PPv3-replay-click")] private void PPv3ReplayClick() => _ = PPv3ReplayClicked(); #endif @@ -70,6 +124,7 @@ internal class ScoreSaberLeaderboardViewController : IInitializable, IDisposable private readonly PlayerDataModel _playerDataModel; internal readonly PlatformLeaderboardViewController _platformLeaderboardViewController; private readonly MaxScoreCache _maxScoreCache; + private readonly BeatmapLevelsModel _beatmapLevelsModel; // TODO: Put this somewhere nicer? public enum UploadStatus { @@ -93,7 +148,7 @@ public bool isOST { } } - public ScoreSaberLeaderboardViewController(DiContainer container, PanelView panelView, IUploadDaemon uploadDaemon, ReplayLoader replayLoader, PlayerService playerService, LeaderboardService leaderboardService, PlayerDataModel playerDataModel, PlatformLeaderboardViewController platformLeaderboardViewController, List profilePictureView, List cellClickingViews, MaxScoreCache maxScoreCache) { + public ScoreSaberLeaderboardViewController(DiContainer container, PanelView panelView, IUploadDaemon uploadDaemon, ReplayLoader replayLoader, PlayerService playerService, LeaderboardService leaderboardService, PlayerDataModel playerDataModel, PlatformLeaderboardViewController platformLeaderboardViewController, List profilePictureView, List cellClickingViews, MaxScoreCache maxScoreCache, BeatmapLevelsModel beatmapLevelsModel) { _container = container; _panelView = panelView; @@ -109,6 +164,8 @@ public ScoreSaberLeaderboardViewController(DiContainer container, PanelView pane _infoButtons = new EntryHolder(); _scoreDetailView = new ScoreDetailView(); + + _beatmapLevelsModel = beatmapLevelsModel; } public void Initialize() { @@ -126,9 +183,11 @@ public void Initialize() { public void Parsed() { _upButton.transform.localScale *= .5f; _downButton.transform.localScale *= .5f; - _root.name = "ScoreSaberLeaderboardElements"; + //_root.name = "ScoreSaberLeaderboardElements"; ByeImages(); + _loadingControl = leaderboardTransform.gameObject.GetComponent(); activated = true; + errorText.gameObject.SetActive(false); } public void AllowReplayWatching(bool value) { @@ -209,6 +268,8 @@ private void playerService_LoginStatusChanged(PlayerService.LoginStatus loginSta Plugin.Log.Debug(status); } + + private void uploadDaemon_UploadStatusChanged(UploadStatus status, string statusText) { if (statusText != string.Empty) { Plugin.Log.Debug($"{statusText}"); @@ -240,7 +301,7 @@ private void uploadDaemon_UploadStatusChanged(UploadStatus status, string status private CancellationTokenSource cancellationToken; - public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, PlatformLeaderboardsModel.ScoresScope scope, LoadingControl loadingControl, string refreshId) { + public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, ScoreSaberScoresScope scope, LoadingControl loadingControl, string refreshId) { try { @@ -248,7 +309,7 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (_uploadDaemon.uploading) { return; } if (!activated) { return; } - if (scope == PlatformLeaderboardsModel.ScoresScope.AroundPlayer && !_filterAroundCountry) { + if (scope == ScoreSaberScoresScope.AroundPlayer) { _upButton.interactable = false; _downButton.interactable = false; } else { @@ -288,10 +349,11 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm List leaderboardTableScoreData = leaderboardData.ToScoreData(); int playerScoreIndex = GetPlayerScoreIndex(leaderboardData); if (leaderboardTableScoreData.Count != 0) { - if (scope == PlatformLeaderboardsModel.ScoresScope.AroundPlayer && playerScoreIndex == -1 && !_filterAroundCountry) { + if (scope == ScoreSaberScoresScope.AroundPlayer && playerScoreIndex == -1 && !_filterAroundCountry) { SetErrorState(tableView, loadingControl, null, null, "You haven't set a score on this leaderboard"); } else { tableView.SetScores(leaderboardTableScoreData, playerScoreIndex); + RichMyText(tableView); for (int i = 0; i < leaderboardTableScoreData.Count; i++) { _ImageHolders[i].setProfileImage(leaderboardData.scores[i].score.leaderboardPlayerInfo.profilePicture, i, cancellationToken.Token); } @@ -317,6 +379,40 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm } } + private bool obtainedAnchor = false; + private Vector2 normalAnchor = Vector2.zero; + + void RichMyText(LeaderboardTableView tableView) { + int i = 0; + foreach (LeaderboardTableCell tableCell in tableView.GetComponentsInChildren()) { + + CellClicker cellClicker = _cellClickingHolders[i].cellClickerImage.gameObject.AddComponent(); + cellClicker.onClick = _infoButtons.InfoButtonClicked; + cellClicker.index = i; + cellClicker.seperator = tableCell.GetField("_separatorImage") as ImageView; + + TextMeshProUGUI _playerNameText = tableCell.GetField("_playerNameText"); + + if (!obtainedAnchor) { + normalAnchor = _playerNameText.rectTransform.anchoredPosition; + obtainedAnchor = true; + } + + if (isOST) { + _playerNameText.richText = false; + _playerNameText.rectTransform.anchoredPosition = normalAnchor; + tableCell.showSeparator = i != tableView._scores.Count - 1; + } else { + _playerNameText.richText = true; + Vector2 newPosition = new Vector2(normalAnchor.x + 2.5f, 0f); + _playerNameText.rectTransform.anchoredPosition = newPosition; + tableCell.showSeparator = true; + } + i++; + } + + } + private void SetRankedStatus(LeaderboardInfo leaderboardInfo) { if (leaderboardInfo.ranked) { if (leaderboardInfo.positiveModifiers) { @@ -408,7 +504,7 @@ public void RefreshLeaderboard() { if (!activated) return; - _platformLeaderboardViewController?.InvokeMethod("Refresh", true, true); + OnLeaderboardSet(currentBeatmapKey); } public void ChangeScope(bool filterAroundCountry) { @@ -457,6 +553,101 @@ public void Dispose() { _scoreDetailView.showProfile -= scoreDetailView_showProfile; } + public void OnLeaderboardSet(BeatmapKey beatmapKey) { + currentBeatmapKey = beatmapKey; + // clean up cell clickers + foreach (var holder in _cellClickingHolders) { + CellClicker existingCellClicker = holder?.cellClickerImage?.gameObject?.GetComponent(); + if (existingCellClicker != null) { + GameObject.Destroy(existingCellClicker); + } + } + + if (beatmapKey.levelId.StartsWith("custom_level_")) { + _loadingControl.gameObject.SetActive(true); + + isOST = false; + BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); + RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, scoreSaberScoresScope, _loadingControl, Guid.NewGuid().ToString()).RunTask(); + } else { + isOST = true; + } + } + + // probably a better place to put this + public class CellClicker : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { + public Action onClick; + public int index; + public ImageView seperator; + private Vector3 originalScale; + private bool isScaled = false; + + private Color origColour = new Color(1, 1, 1, 1); + private Color origColour0 = new Color(1, 1, 1, 0.2509804f); + private Color origColour1 = new Color(1, 1, 1, 0); + + private void Start() { + originalScale = seperator.transform.localScale; + } + + public void OnPointerClick(PointerEventData data) { + BeatSaberUI.BasicUIAudioManager.HandleButtonClickEvent(); + onClick(index); + } + + public void OnPointerEnter(PointerEventData eventData) { + if (!isScaled) { + seperator.transform.localScale = originalScale * 1.8f; + isScaled = true; + } + + Color targetColor = Color.white; + Color targetColor0 = Color.white; + Color targetColor1 = new Color(1, 1, 1, 0); + + float lerpDuration = 0.15f; + + StopAllCoroutines(); + StartCoroutine(LerpColors(seperator, seperator.color, targetColor, seperator.color0, targetColor0, seperator.color1, targetColor1, lerpDuration)); + } + + public void OnPointerExit(PointerEventData eventData) { + if (isScaled) { + seperator.transform.localScale = originalScale; + isScaled = false; + } + + float lerpDuration = 0.05f; + + StopAllCoroutines(); + StartCoroutine(LerpColors(seperator, seperator.color, origColour, seperator.color0, origColour0, seperator.color1, origColour1, lerpDuration)); + } + + + private IEnumerator LerpColors(ImageView target, Color startColor, Color endColor, Color startColor0, Color endColor0, Color startColor1, Color endColor1, float duration) { + float elapsedTime = 0f; + while (elapsedTime < duration) { + float t = elapsedTime / duration; + target.color = Color.Lerp(startColor, endColor, t); + target.color0 = Color.Lerp(startColor0, endColor0, t); + target.color1 = Color.Lerp(startColor1, endColor1, t); + elapsedTime += Time.deltaTime; + yield return null; + } + target.color = endColor; + target.color0 = endColor0; + target.color1 = endColor1; + } + + private void OnDestroy() { + StopAllCoroutines(); + onClick = null; + seperator.color = origColour; + seperator.color0 = origColour0; + seperator.color1 = origColour1; + } + } + #if PPV3 public void UpdatePPv3ButtonState(bool active) { From f59914c4be87061ef1c0e43428c44f0c34d6fc74 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:09:54 +1100 Subject: [PATCH 02/92] More stuff i forgot, not working --- ScoreSaber/ScoreSaber.csproj | 2 +- ScoreSaber/UI/Leaderboard/PanelView.cs | 43 +-------- .../ScoreSaberLeaderboardViewController.bsml | 89 ++++++++++-------- .../ScoreSaberLeaderboardViewController.cs | 91 +++++++++++-------- ...coreSaberLeaderboardViewControllerOLD.bsml | 80 ++++++++++++++++ 5 files changed, 186 insertions(+), 119 deletions(-) create mode 100644 ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml diff --git a/ScoreSaber/ScoreSaber.csproj b/ScoreSaber/ScoreSaber.csproj index 51a401b..ac8f23a 100644 --- a/ScoreSaber/ScoreSaber.csproj +++ b/ScoreSaber/ScoreSaber.csproj @@ -291,7 +291,7 @@ - + diff --git a/ScoreSaber/UI/Leaderboard/PanelView.cs b/ScoreSaber/UI/Leaderboard/PanelView.cs index 3df83ce..07e7759 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.cs +++ b/ScoreSaber/UI/Leaderboard/PanelView.cs @@ -88,22 +88,18 @@ protected bool isLoaded { #endregion private bool _gayMode; - private bool _initialized; private float _theWilliamVal; private Sprite _denyahSprite; private ImageView _background; private Tween _activeDisableTween; internal PlayerInfo _currentPlayerInfo; private CanvasGroup _promptCanvasGroup; - private FloatingScreen _floatingScreen; - public Action disabling; public Action statusWasSelected; public Action rankingWasSelected; private PlayerService _playerService = null; private TimeTweeningManager _timeTweeningManager = null; - private PlatformLeaderboardViewController _platformLeaderboardViewController = null; private Color _scoreSaberBlue; private Gradient _theWilliamGradient; @@ -143,34 +139,14 @@ internal bool isDenyah { } [Inject] - protected void Construct(PlayerService playerService, TimeTweeningManager timeTweeningManager, PlatformLeaderboardViewController platformLeaderboardViewController) { + protected void Construct(PlayerService playerService, TimeTweeningManager timeTweeningManager) { _scoreSaberBlue = new Color(0f, 0.4705882f, 0.7254902f); _theWilliamGradient = new Gradient { mode = GradientMode.Blend, colorKeys = new GradientColorKey[] { new GradientColorKey(Color.red, 0f), new GradientColorKey(new Color(1f, 0.5f, 0f), 0.17f), new GradientColorKey(Color.yellow, 0.34f), new GradientColorKey(Color.green, 0.51f), new GradientColorKey(Color.blue, 0.68f), new GradientColorKey(new Color(0.5f, 0f, 0.5f), 0.85f), new GradientColorKey(Color.red, 1.15f) } }; _playerService = playerService; _timeTweeningManager = timeTweeningManager; - _platformLeaderboardViewController = platformLeaderboardViewController; Plugin.Log.Debug("PanelView Setup!"); } - protected void OnDisable() { - - disabling?.Invoke(); - } - - internal void Init() { - - _floatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(100f, 15f), false, Vector3.zero, Quaternion.identity); - _floatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(100f, 25f), false, Vector3.zero, Quaternion.identity); - _floatingScreen.name = "ScoreSaberPanelScreen"; - - _floatingScreen.transform.SetParent(_platformLeaderboardViewController.transform, false); - _floatingScreen.transform.localPosition = new Vector3(3f, 50f); - _floatingScreen.transform.localScale = Vector3.one; - _floatingScreen.gameObject.SetActive(false); - _floatingScreen.gameObject.SetActive(true); - } - - [UIAction("#post-parse")] protected void Parsed() { @@ -247,21 +223,6 @@ private async Task BlinkLogo() { } } - public void Show() { - - if (!_initialized) { - Init(); - _initialized = true; - } - - _floatingScreen.SetRootViewController(this, AnimationType.In); - } - - public void Hide() { - - _floatingScreen.SetRootViewController(null, AnimationType.Out); - } - public void SetGlobalRanking(string globalRanking, bool withPrefix = true) { if (withPrefix) { @@ -353,7 +314,7 @@ public void Loaded(bool value) { protected void Update() { - if (_initialized && _gayMode) { + if (_gayMode) { _background.color = _theWilliamGradient.Evaluate(_theWilliamVal); _theWilliamVal += Time.deltaTime * 0.1f; if (_theWilliamVal > 1f) _theWilliamVal = 0f; diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml index a6a4ccd..fe89c5b 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml @@ -1,6 +1,49 @@ - + + + + + + + + + - + + + + + + + - - - - - - - - - - - - + @@ -105,9 +120,9 @@ + + - - \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 021820b..f5c8845 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -30,6 +30,8 @@ using System.Linq; using System.Collections; using UnityEngine.EventSystems; +using BeatSaberMarkupLanguage.Components; +using UnityEngine.Diagnostics; namespace ScoreSaber.UI.Leaderboard { @@ -47,14 +49,20 @@ public enum ScoreSaberScoresScope { internal BeatmapKey currentBeatmapKey; #region BSML Components - [UIComponent("root")] - protected readonly RectTransform _root = null; [UIParams] private readonly BSMLParserParams _parserParams = null; - [UIComponent("up-button")] - protected readonly Button _upButton = null; - [UIComponent("down-button")] - protected readonly Button _downButton = null; + + [UIComponent("leaderboardTableView")] + internal readonly LeaderboardTableView leaderboardTableView = null; + + [UIComponent("leaderboardTableView")] + internal readonly Transform leaderboardTransform = null; + + [UIComponent("myHeader")] + private readonly Backgroundable myHeader = null; + + [UIComponent("errorText")] + private readonly TextMeshProUGUI errorText = null; [UIValue("imageHolders")] internal List _ImageHolders = null; @@ -70,17 +78,19 @@ public enum ScoreSaberScoresScope { [UIAction("up-button-click")] private void UpButtonClicked() => DirectionalButtonClicked(false); [UIAction("down-button-click")] private void DownButtonClicked() => DirectionalButtonClicked(true); + [UIComponent("up_button")] + internal readonly Button _upButton = null; - [UIComponent("leaderboardTableView")] - private readonly LeaderboardTableView leaderboardTableView = null; - - [UIComponent("leaderboardTableView")] - private readonly Transform leaderboardTransform = null; + [UIComponent("down_button")] + internal readonly Button _downButton = null; - [UIComponent("errorText")] - private readonly TextMeshProUGUI errorText = null; + [UIObject("loadingLB")] + private readonly GameObject _loadingControl = null; - private LoadingControl _loadingControl; + [UIAction("downloadPlaylistCLICK")] + private void downloadPlaylistCLICK() { + Application.OpenURL($"https://fortnite.com"); + } [UIValue("leaderboardIcons")] private List leaderboardIcons { @@ -175,19 +185,35 @@ public void Initialize() { _playerService.LoginStatusChanged += playerService_LoginStatusChanged; _infoButtons.infoButtonClicked += infoButtons_infoButtonClicked; _uploadDaemon.UploadStatusChanged += uploadDaemon_UploadStatusChanged; - _platformLeaderboardViewController.didActivateEvent += LeaderboardViewActivate; - _platformLeaderboardViewController.didDeactivateEvent += LeaderboardViewDeactivate; } + internal static readonly FieldAccessor.Accessor ImageSkew = FieldAccessor.GetAccessor("_skew"); + internal static readonly FieldAccessor.Accessor ImageGradient = FieldAccessor.GetAccessor("_gradient"); + [UIAction("#post-parse")] public void Parsed() { - _upButton.transform.localScale *= .5f; - _downButton.transform.localScale *= .5f; - //_root.name = "ScoreSaberLeaderboardElements"; + //_upButton.transform.localScale *= .5f; + //_downButton.transform.localScale *= .5f; + myHeader.Background.material = Resources.FindObjectsOfTypeAll().Where(m => m.name == "UINoGlowRoundEdge").First(); ByeImages(); - _loadingControl = leaderboardTransform.gameObject.GetComponent(); - activated = true; errorText.gameObject.SetActive(false); + + var _loadingControlA = leaderboardTransform.Find("LoadingControl").gameObject; + Transform loadingContainer = _loadingControlA.transform.Find("LoadingContainer"); + loadingContainer.gameObject.SetActive(false); + Destroy(loadingContainer.Find("Text").gameObject); + Destroy(_loadingControlA.transform.Find("RefreshContainer").gameObject); + Destroy(_loadingControlA.transform.Find("DownloadingContainer").gameObject); + + var _imgView = myHeader.Background as ImageView; + Color color = new Color(255f / 255f, 222f / 255f, 24f / 255f); + _imgView.color = color; + _imgView.color0 = color; + _imgView.color1 = color; + ImageSkew(ref _imgView) = 0.18f; + ImageGradient(ref _imgView) = true; + + activated = true; } public void AllowReplayWatching(bool value) { @@ -195,23 +221,10 @@ public void AllowReplayWatching(bool value) { _scoreDetailView.AllowReplayWatching(value); } - private void LeaderboardViewActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { if (!firstActivation) { return; } - BSMLParser.Instance.Parse(Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), "ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController.bsml"), _platformLeaderboardViewController.gameObject, this); - - _panelView.Show(); - - _panelView.disabling = delegate () { - if (_scoreDetailView.detailModalRoot != null && _profileDetailView.profileModalRoot != null) { - _scoreDetailView.detailModalRoot.gameObject.SetActive(false); - _profileDetailView.profileModalRoot.gameObject.SetActive(false); - Accessors.animateParentCanvas(ref _scoreDetailView.detailModalRoot) = true; - Accessors.animateParentCanvas(ref _profileDetailView.profileModalRoot) = true; - } - }; - _panelView.statusWasSelected = delegate () { if (_leaderboardService.currentLoadedLeaderboard == null) { return; } _parserParams.EmitEvent("close-modals"); @@ -221,14 +234,14 @@ private void LeaderboardViewActivate(bool firstActivation, bool addedToHierarchy _panelView.rankingWasSelected = delegate () { _parserParams.EmitEvent("close-modals"); _parserParams.EmitEvent("show-profile"); - _profileDetailView.ShowProfile(_playerService.localPlayerInfo.playerId).RunTask(); + //_profileDetailView.ShowProfile(_playerService.localPlayerInfo.playerId).RunTask(); }; _container.Inject(_profileDetailView); _playerService.GetLocalPlayerInfo(); } - private void LeaderboardViewDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { + protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { if (_scoreDetailView.detailModalRoot != null) _scoreDetailView.detailModalRoot.Hide(false); if (_profileDetailView.profileModalRoot != null) _profileDetailView.profileModalRoot.Hide(false); } @@ -544,8 +557,6 @@ private async Task StartReplay(ScoreMap score) { public void Dispose() { - _platformLeaderboardViewController.didActivateEvent -= LeaderboardViewActivate; - _platformLeaderboardViewController.didDeactivateEvent -= LeaderboardViewDeactivate; _playerService.LoginStatusChanged -= playerService_LoginStatusChanged; _uploadDaemon.UploadStatusChanged -= uploadDaemon_UploadStatusChanged; _infoButtons.infoButtonClicked -= infoButtons_infoButtonClicked; @@ -568,7 +579,7 @@ public void OnLeaderboardSet(BeatmapKey beatmapKey) { isOST = false; BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); - RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, scoreSaberScoresScope, _loadingControl, Guid.NewGuid().ToString()).RunTask(); + RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, scoreSaberScoresScope, null, Guid.NewGuid().ToString()).RunTask(); } else { isOST = true; } diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml new file mode 100644 index 0000000..3b42d9e --- /dev/null +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b4caac8bba8a853536063bcb4c158a70ff2a1295 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:07:09 +1100 Subject: [PATCH 03/92] Closer, but bsml macros are laughing at me --- ScoreSaber/Core/MainInstaller.cs | 1 - .../Core/Services/LeaderboardService.cs | 10 +- ScoreSaber/ScoreSaber.csproj | 3 +- .../Leaderboard/ProfilePictureView.cs | 61 +- ScoreSaber/UI/Leaderboard/PanelView.bsml | 3 - ScoreSaber/UI/Leaderboard/PanelView.cs | 8 +- .../ScoreSaberLeaderboardViewController.bsml | 45 +- .../ScoreSaberLeaderboardViewController.cs | 569 ++++++------------ ...coreSaberLeaderboardViewControllerOLD.bsml | 80 --- 9 files changed, 284 insertions(+), 496 deletions(-) delete mode 100644 ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml diff --git a/ScoreSaber/Core/MainInstaller.cs b/ScoreSaber/Core/MainInstaller.cs index f5afab9..b231598 100644 --- a/ScoreSaber/Core/MainInstaller.cs +++ b/ScoreSaber/Core/MainInstaller.cs @@ -62,7 +62,6 @@ public override void InstallBindings() { clickingViews.ForEach(y => Container.QueueForInject(y)); - #if RELEASE Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); #else diff --git a/ScoreSaber/Core/Services/LeaderboardService.cs b/ScoreSaber/Core/Services/LeaderboardService.cs index 8f6bc90..f890171 100644 --- a/ScoreSaber/Core/Services/LeaderboardService.cs +++ b/ScoreSaber/Core/Services/LeaderboardService.cs @@ -17,9 +17,9 @@ public LeaderboardService() { Plugin.Log.Debug("LeaderboardService Setup"); } - public async Task GetLeaderboardData(int maxMultipliedScore, BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page, PlayerSpecificSettings playerSpecificSettings, bool filterAroundCountry = false) { + public async Task GetLeaderboardData(int maxMultipliedScore, BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page, PlayerSpecificSettings playerSpecificSettings) { - string leaderboardUrl = GetLeaderboardUrl(beatmapKey, scope, page, filterAroundCountry); + string leaderboardUrl = GetLeaderboardUrl(beatmapKey, scope, page); string leaderboardRawData = await Plugin.HttpInstance.GetAsync(leaderboardUrl); Leaderboard leaderboardData = JsonConvert.DeserializeObject(leaderboardRawData); @@ -30,7 +30,7 @@ public async Task GetLeaderboardData(int maxMultipliedScore, Bea public async Task GetCurrentLeaderboard(BeatmapKey beatmapKey) { - string leaderboardUrl = GetLeaderboardUrl(beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Global, 1, false); + string leaderboardUrl = GetLeaderboardUrl(beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Global, 1); int attempts = 0; while (attempts < 4) { @@ -46,7 +46,7 @@ public async Task GetCurrentLeaderboard(BeatmapKey beatmapKey) { return null; } - private string GetLeaderboardUrl(BeatmapKey beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page, bool filterAroundCountry) { + private string GetLeaderboardUrl(BeatmapKey beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page) { string url = "/game/leaderboard"; string leaderboardId = beatmapKey.levelId.Split('_')[2]; @@ -59,7 +59,7 @@ private string GetLeaderboardUrl(BeatmapKey beatmapKey, ScoreSaberLeaderboardVie case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Global: url = $"{url}/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}?page={page}"; break; - case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.AroundPlayer: + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Player: url = $"{url}/around-player/{leaderboardId}/mode/{gameMode}/difficulty/{difficulty}"; hasPage = false; break; diff --git a/ScoreSaber/ScoreSaber.csproj b/ScoreSaber/ScoreSaber.csproj index ac8f23a..d640f30 100644 --- a/ScoreSaber/ScoreSaber.csproj +++ b/ScoreSaber/ScoreSaber.csproj @@ -34,6 +34,7 @@ + @@ -291,7 +292,7 @@ - + diff --git a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs index 8f7eac3..08b3dd0 100644 --- a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs +++ b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs @@ -1,12 +1,19 @@ -using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage; +using BeatSaberMarkupLanguage.Attributes; using HMUI; +using IPA.Utilities.Async; +using ScoreSaber.UI.Leaderboard; using System; using System.Collections; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; +using TMPro; +using Tweening; using UnityEngine; using UnityEngine.Networking; using Zenject; +#pragma warning disable CS0618 // Type or member is obsolete namespace ScoreSaber.UI.Elements.Leaderboard { internal class ProfilePictureView { @@ -138,4 +145,56 @@ internal static void AddSpriteToCache(string url, Sprite sprite) { spriteCacheQueue.Enqueue(url); } } + + //internal class TweeningService { + // [Inject] private TimeTweeningManager _tweeningManager; + // private HashSet activeRotationTweens = new HashSet(); + + // public void RotateTransform(Transform transform, float rotationAmount, float time, Action callback = null) { + // if (activeRotationTweens.Contains(transform)) return; + // float startRotation = transform.rotation.eulerAngles.z; + // float endRotation = startRotation + rotationAmount; + + // Tween tween = new FloatTween(startRotation, endRotation, (float u) => + // { + // transform.rotation = Quaternion.Euler(0f, 0f, u); + // }, 0.1f, EaseType.Linear, 0f); + // tween.onCompleted = () => + // { + // callback?.Invoke(); + // activeRotationTweens.Remove(transform); + // }; + // tween.onKilled = () => + // { + // if (transform != null) transform.rotation = Quaternion.Euler(0f, 0f, endRotation); + // callback?.Invoke(); + // activeRotationTweens.Remove(transform); + // }; + // activeRotationTweens.Add(transform); + // _tweeningManager.AddTween(tween, transform); + // } + + // public void FadeText(TextMeshProUGUI text, bool fadeIn, float time) { + // float startAlpha = fadeIn ? 0f : 1f; + // float endAlpha = fadeIn ? 1f : 0f; + + // Tween tween = new FloatTween(startAlpha, endAlpha, (float u) => + // { + // text.color = text.color.ColorWithAlpha(u); + // }, 0.4f, EaseType.Linear, 0f); + // tween.onCompleted = () => + // { + // if (text == null) return; + // text.gameObject.SetActive(fadeIn); + // }; + // tween.onKilled = () => + // { + // if (text == null) return; + // text.gameObject.SetActive(fadeIn); + // text.color = text.color.ColorWithAlpha(endAlpha); + // }; + // text.gameObject.SetActive(true); + // _tweeningManager.AddTween(tween, text); + // } + //} } diff --git a/ScoreSaber/UI/Leaderboard/PanelView.bsml b/ScoreSaber/UI/Leaderboard/PanelView.bsml index 6328c40..9e492a8 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.bsml +++ b/ScoreSaber/UI/Leaderboard/PanelView.bsml @@ -19,8 +19,5 @@ - - - \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/PanelView.cs b/ScoreSaber/UI/Leaderboard/PanelView.cs index 07e7759..46f2455 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.cs +++ b/ScoreSaber/UI/Leaderboard/PanelView.cs @@ -190,11 +190,11 @@ protected void PressedLogo() { } } - [UIAction("clicked-settings")] - protected void ClickedSettings() { + //[UIAction("clicked-settings")] + //protected void ClickedSettings() { - ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); - } + // ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); + //} [UIAction("clicked-ranking")] protected void ClickedRanking() { diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml index fe89c5b..4d084b2 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml @@ -1,6 +1,6 @@ - + xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/ https://raw.githubusercontent.com/monkeymanboy/BSML-Docs/gh-pages/BSMLSchema.xsd'> + @@ -25,39 +25,39 @@ + size-delta-y="25"/> - - + + + anchor-pos-y="5.3" spacing="-19.4"> - - + + - + @@ -69,7 +69,6 @@ - + + + + @@ -107,7 +110,7 @@ + --> @@ -120,8 +123,6 @@ - - diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index f5c8845..70b41e8 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -1,250 +1,134 @@ using BeatSaberMarkupLanguage; using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components; using BeatSaberMarkupLanguage.Parser; +using BeatSaberMarkupLanguage.Tags; +using BeatSaberMarkupLanguage.ViewControllers; +using HMUI; +using IPA.Loader; using IPA.Utilities; +using IPA.Utilities.Async; +using LeaderboardCore.Interfaces; using ScoreSaber.Core.Daemons; using ScoreSaber.Core.Data.Models; using ScoreSaber.Core.Data.Wrappers; using ScoreSaber.Core.ReplaySystem; +using ScoreSaber.Core.ReplaySystem.Data; using ScoreSaber.Core.Services; +using ScoreSaber.Core.Utils; using ScoreSaber.Extensions; using ScoreSaber.UI.Elements.Leaderboard; using ScoreSaber.UI.Elements.Profile; +using ScoreSaber.UI.Leaderboard; +using ScoreSaber.UI.Main; +using SiraUtil.Logging; using System; using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; -using UnityEngine; -using UnityEngine.UI; -using Zenject; -using ScoreSaber.Core.Utils; -using ScoreSaber.Core.ReplaySystem.Data; +using System.Linq; +using System.Net; using System.Threading; -using BeatSaberMarkupLanguage.ViewControllers; -using LeaderboardCore.Interfaces; -using static ScoreSaber.Patches.LeaderboardPatchesREMOVE; +using System.Threading.Tasks; using TMPro; -using HMUI; -using SiraUtil.Affinity; -using static HMUI.IconSegmentedControl; -using System.Linq; -using System.Collections; -using UnityEngine.EventSystems; -using BeatSaberMarkupLanguage.Components; +using UnityEngine; using UnityEngine.Diagnostics; +using Zenject; +using Button = UnityEngine.UI.Button; namespace ScoreSaber.UI.Leaderboard { - internal class ScoreSaberLeaderboardViewController : BSMLAutomaticViewController, IInitializable, IDisposable, INotifyLeaderboardSet { + [HotReload(RelativePathToLayout = @"./ScoreSaberLeaderboardViewController.bsml")] + [ViewDefinition("ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController.bsml")] + internal class ScoreSaberLeaderboardViewController : BSMLAutomaticViewController, INotifyLeaderboardSet, IInitializable { + + // TODO: Put both of these somewhere nicer? +#pragma warning disable CS0169 // The field 'ScoreSaberLeaderboardViewController.headerText' is never used +#pragma warning disable CS0649 // Field 'ScoreSaberLeaderboardViewController.myHeader' is never assigned to, and will always have its default value null public enum ScoreSaberScoresScope { Global, - AroundPlayer, + Player, Friends, Area } - ScoreSaberScoresScope scoreSaberScoresScope; - - internal BeatmapKey currentBeatmapKey; + public enum UploadStatus { + Packaging = 0, + Uploading = 1, + Success = 2, + Retrying = 3, + Error = 4, + Done + } - #region BSML Components [UIParams] private readonly BSMLParserParams _parserParams = null; [UIComponent("leaderboardTableView")] - internal readonly LeaderboardTableView leaderboardTableView = null; + private readonly LeaderboardTableView leaderboardTableView = null; [UIComponent("leaderboardTableView")] internal readonly Transform leaderboardTransform = null; [UIComponent("myHeader")] - private readonly Backgroundable myHeader = null; + private readonly Backgroundable myHeader; + + [UIComponent("headerText")] + private readonly TextMeshProUGUI headerText; [UIComponent("errorText")] - private readonly TextMeshProUGUI errorText = null; + private readonly TextMeshProUGUI _errorText; [UIValue("imageHolders")] internal List _ImageHolders = null; + [UIValue("cellClickerHolders")] internal List _cellClickingHolders = null; + [UIValue("entry-holder")] - internal readonly EntryHolder _infoButtons = null; + internal EntryHolder _infoButtons = null; + [UIValue("score-detail-view")] - protected readonly ScoreDetailView _scoreDetailView = null; + protected ScoreDetailView _scoreDetailView = null; + [UIComponent("profile-detail-view")] protected readonly ProfileDetailView _profileDetailView = null; - [UIAction("up-button-click")] private void UpButtonClicked() => DirectionalButtonClicked(false); - [UIAction("down-button-click")] private void DownButtonClicked() => DirectionalButtonClicked(true); - [UIComponent("up_button")] - internal readonly Button _upButton = null; + private readonly Button _upButton; [UIComponent("down_button")] - internal readonly Button _downButton = null; + private readonly Button _downButton; [UIObject("loadingLB")] - private readonly GameObject _loadingControl = null; - - [UIAction("downloadPlaylistCLICK")] - private void downloadPlaylistCLICK() { - Application.OpenURL($"https://fortnite.com"); - } - - [UIValue("leaderboardIcons")] - private List leaderboardIcons { - get { -#pragma warning disable CS0618 // Type or member is obsolete - return new IconSegmentedControl.DataItem[] { - new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.globe.png"), "Global"), - new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Around You"), - new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Friends"), - new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.country.png"), "Area"), - }.ToList(); -#pragma warning restore CS0618 // Type or member is obsolete - } - } - - [UIAction("OnIconSelected")] - private void OnIconSelected(SegmentedControl segmentedControl, int index) { - scoreSaberScoresScope = (ScoreSaberScoresScope)index; - OnLeaderboardSet(currentBeatmapKey); - } + private readonly GameObject loadingLB; + [UIAction("up-button-click")] private void UpButtonClicked() => DirectionalButtonClicked(false); + [UIAction("down-button-click")] private void DownButtonClicked() => DirectionalButtonClicked(true); -#if PPV3 - [UIAction("PPv3-replay-click")] private void PPv3ReplayClick() => _ = PPv3ReplayClicked(); -#endif - #endregion public bool activated { get; private set; } public int leaderboardPage { get; set; } = 1; - private bool _filterAroundCountry; + public ScoreSaberScoresScope currentScoreScope { get; set; } + private bool _replayDownloading; private string _currentLeaderboardRefreshId = string.Empty; + private BeatmapKey _currentBeatmapKey; - private readonly PanelView _panelView; - private readonly DiContainer _container; - private readonly IUploadDaemon _uploadDaemon; - private readonly ReplayLoader _replayLoader; - private readonly PlayerService _playerService; - private readonly LeaderboardService _leaderboardService; - private readonly PlayerDataModel _playerDataModel; - internal readonly PlatformLeaderboardViewController _platformLeaderboardViewController; - private readonly MaxScoreCache _maxScoreCache; - private readonly BeatmapLevelsModel _beatmapLevelsModel; - - // TODO: Put this somewhere nicer? - public enum UploadStatus { - Packaging = 0, - Uploading = 1, - Success = 2, - Retrying = 3, - Error = 4, - Done - } - - private bool _isOST = false; - public bool isOST { - get { return _isOST; } - set { - if (!_isOST && value == true) { - ByeImages(); - _panelView.SetRankedStatus("N/A (Not Custom Song)"); - } - _isOST = value; - } - } - - public ScoreSaberLeaderboardViewController(DiContainer container, PanelView panelView, IUploadDaemon uploadDaemon, ReplayLoader replayLoader, PlayerService playerService, LeaderboardService leaderboardService, PlayerDataModel playerDataModel, PlatformLeaderboardViewController platformLeaderboardViewController, List profilePictureView, List cellClickingViews, MaxScoreCache maxScoreCache, BeatmapLevelsModel beatmapLevelsModel) { - - _container = container; - _panelView = panelView; - _uploadDaemon = uploadDaemon; - _replayLoader = replayLoader; - _playerService = playerService; - _playerDataModel = playerDataModel; - _leaderboardService = leaderboardService; - _ImageHolders = profilePictureView; - _cellClickingHolders = cellClickingViews; - _platformLeaderboardViewController = platformLeaderboardViewController; - _maxScoreCache = maxScoreCache; - - _infoButtons = new EntryHolder(); - _scoreDetailView = new ScoreDetailView(); - - _beatmapLevelsModel = beatmapLevelsModel; - } - - public void Initialize() { - - _scoreDetailView.showProfile += scoreDetailView_showProfile; - _scoreDetailView.startReplay += scoreDetailView_startReplay; - _playerService.LoginStatusChanged += playerService_LoginStatusChanged; - _infoButtons.infoButtonClicked += infoButtons_infoButtonClicked; - _uploadDaemon.UploadStatusChanged += uploadDaemon_UploadStatusChanged; - } - - internal static readonly FieldAccessor.Accessor ImageSkew = FieldAccessor.GetAccessor("_skew"); - internal static readonly FieldAccessor.Accessor ImageGradient = FieldAccessor.GetAccessor("_gradient"); - - [UIAction("#post-parse")] - public void Parsed() { - //_upButton.transform.localScale *= .5f; - //_downButton.transform.localScale *= .5f; - myHeader.Background.material = Resources.FindObjectsOfTypeAll().Where(m => m.name == "UINoGlowRoundEdge").First(); - ByeImages(); - errorText.gameObject.SetActive(false); - - var _loadingControlA = leaderboardTransform.Find("LoadingControl").gameObject; - Transform loadingContainer = _loadingControlA.transform.Find("LoadingContainer"); - loadingContainer.gameObject.SetActive(false); - Destroy(loadingContainer.Find("Text").gameObject); - Destroy(_loadingControlA.transform.Find("RefreshContainer").gameObject); - Destroy(_loadingControlA.transform.Find("DownloadingContainer").gameObject); - - var _imgView = myHeader.Background as ImageView; - Color color = new Color(255f / 255f, 222f / 255f, 24f / 255f); - _imgView.color = color; - _imgView.color0 = color; - _imgView.color1 = color; - ImageSkew(ref _imgView) = 0.18f; - ImageGradient(ref _imgView) = true; - - activated = true; - } - - public void AllowReplayWatching(bool value) { - - _scoreDetailView.AllowReplayWatching(value); - } + [Inject] private readonly PanelView _panelView; + [Inject] private readonly SiraLog _log; + [Inject] private readonly DiContainer _container; + [Inject] private readonly IUploadDaemon _uploadDaemon; + [Inject] private readonly ReplayLoader _replayLoader; + [Inject] private readonly PlayerService _playerService; + [Inject] private readonly LeaderboardService _leaderboardService; + [Inject] private readonly PlayerDataModel _playerDataModel; + [Inject] internal readonly PlatformLeaderboardViewController _platformLeaderboardViewController; + [Inject] private readonly MaxScoreCache _maxScoreCache; + [Inject] private readonly PlatformLeaderboardViewController _plvc; + [Inject] private readonly BeatmapLevelsModel _beatmapLevelsModel; - protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { - - if (!firstActivation) { return; } - - _panelView.statusWasSelected = delegate () { - if (_leaderboardService.currentLoadedLeaderboard == null) { return; } - _parserParams.EmitEvent("close-modals"); - Application.OpenURL($"https://scoresaber.com/leaderboard/{_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.id}"); - }; - - _panelView.rankingWasSelected = delegate () { - _parserParams.EmitEvent("close-modals"); - _parserParams.EmitEvent("show-profile"); - //_profileDetailView.ShowProfile(_playerService.localPlayerInfo.playerId).RunTask(); - }; - _container.Inject(_profileDetailView); - _playerService.GetLocalPlayerInfo(); - } - - protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { - if (_scoreDetailView.detailModalRoot != null) _scoreDetailView.detailModalRoot.Hide(false); - if (_profileDetailView.profileModalRoot != null) _profileDetailView.profileModalRoot.Hide(false); - } private void infoButtons_infoButtonClicked(int index) { if (_leaderboardService.currentLoadedLeaderboard == null) { return; } @@ -281,8 +165,6 @@ private void playerService_LoginStatusChanged(PlayerService.LoginStatus loginSta Plugin.Log.Debug(status); } - - private void uploadDaemon_UploadStatusChanged(UploadStatus status, string statusText) { if (statusText != string.Empty) { Plugin.Log.Debug($"{statusText}"); @@ -312,9 +194,100 @@ private void uploadDaemon_UploadStatusChanged(UploadStatus status, string status } } + private GameObject _loadingControl; + private ImageView _imgView; + + internal static readonly FieldAccessor.Accessor ImageSkew = FieldAccessor.GetAccessor("_skew"); + internal static readonly FieldAccessor.Accessor ImageGradient = FieldAccessor.GetAccessor("_gradient"); + + [UIAction("#post-parse")] + private void PostParse() { + myHeader.Background.material = Utilities.ImageResources.NoGlowMat; + _loadingControl = leaderboardTransform.Find("LoadingControl").gameObject; + Transform loadingContainer = _loadingControl.transform.Find("LoadingContainer"); + loadingContainer.gameObject.SetActive(false); + Destroy(loadingContainer.Find("Text").gameObject); + Destroy(_loadingControl.transform.Find("RefreshContainer").gameObject); + Destroy(_loadingControl.transform.Find("DownloadingContainer").gameObject); + _imgView = myHeader.Background as ImageView; + Color color = new Color(255f / 255f, 222f / 255f, 24f / 255f); + _imgView.color = color; + _imgView.color0 = color; + _imgView.color1 = color; + ImageSkew(ref _imgView) = 0.18f; + ImageGradient(ref _imgView) = true; + _infoButtons = new EntryHolder(); + _scoreDetailView = new ScoreDetailView(); + } + + [UIAction("OpenLeaderboardPage")] + internal void OpenLeaderboardPage() { + Application.OpenURL($"https://scoresaber.com/leaderboard/{_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.id}"); + } + + [UIAction("SettingsClicked")] + internal void OpenBugPage() => ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); + + + [UIAction("OnIconSelected")] + private void OnIconSelected(SegmentedControl segmentedControl, int index) { + currentScoreScope = (ScoreSaberScoresScope)index; + leaderboardPage = 0; + CheckPage(); + OnLeaderboardSet(_currentBeatmapKey); + } + + [UIValue("leaderboardIcons")] + private List leaderboardIcons { + get { +#pragma warning disable CS0618 // Type or member is obsolete + return new List + { + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.globe.png"), "Global"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Around you"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Friends"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.country.png"), "Country") + }; +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { + base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); + if (!base.isActiveAndEnabled) return; + if (!_plvc) return; + if (firstActivation) { + _panelView.statusWasSelected = delegate () { + if (_leaderboardService.currentLoadedLeaderboard == null) { return; } + _parserParams.EmitEvent("close-modals"); + Application.OpenURL($"https://scoresaber.com/leaderboard/{_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.id}"); + }; + + _panelView.rankingWasSelected = delegate () { + _parserParams.EmitEvent("close-modals"); + _parserParams.EmitEvent("show-profile"); + _profileDetailView.ShowProfile(_playerService.localPlayerInfo.playerId).RunTask(); + }; + + _container.Inject(_profileDetailView); + _playerService.GetLocalPlayerInfo(); + } + Transform header = _plvc.transform.Find("HeaderPanel"); + _plvc.GetComponentInChildren().color = new Color(0, 0, 0, 0); + } + + protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { + base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); + if (!_plvc || !_plvc.isActivated) return; + _plvc.GetComponentInChildren().color = Color.white; + if (!_plvc.isActivated) return; + if (_scoreDetailView.detailModalRoot != null) _scoreDetailView.detailModalRoot.Hide(false); + if (_profileDetailView.profileModalRoot != null) _profileDetailView.profileModalRoot.Hide(false); + } + private CancellationTokenSource cancellationToken; - public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, ScoreSaberScoresScope scope, LoadingControl loadingControl, string refreshId) { + public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, ScoreSaberScoresScope scope, GameObject loadingControl, string refreshId) { try { @@ -322,7 +295,7 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (_uploadDaemon.uploading) { return; } if (!activated) { return; } - if (scope == ScoreSaberScoresScope.AroundPlayer) { + if (scope == ScoreSaberScoresScope.Player) { _upButton.interactable = false; _downButton.interactable = false; } else { @@ -331,8 +304,10 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm } ByeImages(); + _errorText.gameObject.SetActive(false); + loadingControl.SetActive(true); - if(cancellationToken != null) { + if (cancellationToken != null) { cancellationToken.Cancel(); cancellationToken.Dispose(); } @@ -354,7 +329,7 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (_currentLeaderboardRefreshId == refreshId) { int maxMultipliedScore = await _maxScoreCache.GetMaxScore(beatmapLevel, beatmapKey); - LeaderboardMap leaderboardData = await _leaderboardService.GetLeaderboardData(maxMultipliedScore, beatmapLevel, beatmapKey, scope, leaderboardPage, _playerDataModel.playerData.playerSpecificSettings, _filterAroundCountry); + LeaderboardMap leaderboardData = await _leaderboardService.GetLeaderboardData(maxMultipliedScore, beatmapLevel, beatmapKey, scope, leaderboardPage, _playerDataModel.playerData.playerSpecificSettings); if (_currentLeaderboardRefreshId != refreshId) { return; // we need to check this again, since some time may have passed due to waiting for leaderboard data } @@ -362,16 +337,18 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm List leaderboardTableScoreData = leaderboardData.ToScoreData(); int playerScoreIndex = GetPlayerScoreIndex(leaderboardData); if (leaderboardTableScoreData.Count != 0) { - if (scope == ScoreSaberScoresScope.AroundPlayer && playerScoreIndex == -1 && !_filterAroundCountry) { + if (scope == ScoreSaberScoresScope.Player && playerScoreIndex == -1) { SetErrorState(tableView, loadingControl, null, null, "You haven't set a score on this leaderboard"); } else { + if (_currentLeaderboardRefreshId != refreshId) { + return; // we need to check this again, since some time may have passed due to waiting for leaderboard data + } tableView.SetScores(leaderboardTableScoreData, playerScoreIndex); - RichMyText(tableView); for (int i = 0; i < leaderboardTableScoreData.Count; i++) { _ImageHolders[i].setProfileImage(leaderboardData.scores[i].score.leaderboardPlayerInfo.profilePicture, i, cancellationToken.Token); } - loadingControl.ShowText("", false); - loadingControl.Hide(); + loadingControl.gameObject.SetActive(false); + _errorText.gameObject.SetActive(false); if (_uploadDaemon.uploading) { _panelView.DismissPrompt(); } @@ -392,40 +369,6 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm } } - private bool obtainedAnchor = false; - private Vector2 normalAnchor = Vector2.zero; - - void RichMyText(LeaderboardTableView tableView) { - int i = 0; - foreach (LeaderboardTableCell tableCell in tableView.GetComponentsInChildren()) { - - CellClicker cellClicker = _cellClickingHolders[i].cellClickerImage.gameObject.AddComponent(); - cellClicker.onClick = _infoButtons.InfoButtonClicked; - cellClicker.index = i; - cellClicker.seperator = tableCell.GetField("_separatorImage") as ImageView; - - TextMeshProUGUI _playerNameText = tableCell.GetField("_playerNameText"); - - if (!obtainedAnchor) { - normalAnchor = _playerNameText.rectTransform.anchoredPosition; - obtainedAnchor = true; - } - - if (isOST) { - _playerNameText.richText = false; - _playerNameText.rectTransform.anchoredPosition = normalAnchor; - tableCell.showSeparator = i != tableView._scores.Count - 1; - } else { - _playerNameText.richText = true; - Vector2 newPosition = new Vector2(normalAnchor.x + 2.5f, 0f); - _playerNameText.rectTransform.anchoredPosition = newPosition; - tableCell.showSeparator = true; - } - i++; - } - - } - private void SetRankedStatus(LeaderboardInfo leaderboardInfo) { if (leaderboardInfo.ranked) { if (leaderboardInfo.positiveModifiers) { @@ -455,7 +398,12 @@ public int GetPlayerScoreIndex(LeaderboardMap leaderboardMap) { return -1; } - private void SetErrorState(LeaderboardTableView tableView, LoadingControl loadingControl, HttpErrorException httpErrorException = null, Exception exception = null, string errorText = "Failed to load leaderboard, score won't upload", bool showRefreshButton = true) { + public void AllowReplayWatching(bool value) { + + _scoreDetailView.AllowReplayWatching(value); + } + + private void SetErrorState(LeaderboardTableView tableView, GameObject loadingControl, HttpErrorException httpErrorException = null, Exception exception = null, string errorText = "Failed to load leaderboard, score won't upload", bool showRefreshButton = true) { if (httpErrorException != null) { if (httpErrorException.isNetworkError) { @@ -473,8 +421,9 @@ private void SetErrorState(LeaderboardTableView tableView, LoadingControl loadin if (exception != null) { Plugin.Log.Error(exception.ToString()); } - loadingControl.Hide(); - loadingControl.ShowText(errorText, showRefreshButton); + loadingControl.gameObject.SetActive(false); + _errorText.gameObject.SetActive(true); + _errorText.text = errorText; tableView.SetScores(new List(), -1); ByeImages(); } @@ -517,17 +466,7 @@ public void RefreshLeaderboard() { if (!activated) return; - OnLeaderboardSet(currentBeatmapKey); - } - - public void ChangeScope(bool filterAroundCountry) { - - if (activated) { - _filterAroundCountry = filterAroundCountry; - leaderboardPage = 1; - RefreshLeaderboard(); - CheckPage(); - } + _platformLeaderboardViewController?.InvokeMethod("Refresh", true, true); } internal void ByeImages() { @@ -555,6 +494,15 @@ private async Task StartReplay(ScoreMap score) { _replayDownloading = false; } + public void Initialize() { + + _scoreDetailView.showProfile += scoreDetailView_showProfile; + _scoreDetailView.startReplay += scoreDetailView_startReplay; + _playerService.LoginStatusChanged += playerService_LoginStatusChanged; + _infoButtons.infoButtonClicked += infoButtons_infoButtonClicked; + _uploadDaemon.UploadStatusChanged += uploadDaemon_UploadStatusChanged; + } + public void Dispose() { _playerService.LoginStatusChanged -= playerService_LoginStatusChanged; @@ -565,145 +513,8 @@ public void Dispose() { } public void OnLeaderboardSet(BeatmapKey beatmapKey) { - currentBeatmapKey = beatmapKey; - // clean up cell clickers - foreach (var holder in _cellClickingHolders) { - CellClicker existingCellClicker = holder?.cellClickerImage?.gameObject?.GetComponent(); - if (existingCellClicker != null) { - GameObject.Destroy(existingCellClicker); - } - } - - if (beatmapKey.levelId.StartsWith("custom_level_")) { - _loadingControl.gameObject.SetActive(true); - - isOST = false; - BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); - RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, scoreSaberScoresScope, null, Guid.NewGuid().ToString()).RunTask(); - } else { - isOST = true; - } - } - - // probably a better place to put this - public class CellClicker : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { - public Action onClick; - public int index; - public ImageView seperator; - private Vector3 originalScale; - private bool isScaled = false; - - private Color origColour = new Color(1, 1, 1, 1); - private Color origColour0 = new Color(1, 1, 1, 0.2509804f); - private Color origColour1 = new Color(1, 1, 1, 0); - - private void Start() { - originalScale = seperator.transform.localScale; - } - - public void OnPointerClick(PointerEventData data) { - BeatSaberUI.BasicUIAudioManager.HandleButtonClickEvent(); - onClick(index); - } - - public void OnPointerEnter(PointerEventData eventData) { - if (!isScaled) { - seperator.transform.localScale = originalScale * 1.8f; - isScaled = true; - } - - Color targetColor = Color.white; - Color targetColor0 = Color.white; - Color targetColor1 = new Color(1, 1, 1, 0); - - float lerpDuration = 0.15f; - - StopAllCoroutines(); - StartCoroutine(LerpColors(seperator, seperator.color, targetColor, seperator.color0, targetColor0, seperator.color1, targetColor1, lerpDuration)); - } - - public void OnPointerExit(PointerEventData eventData) { - if (isScaled) { - seperator.transform.localScale = originalScale; - isScaled = false; - } - - float lerpDuration = 0.05f; - - StopAllCoroutines(); - StartCoroutine(LerpColors(seperator, seperator.color, origColour, seperator.color0, origColour0, seperator.color1, origColour1, lerpDuration)); - } - - - private IEnumerator LerpColors(ImageView target, Color startColor, Color endColor, Color startColor0, Color endColor0, Color startColor1, Color endColor1, float duration) { - float elapsedTime = 0f; - while (elapsedTime < duration) { - float t = elapsedTime / duration; - target.color = Color.Lerp(startColor, endColor, t); - target.color0 = Color.Lerp(startColor0, endColor0, t); - target.color1 = Color.Lerp(startColor1, endColor1, t); - elapsedTime += Time.deltaTime; - yield return null; - } - target.color = endColor; - target.color0 = endColor0; - target.color1 = endColor1; - } - - private void OnDestroy() { - StopAllCoroutines(); - onClick = null; - seperator.color = origColour; - seperator.color0 = origColour0; - seperator.color1 = origColour1; - } - } - -#if PPV3 - - public void UpdatePPv3ButtonState(bool active) { - buttonPPv3Replay.gameObject.SetActive(active); - } - - private LeaderboardScoreData _currentPPv3ScoreData = null; - public void CheckPPv3ReplayExists(LeaderboardScoreData scoreData) { - - _currentPPv3ScoreData = scoreData; - string levelId = _currentPPv3ScoreData.level.level.levelID.Replace(" WIP", "").Split('_')[2]; - string ppv3ReplayPath = $@"{Settings.replayPath}\PPv3-{Shared.ReplaceInvalidChars(_currentPPv3ScoreData.level.level.songName)}-{levelId}-{(_currentPPv3ScoreData.level.difficulty.SerializedName())}.dat"; - Plugin.Log.Info(ppv3ReplayPath); - if (File.Exists(ppv3ReplayPath)) { - UpdatePPv3ButtonState(true); - } else { - UpdatePPv3ButtonState(false); - } + BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); + RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, currentScoreScope, _loadingControl, Guid.NewGuid().ToString()).RunTask(); } - - private async Task PPv3ReplayClicked() { - - _replayDownloading = true; - buttonPPv3Replay.interactable = false; - parserParams.EmitEvent("close-modals"); - try { - string levelId = _currentPPv3ScoreData.level.level.levelID.Replace(" WIP", "").Split('_')[2]; - string ppv3ReplayPath = $@"{Settings.replayPath}\PPv3-{Shared.ReplaceInvalidChars(_currentPPv3ScoreData.level.level.songName)}-{levelId}-{(_currentPPv3ScoreData.level.difficulty.SerializedName())}.dat"; - - if (File.Exists(ppv3ReplayPath)) { - byte[] replay = File.ReadAllBytes(ppv3ReplayPath); - if (replay != null) { - SetPanelViewPromptInfo("Replay loaded! Unpacking...", true); - await Shared.replayLoader.Load(replay, _currentPPv3ScoreData.level, new GameplayModifiers(), "PPv3"); - SetPanelViewPromptSuccess("Replay Started!", false, 1f); - buttonPPv3Replay.interactable = true; - } - } - } catch (Exception ex) { - SetPanelViewPromptError("Failed to start replay! Error written to log.", false); - Plugin.Log.Error($"Failed to start replay: {ex}"); - } - _replayDownloading = false; - } - -#endif } } \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml deleted file mode 100644 index 3b42d9e..0000000 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewControllerOLD.bsml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From b5c256994329338439766195844ba6fee699505c Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:21:12 +1100 Subject: [PATCH 04/92] somewhere --- ScoreSaber/UI/Leaderboard/PanelView.cs | 6 ------ .../UI/Leaderboard/ScoreSaberLeaderboardViewController.cs | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ScoreSaber/UI/Leaderboard/PanelView.cs b/ScoreSaber/UI/Leaderboard/PanelView.cs index 46f2455..9bf404d 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.cs +++ b/ScoreSaber/UI/Leaderboard/PanelView.cs @@ -190,12 +190,6 @@ protected void PressedLogo() { } } - //[UIAction("clicked-settings")] - //protected void ClickedSettings() { - - // ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); - //} - [UIAction("clicked-ranking")] protected void ClickedRanking() { diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 70b41e8..53f0378 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -79,16 +79,16 @@ public enum UploadStatus { private readonly TextMeshProUGUI _errorText; [UIValue("imageHolders")] - internal List _ImageHolders = null; + [Inject] internal List _ImageHolders = null; [UIValue("cellClickerHolders")] - internal List _cellClickingHolders = null; + [Inject] internal List _cellClickingHolders = null; [UIValue("entry-holder")] - internal EntryHolder _infoButtons = null; + internal EntryHolder _infoButtons = null; [UIValue("score-detail-view")] - protected ScoreDetailView _scoreDetailView = null; + protected ScoreDetailView _scoreDetailView = null; [UIComponent("profile-detail-view")] protected readonly ProfileDetailView _profileDetailView = null; From fbcc9d4229d2262dc81eb724b47415deb6f61882 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:28:28 +1100 Subject: [PATCH 05/92] Fixed Page --- .../UI/Leaderboard/ScoreSaberLeaderboardViewController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 53f0378..c965cb9 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -102,8 +102,8 @@ public enum UploadStatus { [UIObject("loadingLB")] private readonly GameObject loadingLB; - [UIAction("up-button-click")] private void UpButtonClicked() => DirectionalButtonClicked(false); - [UIAction("down-button-click")] private void DownButtonClicked() => DirectionalButtonClicked(true); + [UIAction("OnPageUp")] private void UpButtonClicked() => DirectionalButtonClicked(false); + [UIAction("OnPageDown")] private void DownButtonClicked() => DirectionalButtonClicked(true); public bool activated { get; private set; } @@ -465,8 +465,8 @@ public void RefreshLeaderboard() { if (!activated) return; - - _platformLeaderboardViewController?.InvokeMethod("Refresh", true, true); + BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(_currentBeatmapKey.levelId); + RefreshLeaderboard(beatmapLevel, _currentBeatmapKey, leaderboardTableView, currentScoreScope, _loadingControl, Guid.NewGuid().ToString()).RunTask(); } internal void ByeImages() { From 1479e6b3cf398de91d69b865ebb4d7bfd71740b8 Mon Sep 17 00:00:00 2001 From: Riley Date: Wed, 16 Oct 2024 12:44:28 +1100 Subject: [PATCH 06/92] Showing scores now, still buggy --- ScoreSaber/Core/MainInstaller.cs | 4 +- ScoreSaber/UI/Leaderboard/PanelView.bsml | 42 ++--- .../ScoreSaberLeaderboardViewController.cs | 155 ++++++++++++++++-- 3 files changed, 160 insertions(+), 41 deletions(-) diff --git a/ScoreSaber/Core/MainInstaller.cs b/ScoreSaber/Core/MainInstaller.cs index b231598..051d110 100644 --- a/ScoreSaber/Core/MainInstaller.cs +++ b/ScoreSaber/Core/MainInstaller.cs @@ -23,8 +23,8 @@ internal class MainInstaller : Installer { public override void InstallBindings() { Container.BindInstance(new object()).WithId("ScoreSaberUIBindings").AsCached(); - Container.Bind().FromNewComponentAsViewController().AsSingle(); - Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); + Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); Container.BindInterfacesTo().AsSingle(); diff --git a/ScoreSaber/UI/Leaderboard/PanelView.bsml b/ScoreSaber/UI/Leaderboard/PanelView.bsml index 9e492a8..0eca343 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.bsml +++ b/ScoreSaber/UI/Leaderboard/PanelView.bsml @@ -1,23 +1,23 @@  - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index c965cb9..63482c9 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -21,8 +21,10 @@ using ScoreSaber.UI.Elements.Profile; using ScoreSaber.UI.Leaderboard; using ScoreSaber.UI.Main; +using SiraUtil.Affinity; using SiraUtil.Logging; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; @@ -31,6 +33,8 @@ using TMPro; using UnityEngine; using UnityEngine.Diagnostics; +using UnityEngine.EventSystems; +using UnityEngine.UI; using Zenject; using Button = UnityEngine.UI.Button; @@ -128,8 +132,6 @@ public enum UploadStatus { [Inject] private readonly PlatformLeaderboardViewController _plvc; [Inject] private readonly BeatmapLevelsModel _beatmapLevelsModel; - - private void infoButtons_infoButtonClicked(int index) { if (_leaderboardService.currentLoadedLeaderboard == null) { return; } @@ -194,7 +196,6 @@ private void uploadDaemon_UploadStatusChanged(UploadStatus status, string status } } - private GameObject _loadingControl; private ImageView _imgView; internal static readonly FieldAccessor.Accessor ImageSkew = FieldAccessor.GetAccessor("_skew"); @@ -203,12 +204,12 @@ private void uploadDaemon_UploadStatusChanged(UploadStatus status, string status [UIAction("#post-parse")] private void PostParse() { myHeader.Background.material = Utilities.ImageResources.NoGlowMat; - _loadingControl = leaderboardTransform.Find("LoadingControl").gameObject; - Transform loadingContainer = _loadingControl.transform.Find("LoadingContainer"); + var loadingLB = leaderboardTransform.Find("LoadingControl").gameObject; + Transform loadingContainer = loadingLB.transform.Find("LoadingContainer"); loadingContainer.gameObject.SetActive(false); Destroy(loadingContainer.Find("Text").gameObject); - Destroy(_loadingControl.transform.Find("RefreshContainer").gameObject); - Destroy(_loadingControl.transform.Find("DownloadingContainer").gameObject); + Destroy(loadingLB.transform.Find("RefreshContainer").gameObject); + Destroy(loadingLB.transform.Find("DownloadingContainer").gameObject); _imgView = myHeader.Background as ImageView; Color color = new Color(255f / 255f, 222f / 255f, 24f / 255f); _imgView.color = color; @@ -216,8 +217,6 @@ private void PostParse() { _imgView.color1 = color; ImageSkew(ref _imgView) = 0.18f; ImageGradient(ref _imgView) = true; - _infoButtons = new EntryHolder(); - _scoreDetailView = new ScoreDetailView(); } [UIAction("OpenLeaderboardPage")] @@ -271,6 +270,8 @@ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, _container.Inject(_profileDetailView); _playerService.GetLocalPlayerInfo(); + _ImageHolders.ForEach(holder => holder.ClearSprite()); + activated = true; } Transform header = _plvc.transform.Find("HeaderPanel"); _plvc.GetComponentInChildren().color = new Color(0, 0, 0, 0); @@ -288,9 +289,11 @@ protected override void DidDeactivate(bool removedFromHierarchy, bool screenSyst private CancellationTokenSource cancellationToken; public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, ScoreSaberScoresScope scope, GameObject loadingControl, string refreshId) { - + Plugin.Log.Info("begin refresh leaderboard"); try { - + loadingControl.SetActive(false); + _errorText.gameObject.SetActive(false); + tableView.SetScores(new List(), -1); _currentLeaderboardRefreshId = refreshId; if (_uploadDaemon.uploading) { return; } if (!activated) { return; } @@ -326,13 +329,17 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm await Task.Delay(500); // Delay before doing anything to prevent leaderboard spam - + Plugin.Log.Info("AFTER TASK DELAY"); if (_currentLeaderboardRefreshId == refreshId) { int maxMultipliedScore = await _maxScoreCache.GetMaxScore(beatmapLevel, beatmapKey); LeaderboardMap leaderboardData = await _leaderboardService.GetLeaderboardData(maxMultipliedScore, beatmapLevel, beatmapKey, scope, leaderboardPage, _playerDataModel.playerData.playerSpecificSettings); + Plugin.Log.Info("AFTER LB DATA"); + if (_currentLeaderboardRefreshId != refreshId) { return; // we need to check this again, since some time may have passed due to waiting for leaderboard data } + Plugin.Log.Info("AFTER CHECK"); + SetRankedStatus(leaderboardData.leaderboardInfoMap.leaderboardInfo); List leaderboardTableScoreData = leaderboardData.ToScoreData(); int playerScoreIndex = GetPlayerScoreIndex(leaderboardData); @@ -344,6 +351,8 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm return; // we need to check this again, since some time may have passed due to waiting for leaderboard data } tableView.SetScores(leaderboardTableScoreData, playerScoreIndex); + Plugin.Log.Info("AFTER TABLE ST SCORE DELAY"); + PatchLeaderboardTableView(tableView); for (int i = 0; i < leaderboardTableScoreData.Count; i++) { _ImageHolders[i].setProfileImage(leaderboardData.scores[i].score.leaderboardPlayerInfo.profilePicture, i, cancellationToken.Token); } @@ -454,7 +463,7 @@ public void ChangePageButtonsEnabledState(bool state) { public void CheckPage() { - if (leaderboardPage > 1) { + if (leaderboardPage > 0) { _upButton.interactable = true; } else { _upButton.interactable = false; @@ -463,10 +472,10 @@ public void CheckPage() { public void RefreshLeaderboard() { - if (!activated) + if (!activated || _currentBeatmapKey == null) return; BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(_currentBeatmapKey.levelId); - RefreshLeaderboard(beatmapLevel, _currentBeatmapKey, leaderboardTableView, currentScoreScope, _loadingControl, Guid.NewGuid().ToString()).RunTask(); + RefreshLeaderboard(beatmapLevel, _currentBeatmapKey, leaderboardTableView, currentScoreScope, loadingLB, Guid.NewGuid().ToString()).RunTask(); } internal void ByeImages() { @@ -494,8 +503,38 @@ private async Task StartReplay(ScoreMap score) { _replayDownloading = false; } - public void Initialize() { + private bool obtainedAnchor = false; + private Vector2 normalAnchor = Vector2.zero; + + void PatchLeaderboardTableView(LeaderboardTableView tableView) { + int i = 0; + foreach (LeaderboardTableCell cell in tableView.GetComponentsInChildren()) { + + LeaderboardTableCell tableCell = (LeaderboardTableCell)cell; + CellClicker cellClicker = _cellClickingHolders[i].cellClickerImage.gameObject.AddComponent(); + cellClicker.onClick = _infoButtons.InfoButtonClicked; + cellClicker.index = i; + cellClicker.seperator = tableCell.GetField("_separatorImage") as ImageView; + + TextMeshProUGUI _playerNameText = tableCell.GetField("_playerNameText"); + + if (!obtainedAnchor) { + normalAnchor = _playerNameText.rectTransform.anchoredPosition; + obtainedAnchor = true; + } + + _playerNameText.richText = true; + Vector2 newPosition = new Vector2(normalAnchor.x + 2.5f, 0f); + _playerNameText.rectTransform.anchoredPosition = newPosition; + tableCell.showSeparator = true; + i++; + } + } + + public void Initialize() { + _infoButtons = new EntryHolder(); + _scoreDetailView = new ScoreDetailView(); _scoreDetailView.showProfile += scoreDetailView_showProfile; _scoreDetailView.startReplay += scoreDetailView_startReplay; _playerService.LoginStatusChanged += playerService_LoginStatusChanged; @@ -513,8 +552,88 @@ public void Dispose() { } public void OnLeaderboardSet(BeatmapKey beatmapKey) { - BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); - RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, currentScoreScope, _loadingControl, Guid.NewGuid().ToString()).RunTask(); + _currentBeatmapKey = beatmapKey; + try { + Plugin.Log.Notice("OnLeaderboardSet"); + BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); + Plugin.Log.Notice("Got beatmaplevel"); + RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, currentScoreScope, loadingLB, Guid.NewGuid().ToString()).RunTask(); + } catch(Exception ex) { Plugin.Log.Error(ex.Message); } + } + + + // probably a better place to put this + public class CellClicker : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { + public Action onClick; + public int index; + public ImageView seperator; + public Vector3 originalScale; + private bool isScaled = false; + + private Color origColour = new Color(1, 1, 1, 1); + private Color origColour0 = new Color(1, 1, 1, 0.2509804f); + private Color origColour1 = new Color(1, 1, 1, 0); + + private void Start() { + originalScale = seperator.transform.localScale; + } + + public void OnPointerClick(PointerEventData data) { + BeatSaberUI.BasicUIAudioManager.HandleButtonClickEvent(); + onClick(index); + } + + public void OnPointerEnter(PointerEventData eventData) { + if (!isScaled) { + seperator.transform.localScale = originalScale * 1.8f; + isScaled = true; + } + + Color targetColor = Color.white; + Color targetColor0 = Color.white; + Color targetColor1 = new Color(1, 1, 1, 0); + + float lerpDuration = 0.15f; + + StopAllCoroutines(); + StartCoroutine(LerpColors(seperator, seperator.color, targetColor, seperator.color0, targetColor0, seperator.color1, targetColor1, lerpDuration)); + } + + public void OnPointerExit(PointerEventData eventData) { + if (isScaled) { + seperator.transform.localScale = originalScale; + isScaled = false; + } + + float lerpDuration = 0.05f; + + StopAllCoroutines(); + StartCoroutine(LerpColors(seperator, seperator.color, origColour, seperator.color0, origColour0, seperator.color1, origColour1, lerpDuration)); + } + + + private IEnumerator LerpColors(ImageView target, Color startColor, Color endColor, Color startColor0, Color endColor0, Color startColor1, Color endColor1, float duration) { + float elapsedTime = 0f; + while (elapsedTime < duration) { + float t = elapsedTime / duration; + target.color = Color.Lerp(startColor, endColor, t); + target.color0 = Color.Lerp(startColor0, endColor0, t); + target.color1 = Color.Lerp(startColor1, endColor1, t); + elapsedTime += Time.deltaTime; + yield return null; + } + target.color = endColor; + target.color0 = endColor0; + target.color1 = endColor1; + } + + private void OnDestroy() { + StopAllCoroutines(); + onClick = null; + seperator.color = origColour; + seperator.color0 = origColour0; + seperator.color1 = origColour1; + } } } } \ No newline at end of file From ef717072040b29fc2973ce082bea5a0008c7b2ca Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:11:56 +1100 Subject: [PATCH 07/92] VERY WORKING STATE --- ScoreSaber/Core/MainInstaller.cs | 2 + .../Core/ReplaySystem/UI/imber-panel.bsml | 2 +- ScoreSaber/Resources/blank.png | Bin 0 -> 75 bytes ScoreSaber/Resources/friend.png | Bin 0 -> 23768 bytes ScoreSaber/Resources/link.png | Bin 0 -> 9432 bytes ScoreSaber/ScoreSaber.csproj | 6 + .../Leaderboard/ProfilePictureView.cs | 129 +++++--- .../Elements/Profile/ProfileDetailView.bsml | 2 +- ScoreSaber/UI/Elements/Team/TeamHost.bsml | 2 +- ScoreSaber/UI/Leaderboard/PanelView.bsml | 10 +- ScoreSaber/UI/Leaderboard/PanelView.cs | 112 +++---- .../UI/Leaderboard/ScoreSaberLeaderboard.cs | 6 +- .../ScoreSaberLeaderboardViewController.bsml | 83 +++-- .../ScoreSaberLeaderboardViewController.cs | 306 +++++++++++------- .../ScoreSaberSettingsFlowCoordinator.cs | 2 + .../MainSettingsViewController.bsml | 2 +- .../ViewControllers/FAQViewController.bsml | 2 +- .../ViewControllers/GlobalViewController.bsml | 2 +- .../ViewControllers/TeamViewController.bsml | 2 +- 19 files changed, 381 insertions(+), 289 deletions(-) create mode 100644 ScoreSaber/Resources/blank.png create mode 100644 ScoreSaber/Resources/friend.png create mode 100644 ScoreSaber/Resources/link.png diff --git a/ScoreSaber/Core/MainInstaller.cs b/ScoreSaber/Core/MainInstaller.cs index 051d110..fa61800 100644 --- a/ScoreSaber/Core/MainInstaller.cs +++ b/ScoreSaber/Core/MainInstaller.cs @@ -28,6 +28,8 @@ public override void InstallBindings() { Container.BindInterfacesTo().AsSingle(); + Container.Bind().AsSingle(); + Container.Bind().AsSingle().NonLazy(); Container.BindInterfacesTo().AsSingle(); diff --git a/ScoreSaber/Core/ReplaySystem/UI/imber-panel.bsml b/ScoreSaber/Core/ReplaySystem/UI/imber-panel.bsml index d371f97..5acdd53 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/imber-panel.bsml +++ b/ScoreSaber/Core/ReplaySystem/UI/imber-panel.bsml @@ -1,4 +1,4 @@ - + diff --git a/ScoreSaber/Resources/blank.png b/ScoreSaber/Resources/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..96729e159b885d68f678fc6053bd8a170ca1b10b GIT binary patch literal 75 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`LY^*;Ar-fh6C_v{Cy4YgaWF8j X2rw|NckR#t$})Jm`njxgN@xNA05A_g literal 0 HcmV?d00001 diff --git a/ScoreSaber/Resources/friend.png b/ScoreSaber/Resources/friend.png new file mode 100644 index 0000000000000000000000000000000000000000..e4949d37d54ff8bcc962a8b42727b568c7ea833f GIT binary patch literal 23768 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 j!TsY$on}~|5F7C`2TQo}Q z&O%pcpnvI20OI(qSRQ-`1|M{Bgu%!qTD!nVEMdRjaqzjH_wW42F?$G@lCURY zgBB*2qFn%kX^)jr-1Fjqf_Ptv=~j zxO3R=+I77++-i*OllKah0e5^}dA)ezAC{{2?BZBMj=8yZJ&`uhM|d#4oY9^7z@6Ju zIk)_wlh&U+GWX5r$?sDqf|}lr1HhM0Hg|-8I=sKtT~_9#v9k_^nOq^FkLnk?M{i*Z zHz;ejR8eNgmo)b-JzueEJ=kNFx9mg2gq1P+j<^!GiSIywi9@$+ey>W%&k37g+2?$_ z%U;?`m6%R+AdbDrBpyT{g(>gXA)Cv5g~?OBJl{99$%Afs!p&P;t^I0 zF|)XYzHoeUj3s5VQ#Al~G}62vF@8|e=3TJtsTk*#>cz0)`SVp19;H@pxZ1tT@ElZxZ@`;hCzGe9REoAH#q!i5hJ)4NxUo*ZKwQ>rV4tu<3 z(k^3iO>@j&GH9rxeRz|DHq{X}U9gb!$|3h@)Y>SughASrErSj0IeGkERf^uLqqDNU zIhTJbi z1xRT>*guUX%|9#(uQA0^kp25YMV!4SupC6t-xrv zKg^jRMTiEv&Yc$b>kW7y66I6ykG)BI3k)F6ve@ms)kMYvyZ#%1D51;xK?~QWIbMLc znm^H%b`b;UK(sDs>9k^x6=J`KN>!nL5Sqqrrqh8%RU7V&pjv zb&$D3NE*u}GaY?RN7Z$ddAb*o6nZY9hG48Rpsi0*;Q9WM90(Og4# zUCxj?;nxj0ea(G*RXW^pVSKW$t~dg}>o4EN5Z_?EnK=V20#q%elKOhC`T{fXyn*RR z#`&!ioWl%6Fvt*=IOx73M5IFUrTtFnY~sN-;zGpJ%l3$Wlk zRa$X|RE2d8@L0I7_&N*HsI0FT)TR1_W|~}rdn8BVwt@V>w;a}Cr!t4;Lv`~wz*a0c zGB(^mG~y4rZn&=qL{NONbL!s3g`E2gGRQJ6iFnKQlF9v5OR6X&1aV`8;ny}iLT~~( zMP$?6OEVZlrsf@b>(dR~rwJm45oXY&6MQ+REwb~s4b)hrN7p9n^5HxW0Y>f2p&iX2 zuj#e53vCsqu15u+;6>{D8(zZPFMBTPCj)r_6=y6OaiLyR02sEW_IXa*v6$KMW@X6u zHKctEC3Eu>7n~%7>B+VlH&9WzWlj~TF*&{$u)0ZbK96Nt4)*jINgxFySt3Ml1e;5W zvoyFAb^o%A?r0Gm4^VIpzS54LJyfrYIJ zKD{vA0lf9;;af(ufJk_6!dMwJJa92RY!HJKK@_xJoQ@?7laMS-^$LoLIC^x=sMsGE zbEfKN23bI)c4ar+fDZ^@_6d-y6fAO0kifs0-sT9y8!k9Ibn@hUXoxQ)a(bcpMD|Re z^wzv}l#jnOpUIx-R(7UBLODeYeL?a|+u;{R5Pfn0&Iz47l_SIBmh6T*fX2Mao7act zFx|JuEU7v*J&;6hX$X%Vmg6gOgV_fZL9UaxwK|n>gnd3W5xRK9mlb^#JvmgI ztaHp4ZuH(R+s_4B+^3dWYuZL7iC#;+i_Vv%m)XZcoGncuqwT>F(3t$3`IndJ{ti;4 zHtrg3s4BCmL~i12@L@}D76uz2?Thdn;SgA*DfqH{`Ol>vLZru>N7b}D%(p}o(`SBK z@W0f^&=(?~*mGVW$CK=tzK4%`W+T~{rHk+TBAF5Mkt<Wptx4$H_j&$8djiW`3*i@ll{BtP9B;R>uD{&&~^~tKBUM5)8RfwFEdLilH9L|_Ws$1guFtE z`AzMUQGjbYG@erUqkD=UeACT3%BZIW)Kc z8GUQ#v#s9hk&fx1-E%)Is%@iH4(2~EE1-;LY1!$zrPY`yI#zF&V=?T%Q*Vgy7>O`Jr>5MOCQ>~r;CLLo}cc@DxjpK z1ws4pW2MYf?vGHLTj?jl>+?b030$6kJ3ZmqZO6i7;8nmzA-pJZj3xQ@%^u>Xu@9bZ zq9BI^H$4+wUY5qb+tpXKmxLq=Yc|);9yO-D3>$vDODCO86zaIP;rA@Uf6%z-139Hp4Un{0{YO-}3uJ3#iM3MH`1 zo4u0+?8<=bFak7rUfaYD40n~BKD5(hI}qe;Bmo4t1=k!u#FNwtOhgoZY&FLlHS0YS z2h%X?2nr-fEC_#5fg%$de~8c0!s?mpE)8vNbbpd4n=PuWkaE zWu$v`Y?6VPry7A1FBZ!tgN4D-#%l6hQe|Jn?L$08j!d5M`K%|4eM+J%Tr7fCk})O) zOa+|mbNvWpQox~h;Tm>G;xMq1G!_O|Qv-Gf-f4&&!O8Qx(+8os0%SJHykyVZtPx~F zu#DTsoh71iK?=O%6Y#caDh`7XxqXhC8m1pW6hvXPlQF3masdktgI|U?f+aRYUQW~G zG2x15lcXRRw7ia*MMlssP|+UbsB(LR98 z<2haMowQPVD(^e#153!qfRT8%f*fdxygM&9dIxtVm(T}p+>jNZa~)Cvye)X#AmxF^ zd_hTLWUoAr^pX2UEfKSdn_1&`CVx!x=+f@1z7-pT@Mh}nTsH~2$=0}|;6`unga$W* z82?l`^b3na^gp>{RH2IfSHdQ~9*A_~($tczvu>aiN9Zm_4g4?&m$C5gh<_bO@T_pn=bI42`6#6LGG{?Y@21yU8|bBw)4 zj4~*qN%sCq^y<>`g^~(5TBt4Hq?vk2x5WL;6GdKE;^cYgo=!twkB;eC{Pra?El?@G zWaqkv|Fk-yB~>I$NU|SlBp#*8t_Q#s%+TL$F|U`3Q0CP`YV&J|T**Uh=w2Wosi#7` z5FhqlLhD0kr}k$Oa8u+int7=T(@PqQ-ZHDYR+_rft??=!*xz0Cts0k>AP=^j=GLmE z8;k~Oeaw;hT)iq9*g?!B!hRn1a2g6^L6kq}I1hP#a9}p57B645%>?G<)I2D?Bl_ha z30Wij+IdLxX@zR(@n9%gW;*pe=DW9d-}tMN(%t44trClUvSs^0tp zU{~?z`?)^rp^+`)ZQf;TU+l7OUikTplaE9vym;w@7CzQHNw5Xi0^qHNnyx=T-9)Lg zd7ks}OiB8D2D*O$vz5ijTjbB{@sg_MFuv^fqkI~E1he4KjhGjFyc;%f89y+kvCw6D zJw|!FK&F5NKr!&&Ft*Y7!3_lS(hrJrUO0h}2ZGik3*;&<17!cVfd5VoB9$M|MB)D@ za%C|K)@*{40N}e9@AdoEkdc1|Y!fZK*ZN|8{%*%{K6&ojF^2B2)Hj)`JPLT3u;5zCSfpE#H$!)X+T`-?I2^K_asg%RK z&O$qm*~8*~m!VyEy?&dz9?6^+X2I{wr{2!eSY{op9lo^k%3+QQZI_(u0eH32M~f?( zDH2#|G`^knaQ;zRpu`6p*reO{F6}(lla|~*~I9TrV1IF1mqxKiuTb1^K^B{^4+_@^Ca;7M9~Md|mEA|nSXHGo5zLQ*fiNH{FGZVQZ)OMVoT zQIMu^!-78u(cfp{C4QFMu!l9$&4V9B!P9Mm@#B<4up_by@=pWY%+KIuLPOMC0h{NQ-(sgQVn7

7!_R+VTIqv(;Buar1fYRq*#hGY1(do=h&mJ?@xjB(^R%>Rm5B!`SfHfkO>Fly zkaTgun@uVbl!dsYM=VUs5ld@uW!3)Z9)`vb6w>wW;iW&)qhCHF>;edfSQw(0r7J+l zQmz9zgcnny|5!?MO4|BGwdhlNpNph;Rbvb?z)B_=g1fsuBu!Q5q+>1=RR7X{5sW{g#yv*%+du zsKIIIMBtnT@13s+j#Wj-)7qfq;?&C}TbinJUIRZCO)p2>M!6k_y!dDlZP^MDx_(^! z&~4_H!~-^0Wwa47c;(D*53&^Mtf2sia3d`)1fx`o%)>MY?{9 zWcA967wOZ|+FBG=1+7L8mcjL34M>46Fd8AImg zpD&^&Pycd6_mq53GhO-o5Bu;-pUFEl#D7*tkl)O&f{F}Bvr#?s*3j)g=r2t##d@S2 ztN48Pv0KR&#v*tbCioOkey>uwGJLW%7J3@Kj%Q8-@Zk;#x)qD_CspzNuP4df)fsoC z(;Y6-qZ#kF(L@xnVU1Q3SEwherXC381()NLotx;e%U9W_hju!z2J6snPW=dyBid7)^vAfHkVvreuzF7;J^HHGu*Tc3dQIiB*mti* zQjgYj%Q^#aTVnNthAxXH{Pp`|*9$10ZOktP+(pl-_E71{!nfE$1Fu6?+x1lHNR)xU zD-*&6ICjRTl%YwuN-t)hCnJ3n?w)DHjh-y^OHX|1C+zd31r^TbG+dOOtiSA1E7`l$ zn&wwP8CdWbj}aul+MzB9%0VUMg(uv^b1^fBF{LnMA-G98_updrZSqMyfoS29x2$PC zDkQ|3CLTGd>06}p?IxSkhino=R#&%eZXnP86x$Et&8&)DNKAcdPXh&qtkQ)N%u-?N zn>)TlGyC#Z4Az~rkThFAvALzy*vR-7-$2{jS=ZmtNtAgTy-PdMm?o$U z^yxr)`bR0R_nlVu-O3T{bfVWui$?|hJ%pBwF1?qF&$rWh=2~#Nmz2<$a;PGOu;Y?T z6YpH%Z1TaZ8I7jFu>}>jG-gsKV7;zHj%nl-o+huzdPwmqXz5=xJpOx>Gp=)SXUvAezAO#KBH}E^r40W3v7&1(g;aBqARwME!&c1b0kw z|5)H{YbvUSd^UHzME2=nGb4K@PGY?x*3gx|Fi9vAz?A(Syk7OXtPB;21_->7#;SgJ zE>!k$DE0#<-^|s0OjjZD5oF)$X5d+tgL1(+LCnnwf zP<4LdP%T3rIvTUdbuQJLKdcdWSMcn+qDap+D`L!QdND+qENrvLUs&8QX? z`f5S2f!3AZ)WrOn-OT%1pyuXsZIjR|8&Rkpa8z?m>#OzkCQ07207kyG>#J>7kSt%F z!x);FFSpSOs8k7t{Y5jrhjzo`|4t0t|WO2xfwk zv>$X$^Bj^KPtAZ zoZQQ&(-1G0I0rqUVDLtbBF3-#`0z2&@~wP$=THC-co&9;8)6K|kM6jE?mVn&gxNsfr7l)(z(;?lVswNLuTnG$;&&_M$0W;5_$cmY-!dlMcudhw zZ4(^{esb%p7#OA_@uR@hP-W&LiEfO4y)l^i`_bNY^6dwCl-c)cFaO^Gl=5?!Q5ma( zi5q*MY|6ej^Xr1(`-QKymA#ECUq)o@19-s3{E532hzTp-Nrx=7t-L)3f^P3t%u zt+iJRu;H-nTYzN?3_y_l(`dGWp=K)@q5(}dh<4BjLJ~m(Nd9R6@%HG+hvO4g@>D5e8)`M}>cy0wmD-KN@r>NSJ?`e-lG&fGa@rokN;!t3sZE3>JoV zI>NHs=HdKG(!_=ezHu)s?>Y11&Z}?oTjFqkz#i{X>Gjx0E52|)#2NQH?D1jKtN7-y z`;nI5yM5@(e5}!nNrqw2%Xbqd+o(7gLS~YY>1H6gbI@ojdz4Tnc(>rC)%&e}gHjHK z51D=7_X8HXG>P;Dy0PP;Ft; zbPl(PdBg9A)n_w5f<0Un_VFGi_|&~$Zg#7wHItwrdOO$q#whoCwQ~Fta&Ca@p%@=Ql+E)X-cH zhczD#cUrpenl_z*8GtnG>` zq2+Os=ciiYEV;0|kIx&bcbvULs_t!Y;cVqjr}|Tjik)?ERK1_p&WLVX5iDUKGoN*eDH8w+PSbg|2b`6j9l29 z(U_|M{}(ckO}}ZNRoZMm-7y9~-hpW+IpLB*2}xnrA&Ft&1GWogvfB`~+t9?^XP2q9 ziK(@j1rlXtjY3`KJa+vLg4p=bsPOdvPB2>#`56#kAPL7)BCLZ6$HNSh6Qf|^Q88iG z#>U1eghb=i)KufBkoY*m1VZ95qmcMmPpA+&CsG?pP3n c8I&3sl@w+KIR4q7!CTY;r$dgky+_ad4@0hiZvX%Q literal 0 HcmV?d00001 diff --git a/ScoreSaber/ScoreSaber.csproj b/ScoreSaber/ScoreSaber.csproj index aaa3340..558d4fb 100644 --- a/ScoreSaber/ScoreSaber.csproj +++ b/ScoreSaber/ScoreSaber.csproj @@ -31,8 +31,11 @@ True + + + @@ -277,7 +280,10 @@ + + + diff --git a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs index 08b3dd0..5960384 100644 --- a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs +++ b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs @@ -24,7 +24,7 @@ internal class ProfilePictureView { private ICoroutineStarter coroutineStarter; - internal Sprite nullSprite = BeatSaberMarkupLanguage.Utilities.ImageResources.BlankSprite; + internal Sprite nullSprite => Utilities.FindSpriteInAssembly("ScoreSaber.Resources.blank.png"); public ProfilePictureView(int index) { this.index = index; @@ -44,9 +44,10 @@ public void Init(ICoroutineStarter coroutineStarter) { [UIAction("#post-parse")] public void Parsed() { profileImage.material = Plugin.NoGlowMatRound; - profileImage.sprite = nullSprite; + profileImage.sprite = Utilities.FindSpriteInAssembly("ScoreSaber.Resources.blank.png"); profileImage.gameObject.SetActive(true); loadingIndicator.gameObject.SetActive(false); + Active(false); } public void setProfileImage(string url, int pos, CancellationToken cancellationToken) { @@ -124,6 +125,12 @@ public void ClearSprite() { loadingIndicator.gameObject.SetActive(false); } } + + public void Active(bool state) { + if (profileImage != null) { + profileImage.gameObject.SetActive(state); + } + } } internal static class SpriteCache { @@ -146,55 +153,71 @@ internal static void AddSpriteToCache(string url, Sprite sprite) { } } - //internal class TweeningService { - // [Inject] private TimeTweeningManager _tweeningManager; - // private HashSet activeRotationTweens = new HashSet(); - - // public void RotateTransform(Transform transform, float rotationAmount, float time, Action callback = null) { - // if (activeRotationTweens.Contains(transform)) return; - // float startRotation = transform.rotation.eulerAngles.z; - // float endRotation = startRotation + rotationAmount; - - // Tween tween = new FloatTween(startRotation, endRotation, (float u) => - // { - // transform.rotation = Quaternion.Euler(0f, 0f, u); - // }, 0.1f, EaseType.Linear, 0f); - // tween.onCompleted = () => - // { - // callback?.Invoke(); - // activeRotationTweens.Remove(transform); - // }; - // tween.onKilled = () => - // { - // if (transform != null) transform.rotation = Quaternion.Euler(0f, 0f, endRotation); - // callback?.Invoke(); - // activeRotationTweens.Remove(transform); - // }; - // activeRotationTweens.Add(transform); - // _tweeningManager.AddTween(tween, transform); - // } - - // public void FadeText(TextMeshProUGUI text, bool fadeIn, float time) { - // float startAlpha = fadeIn ? 0f : 1f; - // float endAlpha = fadeIn ? 1f : 0f; - - // Tween tween = new FloatTween(startAlpha, endAlpha, (float u) => - // { - // text.color = text.color.ColorWithAlpha(u); - // }, 0.4f, EaseType.Linear, 0f); - // tween.onCompleted = () => - // { - // if (text == null) return; - // text.gameObject.SetActive(fadeIn); - // }; - // tween.onKilled = () => - // { - // if (text == null) return; - // text.gameObject.SetActive(fadeIn); - // text.color = text.color.ColorWithAlpha(endAlpha); - // }; - // text.gameObject.SetActive(true); - // _tweeningManager.AddTween(tween, text); - // } - //} + internal class TweeningService { + [Inject] private TimeTweeningManager _tweeningManager = null; + private HashSet activeRotationTweens = new HashSet(); + + public void RotateTransform(Transform transform, float rotationAmount, float time, Action callback = null) { + if (activeRotationTweens.Contains(transform)) return; + float startRotation = transform.rotation.eulerAngles.z; + float endRotation = startRotation + rotationAmount; + + Tween tween = new FloatTween(startRotation, endRotation, (float u) => { + transform.rotation = Quaternion.Euler(0f, 0f, u); + }, 0.1f, EaseType.Linear, 0f); + tween.onCompleted = () => { + callback?.Invoke(); + activeRotationTweens.Remove(transform); + }; + tween.onKilled = () => { + if (transform != null) transform.rotation = Quaternion.Euler(0f, 0f, endRotation); + callback?.Invoke(); + activeRotationTweens.Remove(transform); + }; + activeRotationTweens.Add(transform); + _tweeningManager.AddTween(tween, transform); + } + + public void FadeText(TextMeshProUGUI text, bool fadeIn, float time) { + float startAlpha = fadeIn ? 0f : 1f; + float endAlpha = fadeIn ? 1f : 0f; + + Tween tween = new FloatTween(startAlpha, endAlpha, (float u) => { + text.color = text.color.ColorWithAlpha(u); + }, 0.4f, EaseType.Linear, 0f); + tween.onCompleted = () => { + if (text == null) return; + text.gameObject.SetActive(fadeIn); + }; + tween.onKilled = () => { + if (text == null) return; + text.gameObject.SetActive(fadeIn); + text.color = text.color.ColorWithAlpha(endAlpha); + }; + text.gameObject.SetActive(true); + _tweeningManager.AddTween(tween, text); + } + + public void LerpColor(ImageView currentImageView, Color newColor) { + + Tween tween = new ColorTween(currentImageView.color, newColor, (Color u) => { + currentImageView.color = u; + currentImageView.color0 = u; + currentImageView.color1 = u; + }, 0.3f, EaseType.Linear, 0f); + tween.onCompleted = () => { + if (currentImageView == null) return; + currentImageView.color = newColor; + currentImageView.color0 = newColor; + currentImageView.color1 = newColor; + }; + tween.onKilled = () => { + if (currentImageView == null) return; + currentImageView.color = newColor; + currentImageView.color0 = newColor; + currentImageView.color1 = newColor; + }; + _tweeningManager.AddTween(tween, currentImageView); + } + } } diff --git a/ScoreSaber/UI/Elements/Profile/ProfileDetailView.bsml b/ScoreSaber/UI/Elements/Profile/ProfileDetailView.bsml index 119e186..d74fc6e 100644 --- a/ScoreSaber/UI/Elements/Profile/ProfileDetailView.bsml +++ b/ScoreSaber/UI/Elements/Profile/ProfileDetailView.bsml @@ -1,4 +1,4 @@ - + diff --git a/ScoreSaber/UI/Elements/Team/TeamHost.bsml b/ScoreSaber/UI/Elements/Team/TeamHost.bsml index da93a87..b02c720 100644 --- a/ScoreSaber/UI/Elements/Team/TeamHost.bsml +++ b/ScoreSaber/UI/Elements/Team/TeamHost.bsml @@ -1,4 +1,4 @@ - + diff --git a/ScoreSaber/UI/Leaderboard/PanelView.bsml b/ScoreSaber/UI/Leaderboard/PanelView.bsml index 0eca343..6a9b643 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.bsml +++ b/ScoreSaber/UI/Leaderboard/PanelView.bsml @@ -1,9 +1,9 @@ - - - + + + - + @@ -12,7 +12,7 @@ - + diff --git a/ScoreSaber/UI/Leaderboard/PanelView.cs b/ScoreSaber/UI/Leaderboard/PanelView.cs index 9bf404d..6ab43d5 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.cs +++ b/ScoreSaber/UI/Leaderboard/PanelView.cs @@ -16,6 +16,10 @@ using UnityEngine; using Zenject; using ScoreSaber.UI.Main; +using IPA.Config.Data; +using TMPro; +using ScoreSaber.UI.Elements.Leaderboard; +using System.Threading; namespace ScoreSaber.UI.Leaderboard { [HotReload] @@ -31,18 +35,32 @@ internal class PanelView : BSMLAutomaticViewController { [UIComponent("scoresaber-logo")] protected readonly ClickableImage _scoresaberLogoClickable = null; - [UIComponent("container")] - protected readonly Backgroundable _container = null; - [UIComponent("prompt-root")] protected readonly RectTransform _promptRoot = null; [UIComponent("prompt-text")] protected readonly CurvedTextMeshPro _promptText = null; + [UIComponent("container")] + protected readonly Backgroundable _container = null; + [UIComponent("prompt-loader")] protected readonly ImageView _promptLoader = null; + //[UIComponent("playerPFP")] + //protected readonly ImageView _playerPFP = null; + + //private string _usernameText = "Loading..."; + + //[UIValue("usernameText")] + //protected string usernameText { + // get => _usernameText; + // set { + // _usernameText = value; + // NotifyPropertyChanged(); + // } + //} + private string _globalLeaderboardRanking = "Global Ranking: Loading..."; [UIValue("global-leaderboard-ranking")] protected string globalLeaderboardRanking { @@ -53,12 +71,12 @@ protected string globalLeaderboardRanking { } } - private string _leaderboardRankedStatus = "Ranked Status: Loading..."; - [UIValue("leaderboard-ranked-status")] - protected string leaderboardRankedStatus { - get => _leaderboardRankedStatus; + private string _countryLeaderboardRanking = "Country Ranking: Loading..."; + [UIValue("country-leaderboard-ranking")] + protected string countryLeaderboardRanking { + get => _countryLeaderboardRanking; set { - _leaderboardRankedStatus = $"Ranked Status: {value}"; + _countryLeaderboardRanking = value; NotifyPropertyChanged(); } } @@ -87,10 +105,8 @@ protected bool isLoaded { } #endregion - private bool _gayMode; - private float _theWilliamVal; - private Sprite _denyahSprite; private ImageView _background; + private Tween _activeDisableTween; internal PlayerInfo _currentPlayerInfo; private CanvasGroup _promptCanvasGroup; @@ -102,46 +118,12 @@ protected bool isLoaded { private TimeTweeningManager _timeTweeningManager = null; private Color _scoreSaberBlue; - private Gradient _theWilliamGradient; internal static readonly FieldAccessor.Accessor ImageSkew = FieldAccessor.GetAccessor("_skew"); internal static readonly FieldAccessor.Accessor ImageGradient = FieldAccessor.GetAccessor("_gradient"); - private bool _isWilliums; - internal bool isWilliums { - get { return _isWilliums; } - set { - if (_isWilliums == value) { return; } - _gayMode = value; - if (!value) { _background.color = _scoreSaberBlue; } - _isWilliums = value; - } - } - - private bool _isDenyah; - internal bool isDenyah { - get { return _isDenyah; } - set { - if (_isDenyah == value) { return; } - - if (_background == null) return; - if (!value) { - _background.color = _scoreSaberBlue; - return; - } - if (_denyahSprite == null) { -#pragma warning disable CS0618 // Type or member is obsolete - _denyahSprite = Utilities.LoadSpriteRaw(Utilities.GetResource(Assembly.GetExecutingAssembly(), "ScoreSaber.Resources.bri-ish.png")); -#pragma warning restore CS0618 // Type or member is obsolete - } - _background.overrideSprite = _denyahSprite; - _isDenyah = value; - } - } - [Inject] protected void Construct(PlayerService playerService, TimeTweeningManager timeTweeningManager) { _scoreSaberBlue = new Color(0f, 0.4705882f, 0.7254902f); - _theWilliamGradient = new Gradient { mode = GradientMode.Blend, colorKeys = new GradientColorKey[] { new GradientColorKey(Color.red, 0f), new GradientColorKey(new Color(1f, 0.5f, 0f), 0.17f), new GradientColorKey(Color.yellow, 0.34f), new GradientColorKey(Color.green, 0.51f), new GradientColorKey(Color.blue, 0.68f), new GradientColorKey(new Color(0.5f, 0f, 0.5f), 0.85f), new GradientColorKey(Color.red, 1.15f) } }; _playerService = playerService; _timeTweeningManager = timeTweeningManager; Plugin.Log.Debug("PanelView Setup!"); @@ -156,6 +138,7 @@ protected void Parsed() { _separator.name = "Separator"; _background.color0 = Color.white; + _background.color = _scoreSaberBlue; _background.color1 = new Color(1f, 1f, 1f, 0f); ImageGradient(ref _background) = true; ImageSkew(ref _background) = 0.18f; @@ -198,11 +181,7 @@ protected void ClickedRanking() { } } - [UIAction("clicked-status")] - protected void ClickedStatus() { - statusWasSelected?.Invoke(); - } private async Task BlinkLogo() { @@ -226,9 +205,12 @@ public void SetGlobalRanking(string globalRanking, bool withPrefix = true) { } } - public void SetRankedStatus(string rankedStatus) { - - leaderboardRankedStatus = rankedStatus; + public void SetCountryRanking(string countryRanking, string countryCode, bool withPrefix = true) { + if(withPrefix) { + countryLeaderboardRanking = $"{countryCode} Ranking: {countryRanking}"; + } else { + countryLeaderboardRanking = countryRanking; + } } public void SetPromptInfo(string status, bool showLoadingIndicator, float dismissTime = -1f) { @@ -267,7 +249,7 @@ public void SetPrompt(string status, bool showLoadingIndicator, float dismissTim if (!_promptRoot.gameObject.activeInHierarchy) { _promptRoot.gameObject.SetActive(true); - _timeTweeningManager.AddTween(new FloatTween(0f, 1f, ChangePromptState, tweenTime, _gayMode ? EaseType.OutBounce : EaseType.InSine), _promptRoot); + _timeTweeningManager.AddTween(new FloatTween(0f, 1f, ChangePromptState, tweenTime, EaseType.InSine), _promptRoot); } if (_promptRoot.gameObject.activeInHierarchy && dismissTime != -1) { @@ -306,33 +288,18 @@ public void Loaded(bool value) { isLoaded = value; } - protected void Update() { - - if (_gayMode) { - _background.color = _theWilliamGradient.Evaluate(_theWilliamVal); - _theWilliamVal += Time.deltaTime * 0.1f; - if (_theWilliamVal > 1f) _theWilliamVal = 0f; - } - } - public async Task RankUpdater() { await TaskEx.WaitUntil(() => _playerService.loginStatus == PlayerService.LoginStatus.Success); - if (_playerService.localPlayerInfo.playerId == PlayerIDs.Williums) { - isWilliums = true; - } - - if (_playerService.localPlayerInfo.playerId == PlayerIDs.Denyah) { - isDenyah = true; - } - while (true) { await UpdateRank(); await Task.Delay(240000); } } + //[Inject] private readonly ICoroutineStarter _coroutineStarter = null; + public async Task UpdateRank() { try { @@ -340,19 +307,24 @@ public async Task UpdateRank() { _currentPlayerInfo = await _playerService.GetPlayerInfo(_playerService.localPlayerInfo.playerId, full: false); if (Plugin.Settings.showLocalPlayerRank) { SetGlobalRanking($"#{string.Format("{0:n0}", _currentPlayerInfo.rank)} ({string.Format("{0:n0}", _currentPlayerInfo.pp)}pp)"); + SetCountryRanking($"#{string.Format("{0:n0}", _currentPlayerInfo.countryRank)}", _currentPlayerInfo.country); } else { SetGlobalRanking("Hidden"); + SetCountryRanking("Hidden", _currentPlayerInfo.country); } Loaded(true); } catch (HttpErrorException ex) { if (ex.isScoreSaberError) { if (ex.scoreSaberError.errorMessage == "Player not found") { SetGlobalRanking("Welcome to ScoreSaber! Set a score to create a profile", false); + SetCountryRanking("", _currentPlayerInfo.country, false); } else { SetGlobalRanking($"Failed to load player ranking: {ex.scoreSaberError.errorMessage}", false); + SetCountryRanking("", _currentPlayerInfo.country, false); } } else { SetGlobalRanking("", false); + SetCountryRanking("", _currentPlayerInfo.country, false); SetPromptError("Failed to update local player ranking", false, 1.5f); Plugin.Log.Error("Failed to update local player ranking " + ex.ToString()); } diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs index 20bb808..94c338a 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboard.cs @@ -1,6 +1,7 @@ using HMUI; using LeaderboardCore.Managers; using LeaderboardCore.Models; +using ScoreSaber.Core.Services; using System; using System.Collections.Generic; using System.Linq; @@ -12,6 +13,7 @@ internal class ScoreSaberLeaderboard : CustomLeaderboard, IDisposable { private readonly CustomLeaderboardManager _manager; private readonly ScoreSaberLeaderboardViewController _leaderboardView; + private readonly PlayerService _playerService; public override bool ShowForLevel(BeatmapKey? selectedLevel) { if (selectedLevel.HasValue) { @@ -23,11 +25,13 @@ public override bool ShowForLevel(BeatmapKey? selectedLevel) { } protected override string leaderboardId => "ScoreSaber"; - internal ScoreSaberLeaderboard(CustomLeaderboardManager customLeaderboardManager, PanelView panelView, ScoreSaberLeaderboardViewController leaderboardView) { + internal ScoreSaberLeaderboard(CustomLeaderboardManager customLeaderboardManager, PanelView panelView, ScoreSaberLeaderboardViewController leaderboardView, PlayerService playerService) { panelViewController = panelView; _leaderboardView = leaderboardView; _manager = customLeaderboardManager; _manager.Register(this); + _playerService = playerService; + _playerService.GetLocalPlayerInfo(); } protected override ViewController panelViewController { get; } diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml index 4d084b2..a3270dc 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml @@ -1,5 +1,5 @@ + xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/ https://monkeymanboy.github.io/BSML-Docs/BSMLSchema.xsd'> - + + rich-text="true" + active="true" + /> + + + - + + + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 63482c9..412ac23 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -36,6 +36,8 @@ using UnityEngine.EventSystems; using UnityEngine.UI; using Zenject; +using static ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController; +using static System.Windows.Forms.VisualStyles.VisualStyleElement; using Button = UnityEngine.UI.Button; namespace ScoreSaber.UI.Leaderboard { @@ -79,6 +81,12 @@ public enum UploadStatus { [UIComponent("headerText")] private readonly TextMeshProUGUI headerText; + [UIObject("headerSTATIC")] + private readonly GameObject headerSTATIC; + + [UIComponent("headerTextSTATIC")] + private readonly TextMeshProUGUI headerTextSTATIC; + [UIComponent("errorText")] private readonly TextMeshProUGUI _errorText; @@ -106,8 +114,11 @@ public enum UploadStatus { [UIObject("loadingLB")] private readonly GameObject loadingLB; - [UIAction("OnPageUp")] private void UpButtonClicked() => DirectionalButtonClicked(false); - [UIAction("OnPageDown")] private void DownButtonClicked() => DirectionalButtonClicked(true); + [UIObject("starRatingBox")] + private readonly GameObject starRatingBox; + + [UIAction("OnPageUp")] private void UpButtonClicked() => UpdatePageChanged(-1); + [UIAction("OnPageDown")] private void DownButtonClicked() => UpdatePageChanged(1); public bool activated { get; private set; } @@ -115,10 +126,18 @@ public enum UploadStatus { public ScoreSaberScoresScope currentScoreScope { get; set; } - private bool _replayDownloading; + private bool _replayDownloading = false; private string _currentLeaderboardRefreshId = string.Empty; private BeatmapKey _currentBeatmapKey; + Color yellow = new Color(250f / 255f, 221f / 255f, 45f / 255f); + Color green = new Color(63 / 255f, 191 / 255f, 100 / 255f); + Color grey = new Color(125f / 255f, 125f / 255f, 125f / 255f); + Color blue = new Color(62 / 255f, 152 / 255f, 237 / 255f); + Color pink = new Color(235 / 255f, 73 / 255f, 232 / 255f); + Color _scoreSaberBlue = new Color(0f, 0.4705882f, 0.7254902f); + + [Inject] private readonly PanelView _panelView; [Inject] private readonly SiraLog _log; [Inject] private readonly DiContainer _container; @@ -131,6 +150,7 @@ public enum UploadStatus { [Inject] private readonly MaxScoreCache _maxScoreCache; [Inject] private readonly PlatformLeaderboardViewController _plvc; [Inject] private readonly BeatmapLevelsModel _beatmapLevelsModel; + [Inject] private readonly TweeningService _tweeningService; private void infoButtons_infoButtonClicked(int index) { if (_leaderboardService.currentLoadedLeaderboard == null) { return; } @@ -161,6 +181,7 @@ private void playerService_LoginStatusChanged(PlayerService.LoginStatus loginSta case PlayerService.LoginStatus.Success: _panelView.SetPromptSuccess(status, false, 3f); _panelView.RankUpdater().RunTask(); + _ImageHolders.ForEach(holder => holder.Active(true)); RefreshLeaderboard(); break; } @@ -196,7 +217,7 @@ private void uploadDaemon_UploadStatusChanged(UploadStatus status, string status } } - private ImageView _imgView; + private ImageView _headerBackground; internal static readonly FieldAccessor.Accessor ImageSkew = FieldAccessor.GetAccessor("_skew"); internal static readonly FieldAccessor.Accessor ImageGradient = FieldAccessor.GetAccessor("_gradient"); @@ -210,13 +231,51 @@ private void PostParse() { Destroy(loadingContainer.Find("Text").gameObject); Destroy(loadingLB.transform.Find("RefreshContainer").gameObject); Destroy(loadingLB.transform.Find("DownloadingContainer").gameObject); - _imgView = myHeader.Background as ImageView; - Color color = new Color(255f / 255f, 222f / 255f, 24f / 255f); - _imgView.color = color; - _imgView.color0 = color; - _imgView.color1 = color; - ImageSkew(ref _imgView) = 0.18f; - ImageGradient(ref _imgView) = true; + _headerBackground = myHeader.Background as ImageView; + + _headerBackground.color = grey; + _headerBackground.color0 = grey; + _headerBackground.color1 = grey; + ImageSkew(ref _headerBackground) = 0.18f; + ImageGradient(ref _headerBackground) = true; + CheckPage(); + } + + private void SetPanelStatus() { + + bool ranked = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.ranked; + bool qualified = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.qualified; + bool loved = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.loved; + + + if (_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.stars != 0) { + starRatingBox.gameObject.SetActive(true); + headerText.text = $" {_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.stars.ToString().Replace(".", ". ")}★"; + headerText.richText = true; + headerSTATIC.gameObject.SetActive(false); + } else { + starRatingBox.gameObject.SetActive(false); + headerSTATIC.gameObject.SetActive(true); + } + + if(!ranked && !qualified && !loved) { + _tweeningService.LerpColor(_headerBackground, grey); + headerTextSTATIC.text = "UNRANKED"; + } + + if (ranked) { + _tweeningService.LerpColor(_headerBackground, yellow); + } + + if (qualified) { + _tweeningService.LerpColor(_headerBackground, _scoreSaberBlue); + headerTextSTATIC.text = "QUALIFIED"; + } + + if (loved) { + _tweeningService.LerpColor(_headerBackground, pink); + headerTextSTATIC.text = "LOVED"; + } } [UIAction("OpenLeaderboardPage")] @@ -225,15 +284,20 @@ internal void OpenLeaderboardPage() { } [UIAction("SettingsClicked")] - internal void OpenBugPage() => ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); + internal void OpenSettingsPage() => ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); + + [UIAction("clicked-status")] + protected void ClickedStatus() { + _panelView.statusWasSelected?.Invoke(); + } [UIAction("OnIconSelected")] private void OnIconSelected(SegmentedControl segmentedControl, int index) { currentScoreScope = (ScoreSaberScoresScope)index; - leaderboardPage = 0; - CheckPage(); + leaderboardPage = 1; OnLeaderboardSet(_currentBeatmapKey); + CheckPage(); } [UIValue("leaderboardIcons")] @@ -244,8 +308,8 @@ private List leaderboardIcons { { new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.globe.png"), "Global"), new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Around you"), - new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.Player.png"), "Friends"), - new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.country.png"), "Country") + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.friend.png"), "Friends"), + new IconSegmentedControl.DataItem(Utilities.FindSpriteInAssembly("ScoreSaber.Resources.country.png"), "Area") }; #pragma warning restore CS0618 // Type or member is obsolete } @@ -269,9 +333,9 @@ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, }; _container.Inject(_profileDetailView); - _playerService.GetLocalPlayerInfo(); _ImageHolders.ForEach(holder => holder.ClearSprite()); activated = true; + OnIconSelected(null, 0); } Transform header = _plvc.transform.Find("HeaderPanel"); _plvc.GetComponentInChildren().color = new Color(0, 0, 0, 0); @@ -289,11 +353,13 @@ protected override void DidDeactivate(bool removedFromHierarchy, bool screenSyst private CancellationTokenSource cancellationToken; public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, ScoreSaberScoresScope scope, GameObject loadingControl, string refreshId) { - Plugin.Log.Info("begin refresh leaderboard"); try { + if (loadingControl == null || tableView == null) return; loadingControl.SetActive(false); _errorText.gameObject.SetActive(false); tableView.SetScores(new List(), -1); + SetClickersOff(); + headerTextSTATIC.text = ""; _currentLeaderboardRefreshId = refreshId; if (_uploadDaemon.uploading) { return; } if (!activated) { return; } @@ -301,14 +367,13 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (scope == ScoreSaberScoresScope.Player) { _upButton.interactable = false; _downButton.interactable = false; - } else { - _upButton.interactable = true; - _downButton.interactable = true; } ByeImages(); _errorText.gameObject.SetActive(false); loadingControl.SetActive(true); + starRatingBox.gameObject.SetActive(false); + headerSTATIC.gameObject.SetActive(true); if (cancellationToken != null) { cancellationToken.Cancel(); @@ -317,7 +382,7 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm cancellationToken = new CancellationTokenSource(); if (_playerService.loginStatus == PlayerService.LoginStatus.Error) { - SetErrorState(tableView, loadingControl, null, null, "ScoreSaber authentication failed, please restart Beat Saber", false); + SetErrorState(tableView, ref loadingControl, null, null, "ScoreSaber authentication failed, please restart Beat Saber", false); ByeImages(); return; } @@ -329,31 +394,30 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm await Task.Delay(500); // Delay before doing anything to prevent leaderboard spam - Plugin.Log.Info("AFTER TASK DELAY"); if (_currentLeaderboardRefreshId == refreshId) { int maxMultipliedScore = await _maxScoreCache.GetMaxScore(beatmapLevel, beatmapKey); LeaderboardMap leaderboardData = await _leaderboardService.GetLeaderboardData(maxMultipliedScore, beatmapLevel, beatmapKey, scope, leaderboardPage, _playerDataModel.playerData.playerSpecificSettings); - Plugin.Log.Info("AFTER LB DATA"); if (_currentLeaderboardRefreshId != refreshId) { return; // we need to check this again, since some time may have passed due to waiting for leaderboard data } - Plugin.Log.Info("AFTER CHECK"); - SetRankedStatus(leaderboardData.leaderboardInfoMap.leaderboardInfo); + SetPanelStatus(); List leaderboardTableScoreData = leaderboardData.ToScoreData(); int playerScoreIndex = GetPlayerScoreIndex(leaderboardData); if (leaderboardTableScoreData.Count != 0) { if (scope == ScoreSaberScoresScope.Player && playerScoreIndex == -1) { - SetErrorState(tableView, loadingControl, null, null, "You haven't set a score on this leaderboard"); + SetErrorState(tableView, ref loadingControl, null, null, "You haven't set a score on this leaderboard"); } else { if (_currentLeaderboardRefreshId != refreshId) { return; // we need to check this again, since some time may have passed due to waiting for leaderboard data } tableView.SetScores(leaderboardTableScoreData, playerScoreIndex); - Plugin.Log.Info("AFTER TABLE ST SCORE DELAY"); - PatchLeaderboardTableView(tableView); + for (int i = 0; i < leaderboardTableScoreData.Count; i++) { + if (_currentLeaderboardRefreshId != refreshId) { + return; // we need to check this again, since some time may have passed due to waiting for leaderboard data + } _ImageHolders[i].setProfileImage(leaderboardData.scores[i].score.leaderboardPlayerInfo.profilePicture, i, cancellationToken.Token); } loadingControl.gameObject.SetActive(false); @@ -361,42 +425,26 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (_uploadDaemon.uploading) { _panelView.DismissPrompt(); } + CheckPage(); } } else { if (leaderboardPage > 1) { - SetErrorState(tableView, loadingControl, null, null, "No scores on this page"); + SetErrorState(tableView, ref loadingControl, null, null, "No scores on this page"); } else { - SetErrorState(tableView, loadingControl, null, null, "No scores on this leaderboard, be the first!"); + SetErrorState(tableView, ref loadingControl, null, null, "No scores on this leaderboard, be the first!"); } ByeImages(); } + PatchLeaderboardTableView(tableView); } } catch (HttpErrorException httpError) { - SetErrorState(tableView, loadingControl, httpError); + SetErrorState(tableView, ref loadingControl, httpError); } catch (Exception exception) { - SetErrorState(tableView, loadingControl, null, exception); + SetErrorState(tableView, ref loadingControl, null, exception); } } - private void SetRankedStatus(LeaderboardInfo leaderboardInfo) { - if (leaderboardInfo.ranked) { - if (leaderboardInfo.positiveModifiers) { - _panelView.SetRankedStatus("Ranked (DA = +0.02, GN +0.04)"); - } else { - _panelView.SetRankedStatus("Ranked (modifiers disabled)"); - } - return; - } - if (leaderboardInfo.qualified) { - _panelView.SetRankedStatus("Qualified"); - return; - } - if (leaderboardInfo.loved) { - _panelView.SetRankedStatus("Loved"); - return; - } - _panelView.SetRankedStatus("Unranked"); - } + public int GetPlayerScoreIndex(LeaderboardMap leaderboardMap) { for (int i = 0; i < leaderboardMap.scores.Length; i++) { @@ -412,62 +460,55 @@ public void AllowReplayWatching(bool value) { _scoreDetailView.AllowReplayWatching(value); } - private void SetErrorState(LeaderboardTableView tableView, GameObject loadingControl, HttpErrorException httpErrorException = null, Exception exception = null, string errorText = "Failed to load leaderboard, score won't upload", bool showRefreshButton = true) { - - if (httpErrorException != null) { - if (httpErrorException.isNetworkError) { - errorText = "Failed to load leaderboard due to a network error, score won't upload"; - _leaderboardService.currentLoadedLeaderboard = null; - } - if (httpErrorException.isScoreSaberError) { - errorText = httpErrorException.scoreSaberError.errorMessage; - if (errorText == "Leaderboard not found") { + private void SetErrorState(LeaderboardTableView tableView, ref GameObject loadingControl, HttpErrorException httpErrorException = null, Exception exception = null, string errorText = "Failed to load leaderboard, score won't upload", bool showRefreshButton = true) { + try { + SetClickersOff(); + if (httpErrorException != null) { + if (httpErrorException.isNetworkError) { + errorText = "Failed to load leaderboard due to a network error, score won't upload"; _leaderboardService.currentLoadedLeaderboard = null; - _panelView.SetRankedStatus(""); + } + if (httpErrorException.isScoreSaberError) { + errorText = httpErrorException.scoreSaberError.errorMessage; + if (errorText == "Leaderboard not found") { + _leaderboardService.currentLoadedLeaderboard = null; + } + if (errorText == "Player hasn't set a score on this leaderboard") { + errorText = "You haven't set a score on this map!"; + } } } - } - if (exception != null) { - Plugin.Log.Error(exception.ToString()); - } - loadingControl.gameObject.SetActive(false); - _errorText.gameObject.SetActive(true); - _errorText.text = errorText; - tableView.SetScores(new List(), -1); - ByeImages(); - } - - public void DirectionalButtonClicked(bool down) { - - if (down) { - leaderboardPage++; - } else { - leaderboardPage--; - } - RefreshLeaderboard(); - CheckPage(); - } - - public void ChangePageButtonsEnabledState(bool state) { - - if (state) { - if (leaderboardPage > 1) { - _upButton.interactable = state; + if (exception != null) { + Plugin.Log.Error(exception.ToString()); } - _downButton.interactable = state; - } else { - _upButton.interactable = state; - _downButton.interactable = state; + loadingControl.gameObject.SetActive(false); + _errorText.gameObject.SetActive(true); + _errorText.text = errorText; + tableView.SetScores(new List(), -1); + ByeImages(); + SetClickersOff(); + } catch { + } } public void CheckPage() { - - if (leaderboardPage > 0) { - _upButton.interactable = true; - } else { + if(_leaderboardService.currentLoadedLeaderboard == null || currentScoreScope == ScoreSaberScoresScope.Player) { _upButton.interactable = false; + _downButton.interactable = false; + return; } + var totalPages = Mathf.CeilToInt((float)_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.plays / 10); + _upButton.interactable = leaderboardPage > 1; + _downButton.interactable = leaderboardPage < totalPages; + } + + private void UpdatePageChanged(int inc) { + if (_leaderboardService.currentLoadedLeaderboard == null) return; + var totalPages = Mathf.CeilToInt((float)_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.plays / 10); + leaderboardPage = Mathf.Clamp(leaderboardPage + inc, 0, totalPages - 1); + RefreshLeaderboard(); + CheckPage(); } public void RefreshLeaderboard() { @@ -503,35 +544,60 @@ private async Task StartReplay(ScoreMap score) { _replayDownloading = false; } + private void SetClickersOff() { + foreach (var holder in _cellClickingHolders) { + var x = holder.cellClickerImage.gameObject.GetComponent(); + if (x != null) { + x.clickable = false; + } + } + } + private bool obtainedAnchor = false; private Vector2 normalAnchor = Vector2.zero; void PatchLeaderboardTableView(LeaderboardTableView tableView) { - int i = 0; - foreach (LeaderboardTableCell cell in tableView.GetComponentsInChildren()) { + LeaderboardTableCell[] cells = tableView.GetComponentsInChildren(); - LeaderboardTableCell tableCell = (LeaderboardTableCell)cell; + for (int i = 0; i < cells.Length; i++) { + LeaderboardTableCell tableCell = cells[i]; - CellClicker cellClicker = _cellClickingHolders[i].cellClickerImage.gameObject.AddComponent(); - cellClicker.onClick = _infoButtons.InfoButtonClicked; - cellClicker.index = i; - cellClicker.seperator = tableCell.GetField("_separatorImage") as ImageView; + int cellIdx = tableCell.idx; - TextMeshProUGUI _playerNameText = tableCell.GetField("_playerNameText"); + if (cellIdx < _cellClickingHolders.Count) { + var clickerHolder = _cellClickingHolders[cellIdx]; - if (!obtainedAnchor) { - normalAnchor = _playerNameText.rectTransform.anchoredPosition; - obtainedAnchor = true; - } + CellClicker cellClicker = clickerHolder.cellClickerImage.gameObject.GetComponent(); + if (cellClicker == null) { + cellClicker = clickerHolder.cellClickerImage.gameObject.AddComponent(); + } - _playerNameText.richText = true; - Vector2 newPosition = new Vector2(normalAnchor.x + 2.5f, 0f); - _playerNameText.rectTransform.anchoredPosition = newPosition; - tableCell.showSeparator = true; - i++; + cellClicker.onClick = _infoButtons.InfoButtonClicked; + cellClicker.index = cellIdx; + cellClicker.seperator = (ImageView)tableCell._separatorImage; + cellClicker.clickable = true; + + TextMeshProUGUI _playerNameText = tableCell._playerNameText; + + _playerNameText.richText = true; + + if (!obtainedAnchor) { + normalAnchor = _playerNameText.rectTransform.anchoredPosition; + obtainedAnchor = true; + } + Vector2 newPosition = new Vector2(normalAnchor.x + 3f, normalAnchor.y); + _playerNameText.rectTransform.anchoredPosition = newPosition; + + tableCell.showSeparator = true; + } } + + _downButton.interactable = cells.Length >= 9; } + + + public void Initialize() { _infoButtons = new EntryHolder(); _scoreDetailView = new ScoreDetailView(); @@ -554,9 +620,7 @@ public void Dispose() { public void OnLeaderboardSet(BeatmapKey beatmapKey) { _currentBeatmapKey = beatmapKey; try { - Plugin.Log.Notice("OnLeaderboardSet"); BeatmapLevel beatmapLevel = _beatmapLevelsModel.GetBeatmapLevel(beatmapKey.levelId); - Plugin.Log.Notice("Got beatmaplevel"); RefreshLeaderboard(beatmapLevel, beatmapKey, leaderboardTableView, currentScoreScope, loadingLB, Guid.NewGuid().ToString()).RunTask(); } catch(Exception ex) { Plugin.Log.Error(ex.Message); } } @@ -567,23 +631,26 @@ public class CellClicker : MonoBehaviour, IPointerClickHandler, IPointerEnterHan public Action onClick; public int index; public ImageView seperator; - public Vector3 originalScale; + public Vector3 originalScale = new Vector3(1, 1, 1); private bool isScaled = false; private Color origColour = new Color(1, 1, 1, 1); private Color origColour0 = new Color(1, 1, 1, 0.2509804f); private Color origColour1 = new Color(1, 1, 1, 0); + public bool clickable = false; private void Start() { - originalScale = seperator.transform.localScale; + seperator.transform.localScale = originalScale; } public void OnPointerClick(PointerEventData data) { + if(!clickable) return; BeatSaberUI.BasicUIAudioManager.HandleButtonClickEvent(); onClick(index); } public void OnPointerEnter(PointerEventData eventData) { + if (!clickable) return; if (!isScaled) { seperator.transform.localScale = originalScale * 1.8f; isScaled = true; @@ -600,6 +667,7 @@ public void OnPointerEnter(PointerEventData eventData) { } public void OnPointerExit(PointerEventData eventData) { + if(!clickable) return; if (isScaled) { seperator.transform.localScale = originalScale; isScaled = false; diff --git a/ScoreSaber/UI/Main/Settings/ScoreSaberSettingsFlowCoordinator.cs b/ScoreSaber/UI/Main/Settings/ScoreSaberSettingsFlowCoordinator.cs index a6fdfe2..f476268 100644 --- a/ScoreSaber/UI/Main/Settings/ScoreSaberSettingsFlowCoordinator.cs +++ b/ScoreSaber/UI/Main/Settings/ScoreSaberSettingsFlowCoordinator.cs @@ -47,8 +47,10 @@ protected override void BackButtonWasPressed(ViewController topViewController) { _scoresaberLeaderboardViewController.RefreshLeaderboard(); if (Plugin.Settings.showLocalPlayerRank) { _panelView.SetGlobalRanking($"#{string.Format("{0:n0}", _panelView._currentPlayerInfo.rank)} ({string.Format("{0:n0}", _panelView._currentPlayerInfo.pp)}pp)"); + _panelView.SetCountryRanking($"#{string.Format("{0:n0}", _panelView._currentPlayerInfo.countryRank)}", _panelView._currentPlayerInfo.country); } else { _panelView.SetGlobalRanking("Hidden"); + _panelView.SetCountryRanking("Hidden", _panelView._currentPlayerInfo.country); } } diff --git a/ScoreSaber/UI/Main/Settings/ViewControllers/MainSettingsViewController.bsml b/ScoreSaber/UI/Main/Settings/ViewControllers/MainSettingsViewController.bsml index 53d9b74..9b959c9 100644 --- a/ScoreSaber/UI/Main/Settings/ViewControllers/MainSettingsViewController.bsml +++ b/ScoreSaber/UI/Main/Settings/ViewControllers/MainSettingsViewController.bsml @@ -1,4 +1,4 @@ - + diff --git a/ScoreSaber/UI/Main/ViewControllers/FAQViewController.bsml b/ScoreSaber/UI/Main/ViewControllers/FAQViewController.bsml index 2f51db5..ea69f4d 100644 --- a/ScoreSaber/UI/Main/ViewControllers/FAQViewController.bsml +++ b/ScoreSaber/UI/Main/ViewControllers/FAQViewController.bsml @@ -1,4 +1,4 @@ - + diff --git a/ScoreSaber/UI/Main/ViewControllers/GlobalViewController.bsml b/ScoreSaber/UI/Main/ViewControllers/GlobalViewController.bsml index 47027af..7c39802 100644 --- a/ScoreSaber/UI/Main/ViewControllers/GlobalViewController.bsml +++ b/ScoreSaber/UI/Main/ViewControllers/GlobalViewController.bsml @@ -1,4 +1,4 @@ - + diff --git a/ScoreSaber/UI/Main/ViewControllers/TeamViewController.bsml b/ScoreSaber/UI/Main/ViewControllers/TeamViewController.bsml index e342841..34fbb6d 100644 --- a/ScoreSaber/UI/Main/ViewControllers/TeamViewController.bsml +++ b/ScoreSaber/UI/Main/ViewControllers/TeamViewController.bsml @@ -1,4 +1,4 @@ - + From 0805e4121d9c6280b93f52317563f6d6eb9afa86 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:15:23 +1100 Subject: [PATCH 08/92] Cache leaderboardInfoMaps and fade cell text in --- .../Core/Data/Wrappers/LeaderboardMap.cs | 3 + .../Core/Services/LeaderboardService.cs | 28 ++++++++ .../Leaderboard/ProfilePictureView.cs | 4 +- .../ScoreSaberLeaderboardViewController.cs | 65 ++++++++++++++++--- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs b/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs index b078f87..c6b5c60 100644 --- a/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs +++ b/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs @@ -1,5 +1,6 @@ using ScoreSaber.Core.Data.Models; using System.Collections.Generic; +using UnityEngine; namespace ScoreSaber.Core.Data.Wrappers { internal class LeaderboardMap { @@ -22,5 +23,7 @@ internal LeaderboardMap(Leaderboard leaderboard, BeatmapLevel beatmapLevel, Beat } return leaderboardTableScoreData; } + + } } diff --git a/ScoreSaber/Core/Services/LeaderboardService.cs b/ScoreSaber/Core/Services/LeaderboardService.cs index f890171..b0ea49e 100644 --- a/ScoreSaber/Core/Services/LeaderboardService.cs +++ b/ScoreSaber/Core/Services/LeaderboardService.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using ScoreSaber.UI.Leaderboard; +using System.Collections.Generic; namespace ScoreSaber.Core.Services { internal class LeaderboardService { @@ -25,6 +26,7 @@ public async Task GetLeaderboardData(int maxMultipliedScore, Bea Plugin.Log.Debug($"Current leaderboard set to: {beatmapKey.levelId}:{beatmapLevel.songName}"); currentLoadedLeaderboard = new LeaderboardMap(leaderboardData, beatmapLevel, beatmapKey, maxMultipliedScore); + AddLeaderboardInfoMapToCache(currentLoadedLeaderboard.leaderboardInfoMap.beatmapKey, currentLoadedLeaderboard.leaderboardInfoMap); return currentLoadedLeaderboard; } @@ -87,5 +89,31 @@ private string GetLeaderboardUrl(BeatmapKey beatmapKey, ScoreSaberLeaderboardVie return url; } + + internal Dictionary cachedLeaderboardInfoMaps = new Dictionary(); + private int MaxLBInfoCacheSize = 100; + internal Queue LBInfoCacheQueue = new Queue(); + internal void MaintainLeaderboardInfoMapCache() { + while (cachedLeaderboardInfoMaps.Count > MaxLBInfoCacheSize) { + BeatmapKey oldestUrl = LBInfoCacheQueue.Dequeue(); + cachedLeaderboardInfoMaps.Remove(oldestUrl); + } + } + + internal void AddLeaderboardInfoMapToCache(BeatmapKey url, LeaderboardInfoMap LeaderboardInfoMap) { + if (cachedLeaderboardInfoMaps.ContainsKey(url)) { + return; + } + cachedLeaderboardInfoMaps.Add(url, LeaderboardInfoMap); + LBInfoCacheQueue.Enqueue(url); + MaintainLeaderboardInfoMapCache(); + } + + internal LeaderboardInfoMap GetLeaderboardInfoMapFromCache(BeatmapKey url) { + if (cachedLeaderboardInfoMaps.ContainsKey(url)) { + return cachedLeaderboardInfoMaps[url]; + } + return null; + } } } diff --git a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs index 5960384..563adb5 100644 --- a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs +++ b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs @@ -198,13 +198,13 @@ public void FadeText(TextMeshProUGUI text, bool fadeIn, float time) { _tweeningManager.AddTween(tween, text); } - public void LerpColor(ImageView currentImageView, Color newColor) { + public void LerpColor(ImageView currentImageView, Color newColor, float time = 0.0f) { Tween tween = new ColorTween(currentImageView.color, newColor, (Color u) => { currentImageView.color = u; currentImageView.color0 = u; currentImageView.color1 = u; - }, 0.3f, EaseType.Linear, 0f); + }, time == 0.0f ? 0.3f : time, EaseType.Linear, 0f); tween.onCompleted = () => { if (currentImageView == null) return; currentImageView.color = newColor; diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 412ac23..bd86df5 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -241,16 +241,20 @@ private void PostParse() { CheckPage(); } - private void SetPanelStatus() { - - bool ranked = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.ranked; - bool qualified = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.qualified; - bool loved = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.loved; + private void SetPanelStatus(LeaderboardInfoMap leaderboardInfoMap = null) { + bool fromCached = true; + if(leaderboardInfoMap == null) { + leaderboardInfoMap = _leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap; + fromCached = false; + } + bool ranked = leaderboardInfoMap.leaderboardInfo.ranked; + bool qualified = leaderboardInfoMap.leaderboardInfo.qualified; + bool loved = leaderboardInfoMap.leaderboardInfo.loved; - if (_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.stars != 0) { + if (leaderboardInfoMap.leaderboardInfo.stars != 0) { starRatingBox.gameObject.SetActive(true); - headerText.text = $" {_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.stars.ToString().Replace(".", ". ")}★"; + headerText.text = $" {leaderboardInfoMap.leaderboardInfo.stars.ToString().Replace(".", ". ")}★"; headerText.richText = true; headerSTATIC.gameObject.SetActive(false); } else { @@ -261,6 +265,9 @@ private void SetPanelStatus() { if(!ranked && !qualified && !loved) { _tweeningService.LerpColor(_headerBackground, grey); headerTextSTATIC.text = "UNRANKED"; + if (!fromCached) { + _tweeningService.FadeText(headerTextSTATIC, true, 0.3f); + } } if (ranked) { @@ -270,11 +277,17 @@ private void SetPanelStatus() { if (qualified) { _tweeningService.LerpColor(_headerBackground, _scoreSaberBlue); headerTextSTATIC.text = "QUALIFIED"; + if (!fromCached) { + _tweeningService.FadeText(headerTextSTATIC, true, 0.3f); + } } if (loved) { _tweeningService.LerpColor(_headerBackground, pink); headerTextSTATIC.text = "LOVED"; + if (!fromCached) { + _tweeningService.FadeText(headerTextSTATIC, true, 0.3f); + } } } @@ -355,6 +368,7 @@ protected override void DidDeactivate(bool removedFromHierarchy, bool screenSyst public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LeaderboardTableView tableView, ScoreSaberScoresScope scope, GameObject loadingControl, string refreshId) { try { if (loadingControl == null || tableView == null) return; + bool setPanelStatusFromCache = false; loadingControl.SetActive(false); _errorText.gameObject.SetActive(false); tableView.SetScores(new List(), -1); @@ -375,6 +389,13 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm starRatingBox.gameObject.SetActive(false); headerSTATIC.gameObject.SetActive(true); + if(_leaderboardService.GetLeaderboardInfoMapFromCache(beatmapKey) != null) { + SetPanelStatus(_leaderboardService.GetLeaderboardInfoMapFromCache(beatmapKey)); + setPanelStatusFromCache = true; + } else { + _tweeningService.LerpColor(_headerBackground, grey, 0.1f); + } + if (cancellationToken != null) { cancellationToken.Cancel(); cancellationToken.Dispose(); @@ -391,7 +412,6 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm return; } - await Task.Delay(500); // Delay before doing anything to prevent leaderboard spam if (_currentLeaderboardRefreshId == refreshId) { @@ -401,8 +421,9 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (_currentLeaderboardRefreshId != refreshId) { return; // we need to check this again, since some time may have passed due to waiting for leaderboard data } - - SetPanelStatus(); + if (!setPanelStatusFromCache) { + SetPanelStatus(); + } List leaderboardTableScoreData = leaderboardData.ToScoreData(); int playerScoreIndex = GetPlayerScoreIndex(leaderboardData); if (leaderboardTableScoreData.Count != 0) { @@ -578,6 +599,8 @@ void PatchLeaderboardTableView(LeaderboardTableView tableView) { cellClicker.clickable = true; TextMeshProUGUI _playerNameText = tableCell._playerNameText; + TextMeshProUGUI _scoreText = tableCell._scoreText; + TextMeshProUGUI _rankText = tableCell._rankText; _playerNameText.richText = true; @@ -589,6 +612,7 @@ void PatchLeaderboardTableView(LeaderboardTableView tableView) { _playerNameText.rectTransform.anchoredPosition = newPosition; tableCell.showSeparator = true; + _tweeningService.FadeText(_playerNameText, true, 0.3f); } } @@ -625,6 +649,27 @@ public void OnLeaderboardSet(BeatmapKey beatmapKey) { } catch(Exception ex) { Plugin.Log.Error(ex.Message); } } + internal static class BeatmapDataCache { + internal static Dictionary cachedSprites = new Dictionary(); + private static int MaxSpriteCacheSize = 150; + internal static Queue spriteCacheQueue = new Queue(); + internal static void MaintainSpriteCache() { + while (cachedSprites.Count > MaxSpriteCacheSize) { + string oldestUrl = spriteCacheQueue.Dequeue(); + cachedSprites.Remove(oldestUrl); + + } + } + + internal static void AddSpriteToCache(string url, Sprite sprite) { + if (cachedSprites.ContainsKey(url)) { + return; + } + cachedSprites.Add(url, sprite); + spriteCacheQueue.Enqueue(url); + } + } + // probably a better place to put this public class CellClicker : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { From 9ad13ed40846b4fd60e2e65077d549f255db263d Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:30:45 +1100 Subject: [PATCH 09/92] Handle ost --- ScoreSaber/Core/Services/LeaderboardService.cs | 8 ++++++++ .../ScoreSaberLeaderboardViewController.cs | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/ScoreSaber/Core/Services/LeaderboardService.cs b/ScoreSaber/Core/Services/LeaderboardService.cs index b0ea49e..12365e4 100644 --- a/ScoreSaber/Core/Services/LeaderboardService.cs +++ b/ScoreSaber/Core/Services/LeaderboardService.cs @@ -21,6 +21,10 @@ public LeaderboardService() { public async Task GetLeaderboardData(int maxMultipliedScore, BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page, PlayerSpecificSettings playerSpecificSettings) { string leaderboardUrl = GetLeaderboardUrl(beatmapKey, scope, page); + if (leaderboardUrl == null) { + currentLoadedLeaderboard = null; + return null; + } string leaderboardRawData = await Plugin.HttpInstance.GetAsync(leaderboardUrl); Leaderboard leaderboardData = JsonConvert.DeserializeObject(leaderboardRawData); @@ -49,6 +53,10 @@ public async Task GetCurrentLeaderboard(BeatmapKey beatmapKey) { } private string GetLeaderboardUrl(BeatmapKey beatmapKey, ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, int page) { + + if(!beatmapKey.levelId.Contains("custom_level_")) { + return null; + } string url = "/game/leaderboard"; string leaderboardId = beatmapKey.levelId.Split('_')[2]; diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index bd86df5..1763380 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -248,6 +248,13 @@ private void SetPanelStatus(LeaderboardInfoMap leaderboardInfoMap = null) { fromCached = false; } + if (leaderboardInfoMap == null) { + _tweeningService.LerpColor(_headerBackground, grey); + headerTextSTATIC.text = "OST"; + _tweeningService.FadeText(headerTextSTATIC, true, 0.3f); + return; + } + bool ranked = leaderboardInfoMap.leaderboardInfo.ranked; bool qualified = leaderboardInfoMap.leaderboardInfo.qualified; bool loved = leaderboardInfoMap.leaderboardInfo.loved; @@ -412,6 +419,13 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm return; } + if(!beatmapKey.levelId.Contains("custom_level_")) { + SetErrorState(tableView, ref loadingControl, null, null, "This is not a custom level", false); + ByeImages(); + SetPanelStatus(); + return; + } + await Task.Delay(500); // Delay before doing anything to prevent leaderboard spam if (_currentLeaderboardRefreshId == refreshId) { From 82bf0984def2f7cfaa589add8c3f39920137d587 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:38:34 +1100 Subject: [PATCH 10/92] MapInfoModal, fix lb pfp on first launch with less than 10 cells --- .../UI/Elements/Leaderboard/MapInfoView.cs | 108 ++++++++++++++++++ .../Leaderboard/ProfilePictureView.cs | 7 +- ScoreSaber/UI/Leaderboard/PanelView.bsml | 5 +- ScoreSaber/UI/Leaderboard/PanelView.cs | 6 + .../ScoreSaberLeaderboardViewController.bsml | 45 +++++++- .../ScoreSaberLeaderboardViewController.cs | 25 +++- 6 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 ScoreSaber/UI/Elements/Leaderboard/MapInfoView.cs diff --git a/ScoreSaber/UI/Elements/Leaderboard/MapInfoView.cs b/ScoreSaber/UI/Elements/Leaderboard/MapInfoView.cs new file mode 100644 index 0000000..0eb3533 --- /dev/null +++ b/ScoreSaber/UI/Elements/Leaderboard/MapInfoView.cs @@ -0,0 +1,108 @@ +using BeatSaberMarkupLanguage.Attributes; +using HMUI; +using System; +using ScoreSaber.Core.Data.Models; +using ScoreSaber.Extensions; +using System.Threading; +using ScoreSaber.UI.Leaderboard; +using BeatSaberMarkupLanguage; +using UnityEngine; + +namespace ScoreSaber.UI.Elements.Leaderboard { + internal class MapInfoView { + #region BSML Components + [UIComponent("map-info-modal-root")] + public ModalView detailModalRoot = null; + [UIComponent("map-name-text")] + protected CurvedTextMeshPro _mapNameText = null; + [UIComponent("map-info-top")] + protected ImageView _mapInfoTop = null; + [UIComponent("map-info-picture")] + protected ImageView _mapInfoPicture = null; + [UIComponent("map-author-text")] + protected CurvedTextMeshPro _mapAuthorText = null; + [UIComponent("map-upload-date-text")] + protected CurvedTextMeshPro _mapUploadDateText = null; + [UIComponent("map-plays-text")] + protected CurvedTextMeshPro _mapPlaysText = null; + [UIComponent("map-status-date-text")] + protected CurvedTextMeshPro _mapStatusDateText = null; + [UIComponent("map-info-line-border")] + protected ImageView _mapInfoLineBorder = null; + + #endregion + + internal LeaderboardInfo _currentMapInfo { get; set; } + internal BeatmapLevel _currentMap { get; set; } + + [UIAction("#post-parse")] + public void Parsed() { + _mapInfoTop.material = Utilities.ImageResources.NoGlowMat; + + var modalPic = _mapInfoPicture; + PanelView.ImageSkew(ref modalPic) = 0f; + PanelView.ImageSkew(ref _mapInfoLineBorder) = 0f; + PanelView.ImageSkew(ref _mapInfoTop) = 0f; + + modalPic.material = Plugin.NoGlowMatRound; + } + + [UIObject("map-info-set")] + internal GameObject mapInfoSet = null; + + [UIObject("map-info-set-loading")] + internal GameObject mapInfoSetLoading = null; + + [UIAction("map-info-url-click")] + public void MapInfoUrlClick() { + if (_currentMapInfo == null) return; + Application.OpenURL($"https://scoresaber.com/leaderboard/{_currentMapInfo.id}"); + } + + public void ResetName() { + _mapNameText.text = "Loading..."; + } + + public void SetImage(BeatmapLevel level) { + _mapInfoPicture.sprite = level.previewMediaData.GetCoverSpriteAsync().Result; + } + + internal string GetMapStatusString() { + bool isRanked = _currentMapInfo.ranked; + bool isQualified = _currentMapInfo.qualified; + bool isLoved = _currentMapInfo.loved; + bool isUnranked = !isRanked && !isQualified && !isLoved; + + if (isRanked) + return $"Ranked - ({(_currentMapInfo.rankedDate.HasValue ? _currentMapInfo.rankedDate.Value.ToString("dd/MM/yy") : string.Empty)}) - {_currentMapInfo.stars}★"; + else if (isQualified) + return $"Qualified - ({(_currentMapInfo.qualifiedDate.HasValue ? _currentMapInfo.qualifiedDate.Value.ToString("dd/MM/yy") : string.Empty)})"; + else if (isLoved) + return $"Loved - ({(_currentMapInfo.lovedDate.HasValue ? _currentMapInfo.lovedDate.Value.ToString("dd/MM/yy") : string.Empty)})"; + else if (isUnranked) + return $"Unranked"; + else + return string.Empty; + } + + public void SetScoreInfo(LeaderboardInfo mapInfo) { + if (mapInfo == null || _currentMap == null) return; + try { + _currentMapInfo = mapInfo; + _mapNameText.text = $"Map Details: {mapInfo.songName}"; + SetImage(_currentMap); + _mapAuthorText.text = $"Mapped By {mapInfo.levelAuthorName}"; + _mapUploadDateText.SetFancyText("Uploaded", $"{mapInfo.createdDate:dd/MM/yy}"); + _mapPlaysText.SetFancyText("Plays", $"{mapInfo.plays} ({mapInfo.dailyPlays} Last 24h)"); + _mapStatusDateText.SetFancyText("Status", $"{GetMapStatusString()}"); + mapInfoSetLoading.gameObject.SetActive(false); + mapInfoSet.SetActive(true); + } catch (Exception e) { + mapInfoSetLoading.gameObject.SetActive(true); + mapInfoSet.SetActive(false); + Plugin.Log.Error(e); + } + } + + } +} \ No newline at end of file diff --git a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs index 563adb5..cb15e4d 100644 --- a/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs +++ b/ScoreSaber/UI/Elements/Leaderboard/ProfilePictureView.cs @@ -24,7 +24,7 @@ internal class ProfilePictureView { private ICoroutineStarter coroutineStarter; - internal Sprite nullSprite => Utilities.FindSpriteInAssembly("ScoreSaber.Resources.blank.png"); + internal Sprite nullSprite => Utilities.ImageResources.BlankSprite; public ProfilePictureView(int index) { this.index = index; @@ -43,11 +43,10 @@ public void Init(ICoroutineStarter coroutineStarter) { [UIAction("#post-parse")] public void Parsed() { + //profileImage.sprite = nullSprite; profileImage.material = Plugin.NoGlowMatRound; - profileImage.sprite = Utilities.FindSpriteInAssembly("ScoreSaber.Resources.blank.png"); profileImage.gameObject.SetActive(true); loadingIndicator.gameObject.SetActive(false); - Active(false); } public void setProfileImage(string url, int pos, CancellationToken cancellationToken) { @@ -128,7 +127,7 @@ public void ClearSprite() { public void Active(bool state) { if (profileImage != null) { - profileImage.gameObject.SetActive(state); + profileImage.sprite = nullSprite; } } } diff --git a/ScoreSaber/UI/Leaderboard/PanelView.bsml b/ScoreSaber/UI/Leaderboard/PanelView.bsml index 6a9b643..d23355d 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.bsml +++ b/ScoreSaber/UI/Leaderboard/PanelView.bsml @@ -3,7 +3,7 @@ - + @@ -19,5 +19,8 @@ + + + \ No newline at end of file diff --git a/ScoreSaber/UI/Leaderboard/PanelView.cs b/ScoreSaber/UI/Leaderboard/PanelView.cs index 6ab43d5..f3a2e6b 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.cs +++ b/ScoreSaber/UI/Leaderboard/PanelView.cs @@ -275,6 +275,12 @@ public void DismissPrompt(float dismissTime = 0f, float tweenTime = 0.5f) { } } + [UIAction("clicked-settings")] + protected void ClickedSettings() { + + ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); + } + private void ChangePromptState(float value) { const float fullY = 10.30f; const float hiddenY = 4.30f; diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml index a3270dc..fa378a7 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml @@ -57,7 +57,7 @@ pref-height="9" on-click="OnPageDown"/> - + @@ -114,6 +114,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -122,7 +163,7 @@ - + diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 1763380..9576209 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -117,6 +117,12 @@ public enum UploadStatus { [UIObject("starRatingBox")] private readonly GameObject starRatingBox; + [UIValue("map-info-view")] + protected MapInfoView _mapInfoView = null; + + [UIComponent("infoIcon")] + protected readonly ClickableImage _infoIcon; + [UIAction("OnPageUp")] private void UpButtonClicked() => UpdatePageChanged(-1); [UIAction("OnPageDown")] private void DownButtonClicked() => UpdatePageChanged(1); @@ -181,7 +187,7 @@ private void playerService_LoginStatusChanged(PlayerService.LoginStatus loginSta case PlayerService.LoginStatus.Success: _panelView.SetPromptSuccess(status, false, 3f); _panelView.RankUpdater().RunTask(); - _ImageHolders.ForEach(holder => holder.Active(true)); + _ImageHolders.ForEach(holder => holder.ClearSprite()); RefreshLeaderboard(); break; } @@ -239,6 +245,7 @@ private void PostParse() { ImageSkew(ref _headerBackground) = 0.18f; ImageGradient(ref _headerBackground) = true; CheckPage(); + _ImageHolders.ForEach(holder => holder.ClearSprite()); } private void SetPanelStatus(LeaderboardInfoMap leaderboardInfoMap = null) { @@ -303,8 +310,10 @@ internal void OpenLeaderboardPage() { Application.OpenURL($"https://scoresaber.com/leaderboard/{_leaderboardService.currentLoadedLeaderboard.leaderboardInfoMap.leaderboardInfo.id}"); } - [UIAction("SettingsClicked")] - internal void OpenSettingsPage() => ScoreSaberSettingsFlowCoordinator.ShowSettingsFlowCoordinator(); + [UIAction("MapInfoClicked")] + internal void MapInfoClicked() { + _parserParams.EmitEvent("present-map-info"); + } [UIAction("clicked-status")] protected void ClickedStatus() { @@ -395,8 +404,12 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm loadingControl.SetActive(true); starRatingBox.gameObject.SetActive(false); headerSTATIC.gameObject.SetActive(true); + _mapInfoView.ResetName(); + _mapInfoView.mapInfoSetLoading.gameObject.SetActive(true); + _mapInfoView.mapInfoSet.SetActive(false); + _infoIcon.gameObject.SetActive(false); - if(_leaderboardService.GetLeaderboardInfoMapFromCache(beatmapKey) != null) { + if (_leaderboardService.GetLeaderboardInfoMapFromCache(beatmapKey) != null) { SetPanelStatus(_leaderboardService.GetLeaderboardInfoMapFromCache(beatmapKey)); setPanelStatusFromCache = true; } else { @@ -438,6 +451,9 @@ public async Task RefreshLeaderboard(BeatmapLevel beatmapLevel, BeatmapKey beatm if (!setPanelStatusFromCache) { SetPanelStatus(); } + _mapInfoView._currentMap = beatmapLevel; + _mapInfoView.SetScoreInfo(leaderboardData.leaderboardInfoMap.leaderboardInfo); + _infoIcon.gameObject.SetActive(true); List leaderboardTableScoreData = leaderboardData.ToScoreData(); int playerScoreIndex = GetPlayerScoreIndex(leaderboardData); if (leaderboardTableScoreData.Count != 0) { @@ -639,6 +655,7 @@ void PatchLeaderboardTableView(LeaderboardTableView tableView) { public void Initialize() { _infoButtons = new EntryHolder(); _scoreDetailView = new ScoreDetailView(); + _mapInfoView = new MapInfoView(); _scoreDetailView.showProfile += scoreDetailView_showProfile; _scoreDetailView.startReplay += scoreDetailView_startReplay; _playerService.LoginStatusChanged += playerService_LoginStatusChanged; From 2c4c5a2b5caad348301c34c8856d4ea51994d550 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:41:37 +1100 Subject: [PATCH 11/92] fix panelview misalignment --- ScoreSaber/UI/Leaderboard/PanelView.bsml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ScoreSaber/UI/Leaderboard/PanelView.bsml b/ScoreSaber/UI/Leaderboard/PanelView.bsml index d23355d..90217f8 100644 --- a/ScoreSaber/UI/Leaderboard/PanelView.bsml +++ b/ScoreSaber/UI/Leaderboard/PanelView.bsml @@ -10,12 +10,12 @@ - + - - + + From 9e37d6a2d6917b609234761f200c8f1144095466 Mon Sep 17 00:00:00 2001 From: Riley Date: Fri, 18 Oct 2024 14:38:18 +1100 Subject: [PATCH 12/92] begin desktop imber ui --- .../ReplaySystem/Installers/ImberInstaller.cs | 1 + .../Installers/PlaybackInstaller.cs | 1 - .../UI/DesktopMainImberPanelView.cs | 227 ++++++++++++++++++ .../Core/ReplaySystem/UI/ImberManager.cs | 34 ++- .../Core/ReplaySystem/UI/NonVRReplayUI.cs | 86 ------- 5 files changed, 261 insertions(+), 88 deletions(-) create mode 100644 ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs delete mode 100644 ScoreSaber/Core/ReplaySystem/UI/NonVRReplayUI.cs diff --git a/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs b/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs index 847136a..9680fca 100644 --- a/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs +++ b/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs @@ -14,6 +14,7 @@ public override void InstallBindings() { Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind(typeof(ITickable), typeof(SpectateAreaController)).To().AsSingle(); } } diff --git a/ScoreSaber/Core/ReplaySystem/Installers/PlaybackInstaller.cs b/ScoreSaber/Core/ReplaySystem/Installers/PlaybackInstaller.cs index 3159468..5840616 100644 --- a/ScoreSaber/Core/ReplaySystem/Installers/PlaybackInstaller.cs +++ b/ScoreSaber/Core/ReplaySystem/Installers/PlaybackInstaller.cs @@ -33,7 +33,6 @@ public override void InstallBindings() { if (_gameplayCoreSceneSetupData.playerSpecificSettings.automaticPlayerHeight) Container.BindInterfacesTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); - Container.Bind().FromNewComponentOnNewGameObject().AsSingle().NonLazy(); Container.Bind().To().AsSingle(); Container.Bind().To().AsSingle(); } else { diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs new file mode 100644 index 0000000..e423116 --- /dev/null +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs @@ -0,0 +1,227 @@ +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components; +using BeatSaberMarkupLanguage.FloatingScreen; +using BeatSaberMarkupLanguage.ViewControllers; +using HMUI; +using ScoreSaber.Core.Data; +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR; +using Zenject; + +namespace ScoreSaber.Core.ReplaySystem.UI { + + [HotReload(RelativePathToLayout = @"imber-panel.bsml")] + [ViewDefinition("ScoreSaber.Core.ReplaySystem.UI.imber-panel.bsml")] + internal class DesktopMainImberPanelView : BSMLAutomaticViewController { + + public event Action DidPositionTabVisibilityChange; + public event Action DidPositionPreviewChange; + public event Action HandDidSwitchEvent; + public event Action DidTimeSyncChange; + public event Action DidChangeVisiblity; + public event Action DidClickPausePlay; + public event Action DidClickRestart; + public event Action DidPositionJump; + public event Action DidClickLoop; + + private int _lastTab = 0; + private int _targetFPS = 90; + private float _initialTime = 1f; + private static readonly Color _goodColor = Color.green; + private static readonly Color _ehColor = Color.yellow; + private static readonly Color _noColor = Color.red; + + public bool didParse { get; private set; } + + public Transform Transform { + get => this.transform; + } + + public Pose defaultPosition { get; set; } + + private float _timeSync; + [UIValue("time-sync")] + public float timeSync { + get => _timeSync; + set { + _timeSync = Mathf.Approximately(_initialTime, value) ? _initialTime : value; + DidTimeSyncChange?.Invoke(_timeSync); + } + } + + private string _playPauseText = "PAUSE"; + [UIValue("play-pause-text")] + public string playPauseText { + get => _playPauseText; + set { + _playPauseText = value; + NotifyPropertyChanged(); + } + } + + private string _loopText = "LOOP"; + [UIValue("loop-text")] + public string loopText { + get => _loopText; + set { + _loopText = value; + NotifyPropertyChanged(); + } + } + + private string _location = ""; + [UIValue("location")] + public string location { + get => _location; + protected set { + _location = value; + DidPositionPreviewChange?.Invoke(_location); + } + } + + public int fps { + set { + fpsText.text = value.ToString(); + if (value > 0.85f * _targetFPS) + fpsText.color = _goodColor; + else if (value > 0.6f * _targetFPS) + fpsText.color = _ehColor; + else + fpsText.color = _noColor; + } + } + + public float leftSaberSpeed { + set { + leftSpeedText.text = $"{value:0.0} m/s"; + leftSpeedText.color = value >= 2f ? _goodColor : _noColor; // 2 is the min. saber speed to hit a note + } + } + + public float rightSaberSpeed { + set { + rightSpeedText.text = $"{value:0.0} m/s"; + rightSpeedText.color = value >= 2f ? _goodColor : _noColor; // 2 is the min. saber speed to hit a note + } + } + + [UIValue("locations")] + protected readonly List locations = new List(); + + [UIComponent("tab-selector")] + protected readonly TabSelector tabSelector = null; + + [UIComponent("fps-text")] + protected readonly CurvedTextMeshPro fpsText = null; + + [UIComponent("left-speed-text")] + protected readonly CurvedTextMeshPro leftSpeedText = null; + + [UIComponent("right-speed-text")] + protected readonly CurvedTextMeshPro rightSpeedText = null; + + [Inject] + protected void Construct() { + + var canvasGameObj = new GameObject(); + var canvas = canvasGameObj.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + + canvas.name = "ScoreSaberDesktopImberUI"; + + canvasGameObj.SetActive(true); + + canvas.overrideSorting = true; + canvas.sortingOrder = 1; + gameObject.transform.parent = canvas.transform; + } + + public void Setup(float initialSongTime, int targetFramerate, string defaultLocation, IEnumerable locations) { + + _initialTime = initialSongTime; + _targetFPS = targetFramerate; + _timeSync = initialSongTime; + _location = defaultLocation; + this.locations.AddRange(locations); + } + + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { + + base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); + tabSelector.TextSegmentedControl.didSelectCellEvent += DidSelect; + didParse = true; + if (firstActivation) { + tabSelector.transform.localScale *= .9f; + } + } + + private void DidSelect(SegmentedControl _, int selected) { + + const int positionTabIndex = 2; + if (_lastTab == 2 || selected == 2) + DidPositionTabVisibilityChange?.Invoke(selected == positionTabIndex); + _lastTab = selected; + } + + protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { + + tabSelector.TextSegmentedControl.didSelectCellEvent -= DidSelect; + base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); + } + + public void SwitchHand(XRNode xrNode) { + + HandDidSwitchEvent?.Invoke(xrNode); + } + + [UIAction("pause-play")] + protected void PausePlay() { + + DidClickPausePlay?.Invoke(); + } + + [UIAction("restart")] + protected void Restart() { + + DidClickRestart?.Invoke(); + } + + [UIAction("loop")] + protected void Loop() { + + DidClickLoop?.Invoke(); + } + + [UIAction("left-hand")] + protected void SwitchHandLeft() { + + SwitchHand(XRNode.LeftHand); + } + + [UIAction("right-hand")] + protected void SwitchHandRight() { + + SwitchHand(XRNode.RightHand); + } + + [UIAction("request-dismiss")] + protected void RequestDismiss() { + + DidChangeVisiblity?.Invoke(false); + } + + [UIAction("format-time-percent")] + protected string FormatTimePercent(float value) { + + return value.ToString("P0"); + } + + [UIAction("jump")] + protected void Jump() { + + DidPositionJump?.Invoke(); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs b/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs index ae77cd0..649760a 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs @@ -24,11 +24,12 @@ internal class ImberManager : IInitializable, IDisposable private readonly AudioTimeSyncController _audioTimeSyncController; private readonly ReplayTimeSyncController _replayTimeSyncController; private readonly ImberUIPositionController _imberUIPositionController; + private readonly DesktopMainImberPanelView _desktopMainImberPanelView; private readonly IEnumerable _positions; public ImberManager(ReplayFile file, IGamePause gamePause, ImberScrubber imberScrubber, ImberSpecsReporter imberSpecsReporter, MainImberPanelView mainImberPanelView, SpectateAreaController spectateAreaController, - AudioTimeSyncController audioTimeSyncController, ReplayTimeSyncController replayTimeSyncController, ImberUIPositionController imberUIPositionController, AudioTimeSyncController.InitData initData, PosePlayer posePlayer) { + AudioTimeSyncController audioTimeSyncController, ReplayTimeSyncController replayTimeSyncController, ImberUIPositionController imberUIPositionController, AudioTimeSyncController.InitData initData, PosePlayer posePlayer, DesktopMainImberPanelView desktopMainImberPanelView) { _gamePause = gamePause; _posePlayer = posePlayer; @@ -43,6 +44,8 @@ public ImberManager(ReplayFile file, IGamePause gamePause, ImberScrubber imberSc _mainImberPanelView.Setup(initData.timeScale, 90, _positions.First(), _positions); _imberScrubber.Setup(file.metadata.FailTime, file.metadata.Modifiers.Contains("NF")); _initialTimeScale = file.noteKeyframes.FirstOrDefault().TimeSyncTimescale; + _desktopMainImberPanelView = desktopMainImberPanelView; + _desktopMainImberPanelView.Setup(file.metadata.FailTime, 90, _positions.First(), _positions); } public void Initialize() { @@ -57,6 +60,17 @@ public void Initialize() { _mainImberPanelView.HandDidSwitchEvent += MainImberPanelView_DidHandSwitchEvent; _mainImberPanelView.DidPositionPreviewChange += MainImberPanelView_DidPositionPreviewChange; _mainImberPanelView.DidPositionTabVisibilityChange += MainImberPanelView_DidPositionTabVisibilityChange; + + _desktopMainImberPanelView.DidClickLoop += MainImberPanelView_DidClickLoop; + _desktopMainImberPanelView.DidPositionJump += MainImberPanelView_DidPositionJump; + _desktopMainImberPanelView.DidClickRestart += MainImberPanelView_DidClickRestart; + _desktopMainImberPanelView.DidClickPausePlay += MainImberPanelView_DidClickPausePlay; + _desktopMainImberPanelView.DidTimeSyncChange += MainImberPanelView_DidTimeSyncChange; + _desktopMainImberPanelView.DidChangeVisiblity += MainImberPanelView_DidChangeVisiblity; + _desktopMainImberPanelView.HandDidSwitchEvent += MainImberPanelView_DidHandSwitchEvent; + _desktopMainImberPanelView.DidPositionPreviewChange += MainImberPanelView_DidPositionPreviewChange; + _desktopMainImberPanelView.DidPositionTabVisibilityChange += MainImberPanelView_DidPositionTabVisibilityChange; + _spectateAreaController.DidUpdatePlayerSpectatorPose += SpectateAreaController_DidUpdatePlayerSpectatorPose; _imberScrubber.DidCalculateNewTime += ImberScrubber_DidCalculateNewTime; _imberSpecsReporter.DidReport += ImberSpecsReporter_DidReport; @@ -92,6 +106,12 @@ private void ImberSpecsReporter_DidReport(int fps, float leftSaberSpeed, float r _mainImberPanelView.leftSaberSpeed = leftSaberSpeed * (_initialTimeScale / _audioTimeSyncController.timeScale); _mainImberPanelView.rightSaberSpeed = rightSaberSpeed * (_initialTimeScale / _audioTimeSyncController.timeScale); } + + if (_desktopMainImberPanelView.didParse) { + _desktopMainImberPanelView.fps = fps; + _desktopMainImberPanelView.leftSaberSpeed = leftSaberSpeed * (_initialTimeScale / _audioTimeSyncController.timeScale); + _desktopMainImberPanelView.rightSaberSpeed = rightSaberSpeed * (_initialTimeScale / _audioTimeSyncController.timeScale); + } } private void SpectateAreaController_DidUpdatePlayerSpectatorPose(Vector3 position, Quaternion rotation) { @@ -161,9 +181,11 @@ private void MainImberPanelView_DidClickPausePlay() { if (_audioTimeSyncController.state == AudioTimeSyncController.State.Playing) { _replayTimeSyncController.CancelAllHitSounds(); _mainImberPanelView.playPauseText = "PLAY"; + _desktopMainImberPanelView.playPauseText = "PLAY"; _audioTimeSyncController.Pause(); } else if (_audioTimeSyncController.state == AudioTimeSyncController.State.Paused) { _mainImberPanelView.playPauseText = "PAUSE"; + _desktopMainImberPanelView.playPauseText = "PAUSE"; _audioTimeSyncController.Resume(); } } @@ -199,6 +221,16 @@ public void Dispose() { _mainImberPanelView.DidClickRestart -= MainImberPanelView_DidClickRestart; _mainImberPanelView.DidPositionJump -= MainImberPanelView_DidPositionJump; _mainImberPanelView.DidClickLoop -= MainImberPanelView_DidClickLoop; + + + _desktopMainImberPanelView.DidPositionPreviewChange -= MainImberPanelView_DidPositionPreviewChange; + _desktopMainImberPanelView.HandDidSwitchEvent -= MainImberPanelView_DidHandSwitchEvent; + _desktopMainImberPanelView.DidChangeVisiblity -= MainImberPanelView_DidChangeVisiblity; + _desktopMainImberPanelView.DidTimeSyncChange -= MainImberPanelView_DidTimeSyncChange; + _desktopMainImberPanelView.DidClickPausePlay -= MainImberPanelView_DidClickPausePlay; + _desktopMainImberPanelView.DidClickRestart -= MainImberPanelView_DidClickRestart; + _desktopMainImberPanelView.DidPositionJump -= MainImberPanelView_DidPositionJump; + _desktopMainImberPanelView.DidClickLoop -= MainImberPanelView_DidClickLoop; } } } \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/UI/NonVRReplayUI.cs b/ScoreSaber/Core/ReplaySystem/UI/NonVRReplayUI.cs deleted file mode 100644 index 3f2c1e3..0000000 --- a/ScoreSaber/Core/ReplaySystem/UI/NonVRReplayUI.cs +++ /dev/null @@ -1,86 +0,0 @@ -using ScoreSaber.Core.ReplaySystem.Data; -using ScoreSaber.Core.ReplaySystem.Playback; -using System.Linq; -using UnityEngine; -using Zenject; - -namespace ScoreSaber.Core.ReplaySystem.UI -{ - internal class NonVRReplayUI : MonoBehaviour - { - [Inject] private readonly AudioTimeSyncController _audioTimeSyncController = null; - [Inject] private readonly PosePlayer _posePlayer = null; - [Inject] private readonly SaberManager _saberManager = null; - [Inject] private readonly ReplayFile _file = null; - - private GUIStyle _headerStyle; - - private int _currentPosition = 0; - const int _offset = 16; - const int _headerOffset = 20; - private float _initialTimeScale; - - private int _fps; - private string _leftSaberSpeed; - private string _rightSaberSpeed; - - protected void Start() { - - _headerStyle = new GUIStyle(); - _headerStyle.fontSize = 16; - _headerStyle.normal.textColor = Color.white; - _initialTimeScale = _file.noteKeyframes.FirstOrDefault().TimeSyncTimescale; - _posePlayer.DidUpdatePose += PosePlayer_DidUpdatePose; - } - - protected void OnDestroy() { - - _posePlayer.DidUpdatePose -= PosePlayer_DidUpdatePose; - } - - private void PosePlayer_DidUpdatePose(VRPoseGroup pose) { - - _fps = pose.FPS; - _leftSaberSpeed = $"{_saberManager.leftSaber.movementDataForLogic.bladeSpeed * (_initialTimeScale / _audioTimeSyncController.timeScale):0.0} m/s"; - _rightSaberSpeed = $"{ _saberManager.rightSaber.movementDataForLogic.bladeSpeed * (_initialTimeScale / _audioTimeSyncController.timeScale):0.0} m/s"; - } - - protected void OnGUI() { - - if (!Plugin.Settings.hideReplayUI) { - _currentPosition = 0; - DrawLabel("Replay Controls -", header: true); - DrawLabel("Pause: Space"); - DrawLabel("Seek: 1-9"); - DrawLabel("Increase Time Scale: +"); - DrawLabel("Decrease Time Scale: -"); - DrawLabel("Hide Sabers: H"); - DrawLabel("Hide Desktop Replay UI: C"); - DrawLabel("Replay Player Status -", header: true); - DrawLabel($"Current Song Time: {string.Format("{0}:{1:00}", (int)_audioTimeSyncController.songTime / 60, _audioTimeSyncController.songTime % 60f)}"); - DrawLabel($"Current Time Scale: {_audioTimeSyncController.timeScale:P0}"); - DrawLabel($"Player's FPS: {_fps}"); - DrawLabel($"Left Saber Speed: {_leftSaberSpeed}"); - DrawLabel($"Right Saber Speed: {_rightSaberSpeed}"); - } - } - - protected void Update() { - - if (Input.GetKeyDown(KeyCode.C)) { - Plugin.Settings.hideReplayUI = !Plugin.Settings.hideReplayUI; - } - } - - private void DrawLabel(string text, bool header = false) { - - if (header) { - _currentPosition += _headerOffset; - GUI.Label(new Rect(10, _currentPosition, 300, 20), text, _headerStyle); - } else { - _currentPosition += _offset; - GUI.Label(new Rect(10, _currentPosition, 300, 20), text); - } - } - } -} From d53b9ae91560d66f5fbaa7caf728b1a930879023 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:31:41 +1100 Subject: [PATCH 13/92] Ensure seperator is not scaled --- ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs index 9576209..eb8f00b 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.cs @@ -627,6 +627,7 @@ void PatchLeaderboardTableView(LeaderboardTableView tableView) { cellClicker.index = cellIdx; cellClicker.seperator = (ImageView)tableCell._separatorImage; cellClicker.clickable = true; + cellClicker.OnPointerExit(null); TextMeshProUGUI _playerNameText = tableCell._playerNameText; TextMeshProUGUI _scoreText = tableCell._scoreText; From 7f41d332faa7b5874fb67711b9cd379d52842d35 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:55:52 +1100 Subject: [PATCH 14/92] Add graphic raycaster and scaler --- .../Core/ReplaySystem/UI/DesktopMainImberPanelView.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs index e423116..45f523b 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using UnityEngine; +using UnityEngine.UI; using UnityEngine.XR; using Zenject; @@ -127,6 +128,9 @@ protected void Construct() { var canvasGameObj = new GameObject(); var canvas = canvasGameObj.AddComponent(); + var canvasScaler = canvasGameObj.AddComponent(); + canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + canvasScaler.referenceResolution = new Vector2(1920, 1080); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.name = "ScoreSaberDesktopImberUI"; @@ -135,7 +139,9 @@ protected void Construct() { canvas.overrideSorting = true; canvas.sortingOrder = 1; + gameObject.AddComponent(); gameObject.transform.parent = canvas.transform; + gameObject.transform.localPosition = new Vector2(0.5f, 0.5f); } public void Setup(float initialSongTime, int targetFramerate, string defaultLocation, IEnumerable locations) { From c25e39cc814d3c51695a5bbd23a066b5ace9640d Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:16:04 +1100 Subject: [PATCH 15/92] Display Imber on desktop, not clickable rn --- .../UI/DesktopMainImberPanelView.cs | 23 ++++++++++++++----- .../ScoreSaberLeaderboardViewController.bsml | 4 ++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs index 45f523b..ab60948 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs @@ -129,19 +129,30 @@ protected void Construct() { var canvasGameObj = new GameObject(); var canvas = canvasGameObj.AddComponent(); var canvasScaler = canvasGameObj.AddComponent(); + canvasScaler.referenceResolution = new Vector2(350, 300); canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; - canvasScaler.referenceResolution = new Vector2(1920, 1080); - canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvasScaler.dynamicPixelsPerUnit = 3.44f; + canvasScaler.referencePixelsPerUnit = 10f; + + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + HMUI.Screen screen = gameObject.AddComponent(); canvas.name = "ScoreSaberDesktopImberUI"; canvasGameObj.SetActive(true); - canvas.overrideSorting = true; canvas.sortingOrder = 1; - gameObject.AddComponent(); - gameObject.transform.parent = canvas.transform; - gameObject.transform.localPosition = new Vector2(0.5f, 0.5f); + canvas.overrideSorting = true; + + canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.TexCoord1 | AdditionalCanvasShaderChannels.TexCoord2; + var canvasGR = canvas.gameObject.AddComponent(); + canvasGR.blockingObjects = GraphicRaycaster.BlockingObjects.None; + + gameObject.transform.SetParent(canvas.transform, false); + gameObject.transform.position = new Vector2(0.5f, 0.5f); + + __Init(screen, parentViewController, containerViewController); + screen.SetRootViewController(this, ViewController.AnimationType.None); } public void Setup(float initialSongTime, int targetFramerate, string defaultLocation, IEnumerable locations) { diff --git a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml index fa378a7..bc62250 100644 --- a/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml +++ b/ScoreSaber/UI/Leaderboard/ScoreSaberLeaderboardViewController.bsml @@ -33,7 +33,7 @@ - + - + From 130bc29b298fda3ac1481814f041cfb08f1e6590 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:24:58 +1100 Subject: [PATCH 16/92] Graphic raycaster on viewcontroller --- ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs index ab60948..d282acb 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs @@ -145,8 +145,9 @@ protected void Construct() { canvas.overrideSorting = true; canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.TexCoord1 | AdditionalCanvasShaderChannels.TexCoord2; - var canvasGR = canvas.gameObject.AddComponent(); - canvasGR.blockingObjects = GraphicRaycaster.BlockingObjects.None; + + var canvasGR = gameObject.AddComponent(); + //canvasGR.blockingObjects = GraphicRaycaster.BlockingObjects.All; gameObject.transform.SetParent(canvas.transform, false); gameObject.transform.position = new Vector2(0.5f, 0.5f); From d92b5d22ebdf0999c7e95d2fc95414edef8ff163 Mon Sep 17 00:00:00 2001 From: Riley <90689870+speecil@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:09:04 +1100 Subject: [PATCH 17/92] Working imber ui + scrubber, cant move it around yet --- ScoreSaber/Core/Data/Internal/Settings.cs | 22 +- .../ReplaySystem/Installers/ImberInstaller.cs | 2 +- .../Playback/ReplayTimeSyncController.cs | 50 ++-- .../UI/DesktopMainImberPanelView.cs | 240 +++++++++++++----- .../Core/ReplaySystem/UI/ImberManager.cs | 19 +- .../Core/ReplaySystem/UI/ImberScrubber.cs | 2 +- .../ReplaySystem/UI/MainImberPanelView.cs | 3 +- .../ReplaySystem/UI/desktop-imber-panel.bsml | 56 ++++ ScoreSaber/Resources/bluePixel.png | Bin 0 -> 437 bytes ScoreSaber/Resources/redPixel.png | Bin 0 -> 221 bytes ScoreSaber/ScoreSaber.csproj | 8 + 11 files changed, 304 insertions(+), 98 deletions(-) create mode 100644 ScoreSaber/Core/ReplaySystem/UI/desktop-imber-panel.bsml create mode 100644 ScoreSaber/Resources/bluePixel.png create mode 100644 ScoreSaber/Resources/redPixel.png diff --git a/ScoreSaber/Core/Data/Internal/Settings.cs b/ScoreSaber/Core/Data/Internal/Settings.cs index d7bfa48..d37d25c 100644 --- a/ScoreSaber/Core/Data/Internal/Settings.cs +++ b/ScoreSaber/Core/Data/Internal/Settings.cs @@ -8,7 +8,7 @@ namespace ScoreSaber.Core.Data { internal class Settings { - private static int _currentVersion => 8; + private static int _currentVersion => 9; public bool hideReplayUI = false; @@ -36,6 +36,7 @@ internal class Settings public bool leftHandedReplayUI { get; set; } public bool lockedReplayUIMode { get; set; } public List spectatorPositions { get; set; } + public Vec2 replayUIPosition { get; set; } internal static string dataPath => "UserData"; internal static string configPath => dataPath + @"\ScoreSaber"; @@ -113,6 +114,9 @@ internal static Settings LoadSettings() { if(decoded.fileVersion < 8) { decoded.replayCameraSmoothing = true; } + if (decoded.fileVersion < 9) { + decoded.replayUIPosition = new Vec2(new Vector2(0.25f, 0.25f)); + } SaveSettings(decoded); } return decoded; @@ -136,6 +140,22 @@ internal static void SaveSettings(Settings settings) { } } + internal struct Vec2 { + [JsonProperty("x")] + internal float x { get; set; } + [JsonProperty("y")] + internal float y { get; set; } + + internal Vec2(Vector2 position) { + x = position.x; + y = position.y; + } + + internal Vector2 ToVector2() { + return new Vector2(x, y); + } + } + internal struct SpectatorPoseRoot { [JsonProperty("name")] internal string name { get; set; } diff --git a/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs b/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs index 9680fca..2d235fd 100644 --- a/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs +++ b/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs @@ -14,7 +14,7 @@ public override void InstallBindings() { Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); - Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); Container.Bind(typeof(ITickable), typeof(SpectateAreaController)).To().AsSingle(); } } diff --git a/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs b/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs index 01c0362..b555dfb 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs @@ -29,31 +29,31 @@ public ReplayTimeSyncController(List scrollers, BasicBeatmapObjectMan } public void Tick() { - int index = -1; - if (Input.GetKeyDown(KeyCode.Alpha1)) - index = 0; - else if (Input.GetKeyDown(KeyCode.Alpha2)) - index = 1; - else if (Input.GetKeyDown(KeyCode.Alpha3)) - index = 2; - else if (Input.GetKeyDown(KeyCode.Alpha4)) - index = 3; - else if (Input.GetKeyDown(KeyCode.Alpha5)) - index = 4; - else if (Input.GetKeyDown(KeyCode.Alpha6)) - index = 5; - else if (Input.GetKeyDown(KeyCode.Alpha7)) - index = 6; - else if (Input.GetKeyDown(KeyCode.Alpha8)) - index = 7; - else if (Input.GetKeyDown(KeyCode.Alpha9)) - index = 8; - else if (Input.GetKeyDown(KeyCode.Alpha0)) - index = 9; - - if (index != -1) { - OverrideTime(audioTimeSyncController.songLength * (index * 0.1f)); - } + //int index = -1; + //if (Input.GetKeyDown(KeyCode.Alpha1)) + // index = 0; + //else if (Input.GetKeyDown(KeyCode.Alpha2)) + // index = 1; + //else if (Input.GetKeyDown(KeyCode.Alpha3)) + // index = 2; + //else if (Input.GetKeyDown(KeyCode.Alpha4)) + // index = 3; + //else if (Input.GetKeyDown(KeyCode.Alpha5)) + // index = 4; + //else if (Input.GetKeyDown(KeyCode.Alpha6)) + // index = 5; + //else if (Input.GetKeyDown(KeyCode.Alpha7)) + // index = 6; + //else if (Input.GetKeyDown(KeyCode.Alpha8)) + // index = 7; + //else if (Input.GetKeyDown(KeyCode.Alpha9)) + // index = 8; + //else if (Input.GetKeyDown(KeyCode.Alpha0)) + // index = 9; + + //if (index != -1) { + // OverrideTime(audioTimeSyncController.songLength * (index * 0.1f)); + //} if (Input.GetKeyDown(KeyCode.Minus)) { if (audioTimeSyncController.timeScale > 0.1f) { diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs index d282acb..9387318 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs @@ -6,34 +6,41 @@ using ScoreSaber.Core.Data; using System; using System.Collections.Generic; +using System.Linq; +using TMPro; using UnityEngine; +using UnityEngine.EventSystems; using UnityEngine.UI; using UnityEngine.XR; +using VRUIControls; using Zenject; namespace ScoreSaber.Core.ReplaySystem.UI { - [HotReload(RelativePathToLayout = @"imber-panel.bsml")] - [ViewDefinition("ScoreSaber.Core.ReplaySystem.UI.imber-panel.bsml")] - internal class DesktopMainImberPanelView : BSMLAutomaticViewController { + [HotReload(RelativePathToLayout = @"desktop-imber-panel.bsml")] + [ViewDefinition("ScoreSaber.Core.ReplaySystem.UI.desktop-imber-panel.bsml")] + internal class DesktopMainImberPanelView : BSMLAutomaticViewController, IDisposable { - public event Action DidPositionTabVisibilityChange; - public event Action DidPositionPreviewChange; public event Action HandDidSwitchEvent; public event Action DidTimeSyncChange; - public event Action DidChangeVisiblity; public event Action DidClickPausePlay; public event Action DidClickRestart; public event Action DidPositionJump; public event Action DidClickLoop; - private int _lastTab = 0; private int _targetFPS = 90; private float _initialTime = 1f; private static readonly Color _goodColor = Color.green; private static readonly Color _ehColor = Color.yellow; private static readonly Color _noColor = Color.red; + [Inject] private readonly VRInputModule _inputModule = null; + [Inject] private readonly AudioTimeSyncController _audioTimeSyncController = null; + [Inject] private readonly ImberScrubber _imberScrubber = null; + + private EventSystem originalEventSystem; + private EventSystem imberEventSystem; + public bool didParse { get; private set; } public Transform Transform { @@ -42,7 +49,21 @@ public Transform Transform { public Pose defaultPosition { get; set; } - private float _timeSync; + private float _timeSync = 1f; + + + [UIComponent("currentTimeText")] + public TextMeshProUGUI currentTimeText = null; + + [UIComponent("timebarbg")] + public ImageView timebarbg = null; + + [UIComponent("timebarActive")] + public ImageView timebarActive = null; + + [UIComponent("fadedBoxVertTimeline")] + public HorizontalLayoutGroup fadedBoxVertTimeline = null; + [UIValue("time-sync")] public float timeSync { get => _timeSync; @@ -72,16 +93,6 @@ public string loopText { } } - private string _location = ""; - [UIValue("location")] - public string location { - get => _location; - protected set { - _location = value; - DidPositionPreviewChange?.Invoke(_location); - } - } - public int fps { set { fpsText.text = value.ToString(); @@ -108,9 +119,6 @@ public float rightSaberSpeed { } } - [UIValue("locations")] - protected readonly List locations = new List(); - [UIComponent("tab-selector")] protected readonly TabSelector tabSelector = null; @@ -123,20 +131,27 @@ public float rightSaberSpeed { [UIComponent("right-speed-text")] protected readonly CurvedTextMeshPro rightSpeedText = null; + [UIObject("container")] + public GameObject _container = null; + [Inject] protected void Construct() { + if (!Environment.GetCommandLineArgs().Contains("fpfc")) return; + GameObject inputOBJ; + var canvasGameObj = new GameObject(); var canvas = canvasGameObj.AddComponent(); - var canvasScaler = canvasGameObj.AddComponent(); + + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + HMUI.Screen screen = canvasGameObj.AddComponent(); + var canvasScaler = screen.gameObject.AddComponent(); canvasScaler.referenceResolution = new Vector2(350, 300); canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; canvasScaler.dynamicPixelsPerUnit = 3.44f; canvasScaler.referencePixelsPerUnit = 10f; - canvas.renderMode = RenderMode.ScreenSpaceOverlay; - HMUI.Screen screen = gameObject.AddComponent(); canvas.name = "ScoreSaberDesktopImberUI"; canvasGameObj.SetActive(true); @@ -145,47 +160,66 @@ protected void Construct() { canvas.overrideSorting = true; canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.TexCoord1 | AdditionalCanvasShaderChannels.TexCoord2; - - var canvasGR = gameObject.AddComponent(); - //canvasGR.blockingObjects = GraphicRaycaster.BlockingObjects.All; - gameObject.transform.SetParent(canvas.transform, false); - gameObject.transform.position = new Vector2(0.5f, 0.5f); + var canvasGR = canvas.gameObject.AddComponent(); + gameObject.AddComponent(); + + originalEventSystem = _inputModule.GetComponent(); + inputOBJ = new GameObject("ImberInputGO"); + inputOBJ.AddComponent(); + Cursor.visible = true; + + if(inputOBJ.GetComponent() == null) { + imberEventSystem = inputOBJ.AddComponent(); + } + + EventSystem.current = imberEventSystem; + + gameObject.transform.SetParent(canvas.transform, false); __Init(screen, parentViewController, containerViewController); screen.SetRootViewController(this, ViewController.AnimationType.None); + + + } + + + public void Dispose() { + if (!Environment.GetCommandLineArgs().Contains("fpfc")) return; + //EventSystem.current = originalEventSystem; + Cursor.visible = false; } - public void Setup(float initialSongTime, int targetFramerate, string defaultLocation, IEnumerable locations) { + + public void Setup(float initialSongTime, int targetFramerate) { _initialTime = initialSongTime; _targetFPS = targetFramerate; _timeSync = initialSongTime; - _location = defaultLocation; - this.locations.AddRange(locations); } protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); - tabSelector.TextSegmentedControl.didSelectCellEvent += DidSelect; - didParse = true; if (firstActivation) { - tabSelector.transform.localScale *= .9f; + var x = timebarActive.gameObject.AddComponent(); + x.timebarBackground = timebarbg; + x.timebarActive = timebarActive; + x.OnProgressUpdated += (progress) => { + _imberScrubber.MainNode_PositionDidChange(progress); + }; + //var containerRect = this.GetComponent(); + //_container.GetComponent().position = Vector3.zero; + //_container.GetComponent().anchorMax = new Vector2(0.2f, 0.2f); + + //Plugin.Log.Notice($"{Plugin.Settings.replayUIPosition.x},{Plugin.Settings.replayUIPosition.y}"); + //containerRect.anchorMax = new Vector2(Plugin.Settings.replayUIPosition.x, Plugin.Settings.replayUIPosition.y); } - } - - private void DidSelect(SegmentedControl _, int selected) { - - const int positionTabIndex = 2; - if (_lastTab == 2 || selected == 2) - DidPositionTabVisibilityChange?.Invoke(selected == positionTabIndex); - _lastTab = selected; + didParse = true; } protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { - tabSelector.TextSegmentedControl.didSelectCellEvent -= DidSelect; base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); } @@ -212,34 +246,120 @@ protected void Loop() { DidClickLoop?.Invoke(); } - [UIAction("left-hand")] - protected void SwitchHandLeft() { + [UIAction("format-time-percent")] + protected string FormatTimePercent(float value) { - SwitchHand(XRNode.LeftHand); + return value.ToString("P0"); } - [UIAction("right-hand")] - protected void SwitchHandRight() { + [UIAction("jump")] + protected void Jump() { - SwitchHand(XRNode.RightHand); + DidPositionJump?.Invoke(); } + private string FloatToTimeStamp(float timeInSeconds) { + int minutes = (int)timeInSeconds / 60; + int seconds = (int)timeInSeconds % 60; - [UIAction("request-dismiss")] - protected void RequestDismiss() { - - DidChangeVisiblity?.Invoke(false); + return $"{minutes:D2}:{seconds:D2}"; } - [UIAction("format-time-percent")] - protected string FormatTimePercent(float value) { + public void FixedUpdate() { - return value.ToString("P0"); + if (!didParse) return; + currentTimeText.text = FloatToTimeStamp(_audioTimeSyncController.songTime) + "/" + FloatToTimeStamp(_audioTimeSyncController.songLength); + + float progressPercentage = Mathf.Clamp01(_audioTimeSyncController.songTime / _audioTimeSyncController.songLength); + + float timebarActiveX = Mathf.Lerp(-19, 19, progressPercentage); + timebarActive.rectTransform.anchoredPosition = new Vector2(timebarActiveX, 0); } - [UIAction("jump")] - protected void Jump() { + public class ProgressHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler { - DidPositionJump?.Invoke(); + public ImageView timebarActive; + public ImageView timebarBackground; + + public event Action OnProgressUpdated; + + private bool isDragging = false; + private float minX = -19f; + private float maxX = 19f; + + public void OnPointerDown(PointerEventData eventData) { + isDragging = true; + UpdateTimebarPosition(eventData); + } + + public void OnPointerUp(PointerEventData eventData) { + isDragging = false; + UpdateTimebarPosition(eventData); + } + + public void OnDrag(PointerEventData eventData) { + if (isDragging) { + UpdateTimebarPosition(eventData); + } + } + + private void UpdateTimebarPosition(PointerEventData eventData) { + RectTransform timebarRect = timebarBackground.rectTransform; + Vector2 localPoint; + + if (RectTransformUtility.ScreenPointToLocalPointInRectangle(timebarRect, eventData.position, eventData.pressEventCamera, out localPoint)) { + float clampedX = Mathf.Clamp(localPoint.x, minX, maxX); + + timebarActive.rectTransform.anchoredPosition = new Vector2(clampedX, 0); + + float progress = Mathf.InverseLerp(minX, maxX, clampedX); + + OnProgressUpdated?.Invoke(progress); + } + } } + + + //public class DraggableViewController : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { + + // public RectTransform draggableRectTransform; + // public GameObject canvas; + + // private Vector2 lastMousePosition; + // private bool isDragging = false; + + // public void OnPointerDown(PointerEventData eventData) { + // isDragging = true; + // RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.GetComponent(), eventData.position, eventData.pressEventCamera, out lastMousePosition); + // } + + // public void OnDrag(PointerEventData eventData) { + // if (isDragging && draggableRectTransform != null) { + // Vector2 mousePosition; + // RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.GetComponent(), eventData.position, eventData.pressEventCamera, out mousePosition); + + // Vector2 delta = mousePosition - lastMousePosition; + + // draggableRectTransform.anchoredPosition += delta; + + // Vector2 normalizedPosition = new Vector2( + // (draggableRectTransform.anchoredPosition.x + canvas.GetComponent().rect.width / 2) / canvas.GetComponent().rect.width, + // (draggableRectTransform.anchoredPosition.y + canvas.GetComponent().rect.height / 2) / canvas.GetComponent().rect.height + // ); + + // normalizedPosition.x = Mathf.Clamp01(normalizedPosition.x); + // normalizedPosition.y = Mathf.Clamp01(normalizedPosition.y); + + // draggableRectTransform.anchorMin = normalizedPosition; + // draggableRectTransform.anchorMax = normalizedPosition; + + // lastMousePosition = mousePosition; + // } + // } + + // public void OnPointerUp(PointerEventData eventData) { + // isDragging = false; + // } + //} + } } \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs b/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs index 649760a..8bfbb4b 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs @@ -12,8 +12,7 @@ namespace ScoreSaber.Core.ReplaySystem.UI { - internal class ImberManager : IInitializable, IDisposable - { + internal class ImberManager : IInitializable, IDisposable, ITickable { private readonly IGamePause _gamePause; private readonly float _initialTimeScale; private readonly PosePlayer _posePlayer; @@ -40,12 +39,13 @@ public ImberManager(ReplayFile file, IGamePause gamePause, ImberScrubber imberSc _audioTimeSyncController = audioTimeSyncController; _replayTimeSyncController = replayTimeSyncController; _imberUIPositionController = imberUIPositionController; + _desktopMainImberPanelView = desktopMainImberPanelView; + _positions = Plugin.Settings.spectatorPositions.Select(sp => sp.name); _mainImberPanelView.Setup(initData.timeScale, 90, _positions.First(), _positions); _imberScrubber.Setup(file.metadata.FailTime, file.metadata.Modifiers.Contains("NF")); _initialTimeScale = file.noteKeyframes.FirstOrDefault().TimeSyncTimescale; - _desktopMainImberPanelView = desktopMainImberPanelView; - _desktopMainImberPanelView.Setup(file.metadata.FailTime, 90, _positions.First(), _positions); + _desktopMainImberPanelView.Setup(1f, 90); } public void Initialize() { @@ -66,10 +66,7 @@ public void Initialize() { _desktopMainImberPanelView.DidClickRestart += MainImberPanelView_DidClickRestart; _desktopMainImberPanelView.DidClickPausePlay += MainImberPanelView_DidClickPausePlay; _desktopMainImberPanelView.DidTimeSyncChange += MainImberPanelView_DidTimeSyncChange; - _desktopMainImberPanelView.DidChangeVisiblity += MainImberPanelView_DidChangeVisiblity; _desktopMainImberPanelView.HandDidSwitchEvent += MainImberPanelView_DidHandSwitchEvent; - _desktopMainImberPanelView.DidPositionPreviewChange += MainImberPanelView_DidPositionPreviewChange; - _desktopMainImberPanelView.DidPositionTabVisibilityChange += MainImberPanelView_DidPositionTabVisibilityChange; _spectateAreaController.DidUpdatePlayerSpectatorPose += SpectateAreaController_DidUpdatePlayerSpectatorPose; _imberScrubber.DidCalculateNewTime += ImberScrubber_DidCalculateNewTime; @@ -223,14 +220,18 @@ public void Dispose() { _mainImberPanelView.DidClickLoop -= MainImberPanelView_DidClickLoop; - _desktopMainImberPanelView.DidPositionPreviewChange -= MainImberPanelView_DidPositionPreviewChange; _desktopMainImberPanelView.HandDidSwitchEvent -= MainImberPanelView_DidHandSwitchEvent; - _desktopMainImberPanelView.DidChangeVisiblity -= MainImberPanelView_DidChangeVisiblity; _desktopMainImberPanelView.DidTimeSyncChange -= MainImberPanelView_DidTimeSyncChange; _desktopMainImberPanelView.DidClickPausePlay -= MainImberPanelView_DidClickPausePlay; _desktopMainImberPanelView.DidClickRestart -= MainImberPanelView_DidClickRestart; _desktopMainImberPanelView.DidPositionJump -= MainImberPanelView_DidPositionJump; _desktopMainImberPanelView.DidClickLoop -= MainImberPanelView_DidClickLoop; } + + public void Tick() { + if (Input.GetKeyDown(KeyCode.I)) { + _desktopMainImberPanelView.gameObject.SetActive(!_desktopMainImberPanelView.gameObject.activeSelf); + } + } } } \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs b/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs index 999c920..441d1ec 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs @@ -110,7 +110,7 @@ public void Initialize() { visibility = false; } - private void MainNode_PositionDidChange(float value) { + public void MainNode_PositionDidChange(float value) { _bar.barFill = value; DidCalculateNewTime?.Invoke(_audioTimeSyncController.songLength * value); diff --git a/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs index 8bc9858..3fb9c1c 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs @@ -6,6 +6,7 @@ using ScoreSaber.Core.Data; using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityEngine.XR; using Zenject; @@ -129,7 +130,7 @@ public float rightSaberSpeed { [Inject] protected void Construct() { - + if (Environment.GetCommandLineArgs().Contains("fpfc")) return; _floatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(60f, 45f), false, defaultPosition.position, defaultPosition.rotation); _floatingScreen.GetComponent().sortingOrder = 31; _floatingScreen.name = "Imber Replay Panel (Screen)"; diff --git a/ScoreSaber/Core/ReplaySystem/UI/desktop-imber-panel.bsml b/ScoreSaber/Core/ReplaySystem/UI/desktop-imber-panel.bsml new file mode 100644 index 0000000..3a94245 --- /dev/null +++ b/ScoreSaber/Core/ReplaySystem/UI/desktop-imber-panel.bsml @@ -0,0 +1,56 @@ + + + + + + + + + + + + +