diff --git a/.gitignore b/.gitignore index 140ae3e..f33ff37 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ MigrationBackup/ ScoreSaber/ObfuscationSettings.cs ScoreSaber/Core/ReplaySystem/Legacy/LegacyRecorder.cs ScoreSaber/Core/ReplaySystem/Legacy/OldLegacyReplayPlayer.cs +/.idea diff --git a/README.md b/README.md index 4a1c04e..6dc48d8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ # ScoreSaber-Plugin -Clone, and goodluck. The open source release of ScoreSaber was done hastily and quite a lot needs to be changed and plenty of "security through obscurity" practices covered by obfuscation can finally be removed, cleaning up the project. +ScoreSaber is Beat Saber's largest leaderboard system for custom songs, hosting 60 million scores across 170,000+ leaderboards, with more than 1 million users worldwide -Everything here is licensed under MIT and we'll be accepting contributors immediately, so if you're looking to help us out, that'd be greatly appreciated, we're going to need it. \ No newline at end of file +![image](https://github.com/user-attachments/assets/f638f92b-d961-46e1-8277-d2628676128a) + +Scores can also be viewed on [the website](https://scoresaber.com) + +# Installation +## Method 1 +- Install from your preferred mod manager +## Method 2 +- Download from the [releases](https://github.com/ScoreSaber/scoresaber-plugin/releases/latest) diff --git a/ScoreSaber/Core/AffinityPatches/MenuPresencePatches.cs b/ScoreSaber/Core/AffinityPatches/MenuPresencePatches.cs new file mode 100644 index 0000000..548fcb5 --- /dev/null +++ b/ScoreSaber/Core/AffinityPatches/MenuPresencePatches.cs @@ -0,0 +1,102 @@ +using HarmonyLib; +using ScoreSaber.Core.Data.Models; +using ScoreSaber.Core.Services; +using ScoreSaber.Core.Utils; +using SiraUtil.Affinity; +using SiraUtil.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Zenject; + +namespace ScoreSaber.Core.AffinityPatches { + public class MenuPresencePatches : IAffinity { + + [Inject] private readonly RichPresenceService _richPresenceService = null; + + [AffinityPatch(typeof(SinglePlayerLevelSelectionFlowCoordinator), "HandleStandardLevelDidFinish")] + [AffinityPostfix] + public void HandleStopStandardLevelPostfix() { + var jsonObject = new SceneChangeEvent() { + Timestamp = _richPresenceService.TimeRightNow, + Scene = Scene.menu + }; + + _richPresenceService.SendUserProfileChannel("scene_change", jsonObject); + } + + [AffinityPatch(typeof(SinglePlayerLevelSelectionFlowCoordinator), nameof(SinglePlayerLevelSelectionFlowCoordinator.StartLevel))] + [AffinityPostfix] + public void HandleStartLevelPostfix(SinglePlayerLevelSelectionFlowCoordinator __instance, Action beforeSceneSwitchCallback, bool practice) { + + bool isPractice = practice; + GameMode gameMode = GameMode.solo; + + if (isPractice) { + gameMode = GameMode.practice; + + // this is to privatise the practice mode song, as it would be exposed in the rich presence, still not shown in UI though. + var songEventPrivate = new SongRichPresenceInfo(_richPresenceService.TimeRightNow, gameMode, + "PRACTICE", + string.Empty, + "PRACTICE AUTHOR", + "PRACTICE MAPPER", + "Standard", + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", + (int)2000000000, + 1, + 0, + 1); + + _richPresenceService.SendUserProfileChannel("start_song", songEventPrivate); + return; + } + + GameplayModifiers gameplayModifiers = __instance.gameplayModifiers; + int startTime = 0; + if (__instance.isInPracticeView) { + startTime = (int)__instance._practiceViewController._practiceSettings.startSongTime; + } + + var songEvent = CreateSongStartEvent(__instance.selectedBeatmapLevel, __instance.selectedBeatmapKey, gameplayModifiers, startTime, gameMode); + + _richPresenceService.SendUserProfileChannel("start_song", songEvent); + } + + [AffinityPatch(typeof(MultiplayerLevelSelectionFlowCoordinator), nameof(MultiplayerLevelSelectionFlowCoordinator.HandleLobbyGameStateControllerGameStarted))] + [AffinityPostfix] + public void HandleMultiplayerGameStartPostfix(MultiplayerLevelSelectionFlowCoordinator __instance, ILevelGameplaySetupData levelGameplaySetupData) { + + // i dont like this, but i have to do it, just in case the users selected level doesnt match what the game started with + BeatmapLevel beatmapLevel = SongCore.Loader.GetLevelById(levelGameplaySetupData.beatmapKey.levelId); + + GameplayModifiers gameplayModifiers = levelGameplaySetupData.gameplayModifiers; + int startTime = 0; + + var songEvent = CreateSongStartEvent(beatmapLevel, levelGameplaySetupData.beatmapKey, gameplayModifiers, startTime, GameMode.multiplayer); + + _richPresenceService.SendUserProfileChannel("start_song", songEvent); + } + + private SongRichPresenceInfo CreateSongStartEvent(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, GameplayModifiers gameplayModifiers, int startTime, GameMode gameMode) { + + string hash = BeatmapUtils.GetHashFromLevelID(beatmapLevel, out bool isOst); + + var songEvent = new SongRichPresenceInfo(_richPresenceService.TimeRightNow, gameMode, + beatmapLevel.songName, + beatmapLevel.songSubName, + BeatmapUtils.FriendlyLevelAuthorName(beatmapLevel.allMappers, beatmapLevel.allLighters), + beatmapLevel.songAuthorName, + beatmapKey.beatmapCharacteristic.SerializedName(), + hash, + (int)beatmapLevel.songDuration, + ((int)beatmapKey.difficulty * 2) + 1, + startTime, + gameplayModifiers.songSpeedMul); + + return songEvent; + } + } +} diff --git a/ScoreSaber/Core/AppInstaller.cs b/ScoreSaber/Core/AppInstaller.cs index d398b3e..728daf0 100644 --- a/ScoreSaber/Core/AppInstaller.cs +++ b/ScoreSaber/Core/AppInstaller.cs @@ -1,4 +1,6 @@ using ScoreSaber.Core.Daemons; +using ScoreSaber.Core.Http; +using ScoreSaber.Core.Services; using System.Reflection; using Zenject; @@ -7,8 +9,11 @@ internal class AppInstaller : Installer { public override void InstallBindings() { Plugin.Container = Container; + Container.Bind().AsSingle(); Container.Bind().AsSingle().NonLazy(); + Container.BindInterfacesAndSelfTo().AsSingle(); + Container.Bind().FromInstance(new ScoreSaberHttpClient(new HttpClientOptions(applicationName: "ScoreSaber-PC", version: Plugin.Instance.LibVersion, defaultTimeout: 5, uploadTimeout: 120))).AsSingle(); } } } diff --git a/ScoreSaber/Core/Daemons/UploadDaemon.cs b/ScoreSaber/Core/Daemons/UploadDaemon.cs index 8008589..e133624 100644 --- a/ScoreSaber/Core/Daemons/UploadDaemon.cs +++ b/ScoreSaber/Core/Daemons/UploadDaemon.cs @@ -15,6 +15,9 @@ using ScoreSaber.Core.Utils; using static ScoreSaber.UI.Leaderboard.ScoreSaberLeaderboardViewController; using System.Threading; +using ScoreSaber.Core.Http; +using ScoreSaber.Core.Http.Configuration; +using ScoreSaber.Core.Http.Endpoints.API; namespace ScoreSaber.Core.Daemons { @@ -28,18 +31,20 @@ internal class UploadDaemon : IDisposable, IUploadDaemon { private readonly PlayerService _playerService = null; private readonly ReplayService _replayService = null; private readonly LeaderboardService _leaderboardService = null; + private readonly ScoreSaberHttpClient _scoreSaberHttpClient = null; private readonly PlayerDataModel _playerDataModel = null; private readonly MaxScoreCache _maxScoreCache = null; private const string UPLOAD_SECRET = "f0b4a81c9bd3ded1081b365f7628781f"; - public UploadDaemon(PlayerService playerService, LeaderboardService leaderboardService, ReplayService replayService, PlayerDataModel playerDataModel, MaxScoreCache maxScoreCache) { + public UploadDaemon(PlayerService playerService, LeaderboardService leaderboardService, ReplayService replayService, PlayerDataModel playerDataModel, MaxScoreCache maxScoreCache, ScoreSaberHttpClient scoreSaberHttpClient) { _playerService = playerService; _replayService = replayService; _leaderboardService = leaderboardService; _playerDataModel = playerDataModel; _maxScoreCache = maxScoreCache; + _scoreSaberHttpClient = scoreSaberHttpClient; SetupUploader(); Plugin.Log.Debug("Upload service setup!"); @@ -127,33 +132,31 @@ public void Five(string gameMode, BeatmapLevel beatmapLevel, BeatmapKey beatmapK //This starts the upload processs async void Six(BeatmapLevel beatmapLevel, BeatmapKey beatmapKey, LevelCompletionResults levelCompletionResults) { - if (!beatmapLevel.hasPrecalculatedData) { - int maxScore = await _maxScoreCache.GetMaxScore(beatmapLevel, beatmapKey); + int maxScore = await _maxScoreCache.GetMaxScore(beatmapLevel, beatmapKey); - if (levelCompletionResults.multipliedScore > maxScore) { - UploadStatusChanged?.Invoke(UploadStatus.Error, "Failed to upload (score was impossible)"); - Plugin.Log.Debug($"Score was better than possible, not uploading!"); - return; - } + if (levelCompletionResults.multipliedScore > maxScore) { + UploadStatusChanged?.Invoke(UploadStatus.Error, "Failed to upload (score was impossible)"); + Plugin.Log.Debug($"Score was better than possible, not uploading!"); + return; + } - try { - UploadStatusChanged?.Invoke(UploadStatus.Packaging, "Packaging score..."); - ScoreSaberUploadData data = ScoreSaberUploadData.Create(beatmapLevel, beatmapKey, levelCompletionResults, _playerService.localPlayerInfo, GetVersionHash()); - string scoreData = JsonConvert.SerializeObject(data); - - // TODO: Simplify now that we're open source - byte[] encodedPassword = new UTF8Encoding().GetBytes($"{UPLOAD_SECRET}-{_playerService.localPlayerInfo.playerKey}-{_playerService.localPlayerInfo.playerId}-{UPLOAD_SECRET}"); - byte[] keyHash = ((HashAlgorithm)CryptoConfig.CreateFromName("MD5")).ComputeHash(encodedPassword); - string key = BitConverter.ToString(keyHash) - .Replace("-", string.Empty) - .ToLower(); - - string scoreDataHex = BitConverter.ToString(Swap(Encoding.UTF8.GetBytes(scoreData), Encoding.UTF8.GetBytes(key))).Replace("-", ""); - Seven(data, scoreDataHex, beatmapLevel, beatmapKey, levelCompletionResults).RunTask(); - } catch (Exception ex) { - UploadStatusChanged?.Invoke(UploadStatus.Error, "Failed to upload score, error written to log."); - Plugin.Log.Error($"Failed to upload score: {ex}"); - } + try { + UploadStatusChanged?.Invoke(UploadStatus.Packaging, "Packaging score..."); + ScoreSaberUploadData data = ScoreSaberUploadData.Create(beatmapLevel, beatmapKey, levelCompletionResults, _playerService.localPlayerInfo, GetVersionHash()); + string scoreData = JsonConvert.SerializeObject(data); + + // TODO: Simplify now that we're open source + byte[] encodedPassword = new UTF8Encoding().GetBytes($"{UPLOAD_SECRET}-{_playerService.localPlayerInfo.playerKey}-{_playerService.localPlayerInfo.playerId}-{UPLOAD_SECRET}"); + byte[] keyHash = ((HashAlgorithm)CryptoConfig.CreateFromName("MD5")).ComputeHash(encodedPassword); + string key = BitConverter.ToString(keyHash) + .Replace("-", string.Empty) + .ToLower(); + + string scoreDataHex = BitConverter.ToString(Swap(Encoding.UTF8.GetBytes(scoreData), Encoding.UTF8.GetBytes(key))).Replace("-", ""); + Seven(data, scoreDataHex, beatmapLevel, beatmapKey, levelCompletionResults).RunTask(); + } catch (Exception ex) { + UploadStatusChanged?.Invoke(UploadStatus.Error, "Failed to upload score, error written to log."); + Plugin.Log.Error($"Failed to upload score: {ex}"); } } @@ -165,7 +168,7 @@ public async Task Seven(ScoreSaberUploadData scoreSaberUploadData, string upload UploadStatusChanged?.Invoke(UploadStatus.Packaging, "Checking leaderboard ranked status..."); bool ranked = true; - Leaderboard currentLeaderboard = await _leaderboardService.GetCurrentLeaderboard(beatmapKey); + Leaderboard currentLeaderboard = await _leaderboardService.GetCurrentLeaderboard(beatmapKey, beatmapLevel); if (currentLeaderboard != null) { ranked = currentLeaderboard.leaderboardInfo.ranked; @@ -204,12 +207,12 @@ public async Task Seven(ScoreSaberUploadData scoreSaberUploadData, string upload Plugin.Log.Info("Attempting score upload..."); UploadStatusChanged?.Invoke(UploadStatus.Uploading, "Uploading score..."); try { - response = await Plugin.HttpInstance.PostAsync("/game/upload", form); - } catch (HttpErrorException httpException) { - if (httpException.isScoreSaberError) { - Plugin.Log.Error($"Failed to upload score: {httpException.scoreSaberError.errorMessage}:{httpException}"); + response = await _scoreSaberHttpClient.PostRawAsync(new UploadRequest().BuildUrl(), form); // this endpoint doesnt give back json, just a raw string, so we have to use PostRawAsync + } catch (HttpRequestException httpException) { + if (httpException.IsScoreSaberError) { + Plugin.Log.Error($"Failed to upload score: {httpException.ScoreSaberError.errorMessage}:{httpException}"); } else { - Plugin.Log.Error($"Failed to upload score: {httpException.isNetworkError}:{httpException.isHttpError}:{httpException}"); + Plugin.Log.Error($"Failed to upload score: {httpException.IsNetworkError}:{httpException.IsHttpError}:{httpException}"); } } catch (Exception ex) { Plugin.Log.Error($"Failed to upload score: {ex.ToString()}"); diff --git a/ScoreSaber/Core/Data/Internal/Settings.cs b/ScoreSaber/Core/Data/Internal/Settings.cs index d7bfa48..73788e3 100644 --- a/ScoreSaber/Core/Data/Internal/Settings.cs +++ b/ScoreSaber/Core/Data/Internal/Settings.cs @@ -8,12 +8,12 @@ namespace ScoreSaber.Core.Data { internal class Settings { - private static int _currentVersion => 8; + private static int _currentVersion => 9; public bool hideReplayUI = false; public int fileVersion { get; set; } - public bool disableScoreSaber { get; set; } + public bool disableScoreSaber { get; set; } // Unused public bool showLocalPlayerRank { get; set; } public bool showScorePP { get; set; } public bool showStatusText { get; set; } @@ -36,6 +36,13 @@ internal class Settings public bool leftHandedReplayUI { get; set; } public bool lockedReplayUIMode { get; set; } public List spectatorPositions { get; set; } + public Vec2 replayUIPosition { get; set; } + public float replayUISize { get; set; } + public bool startReplayUIHidden { get; set; } + public bool hideWatermarkIfUsersReplay { get; set; } + public bool enableRichPresence { get; set; } + public bool hasAcceptedRichPresenceDisclaimer { get; set; } + public bool showMainMenuButton { get; set; } internal static string dataPath => "UserData"; internal static string configPath => dataPath + @"\ScoreSaber"; @@ -65,6 +72,13 @@ public void SetDefaults() { hasOpenedReplayUI = false; leftHandedReplayUI = false; lockedReplayUIMode = false; + replayUIPosition = new Vec2(new Vector2(0.12f, 0.14f)); + replayUISize = 1.25f; + startReplayUIHidden = false; + hideWatermarkIfUsersReplay = false; + enableRichPresence = false; + hasAcceptedRichPresenceDisclaimer = false; + showMainMenuButton = true; SetDefaultSpectatorPositions(); } @@ -113,6 +127,15 @@ internal static Settings LoadSettings() { if(decoded.fileVersion < 8) { decoded.replayCameraSmoothing = true; } + if (decoded.fileVersion < 9) { + decoded.replayUIPosition = new Vec2(new Vector2(0.12f, 0.14f)); + decoded.replayUISize = 1.25f; + decoded.startReplayUIHidden = false; + decoded.enableRichPresence = false; + decoded.hasAcceptedRichPresenceDisclaimer = false; + decoded.hideWatermarkIfUsersReplay = false; + decoded.showMainMenuButton = true; + } SaveSettings(decoded); } return decoded; @@ -136,6 +159,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/Data/Models/GameMode.cs b/ScoreSaber/Core/Data/Models/GameMode.cs new file mode 100644 index 0000000..5377aac --- /dev/null +++ b/ScoreSaber/Core/Data/Models/GameMode.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public enum GameMode { + [JsonProperty("solo")] + solo, + [JsonProperty("multi")] + multiplayer, + [JsonProperty("practice")] + practice + } +} diff --git a/ScoreSaber/Core/Data/Models/LeaderboardUploadData.cs b/ScoreSaber/Core/Data/Models/LeaderboardUploadData.cs index e95842e..624162c 100644 --- a/ScoreSaber/Core/Data/Models/LeaderboardUploadData.cs +++ b/ScoreSaber/Core/Data/Models/LeaderboardUploadData.cs @@ -60,11 +60,11 @@ internal static ScoreSaberUploadData Create(BeatmapLevel beatmapLevel, BeatmapKe data.gameMode = $"Solo{beatmapKey.beatmapCharacteristic.serializedName}"; data.difficulty = BeatmapDifficultyMethods.DefaultRating(beatmapKey.difficulty); data.infoHash = infoHash; - data.leaderboardId = levelInfo[2]; + data.leaderboardId = BeatmapUtils.GetHashFromLevelID(beatmapKey, out bool isOst); data.songName = beatmapLevel.songName; data.songSubName = beatmapLevel.songSubName; data.songAuthorName = beatmapLevel.songAuthorName; - data.levelAuthorName = friendlyLevelAuthorName(beatmapLevel.allMappers, beatmapLevel.allLighters); + data.levelAuthorName = BeatmapUtils.FriendlyLevelAuthorName(beatmapLevel.allMappers, beatmapLevel.allLighters) ?? ""; data.bpm = Convert.ToInt32(beatmapLevel.beatsPerMinute); data.playerName = playerInfo.playerName; @@ -83,16 +83,5 @@ internal static ScoreSaberUploadData Create(BeatmapLevel beatmapLevel, BeatmapKe data.deviceControllerRightIdentifier = VRDevices.GetDeviceControllerRight(); return data; } - - static string friendlyLevelAuthorName(string[] mappers, string[] lighters) { - List mappersAndLighters = new List(); - mappersAndLighters.AddRange(mappers); - mappersAndLighters.AddRange(lighters); - - if(mappersAndLighters.Count <= 1) { - return mappersAndLighters.FirstOrDefault(); - } - return $"{string.Join(", ", mappersAndLighters.Take(mappersAndLighters.Count - 1))} & {mappersAndLighters.Last()}"; - } } } \ No newline at end of file diff --git a/ScoreSaber/Core/Data/Models/PauseType.cs b/ScoreSaber/Core/Data/Models/PauseType.cs new file mode 100644 index 0000000..ad47df6 --- /dev/null +++ b/ScoreSaber/Core/Data/Models/PauseType.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public enum PauseType { + [JsonProperty("pause")] + Pause, + [JsonProperty("unpause")] + Unpause + } +} diff --git a/ScoreSaber/Core/Data/Models/PauseUnpauseEvent.cs b/ScoreSaber/Core/Data/Models/PauseUnpauseEvent.cs new file mode 100644 index 0000000..ca95616 --- /dev/null +++ b/ScoreSaber/Core/Data/Models/PauseUnpauseEvent.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public class PauseUnpauseEvent { + [JsonProperty("timestamp")] + public string Timestamp { get; set; } = string.Empty; + + [JsonProperty("songTime")] + public double SongTime { get; set; } // Time since start of song in seconds + + [JsonProperty("eventType")] + [JsonConverter(typeof(StringEnumConverter))] + public PauseType EventType { get; set; } + } +} diff --git a/ScoreSaber/Core/Data/Models/RichPresenceResponse.cs b/ScoreSaber/Core/Data/Models/RichPresenceResponse.cs new file mode 100644 index 0000000..d84a9f4 --- /dev/null +++ b/ScoreSaber/Core/Data/Models/RichPresenceResponse.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public class RichPresenceResponse { + [JsonProperty("id")] + public string Id { get; set; } = string.Empty; + + [JsonProperty("state")] + public State state { get; set; } + } +} diff --git a/ScoreSaber/Core/Data/Models/Scene.cs b/ScoreSaber/Core/Data/Models/Scene.cs new file mode 100644 index 0000000..600aaa5 --- /dev/null +++ b/ScoreSaber/Core/Data/Models/Scene.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public enum Scene { + [JsonProperty("offline")] + offline, + [JsonProperty("online")] + online, + [JsonProperty("menu")] + menu, + [JsonProperty("playing")] + playing + } +} diff --git a/ScoreSaber/Core/Data/Models/SceneChangeEvent.cs b/ScoreSaber/Core/Data/Models/SceneChangeEvent.cs new file mode 100644 index 0000000..3e97acf --- /dev/null +++ b/ScoreSaber/Core/Data/Models/SceneChangeEvent.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public class SceneChangeEvent { + [JsonProperty("timestamp")] + public string Timestamp { get; set; } = string.Empty; + + [JsonProperty("scene")] + [JsonConverter(typeof(StringEnumConverter))] + public Scene Scene { get; set; } + } +} diff --git a/ScoreSaber/Core/Data/Models/ScoreSaberTeam.cs b/ScoreSaber/Core/Data/Models/ScoreSaberTeam.cs index d36d298..045001d 100644 --- a/ScoreSaber/Core/Data/Models/ScoreSaberTeam.cs +++ b/ScoreSaber/Core/Data/Models/ScoreSaberTeam.cs @@ -1,18 +1,17 @@ #pragma warning disable IDE1006 // Naming Styles using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; -namespace ScoreSaber.Core.Data.Models -{ - internal class ScoreSaberTeam - { +namespace ScoreSaber.Core.Data.Models { + internal class ScoreSaberTeam { [JsonProperty("TeamMembers")] + [JsonConverter(typeof(TeamMembersJsonConverter))] public Dictionary> TeamMembers { get; set; } } - internal class TeamMember - { + internal class TeamMember { [JsonProperty("Name")] internal string Name { get; set; } [JsonProperty("ProfilePicture")] @@ -29,17 +28,72 @@ internal class TeamMember internal string YouTube { get; set; } } - [JsonConverter(typeof(StringEnumConverter))] - internal enum TeamType - { - Backend, - Frontend, - Mod, - PPv3, - Admin, - RT, - NAT, - QAT, - CAT + internal class TeamType { + private readonly string _value; + + private TeamType(string value) => _value = value; + + public static readonly TeamType Backend = new TeamType("Backend"); + public static readonly TeamType Frontend = new TeamType("Frontend"); + public static readonly TeamType Mod = new TeamType("Mod"); + public static readonly TeamType PPv3 = new TeamType("PPv3"); + public static readonly TeamType Admin = new TeamType("Admin"); + public static readonly TeamType RT = new TeamType("RT"); + public static readonly TeamType NAT = new TeamType("NAT"); + public static readonly TeamType QAT = new TeamType("QAT"); + public static readonly TeamType CAT = new TeamType("CAT"); + public static readonly TeamType CCT = new TeamType("CCT"); + + + // holds the known team types as of building the plugin, can handle unknown team types + public static TeamType FromString(string value) => value switch { + "Backend" => Backend, + "Frontend" => Frontend, + "Mod" => Mod, + "PPv3" => PPv3, + "Admin" => Admin, + "RT" => RT, + "NAT" => NAT, + "QAT" => QAT, + "CAT" => CAT, + "CCT" => CCT, + _ => new TeamType(value) + }; + + public override string ToString() => _value; + + public override bool Equals(object obj) => obj is TeamType other && _value == other._value; + + public override int GetHashCode() => _value.GetHashCode(); + + public static implicit operator string(TeamType teamType) => teamType._value; } + + internal class TeamMembersJsonConverter : JsonConverter>> { + public override Dictionary> ReadJson(JsonReader reader, Type objectType, Dictionary> existingValue, bool hasExistingValue, JsonSerializer serializer) { + var dictionary = new Dictionary>(); + + var jsonObject = JObject.Load(reader); + + foreach (var property in jsonObject.Properties()) { + var teamType = TeamType.FromString(property.Name); + + var teamMembers = property.Value.ToObject>(serializer); + + dictionary[teamType] = teamMembers; + } + + return dictionary; + } + + public override void WriteJson(JsonWriter writer, Dictionary> value, JsonSerializer serializer) { + writer.WriteStartObject(); + foreach (var kvp in value) { + writer.WritePropertyName(kvp.Key.ToString()); + serializer.Serialize(writer, kvp.Value); + } + writer.WriteEndObject(); + } + } + } diff --git a/ScoreSaber/Core/Data/Models/SongRichPresenceInfo.cs b/ScoreSaber/Core/Data/Models/SongRichPresenceInfo.cs new file mode 100644 index 0000000..e35fa47 --- /dev/null +++ b/ScoreSaber/Core/Data/Models/SongRichPresenceInfo.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public class SongRichPresenceInfo { + [JsonProperty("timestamp")] + public string Timestamp { get; set; } = string.Empty; + + [JsonProperty("mode")] + [JsonConverter(typeof(StringEnumConverter))] + public GameMode Mode { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("subName")] + public string SubName { get; set; } = string.Empty; + + [JsonProperty("authorName")] + public string AuthorName { get; set; } = string.Empty; + + [JsonProperty("artist")] + public string Artist { get; set; } = string.Empty; + + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; // Standard, Lawless, OneSaber etc + + [JsonProperty("hash")] + public string Hash { get; set; } = string.Empty; + + [JsonProperty("duration")] + public int Duration { get; set; } // Song duration in seconds + + [JsonProperty("difficulty")] + public int Difficulty { get; set; } = -1; // Difficulty, 0-9, odd numba + + [JsonProperty("startTime", NullValueHandling = NullValueHandling.Ignore)] + public int? StartTime { get; set; } // Start time if in practice mode + + [JsonProperty("playSpeed", NullValueHandling = NullValueHandling.Ignore)] + public double? PlaySpeed { get; set; } // Playback speed, from either practice mode or speed modifies + + + public SongRichPresenceInfo(string timestamp, GameMode mode, string name, string subName, string authorName, string artist, string type, string hash, int duration, int difficulty, int? startTime, double? playSpeed) { + Timestamp = timestamp; + Mode = mode; + Name = name; + SubName = subName; + AuthorName = authorName; + Artist = artist; + Type = type; + Hash = hash; + Duration = duration; + Difficulty = difficulty; + StartTime = startTime; + PlaySpeed = playSpeed; + } + } +} diff --git a/ScoreSaber/Core/Data/Models/State.cs b/ScoreSaber/Core/Data/Models/State.cs new file mode 100644 index 0000000..295c511 --- /dev/null +++ b/ScoreSaber/Core/Data/Models/State.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Data.Models { + public class State { + [JsonProperty("scene")] + public Scene Scene { get; set; } = Scene.menu; + + [JsonProperty("currentMap")] + public SongRichPresenceInfo currentMap { get; set; } + } +} diff --git a/ScoreSaber/Core/Data/Wrappers/LeaderboardInfoMap.cs b/ScoreSaber/Core/Data/Wrappers/LeaderboardInfoMap.cs index 848a672..643369c 100644 --- a/ScoreSaber/Core/Data/Wrappers/LeaderboardInfoMap.cs +++ b/ScoreSaber/Core/Data/Wrappers/LeaderboardInfoMap.cs @@ -1,4 +1,5 @@ using ScoreSaber.Core.Data.Models; +using ScoreSaber.Core.Utils; namespace ScoreSaber.Core.Data.Wrappers { internal class LeaderboardInfoMap { @@ -6,12 +7,14 @@ internal class LeaderboardInfoMap { internal BeatmapLevel beatmapLevel { get; set; } internal BeatmapKey beatmapKey { get; set; } internal string songHash { get; set; } + internal bool isOst { get; set; } internal LeaderboardInfoMap(LeaderboardInfo leaderboardInfo, BeatmapLevel beatmapLevel, BeatmapKey beatmapKey) { this.beatmapLevel = beatmapLevel; this.beatmapKey = beatmapKey; this.leaderboardInfo = leaderboardInfo; - this.songHash = beatmapKey.levelId.Split('_')[2]; + this.songHash = BeatmapUtils.GetHashFromLevelID(beatmapKey, out bool isOst); + this.isOst = isOst; } } } diff --git a/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs b/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs index b078f87..e11fe96 100644 --- a/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs +++ b/ScoreSaber/Core/Data/Wrappers/LeaderboardMap.cs @@ -22,5 +22,7 @@ internal LeaderboardMap(Leaderboard leaderboard, BeatmapLevel beatmapLevel, Beat } return leaderboardTableScoreData; } + + } } diff --git a/ScoreSaber/Core/Http/Configuration/ApiConfig.cs b/ScoreSaber/Core/Http/Configuration/ApiConfig.cs new file mode 100644 index 0000000..6c23ffd --- /dev/null +++ b/ScoreSaber/Core/Http/Configuration/ApiConfig.cs @@ -0,0 +1,24 @@ +#nullable enable +using ScoreSaber.Core.Http.Endpoints; + +namespace ScoreSaber.Core.Http.Configuration { + internal static class ApiConfig { + private const string Domain = "scoresaber.com"; + internal static class Protocols { + internal const string Https = "https://"; + internal const string WebSocket = "wss://"; + } + internal static class Subdomains { + internal const string CDN = "cdn"; + internal const string Realtime = "realtime"; + } + internal static class UrlBases { + internal static readonly UrlBase APIv1 = new(Protocols.Https, string.Empty, Domain + "/api/game"); + internal static readonly UrlBase APIv1Public = new(Protocols.Https, string.Empty, Domain + "/api"); + internal static readonly UrlBase Web = new(Protocols.Https, string.Empty, Domain); + internal static readonly UrlBase CDN = new(Protocols.Https, Subdomains.CDN, Domain); + internal static readonly UrlBase Realtime = new(Protocols.Https, Subdomains.Realtime, Domain); + internal static readonly UrlBase RealtimeWS = new(Protocols.WebSocket, Subdomains.Realtime, Domain); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/API/GlobalLeaderboard.cs b/ScoreSaber/Core/Http/Endpoints/API/GlobalLeaderboard.cs new file mode 100644 index 0000000..80fb053 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/API/GlobalLeaderboard.cs @@ -0,0 +1,32 @@ +#nullable enable +using ScoreSaber.Core.Http.Configuration; +using ScoreSaber.Core.Services; +using System.Net; +namespace ScoreSaber.Core.Http.Endpoints.API { + internal class GlobalLeaderboardRequest : Endpoint { + public GlobalLeaderboardRequest(GlobalLeaderboardService.GlobalPlayerScope scope, int page = 1) + : base(ApiConfig.UrlBases.APIv1) { + PathSegments.Add("players"); + switch (scope) { + case GlobalLeaderboardService.GlobalPlayerScope.Global: + QueryParams["page"] = page.ToString(); + break; + case GlobalLeaderboardService.GlobalPlayerScope.AroundPlayer: + PathSegments.Add("around-player"); + break; + case GlobalLeaderboardService.GlobalPlayerScope.Friends: + PathSegments.Add("around-friends"); + QueryParams["page"] = page.ToString(); + break; + case GlobalLeaderboardService.GlobalPlayerScope.Country: + PathSegments.Add("around-country"); + QueryParams["page"] = page.ToString(); + break; + case GlobalLeaderboardService.GlobalPlayerScope.Region: + PathSegments.Add("around-region"); + QueryParams["page"] = page.ToString(); + break; + } + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/API/Leaderboard.cs b/ScoreSaber/Core/Http/Endpoints/API/Leaderboard.cs new file mode 100644 index 0000000..66f7862 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/API/Leaderboard.cs @@ -0,0 +1,39 @@ +using ScoreSaber.Core.Http.Configuration; +using ScoreSaber.UI.Leaderboard; +using System.Net; +namespace ScoreSaber.Core.Http.Endpoints.API { + internal class LeaderboardRequest : Endpoint { + public LeaderboardRequest(string leaderboardId, + string gameMode, + string difficulty, + ScoreSaberLeaderboardViewController.ScoreSaberScoresScope scope, + int page = 1, + bool hideNA = false) : base(ApiConfig.UrlBases.APIv1) { + + PathSegments.Add("leaderboard"); + // Add scope-specific path segments + switch (scope) { + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Player: + PathSegments.Add("around-player"); + break; + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Friends: + PathSegments.Add("around-friends"); + break; + case ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Area: + PathSegments.Add($"around-{Plugin.Settings.locationFilterMode.ToLower()}"); + break; + } + PathSegments.Add(leaderboardId); + PathSegments.Add("mode"); + PathSegments.Add(gameMode); + PathSegments.Add("difficulty"); + PathSegments.Add(difficulty); + if (scope != ScoreSaberLeaderboardViewController.ScoreSaberScoresScope.Player) { + QueryParams["page"] = page.ToString(); + } + if (hideNA) { + QueryParams["hideNA"] = "1"; + } + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/API/Player.cs b/ScoreSaber/Core/Http/Endpoints/API/Player.cs new file mode 100644 index 0000000..9721a1c --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/API/Player.cs @@ -0,0 +1,40 @@ +#nullable enable +using ScoreSaber.Core.Http.Configuration; +using System.Net; +using UnityEngine; +namespace ScoreSaber.Core.Http.Endpoints.API.Player { + internal class AuthenticateRequest : Endpoint { + public AuthenticateRequest(string playerId, + string authType, + string nonce, + string friends, + string name) : base(ApiConfig.UrlBases.APIv1) { + + PathSegments.Add("auth"); + Form = new WWWForm(); + Form.AddField("at", authType); + Form.AddField("playerId", playerId); + Form.AddField("nonce", nonce); + Form.AddField("friends", friends); + Form.AddField("name", name); + } + public WWWForm Form { get; } + } + internal class ProfileRequest : Endpoint { + public ProfileRequest(string playerId, bool full = false) + : base(ApiConfig.UrlBases.APIv1Public) { + PathSegments.Add("player"); + PathSegments.Add(playerId); + PathSegments.Add(full ? "full" : "basic"); + } + } + internal class ReplayRequest : Endpoint { + public ReplayRequest(string playerId, string leaderboardId) + : base(ApiConfig.UrlBases.APIv1) { + PathSegments.Add("telemetry"); + PathSegments.Add("downloadReplay"); + QueryParams["playerId"] = playerId; + QueryParams["leaderboardId"] = leaderboardId; + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/API/UploadRequest.cs b/ScoreSaber/Core/Http/Endpoints/API/UploadRequest.cs new file mode 100644 index 0000000..d001044 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/API/UploadRequest.cs @@ -0,0 +1,15 @@ +using ScoreSaber.Core.Http.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace ScoreSaber.Core.Http.Endpoints.API { + internal class UploadRequest : Endpoint { + public UploadRequest() : base(ApiConfig.UrlBases.APIv1) { + PathSegments.Add("upload"); + } + } +} diff --git a/ScoreSaber/Core/Http/Endpoints/CDN/Flags.cs b/ScoreSaber/Core/Http/Endpoints/CDN/Flags.cs new file mode 100644 index 0000000..ecceb3f --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/CDN/Flags.cs @@ -0,0 +1,11 @@ +using ScoreSaber.Core.Http.Configuration; +using System.Net; +namespace ScoreSaber.Core.Http.Endpoints.CDN { + internal class Flags : Endpoint { + public Flags(string country) + : base(ApiConfig.UrlBases.CDN) { + PathSegments.Add("flags"); + PathSegments.Add($"{country.ToLower()}.png"); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/CDN/SongCovers.cs b/ScoreSaber/Core/Http/Endpoints/CDN/SongCovers.cs new file mode 100644 index 0000000..f115485 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/CDN/SongCovers.cs @@ -0,0 +1,16 @@ +using ScoreSaber.Core.Http.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Http.Endpoints.CDN { + internal class SongCover : Endpoint { + public SongCover(string hash) + : base(ApiConfig.UrlBases.CDN) { + PathSegments.Add("covers"); + PathSegments.Add($"{hash}.png"); + } + } +} diff --git a/ScoreSaber/Core/Http/Endpoints/Endpoint.cs b/ScoreSaber/Core/Http/Endpoints/Endpoint.cs new file mode 100644 index 0000000..6ce516e --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/Endpoint.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace ScoreSaber.Core.Http.Endpoints { + internal abstract class Endpoint { + protected readonly UrlBase UrlBase; + protected readonly List PathSegments; + protected readonly Dictionary QueryParams; + + protected Endpoint(UrlBase urlBase) { + UrlBase = urlBase; + PathSegments = new List(); + QueryParams = new Dictionary(); + } + + public string BuildUrl() { + var url = UrlBase.BuildUrl(PathSegments.ToArray()); + + if (QueryParams.Any()) { + var queryString = string.Join("&", + QueryParams.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}")); + url += $"?{queryString}"; + } + + return url; + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/Realtime/Socket.cs b/ScoreSaber/Core/Http/Endpoints/Realtime/Socket.cs new file mode 100644 index 0000000..02778a5 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/Realtime/Socket.cs @@ -0,0 +1,10 @@ +using ScoreSaber.Core.Http.Configuration; +using System.Net; +namespace ScoreSaber.Core.Http.Endpoints.Realtime { + internal class SocketPath : Endpoint { + public SocketPath() + : base(ApiConfig.UrlBases.RealtimeWS) { + PathSegments.Add("socket"); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/Realtime/User.cs b/ScoreSaber/Core/Http/Endpoints/Realtime/User.cs new file mode 100644 index 0000000..b33a64d --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/Realtime/User.cs @@ -0,0 +1,11 @@ +using ScoreSaber.Core.Http.Configuration; +using System.Net; +namespace ScoreSaber.Core.Http.Endpoints.Realtime { + internal class UserRequest : Endpoint { + public UserRequest(string playerId) + : base(ApiConfig.UrlBases.Realtime) { + PathSegments.Add("user"); + PathSegments.Add(playerId); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/Endpoints/UrlBase.cs b/ScoreSaber/Core/Http/Endpoints/UrlBase.cs new file mode 100644 index 0000000..1311ff3 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/UrlBase.cs @@ -0,0 +1,15 @@ + +namespace ScoreSaber.Core.Http.Endpoints { + internal record UrlBase(string Protocol, string Subdomain, string Domain) { + private string SubdomainPrefix => string.IsNullOrEmpty(Subdomain) ? "" : $"{Subdomain}."; + public string BaseUrl => $"{Protocol}{SubdomainPrefix}{Domain}"; + public string BuildUrl(params string[] segments) { + var path = string.Join("/", segments); + return $"{BaseUrl}/{path}"; + } + + public static string operator +(UrlBase urlBase, string path) { + return urlBase.BuildUrl(path); + } + } +} diff --git a/ScoreSaber/Core/Http/Endpoints/Web/WebLeaderboard.cs b/ScoreSaber/Core/Http/Endpoints/Web/WebLeaderboard.cs new file mode 100644 index 0000000..1e6d2c9 --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/Web/WebLeaderboard.cs @@ -0,0 +1,15 @@ +using ScoreSaber.Core.Http.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Http.Endpoints.Web { + internal class WebLeaderboard : Endpoint { + public WebLeaderboard(string leaderboardId) : base(ApiConfig.UrlBases.Web) { + PathSegments.Add("leaderboard"); + PathSegments.Add(leaderboardId); + } + } +} diff --git a/ScoreSaber/Core/Http/Endpoints/Web/WebUser.cs b/ScoreSaber/Core/Http/Endpoints/Web/WebUser.cs new file mode 100644 index 0000000..15a206f --- /dev/null +++ b/ScoreSaber/Core/Http/Endpoints/Web/WebUser.cs @@ -0,0 +1,15 @@ +using ScoreSaber.Core.Http.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreSaber.Core.Http.Endpoints.Web { + internal class WebUser : Endpoint { + public WebUser(string playerId) : base(ApiConfig.UrlBases.Web) { + PathSegments.Add("u"); + PathSegments.Add(playerId); + } + } +} diff --git a/ScoreSaber/Core/Http/HttpClient.cs b/ScoreSaber/Core/Http/HttpClient.cs new file mode 100644 index 0000000..6d4d8fe --- /dev/null +++ b/ScoreSaber/Core/Http/HttpClient.cs @@ -0,0 +1,56 @@ +#nullable enable +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; +namespace ScoreSaber.Core.Http { + internal class HttpClient { + private readonly HttpClientOptions options; + public HttpClient(HttpClientOptions? options = null) { + this.options = options ?? new HttpClientOptions(); + } + private async Task SendRequestAsync(UnityWebRequest request) { + foreach (var header in options.Headers) { + request.SetRequestHeader(header.Key, header.Value); + } + var operation = request.SendWebRequest(); + while (!operation.isDone) { + await Task.Delay(100); + } + if (request.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError) { + var errorMessage = request.downloadHandler.data != null + ? Encoding.UTF8.GetString(request.downloadHandler.data) + : request.error; + throw new HttpRequestException( + errorMessage, + request.responseCode, + isNetworkError: request.result == UnityWebRequest.Result.ConnectionError, + isHttpError: request.result == UnityWebRequest.Result.ProtocolError + ); + } + return operation; + } + public async Task GetAsync(string url) { + using var request = UnityWebRequest.Get(url); + request.timeout = options.DefaultTimeout; + await SendRequestAsync(request); + return Encoding.UTF8.GetString(request.downloadHandler.data); + } + public async Task PostAsync(string url, WWWForm form) { + using var request = UnityWebRequest.Post(url, form); + request.timeout = options.UploadTimeout; + await SendRequestAsync(request); + return Encoding.UTF8.GetString(request.downloadHandler.data); + } + public async Task DownloadAsync(string url) { + using var request = UnityWebRequest.Get(url); + request.timeout = options.DefaultTimeout; + await SendRequestAsync(request); + return request.downloadHandler.data; + } + public void SetHeader(string key, string value) { + options.Headers[key] = value; + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/HttpClientOptions.cs b/ScoreSaber/Core/Http/HttpClientOptions.cs new file mode 100644 index 0000000..528d1ac --- /dev/null +++ b/ScoreSaber/Core/Http/HttpClientOptions.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Reflection; +namespace ScoreSaber.Core.Http { + internal class HttpClientOptions { + public Dictionary Headers { get; } + public int DefaultTimeout { get; init; } + public int UploadTimeout { get; init; } + public HttpClientOptions( + string? applicationName = null, + Version? version = null, + int defaultTimeout = 5, + int uploadTimeout = 120) { + DefaultTimeout = defaultTimeout; + UploadTimeout = uploadTimeout; + Headers = new Dictionary(); + if ((applicationName != null && version == null) || + (applicationName == null && version != null)) { + throw new ArgumentException("You must specify either both or none of ApplicationName and Version"); + } + var userAgent = applicationName != null + ? $"{applicationName}/{version}" + : $"Default/{Assembly.GetExecutingAssembly().GetName().Version}"; + Headers.Add("User-Agent", userAgent); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/HttpRequestException.cs b/ScoreSaber/Core/Http/HttpRequestException.cs new file mode 100644 index 0000000..4d756c2 --- /dev/null +++ b/ScoreSaber/Core/Http/HttpRequestException.cs @@ -0,0 +1,51 @@ +#nullable enable +using System; +using Newtonsoft.Json; +using ScoreSaber.Core.Data.Models; + +namespace ScoreSaber.Core.Http { + internal class HttpRequestException : Exception { + public bool IsNetworkError { get; } + public bool IsHttpError { get; } + public bool IsScoreSaberError { get; } + public ScoreSaberError? ScoreSaberError { get; } + public long StatusCode { get; } + + public HttpRequestException(string message, + long statusCode, + bool isNetworkError = false, + bool isHttpError = false) + : base(message) { + + StatusCode = statusCode; + IsNetworkError = isNetworkError; + IsHttpError = isHttpError; + + if (!string.IsNullOrEmpty(message)) { + try { + ScoreSaberError = JsonConvert.DeserializeObject(message); + IsScoreSaberError = true; + } catch (Exception) { + IsScoreSaberError = false; + ScoreSaberError = null; + } + } + } + + public override string ToString() { + string errorDetails = ""; + + if (IsNetworkError) { + errorDetails += " (Network Error)"; + } else if (IsHttpError) { + errorDetails += " (HTTP Error)"; + } + + if (IsScoreSaberError && ScoreSaberError != null) { + errorDetails += $" - ScoreSaber Error: {ScoreSaberError.errorMessage}"; + } + + return $"HttpRequestException: {Message}{errorDetails} - StatusCode: {StatusCode}"; + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/Http/ScoreSaberHttpClient.cs b/ScoreSaber/Core/Http/ScoreSaberHttpClient.cs new file mode 100644 index 0000000..b81bfd5 --- /dev/null +++ b/ScoreSaber/Core/Http/ScoreSaberHttpClient.cs @@ -0,0 +1,35 @@ +#nullable enable +using System.Threading.Tasks; +using Newtonsoft.Json; +using ScoreSaber.Core.Http.Endpoints; +using UnityEngine; +namespace ScoreSaber.Core.Http { + internal class ScoreSaberHttpClient { + private readonly HttpClient client; + public ScoreSaberHttpClient(HttpClientOptions? options = null) { + client = new HttpClient(options); + } + public void SetCookie(string cookie) { + client.SetHeader("Cookie", cookie); + } + public async Task GetAsync(Endpoint endpoint) { + var response = await client.GetAsync(endpoint.BuildUrl()); + return JsonConvert.DeserializeObject(response)!; + } + public async Task PostAsync(Endpoint endpoint, WWWForm form) { + var response = await client.PostAsync(endpoint.BuildUrl(), form); + return JsonConvert.DeserializeObject(response)!; + } + public async Task DownloadAsync(Endpoint endpoint) { + return await client.DownloadAsync(endpoint.BuildUrl()); + } + + public async Task GetRawAsync(string url) { + return await client.GetAsync(url); + } + + public async Task PostRawAsync(string url, WWWForm form) { + return await client.PostAsync(url, form); + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/MainInstaller.cs b/ScoreSaber/Core/MainInstaller.cs index a494c2d..edaed69 100644 --- a/ScoreSaber/Core/MainInstaller.cs +++ b/ScoreSaber/Core/MainInstaller.cs @@ -1,4 +1,6 @@ -using ScoreSaber.Core.Daemons; +using ScoreSaber.Core.AffinityPatches; +using ScoreSaber.Core.Daemons; +using ScoreSaber.Core.Data.Models; using ScoreSaber.Core.ReplaySystem; using ScoreSaber.Core.ReplaySystem.UI; using ScoreSaber.Core.Services; @@ -11,9 +13,10 @@ using ScoreSaber.UI.Main.Settings.ViewControllers; using ScoreSaber.UI.Main.ViewControllers; using ScoreSaber.UI.Multiplayer; +using ScoreSaber.UI.PromoBanner; +using ScoreSaber.UI.ViewControllers; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Zenject; namespace ScoreSaber.Core { @@ -21,17 +24,28 @@ internal class MainInstaller : Installer { public override void InstallBindings() { Container.BindInstance(new object()).WithId("ScoreSaberUIBindings").AsCached(); + + Container.BindInterfacesAndSelfTo().AsSingle(); + + Container.BindInterfacesAndSelfTo().AsSingle(); + + Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); + Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); + + Container.BindInterfacesTo().AsSingle(); + + Container.Bind().AsSingle(); + Container.Bind().AsSingle().NonLazy(); Container.BindInterfacesTo().AsSingle(); Container.Bind().AsSingle(); Container.Bind().AsSingle(); - Container.Bind().AsSingle(); Container.Bind().AsSingle(); - Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); @@ -43,7 +57,7 @@ public override void InstallBindings() { Container.BindInterfacesTo().AsSingle(); Container.BindInterfacesTo().AsSingle(); - Container.BindInterfacesTo().FromNewComponentOnNewGameObject().AsSingle(); + Container.BindInterfacesAndSelfTo().FromNewComponentOnNewGameObject().AsSingle(); Container.BindInterfacesTo().FromNewComponentOnNewGameObject().AsSingle(); List Imageholder = Enumerable.Range(0, 10).Select(x => new ProfilePictureView(x)).ToList(); @@ -54,8 +68,8 @@ public override void InstallBindings() { Container.Bind().FromMethodMultiple(context => clickingViews).AsSingle().WhenInjectedInto(); clickingViews.ForEach(y => Container.QueueForInject(y)); - Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); - Container.BindInterfacesTo().AsSingle(); + Container.Bind().FromNewComponentAsViewController().AsSingle(); + Container.Bind().AsSingle().NonLazy(); #if RELEASE Container.BindInterfacesAndSelfTo().AsSingle().NonLazy(); diff --git a/ScoreSaber/Core/Phoenix/WebSocketSharpAdapter.cs b/ScoreSaber/Core/Phoenix/WebSocketSharpAdapter.cs new file mode 100644 index 0000000..7c8df52 --- /dev/null +++ b/ScoreSaber/Core/Phoenix/WebSocketSharpAdapter.cs @@ -0,0 +1,79 @@ +using Phoenix; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WebSocketSharp; + +namespace ScoreSaber.Core.Phoenix { + public sealed class WebsocketSharpAdapter : IWebsocket { + private readonly WebsocketConfiguration _config; + + private readonly WebSocket _ws; + + public WebsocketSharpAdapter(WebSocket ws, WebsocketConfiguration config) { + _ws = ws; + _config = config; + + ws.OnOpen += OnWebsocketOpen; + ws.OnClose += OnWebsocketClose; + ws.OnError += OnWebsocketError; + ws.OnMessage += OnWebsocketMessage; + } + + public WebsocketState State { + get { + switch (_ws.ReadyState) { + case WebSocketState.Connecting: + return WebsocketState.Connecting; + case WebSocketState.Open: + return WebsocketState.Open; + case WebSocketState.Closing: + return WebsocketState.Closing; + case WebSocketState.Closed: + return WebsocketState.Closed; + default: + throw new NotImplementedException(); + + } + } + } + + public void Connect() { + _ws.Connect(); + } + + public void Send(string message) { + _ws.Send(message); + } + + public void Close(ushort? code = null, string message = null) { + _ws.Close(); + } + + private void OnWebsocketOpen(object sender, EventArgs args) { + _config.onOpenCallback(this); + } + + private void OnWebsocketClose(object sender, CloseEventArgs args) { + _config.onCloseCallback(this, args.Code, args.Reason); + } + + private void OnWebsocketError(object sender, ErrorEventArgs args) { + _config.onErrorCallback(this, args.Message); + } + + private void OnWebsocketMessage(object sender, MessageEventArgs args) { + _config.onMessageCallback(this, args.Data); + } + } + + public sealed class WebsocketSharpFactory : IWebsocketFactory { + public IWebsocket Build(WebsocketConfiguration config) { + var socket = new WebSocket(config.uri.AbsoluteUri); + socket.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12; + return new WebsocketSharpAdapter(socket, config); + } + } +} diff --git a/ScoreSaber/Core/ReplaySystem/Accessors.cs b/ScoreSaber/Core/ReplaySystem/Accessors.cs deleted file mode 100644 index 65724c6..0000000 --- a/ScoreSaber/Core/ReplaySystem/Accessors.cs +++ /dev/null @@ -1,77 +0,0 @@ -using HMUI; -using IPA.Utilities; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Playables; -using UnityEngine.UI; - -namespace ScoreSaber.Core.ReplaySystem -{ -#pragma warning disable IDE1006 // Naming Styles - internal static class Accessors - { - internal static readonly FieldAccessor.Accessor Combo = FieldAccessor.GetAccessor("_combo"); - internal static readonly FieldAccessor.Accessor MaxCombo = FieldAccessor.GetAccessor("_maxCombo"); - internal static readonly FieldAccessor.Accessor TriggerID = FieldAccessor.GetAccessor("_comboLostId"); - internal static readonly FieldAccessor.Accessor ComboWasLost = FieldAccessor.GetAccessor("_fullComboLost"); - internal static readonly FieldAccessor.Accessor ComboAnimator = FieldAccessor.GetAccessor("_animator"); - - internal static readonly FieldAccessor.Accessor EnergyBar = FieldAccessor.GetAccessor("_energyBar"); - internal static readonly PropertyAccessor.Setter ActiveEnergy = PropertyAccessor.GetSetter("energy"); - internal static readonly FieldAccessor.Accessor DidReachZero = FieldAccessor.GetAccessor("_didReach0Energy"); - internal static readonly PropertyAccessor.Setter NoFailPropertyUpdater = PropertyAccessor.GetSetter("noFail"); - internal static readonly FieldAccessor.Accessor NextEnergyChange = FieldAccessor.GetAccessor("_nextFrameEnergyChange"); - internal static readonly FieldAccessor.Accessor Director = FieldAccessor.GetAccessor("_playableDirector"); - - internal static readonly FieldAccessor.Accessor Multiplier = FieldAccessor.GetAccessor("_multiplier"); - internal static readonly FieldAccessor.Accessor Progress = FieldAccessor.GetAccessor("_multiplierIncreaseProgress"); - internal static readonly FieldAccessor.Accessor MaxProgress = FieldAccessor.GetAccessor("_multiplierIncreaseMaxProgress"); - internal static readonly FieldAccessor.Accessor MultiplierCounter = FieldAccessor.GetAccessor("_scoreMultiplierCounter"); - - internal static readonly FieldAccessor.Accessor AfterCutRating = FieldAccessor.GetAccessor("_afterCutRating"); - internal static readonly FieldAccessor.Accessor BeforeCutRating = FieldAccessor.GetAccessor("_beforeCutRating"); - internal static readonly FieldAccessor>.Accessor BombNotePool = FieldAccessor>.GetAccessor("_bombNotePoolContainer"); - internal static readonly FieldAccessor>.Accessor BurstSliderHeadNotePool = FieldAccessor>.GetAccessor("_burstSliderHeadGameNotePoolContainer"); - internal static readonly FieldAccessor>.Accessor BurstSliderNotePool = FieldAccessor>.GetAccessor("_burstSliderGameNotePoolContainer"); - internal static readonly FieldAccessor>.Accessor GameNotePool = FieldAccessor>.GetAccessor("_basicGameNotePoolContainer"); - internal static readonly FieldAccessor>.Accessor ChangeReceivers = FieldAccessor>.GetAccessor("_didChangeReceivers"); - - internal static readonly FieldAccessor.Accessor MultipliedScore = FieldAccessor.GetAccessor("_multipliedScore"); - internal static readonly FieldAccessor.Accessor ImmediateMultipliedPossible = FieldAccessor.GetAccessor("_immediateMaxPossibleMultipliedScore"); - internal static readonly FieldAccessor.Accessor ModifiedScore = FieldAccessor.GetAccessor("_modifiedScore"); - internal static readonly FieldAccessor.Accessor ImmediateModifiedPossible = FieldAccessor.GetAccessor("_immediateMaxPossibleModifiedScore"); - internal static readonly FieldAccessor.Accessor GameplayMultiplier = FieldAccessor.GetAccessor("_prevMultiplierFromModifiers"); - internal static readonly FieldAccessor.Accessor ModifiersModelSO = FieldAccessor.GetAccessor("_gameplayModifiersModel"); - internal static readonly FieldAccessor>.Accessor ModifierPanelsSO = FieldAccessor>.GetAccessor("_gameplayModifierParams"); - - internal static PropertyAccessor.Setter ImmediateRank = PropertyAccessor.GetSetter("immediateRank"); - internal static PropertyAccessor.Setter RelativeScore = PropertyAccessor.GetSetter("relativeScore"); - - internal static readonly FieldAccessor.Accessor StandardLevelScenesTransitionSetupData = FieldAccessor.GetAccessor("_standardLevelScenesTransitionSetupData"); - - internal static readonly FieldAccessor>.Accessor CallbacksInTime = FieldAccessor>.GetAccessor("_callbacksInTimes"); - internal static readonly FieldAccessor.Accessor CallbackStartFilterTime = FieldAccessor.GetAccessor("_startFilterTime"); - internal static readonly FieldAccessor.Accessor InitialStartFilterTime = FieldAccessor.GetAccessor("startFilterTime"); - internal static readonly FieldAccessor>.Accessor NoteCutPool = FieldAccessor>.GetAccessor("_noteCutSoundEffectPoolContainer"); - - internal static readonly FieldAccessor.Accessor AudioManager = FieldAccessor.GetAccessor("_audioManager"); - internal static readonly FieldAccessor.Accessor AudioStartOffset = FieldAccessor.GetAccessor("_audioStartTimeOffsetSinceStart"); - internal static readonly FieldAccessor.Accessor AudioLoopIndex = FieldAccessor.GetAccessor("_playbackLoopIndex"); - internal static readonly FieldAccessor.Accessor AudioTimeScale = FieldAccessor.GetAccessor("_timeScale"); - internal static readonly FieldAccessor.Accessor AudioSongTime = FieldAccessor.GetAccessor("_songTime"); - public static readonly FieldAccessor.Accessor AudioSource = FieldAccessor.GetAccessor("_audioSource"); - - internal static readonly FieldAccessor.Accessor NoteColor = FieldAccessor.GetAccessor("_noteColor"); - internal static readonly PropertyAccessor.Setter SetArrowVisibility = PropertyAccessor.GetSetter("showArrow"); - internal static readonly PropertyAccessor.Setter SetCircleVisibility = PropertyAccessor.GetSetter("showCircle"); - internal static readonly FieldAccessor.Accessor NoteMaterialBlocks = FieldAccessor.GetAccessor("_materialPropertyBlockControllers"); - - internal static readonly FieldAccessor.Accessor resultsViewControllerLevelCompletionResults = FieldAccessor.GetAccessor("_levelCompletionResults"); - - internal static readonly FieldAccessor.Accessor animateParentCanvas = FieldAccessor.GetAccessor("_animateParentCanvas"); - internal static readonly FieldAccessor.Accessor HeadTransform = FieldAccessor.GetAccessor("_headTransform"); - - internal static readonly FieldAccessor.Accessor RatingCounter = FieldAccessor.GetAccessor("_saberSwingRatingCounter"); - internal static readonly PropertyAccessor.Setter ScoringElementFinisher = PropertyAccessor.GetSetter("isFinished"); - } -} diff --git a/ScoreSaber/Core/ReplaySystem/Data/ReplayFileReader.cs b/ScoreSaber/Core/ReplaySystem/Data/ReplayFileReader.cs index 9a28d1a..c39dd0d 100644 --- a/ScoreSaber/Core/ReplaySystem/Data/ReplayFileReader.cs +++ b/ScoreSaber/Core/ReplaySystem/Data/ReplayFileReader.cs @@ -54,7 +54,7 @@ internal ReplayFile Read(byte[] input) { multiplierKeyframes = ReadMultiplierEventList(ref pointers.multiplierKeyframes), energyKeyframes = ReadEnergyEventList(ref pointers.energyKeyframes) }; - } else if (metadata.Version == "3.0.0") { + } else if (metadata.Version == "3.0.0" || metadata.Version == "3.0.1") { return new ReplayFile() { metadata = metadata, poseKeyframes = ReadPoseGroupList(ref pointers.poseKeyframes), diff --git a/ScoreSaber/Core/ReplaySystem/HarmonyPatches/ImmediateRankReinitializer.cs b/ScoreSaber/Core/ReplaySystem/HarmonyPatches/ImmediateRankReinitializer.cs index 2a4d051..dca17d9 100644 --- a/ScoreSaber/Core/ReplaySystem/HarmonyPatches/ImmediateRankReinitializer.cs +++ b/ScoreSaber/Core/ReplaySystem/HarmonyPatches/ImmediateRankReinitializer.cs @@ -10,8 +10,8 @@ internal static bool Prefix(RelativeScoreAndImmediateRankCounter __instance, int if (Plugin.ReplayState.IsPlaybackEnabled && !Plugin.ReplayState.IsLegacyReplay) { if (score == 0 && maxPossibleScore == 0) { - Accessors.RelativeScore(ref __instance, 1f); - Accessors.ImmediateRank(ref __instance, RankModel.Rank.SS); + __instance.relativeScore = 1f; + __instance.immediateRank = RankModel.Rank.SS; ___relativeScoreOrImmediateRankDidChangeEvent.Invoke(); return false; } diff --git a/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs b/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs index 847136a..438abf0 100644 --- a/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs +++ b/ScoreSaber/Core/ReplaySystem/Installers/ImberInstaller.cs @@ -1,4 +1,6 @@ using ScoreSaber.Core.ReplaySystem.UI; +using ScoreSaber.Core.Services; +using ScoreSaber.Core.Utils; using Zenject; namespace ScoreSaber.Core.ReplaySystem.Installers @@ -9,6 +11,8 @@ public override void InstallBindings() { if (Plugin.ReplayState.IsPlaybackEnabled && !Plugin.ReplayState.IsLegacyReplay) { Container.Bind().AsSingle(); + Container.Bind().AsSingle(); + Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); Container.BindInterfacesTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().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/Legacy/LegacyReplayPlayer.cs b/ScoreSaber/Core/ReplaySystem/Legacy/LegacyReplayPlayer.cs index d8c0186..9b205a3 100644 --- a/ScoreSaber/Core/ReplaySystem/Legacy/LegacyReplayPlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Legacy/LegacyReplayPlayer.cs @@ -139,7 +139,7 @@ public void Tick() { var pos = Vector3.Lerp(keyframe1._pos3, keyframe2._pos3, t); Quaternion rot = Quaternion.Lerp(keyframe1._rot3, keyframe2._rot3, t); - Accessors.HeadTransform(ref _playerTransforms).SetPositionAndRotation(pos, rot); + _playerTransforms._headTransform.SetPositionAndRotation(pos, rot); var eulerAngles = rot.eulerAngles; Vector3 headRotationOffset = new Vector3(Plugin.Settings.replayCameraXRotation, Plugin.Settings.replayCameraYRotation, Plugin.Settings.replayCameraZRotation); eulerAngles += headRotationOffset; @@ -170,16 +170,16 @@ private void UpdatePlaybackScore(Z.Keyframe keyframe) { if (_playbackPreviousCombo != keyframe.combo) { comboChanged = true; - Accessors.Combo(ref _comboController) = keyframe.combo; + _comboController._combo = keyframe.combo; } if (_playbackPreviousScore != keyframe.score) { int maxPossibleRawScore = LeaderboardUtils.OldMaxRawScoreForNumberOfNotes(cutOrMissedNotes); - _relativeScoreAndImmediateRankCounter?.InvokeMethod("UpdateRelativeScoreAndImmediateRank", keyframe.score, keyframe.score, maxPossibleRawScore, maxPossibleRawScore); + _relativeScoreAndImmediateRankCounter.UpdateRelativeScoreAndImmediateRank(keyframe.score, keyframe.score, maxPossibleRawScore, maxPossibleRawScore); - _scoreUIController?.InvokeMethod("UpdateScore", keyframe.score, keyframe.score); + _scoreUIController.UpdateScore(keyframe.score, keyframe.score); } @@ -190,7 +190,7 @@ private void UpdatePlaybackScore(Z.Keyframe keyframe) { if (comboChanged) { FieldAccessor>.Get(_scoreController, "scoreDidChangeEvent").Invoke(keyframe.score, - ScoreModel.GetModifiedScoreForGameplayModifiersScoreMultiplier(keyframe.score, Accessors.GameplayMultiplier(ref _scoreController))); + ScoreModel.GetModifiedScoreForGameplayModifiersScoreMultiplier(keyframe.score, _scoreController._prevMultiplierFromModifiers)); } if (multiplierChanged) { @@ -204,12 +204,12 @@ private void PlaybackMultiplierCheck(Z.Keyframe keyframe, bool comboChanged, ref if (keyframe.combo > _playbackPreviousCombo) { if (_multiplier < 8) { - var counter = Accessors.MultiplierCounter(ref _scoreController); + var counter = _scoreController._scoreMultiplierCounter; if (_multiplierIncreaseProgress < _multiplierIncreaseMaxProgress) { _multiplierIncreaseProgress++; - Accessors.Progress(ref counter) = _multiplierIncreaseProgress; + counter._multiplierIncreaseProgress = _multiplierIncreaseProgress; multiplierChanged = true; } if (_multiplierIncreaseProgress >= _multiplierIncreaseMaxProgress) { @@ -217,10 +217,9 @@ private void PlaybackMultiplierCheck(Z.Keyframe keyframe, bool comboChanged, ref _multiplierIncreaseProgress = 0; _multiplierIncreaseMaxProgress = _multiplier * 2; - Accessors.Multiplier(ref counter) = _multiplier; - Accessors.Progress(ref counter) = _multiplierIncreaseProgress; - Accessors.MaxProgress(ref counter) = _multiplierIncreaseMaxProgress; - + counter._multiplier = _multiplier; + counter._multiplierIncreaseProgress = _multiplierIncreaseProgress; + counter._multiplierIncreaseMaxProgress = _multiplierIncreaseMaxProgress; multiplierChanged = true; } } @@ -235,7 +234,7 @@ private void PlaybackMultiplierCheck(Z.Keyframe keyframe, bool comboChanged, ref multiplierChanged = true; } - var counter = Accessors.MultiplierCounter(ref _scoreController); + var counter = _scoreController._scoreMultiplierCounter; counter.ProcessMultiplierEvent(ScoreMultiplierCounter.MultiplierEventType.Negative); FieldAccessor>.Get(_scoreController, "multiplierDidChangeEvent").Invoke(_multiplier, _multiplierIncreaseProgress); } diff --git a/ScoreSaber/Core/ReplaySystem/Playback/ComboPlayer.cs b/ScoreSaber/Core/ReplaySystem/Playback/ComboPlayer.cs index 702e75f..6295c7e 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/ComboPlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/ComboPlayer.cs @@ -37,17 +37,17 @@ private void UpdateCombo(float time, int combo) { var previousComboEvents = _sortedNoteEvents.Where(ne => ne.EventType != NoteEventType.None && time > ne.Time); int cutOrMissRecorded = previousComboEvents.Count(ne => ne.EventType == NoteEventType.BadCut || ne.EventType == NoteEventType.GoodCut || ne.EventType == NoteEventType.Miss); - Accessors.Combo(ref _comboController) = combo; - Accessors.MaxCombo(ref _comboController) = cutOrMissRecorded; + _comboController._combo = combo; + _comboController._maxCombo = cutOrMissRecorded; FieldAccessor>.Get(ref _comboController, "comboDidChangeEvent").Invoke(combo); bool didLoseCombo = _sortedComboEvents.Any(sce => time > sce.Time && sce.Combo == 0); if ((combo == 0 && cutOrMissRecorded == 0) || !didLoseCombo) { - Accessors.ComboAnimator(ref _comboUIController).Rebind(); - Accessors.ComboWasLost(ref _comboUIController) = false; + _comboUIController._animator.Rebind(); + _comboUIController._fullComboLost = false; } else { - Accessors.ComboAnimator(ref _comboUIController).SetTrigger(Accessors.TriggerID(ref _comboUIController)); - Accessors.ComboWasLost(ref _comboUIController) = true; + _comboUIController._animator.SetTrigger(_comboUIController._comboLostId); + _comboUIController._fullComboLost = true; } } } diff --git a/ScoreSaber/Core/ReplaySystem/Playback/EnergyPlayer.cs b/ScoreSaber/Core/ReplaySystem/Playback/EnergyPlayer.cs index 0016d3c..ae3a111 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/EnergyPlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/EnergyPlayer.cs @@ -21,43 +21,44 @@ public EnergyPlayer(ReplayFile file, GameEnergyCounter gameEnergyCounter, DiCont } public void TimeUpdate(float newTime) { + if (_sortedEnergyEvents.Length == 0) { + UpdateEnergy(1.0f); + return; + } for (int c = 0; c < _sortedEnergyEvents.Length; c++) { - // TODO: this has potential to have problems if _sortedEnergyEvents[c].Time is within an epsilon of newTime, potentially applying energy changes twice or not at all if (_sortedEnergyEvents[c].Time > newTime) { - float energy = c != 0 ? _sortedEnergyEvents[c - 1].Energy : 0.5f; + float energy = c != 0 ? _sortedEnergyEvents[c - 1].Energy : _sortedEnergyEvents[0].Energy; UpdateEnergy(energy); return; } } - UpdateEnergy(0.5f); - var lastEvent = _sortedEnergyEvents.LastOrDefault(); - if (newTime >= lastEvent.Time && lastEvent.Energy <= Mathf.Epsilon) { - UpdateEnergy(0f); - } + + UpdateEnergy(_sortedEnergyEvents.Last().Energy); } + private void UpdateEnergy(float energy) { bool isFailingEnergy = energy <= Mathf.Epsilon; bool noFail = _gameEnergyCounter.noFail; - Accessors.NoFailPropertyUpdater(ref _gameEnergyCounter, false); - Accessors.DidReachZero(ref _gameEnergyCounter) = isFailingEnergy; + _gameEnergyCounter.noFail = false; + _gameEnergyCounter._didReach0Energy = isFailingEnergy; _gameEnergyCounter.ProcessEnergyChange(energy); - Accessors.NextEnergyChange(ref _gameEnergyCounter) = 0; - Accessors.ActiveEnergy(ref _gameEnergyCounter, energy); - Accessors.NoFailPropertyUpdater(ref _gameEnergyCounter, noFail); + _gameEnergyCounter._nextFrameEnergyChange = 0; + _gameEnergyCounter.energy = energy; + _gameEnergyCounter.noFail = noFail; if (_gameEnergyUIPanel != null) { _gameEnergyUIPanel.Init(); - var director = Accessors.Director(ref _gameEnergyUIPanel); + var director = _gameEnergyUIPanel._playableDirector; director.Stop(); + director.RebindPlayableGraphOutputs(); director.Evaluate(); - Accessors.EnergyBar(ref _gameEnergyUIPanel).enabled = !isFailingEnergy; + _gameEnergyUIPanel._energyBar.enabled = !isFailingEnergy; } - - FieldAccessor>.Get(_gameEnergyCounter, "gameEnergyDidChangeEvent").Invoke(energy); + _gameEnergyUIPanel.RefreshEnergyUI(energy); } } } \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/Playback/MultiplierPlayer.cs b/ScoreSaber/Core/ReplaySystem/Playback/MultiplierPlayer.cs index 3a4caa6..accf8ba 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/MultiplierPlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/MultiplierPlayer.cs @@ -35,10 +35,10 @@ public void TimeUpdate(float newTime) { private void UpdateMultiplier(int multiplier, float progress) { - var counter = Accessors.MultiplierCounter(ref _scoreController); - Accessors.Multiplier(ref counter) = multiplier; - Accessors.MaxProgress(ref counter) = multiplier * 2; - Accessors.Progress(ref counter) = (int)(progress * (multiplier * 2)); + var counter = _scoreController._scoreMultiplierCounter; + counter._multiplier = multiplier; + counter._multiplierIncreaseMaxProgress = multiplier * 2; + counter._multiplierIncreaseProgress = (int)(progress * (multiplier * 2)); FieldAccessor>.Get(_scoreController, "multiplierDidChangeEvent").Invoke(multiplier, progress); } } diff --git a/ScoreSaber/Core/ReplaySystem/Playback/NotePlayer.cs b/ScoreSaber/Core/ReplaySystem/Playback/NotePlayer.cs index 4f5e3df..b08b2da 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/NotePlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/NotePlayer.cs @@ -6,117 +6,169 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Unity.Mathematics; using UnityEngine; using Zenject; using static NoteData; namespace ScoreSaber.Core.ReplaySystem.Playback { - internal class NotePlayer : TimeSynchronizer, ITickable, IScroller, IAffinity - { + internal class NotePlayer : TimeSynchronizer, ITickable, IScroller, IAffinity { private int _nextIndex = 0; private readonly SiraLog _siraLog; private readonly SaberManager _saberManager; private readonly NoteEvent[] _sortedNoteEvents; + private readonly NoteID[] _sortedNoteIDs; + + private readonly NoteEvent[] _sortedNotMissedNoteEvents; + private readonly NoteID[] _sortedMissedNoteIDs; + private readonly MemoryPoolContainer _gameNotePool; private readonly MemoryPoolContainer _burstSliderHeadNotePool; private readonly MemoryPoolContainer _burstSliderNotePool; private readonly MemoryPoolContainer _bombNotePool; private readonly Dictionary _recognizedNoteCutInfos = new Dictionary(); + private readonly Dictionary _noteCutInfoCache = new Dictionary(); + + private readonly Dictionary>> versionConversion = new Dictionary>>() { + { "3.0.0", new Dictionary>() { // 3.0.0 -> latest + { 2, new List { 2, 6 } }, // Scoring type 2 can be represented as 2 or 6 + { 3, new List { 3, 6 } }, // Scoring type 3 can be represented as 3 or 6 + { 6, new List { 6 } } // Scoring type 6 can only be represented as 6 + } } + }; public NotePlayer(SiraLog siraLog, ReplayFile file, SaberManager saberManager, BasicBeatmapObjectManager basicBeatmapObjectManager) { _siraLog = siraLog; _saberManager = saberManager; - _gameNotePool = Accessors.GameNotePool(ref basicBeatmapObjectManager); - _burstSliderHeadNotePool = Accessors.BurstSliderHeadNotePool(ref basicBeatmapObjectManager); - _burstSliderNotePool = Accessors.BurstSliderNotePool(ref basicBeatmapObjectManager); - _bombNotePool = Accessors.BombNotePool(ref basicBeatmapObjectManager); + _gameNotePool = basicBeatmapObjectManager._basicGameNotePoolContainer; + _burstSliderHeadNotePool = basicBeatmapObjectManager._burstSliderHeadGameNotePoolContainer; + _burstSliderNotePool = basicBeatmapObjectManager._burstSliderGameNotePoolContainer; + _bombNotePool = basicBeatmapObjectManager._bombNotePoolContainer; _sortedNoteEvents = file.noteKeyframes.OrderBy(nk => nk.Time).ToArray(); + _sortedNoteIDs = _sortedNoteEvents.Select(ne => ne.NoteID).ToArray(); + + _sortedNotMissedNoteEvents = _sortedNoteEvents.Where(nk => nk.EventType != NoteEventType.Miss).ToArray(); + _sortedMissedNoteIDs = _sortedNotMissedNoteEvents.Select(ne => ne.NoteID).ToArray(); } public void Tick() { - while (_nextIndex < _sortedNoteEvents.Length && audioTimeSyncController.songTime >= _sortedNoteEvents[_nextIndex].Time) { - NoteEvent activeEvent = _sortedNoteEvents[_nextIndex++]; ProcessEvent(activeEvent); } } private void ProcessEvent(NoteEvent activeEvent) { + // dont process events that are too far away from the current audio time + if (Mathf.Abs(activeEvent.Time - audioTimeSyncController.songTime) > 0.2f) { + return; + } - if (activeEvent.EventType == NoteEventType.GoodCut || activeEvent.EventType == NoteEventType.BadCut) { - foreach (var noteController in _gameNotePool.activeItems) { - if (HandleEvent(activeEvent, noteController)) { - return; - } - } - foreach (var noteController in _burstSliderHeadNotePool.activeItems) { - if (HandleEvent(activeEvent, noteController)) { - return; - } - } - foreach (var noteController in _burstSliderNotePool.activeItems) { - if (HandleEvent(activeEvent, noteController)) { - return; - } + switch (activeEvent.EventType) { + case NoteEventType.BadCut: + case NoteEventType.GoodCut: + ProcessRelevantNotes(activeEvent); + break; + case NoteEventType.Bomb: + ProcessRelevantBombNotes(activeEvent); + break; + default: + break; + } + } + + private void ProcessRelevantNotes(NoteEvent activeEvent) { + var activeNoteControllers = _gameNotePool.activeItems.Cast() + .Concat(_burstSliderHeadNotePool.activeItems.Cast()) + .Concat(_burstSliderNotePool.activeItems.Cast()); + + foreach (var noteController in activeNoteControllers) { + if (DoesNoteMatchID(activeEvent.NoteID, noteController.noteData)) { + HandleEvent(activeEvent, noteController); } - } else if (activeEvent.EventType == NoteEventType.Bomb) { - foreach (var bombController in _bombNotePool.activeItems) { - if (HandleEvent(activeEvent, bombController)) { - return; - } + } + } + + + private void ProcessRelevantBombNotes(NoteEvent activeEvent) { + foreach (var bombController in _bombNotePool.activeItems) { + if (DoesNoteMatchID(activeEvent.NoteID, bombController.noteData)) { + HandleEvent(activeEvent, bombController); } } } private bool HandleEvent(NoteEvent activeEvent, NoteController noteController) { + if (!_noteCutInfoCache.TryGetValue(activeEvent.NoteID, out NoteCutInfo noteCutInfo)) { - if (DoesNoteMatchID(activeEvent.NoteID, noteController.noteData)) { - Saber correctSaber = noteController.noteData.colorType == ColorType.ColorA ? _saberManager.leftSaber : _saberManager.rightSaber; - var noteTransform = noteController.noteTransform; - - NoteCutInfo noteCutInfo = new NoteCutInfo(noteController.noteData, - activeEvent.SaberSpeed > 2f, - activeEvent.DirectionOK, - activeEvent.SaberType == (int)correctSaber.saberType, - false, - activeEvent.SaberSpeed, - activeEvent.SaberDirection.Convert(), - noteController.noteData.colorType == ColorType.ColorA ? SaberType.SaberA : SaberType.SaberB, - noteController.noteData.time - activeEvent.Time, - activeEvent.CutDirectionDeviation, - activeEvent.CutPoint.Convert(), - activeEvent.CutNormal.Convert(), - activeEvent.CutDistanceToCenter, - activeEvent.CutAngle, - - noteController.worldRotation, - noteController.inverseWorldRotation, - noteTransform.rotation, - noteTransform.position, - - correctSaber.movementDataForLogic - ); + noteCutInfo = GetNoteCutInfoFromNoteController(noteController, activeEvent); + _noteCutInfoCache[activeEvent.NoteID] = noteCutInfo; + } + + if(!_recognizedNoteCutInfos.ContainsKey(noteCutInfo)) { _recognizedNoteCutInfos.Add(noteCutInfo, activeEvent); - noteController.InvokeMethod("SendNoteWasCutEvent", noteCutInfo); - return true; } - return false; + + noteController.SendNoteWasCutEvent(noteCutInfo); + return true; } - bool DoesNoteMatchID(NoteID id, NoteData noteData) { + NoteCutInfo GetNoteCutInfoFromNoteController(NoteController noteController, NoteEvent activeEvent) { + + Saber correctSaber = noteController.noteData.colorType == ColorType.ColorA ? _saberManager.leftSaber : _saberManager.rightSaber; + var noteTransform = noteController.noteTransform; + + var noteCutInfo = new NoteCutInfo(noteController.noteData, + activeEvent.SaberSpeed > 2f, + activeEvent.DirectionOK, + activeEvent.SaberType == (int)correctSaber.saberType, + false, + activeEvent.SaberSpeed, + activeEvent.SaberDirection.Convert(), + noteController.noteData.colorType == ColorType.ColorA ? SaberType.SaberA : SaberType.SaberB, + noteController.noteData.time - activeEvent.Time, + activeEvent.CutDirectionDeviation, + activeEvent.CutPoint.Convert(), + activeEvent.CutNormal.Convert(), + activeEvent.CutDistanceToCenter, + activeEvent.CutAngle, + + noteController.worldRotation, + noteController.inverseWorldRotation, + noteTransform.rotation, + noteTransform.position, + + correctSaber.movementDataForLogic + ); + + return noteCutInfo; + + } + bool DoesNoteMatchID(NoteID id, NoteData noteData) { if (!Mathf.Approximately(id.Time, noteData.time) || id.LineIndex != noteData.lineIndex || id.LineLayer != (int)noteData.noteLineLayer || id.ColorType != (int)noteData.colorType || id.CutDirection != (int)noteData.cutDirection) return false; if (id.GameplayType is int gameplayType && gameplayType != (int)noteData.gameplayType) return false; - if (id.ScoringType is int scoringType && scoringType != (int)noteData.scoringType) - return false; + // check if we need to convert scoring type from a pre 1.40.0 replay + if (versionConversion.TryGetValue(Plugin.ReplayState.LoadedReplayFile.metadata.Version, out Dictionary> ScoringTypeConversions)) { + if (id.ScoringType is int scoringType) { + if (ScoringTypeConversions.TryGetValue(scoringType, out List allowedConversions)) { + if (!allowedConversions.Contains((int)noteData.scoringType)) { + return false; + } + } else if (scoringType != (int)noteData.scoringType) { + return false; // cant find conversion, strict matching + } + } + } + else if (id.ScoringType is int scoringType && scoringType != (int)noteData.scoringType) + return false; // strict matching like normal if (id.CutDirectionAngleOffset is float cutDirectionAngleOffset && !Mathf.Approximately(cutDirectionAngleOffset, noteData.cutDirectionAngleOffset)) return false; @@ -124,9 +176,10 @@ bool DoesNoteMatchID(NoteID id, NoteData noteData) { return true; } + [AffinityPostfix, AffinityPatch(typeof(GoodCutScoringElement), nameof(GoodCutScoringElement.Init))] protected void ForceCompleteGoodScoringElements(GoodCutScoringElement __instance, NoteCutInfo noteCutInfo, CutScoreBuffer ____cutScoreBuffer) { - + // Just in case someone else is creating their own scoring elements, we want to ensure that we're only force completing ones we know we've created if (!_recognizedNoteCutInfos.TryGetValue(noteCutInfo, out var activeEvent)) return; @@ -134,23 +187,51 @@ protected void ForceCompleteGoodScoringElements(GoodCutScoringElement __instance _recognizedNoteCutInfos.Remove(noteCutInfo); if (!__instance.isFinished) { + var ratingCounter = ____cutScoreBuffer._saberSwingRatingCounter; - var ratingCounter = Accessors.RatingCounter(ref ____cutScoreBuffer); - - // Supply the rating counter with the proper cut ratings - Accessors.AfterCutRating(ref ratingCounter) = activeEvent.AfterCutRating; - Accessors.BeforeCutRating(ref ratingCounter) = activeEvent.BeforeCutRating; + ratingCounter._afterCutRating = activeEvent.AfterCutRating; + ratingCounter._beforeCutRating = activeEvent.BeforeCutRating; - // Then immediately finish it ____cutScoreBuffer.HandleSaberSwingRatingCounterDidFinish(ratingCounter); ScoringElement element = __instance; - Accessors.ScoringElementFinisher(ref element, true); + element.isFinished = true; } } - public void TimeUpdate(float newTime) { + [AffinityPrefix, AffinityPatch(typeof(GameNoteController), nameof(GameNoteController.NoteDidPassMissedMarker))] + protected bool HandleGhostMissesIfNeeded(GameNoteController __instance) { + // if a note is missed, check if its actually meant to be missed + // only check the notes that we know arent missed, as theres no need to check missed notes + // log(n) binary search + int left = 0; + int right = _sortedNotMissedNoteEvents.Length - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + NoteEvent middleEvent = _sortedNotMissedNoteEvents[mid]; + + if (DoesNoteMatchID(middleEvent.NoteID, __instance.noteData)) { + if (middleEvent.EventType == NoteEventType.Miss) { + return true; + } + _siraLog.Warn("CATCHING MISSED NOTE"); + NoteCutInfo noteCutInfo = GetNoteCutInfoFromNoteController(__instance, middleEvent); + __instance.SendNoteWasCutEvent(noteCutInfo); + return false; + } else if (middleEvent.NoteID.Time < __instance.noteData.time) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return true; + } + + public void TimeUpdate(float newTime) { + DespawnAllObjectsPassedMissedPoint(); for (int c = 0; c < _sortedNoteEvents.Length; c++) { if (_sortedNoteEvents[c].Time > newTime) { _nextIndex = c; @@ -159,5 +240,41 @@ public void TimeUpdate(float newTime) { } _nextIndex = _sortedNoteEvents.Length; } + + // reduces need for note catching by despawning notes that are already past the missed point + // note catching can stay as a fallback + public void DespawnAllObjectsPassedMissedPoint() { + // Process game notes + foreach (var noteController in _gameNotePool.activeItems) { + if (noteController.noteData != null && + noteController.noteData.time - 0.015f < audioTimeSyncController.songTime) { + _gameNotePool.Despawn(noteController); + } + } + + // Process burst slider head notes + foreach (var noteController in _burstSliderHeadNotePool.activeItems) { + if (noteController.noteData != null && + noteController.noteData.time - 0.015f < audioTimeSyncController.songTime) { + _burstSliderHeadNotePool.Despawn(noteController); + } + } + + // Process burst slider notes + foreach (var noteController in _burstSliderNotePool.activeItems) { + if (noteController.noteData != null && + noteController.noteData.time - 0.015f < audioTimeSyncController.songTime) { + _burstSliderNotePool.Despawn(noteController); + } + } + + // Process bomb notes + foreach (var bombController in _bombNotePool.activeItems) { + if (bombController.noteData != null && + bombController.noteData.time - 0.015f < audioTimeSyncController.songTime) { + _bombNotePool.Despawn(bombController); + } + } + } } } \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/Playback/PosePlayer.cs b/ScoreSaber/Core/ReplaySystem/Playback/PosePlayer.cs index b5100d9..ccba955 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/PosePlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/PosePlayer.cs @@ -19,6 +19,7 @@ internal class PosePlayer : TimeSynchronizer, IInitializable, ITickable, IScroll private readonly IFPFCSettings _fpfcSettings; private readonly SettingsManager _settingsManager; private readonly IReturnToMenuController _returnToMenuController; + private readonly PauseController _pauseController; public event Action DidUpdatePose; private PlayerTransforms _playerTransforms; private Camera _spectatorCamera; @@ -28,7 +29,7 @@ internal class PosePlayer : TimeSynchronizer, IInitializable, ITickable, IScroll private bool initialFPFCState; - public PosePlayer(ReplayFile file, MainCamera mainCamera, SaberManager saberManager, IReturnToMenuController returnToMenuController, IFPFCSettings fpfcSettings, PlayerTransforms playerTransforms, SettingsManager settingsManager) { + public PosePlayer(ReplayFile file, MainCamera mainCamera, SaberManager saberManager, IReturnToMenuController returnToMenuController, IFPFCSettings fpfcSettings, PlayerTransforms playerTransforms, SettingsManager settingsManager, PauseController pauseController) { _fpfcSettings = fpfcSettings; initialFPFCState = fpfcSettings.Enabled; @@ -41,6 +42,7 @@ public PosePlayer(ReplayFile file, MainCamera mainCamera, SaberManager saberMana _spectatorOffset = new Vector3(0f, 0f, -2f); _settingsManager = settingsManager; _playerTransforms = playerTransforms; + _pauseController = pauseController; } public void Initialize() { @@ -104,29 +106,40 @@ private void SetupCameras() { } } - public void Tick() { - - if (ReachedEnd()) { - _returnToMenuController.ReturnToMenu(); - return; - } + public bool _replayReachedEnd = false; + public void Tick() { bool foundPoseThisFrame = false; - while (audioTimeSyncController.songTime >= _sortedPoses[_nextIndex].Time) { + + while (_nextIndex < _sortedPoses.Count() && audioTimeSyncController.songTime >= _sortedPoses[_nextIndex].Time) { foundPoseThisFrame = true; VRPoseGroup activePose = _sortedPoses[_nextIndex++]; - if (ReachedEnd()) - return; - - VRPoseGroup nextPose = _sortedPoses[_nextIndex]; - UpdatePoses(activePose, nextPose); + if (_nextIndex < _sortedPoses.Count()) { + VRPoseGroup nextPose = _sortedPoses[_nextIndex]; + UpdatePoses(activePose, nextPose); + } } + if (foundPoseThisFrame) { return; - } else if (_nextIndex > 0 && !ReachedEnd()) { + } else if (_nextIndex > 0 && _nextIndex < _sortedPoses.Count() && !ReachedEnd()) { VRPoseGroup previousGroup = _sortedPoses[_nextIndex - 1]; - UpdatePoses(previousGroup, _sortedPoses[_nextIndex]); + VRPoseGroup nextGroup = _sortedPoses[_nextIndex]; + UpdatePoses(previousGroup, nextGroup); + } + + if (ReachedEnd()) { + _replayReachedEnd = true; + audioTimeSyncController.Pause(); + Plugin.ReplayState.lockPause = true; + return; + } else { + if (_replayReachedEnd) { + audioTimeSyncController.Resume(); + _replayReachedEnd = false; + Plugin.ReplayState.lockPause = false; + } } } @@ -138,7 +151,7 @@ private void UpdatePoses(VRPoseGroup activePose, VRPoseGroup nextPose) { float lerpTime = (audioTimeSyncController.songTime - activePose.Time) / Mathf.Max(0.000001f, nextPose.Time - activePose.Time); //_mainCamera.transform.SetPositionAndRotation(activePose.Head.Position.Convert(), activePose.Head.Rotation.Convert()); - Accessors.HeadTransform(ref _playerTransforms).SetPositionAndRotation(activePose.Head.Position.Convert(), activePose.Head.Rotation.Convert()); + _playerTransforms._headTransform.SetPositionAndRotation(activePose.Head.Position.Convert(), activePose.Head.Rotation.Convert()); if (_saberEnabled) { @@ -176,6 +189,7 @@ private void UpdatePoses(VRPoseGroup activePose, VRPoseGroup nextPose) { DidUpdatePose?.Invoke(activePose); } + // intro skip doesnt skip to the end of the map, it gives a little time, so this is fine private bool ReachedEnd() { return _nextIndex >= _sortedPoses.Length; } diff --git a/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs b/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs index 01c0362..4283926 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/ReplayTimeSyncController.cs @@ -1,12 +1,16 @@ using IPA.Utilities; +using ScoreSaber.Core.ReplaySystem.UI; +using SiraUtil.Affinity; +using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.Remoting.Metadata.W3cXsd2001; using UnityEngine; using Zenject; namespace ScoreSaber.Core.ReplaySystem.Playback { - internal class ReplayTimeSyncController : TimeSynchronizer, ITickable - { + internal class ReplayTimeSyncController : TimeSynchronizer, ITickable { private readonly List _scrollers; private readonly AudioManagerSO _audioManagerSO; private AudioTimeSyncController.InitData _audioInitData; @@ -15,9 +19,12 @@ internal class ReplayTimeSyncController : TimeSynchronizer, ITickable private BeatmapCallbacksController.InitData _callbackInitData; private BeatmapCallbacksController _beatmapObjectCallbackController; private readonly BeatmapObjectSpawnController _beatmapObjectSpawnController; - private bool _paused; + private readonly BeatmapCallbacksUpdater _beatmapCallbacksUpdater = null; + private readonly IReadonlyBeatmapData _beatmapData = null; - public ReplayTimeSyncController(List scrollers, BasicBeatmapObjectManager basicBeatmapObjectManager, NoteCutSoundEffectManager noteCutSoundEffectManager, BeatmapObjectSpawnController beatmapObjectSpawnController, AudioTimeSyncController.InitData audioInitData, BeatmapCallbacksController.InitData initData, BeatmapCallbacksController beatmapObjectCallbackController) { + private bool _paused => audioTimeSyncController.state == AudioTimeSyncController.State.Paused; + + public ReplayTimeSyncController(List scrollers, BasicBeatmapObjectManager basicBeatmapObjectManager, NoteCutSoundEffectManager noteCutSoundEffectManager, BeatmapObjectSpawnController beatmapObjectSpawnController, AudioTimeSyncController.InitData audioInitData, BeatmapCallbacksController.InitData initData, BeatmapCallbacksController beatmapObjectCallbackController, BeatmapCallbacksUpdater beatmapCallbacksUpdater, IReadonlyBeatmapData readonlyBeatmapData) { _scrollers = scrollers; _callbackInitData = initData; _audioInitData = audioInitData; @@ -25,10 +32,15 @@ public ReplayTimeSyncController(List scrollers, BasicBeatmapObjectMan _noteCutSoundEffectManager = noteCutSoundEffectManager; _beatmapObjectSpawnController = beatmapObjectSpawnController; _beatmapObjectCallbackController = beatmapObjectCallbackController; - _audioManagerSO = Accessors.AudioManager(ref noteCutSoundEffectManager); + _beatmapCallbacksUpdater = beatmapCallbacksUpdater; + _beatmapData = readonlyBeatmapData; + _audioManagerSO = noteCutSoundEffectManager._audioManager; } public void Tick() { + if(audioTimeSyncController.songTime >= audioTimeSyncController.songEndTime) { + return; + } int index = -1; if (Input.GetKeyDown(KeyCode.Alpha1)) index = 0; @@ -55,30 +67,25 @@ public void Tick() { OverrideTime(audioTimeSyncController.songLength * (index * 0.1f)); } - if (Input.GetKeyDown(KeyCode.Minus)) { - if (audioTimeSyncController.timeScale > 0.1f) { - OverrideTimeScale(audioTimeSyncController.timeScale - 0.1f); - } - } - - if (Input.GetKeyDown(KeyCode.Equals)) { - if (audioTimeSyncController.timeScale < 2.0f) { - OverrideTimeScale(audioTimeSyncController.timeScale + 0.1f); - } - } - if (Input.GetKeyDown(KeyCode.R)) { OverrideTime(0f); } - if (Input.GetKeyDown(KeyCode.Space)) { + if (Input.GetKeyDown(KeyCode.Space) && !Plugin.ReplayState.lockPause) { if (_paused) { audioTimeSyncController.Resume(); } else { CancelAllHitSounds(); audioTimeSyncController.Pause(); } - _paused = !_paused; + } + + if (Input.GetKeyDown(KeyCode.LeftArrow)) { + OverrideTime(audioTimeSyncController.songTime - 5f); + } + + if (Input.GetKeyDown(KeyCode.RightArrow)) { + OverrideTime(audioTimeSyncController.songTime + 5f); } } @@ -87,90 +94,109 @@ private void UpdateTimes() { scroller.TimeUpdate(audioTimeSyncController.songTime); } - public void OverrideTime(float time) { + internal void OverrideTime(float time) { + if(time >= audioTimeSyncController.songEndTime) time = audioTimeSyncController.songEndTime; + if (float.IsInfinity(time) || float.IsNaN(time) || Mathf.Abs(time - audioTimeSyncController._songTime) < 0.001f) return; + time = Mathf.Clamp(time, audioTimeSyncController._startSongTime, audioTimeSyncController.songEndTime); + var previousState = audioTimeSyncController.state; - if (Mathf.Abs(time - audioTimeSyncController.songTime) <= 0.25f) - return; + _beatmapCallbacksUpdater.Pause(); + audioTimeSyncController.Pause(); var _audioTimeSyncController = audioTimeSyncController; // UMBRAMEGALUL HarmonyPatches.CutSoundEffectOverride.Buffer = true; CancelAllHitSounds(); - // Forcibly enabling all the note/obstacle components to ensure their dissolve coroutine executes (it no likey when game pausey). - // TODO: do we have to do this for arcs aswell? - foreach (var item in Accessors.GameNotePool(ref _basicBeatmapObjectManager).activeItems) { - item.Hide(false); - item.Pause(false); - item.enabled = true; - item.gameObject.SetActive(true); - item.Dissolve(0f); - } - foreach (var item in Accessors.BurstSliderHeadNotePool(ref _basicBeatmapObjectManager).activeItems) { - item.Hide(false); - item.Pause(false); - item.enabled = true; - item.gameObject.SetActive(true); - item.Dissolve(0f); - } - foreach (var item in Accessors.BurstSliderNotePool(ref _basicBeatmapObjectManager).activeItems) { + _basicBeatmapObjectManager._basicGameNotePoolContainer.activeItems.ForEach(x => _basicBeatmapObjectManager.Despawn(x)); + _basicBeatmapObjectManager._burstSliderHeadGameNotePoolContainer.activeItems.ForEach(x => _basicBeatmapObjectManager.Despawn(x)); + _basicBeatmapObjectManager._burstSliderGameNotePoolContainer.activeItems.ForEach(x => _basicBeatmapObjectManager.Despawn(x)); + _basicBeatmapObjectManager._bombNotePoolContainer.activeItems.ForEach(x => _basicBeatmapObjectManager.Despawn(x)); + _basicBeatmapObjectManager._obstaclePoolContainer.activeItems.ForEach(x => _basicBeatmapObjectManager.Despawn(x)); - item.Hide(false); - item.Pause(false); - item.enabled = true; - item.gameObject.SetActive(true); - item.Dissolve(0f); - } - foreach (var item in Accessors.BombNotePool(ref _basicBeatmapObjectManager).activeItems) { + _audioTimeSyncController._prevAudioSamplePos = -1; + audioTimeSyncController.SeekTo(time / audioTimeSyncController.timeScale); + _beatmapObjectCallbackController._prevSongTime = float.MinValue; - item.Hide(false); - item.Pause(false); - item.enabled = true; - item.gameObject.SetActive(true); - item.Dissolve(0f); - } - foreach (var item in _basicBeatmapObjectManager.activeObstacleControllers) { - item.Hide(false); - item.Pause(false); - item.enabled = true; - item.gameObject.SetActive(true); - item.Dissolve(0f); + var locatedNode = LocateBeatmapData(time); + foreach (var callback in _beatmapObjectCallbackController._callbacksInTimes) { + callback.Value.lastProcessedNode = locatedNode; } - var previousState = audioTimeSyncController.state; - - audioTimeSyncController.Pause(); - audioTimeSyncController.SeekTo(time / audioTimeSyncController.timeScale); - if (previousState == AudioTimeSyncController.State.Playing) audioTimeSyncController.Resume(); - Accessors.InitialStartFilterTime(ref _callbackInitData) = time; - Accessors.CallbackStartFilterTime(ref _beatmapObjectCallbackController) = time; + _beatmapCallbacksUpdater.LateUpdate(); + _beatmapCallbacksUpdater.Resume(); + + UpdateTimes(); + } + + private LinkedListNode _lastLocatedNode = null; - foreach (var callback in Accessors.CallbacksInTime(ref _beatmapObjectCallbackController)) { + private List>> _beatmapDataItemsCache; - if (callback.Value.lastProcessedNode != null && callback.Value.lastProcessedNode.Value.time > time) - callback.Value.lastProcessedNode = null; + + // O(n), for the first call + private void CacheBeatmapData() { + if (_beatmapDataItemsCache == null) { + _beatmapDataItemsCache = new List>>(); + var currentNode = _beatmapData.allBeatmapDataItems.First; + while (currentNode != null) { + _beatmapDataItemsCache.Add(new KeyValuePair>(currentNode.Value.time, currentNode)); + currentNode = currentNode.Next; + } } + } - Accessors.AudioSongTime(ref _audioTimeSyncController) = time; + // O(log n), for calls after the first time cache is built + private LinkedListNode LocateBeatmapData(float targetTime) { + CacheBeatmapData(); - audioTimeSyncController.Update(); - UpdateTimes(); + if (_beatmapDataItemsCache.Count == 0) { + _lastLocatedNode = null; + return null; + } + + if (targetTime < _beatmapDataItemsCache[0].Value.Value.time) { + _lastLocatedNode = null; + return null; + } + + int low = 0; + int high = _beatmapDataItemsCache.Count - 1; + + while (low <= high) { + int mid = low + (high - low) / 2; + float midTime = _beatmapDataItemsCache[mid].Value.Value.time; + if (midTime <= targetTime) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + if (high < 0) { + _lastLocatedNode = null; + return null; + } + + _lastLocatedNode = _beatmapDataItemsCache[high].Value; + return _lastLocatedNode; } + + public void OverrideTimeScale(float newScale) { CancelAllHitSounds(); var _audioTimeSyncController = audioTimeSyncController; // UMBRAMEGALUL - Accessors.AudioSource(ref _audioTimeSyncController).pitch = newScale; + _audioTimeSyncController._audioSource.pitch = newScale; - Accessors.AudioTimeScale(ref _audioTimeSyncController) = newScale; - Accessors.AudioStartOffset(ref _audioTimeSyncController) - = (Time.timeSinceLevelLoad * _audioTimeSyncController.timeScale) - (_audioTimeSyncController.songTime + _audioInitData.songTimeOffset); + _audioTimeSyncController._timeScale = newScale; + _audioTimeSyncController._audioStartTimeOffsetSinceStart = (Time.timeSinceLevelLoad * _audioTimeSyncController.timeScale) - (_audioTimeSyncController.songTime + _audioInitData.songTimeOffset); _audioManagerSO.musicPitch = 1f / newScale; _audioTimeSyncController.Update(); @@ -178,7 +204,7 @@ public void OverrideTimeScale(float newScale) { public void CancelAllHitSounds() { - var activeItems = Accessors.NoteCutPool(ref _noteCutSoundEffectManager).activeItems; + var activeItems = _noteCutSoundEffectManager._noteCutSoundEffectPoolContainer.activeItems; for (int i = 0; i < activeItems.Count; i++) { var effect = activeItems[i]; if (effect.isActiveAndEnabled) diff --git a/ScoreSaber/Core/ReplaySystem/Playback/ScorePlayer.cs b/ScoreSaber/Core/ReplaySystem/Playback/ScorePlayer.cs index 8e66b95..4cd26cb 100644 --- a/ScoreSaber/Core/ReplaySystem/Playback/ScorePlayer.cs +++ b/ScoreSaber/Core/ReplaySystem/Playback/ScorePlayer.cs @@ -59,21 +59,21 @@ public void TimeUpdate(float newTime) { private void UpdateMultiplier() { - var totalMultiplier = Accessors.ModifiersModelSO(ref _scoreController).GetTotalMultiplier(Accessors.ModifierPanelsSO(ref _scoreController), _gameEnergyCounter.energy); - Accessors.GameplayMultiplier(ref _scoreController) = totalMultiplier; + var totalMultiplier = _scoreController._gameplayModifiersModel.GetTotalMultiplier(_scoreController._gameplayModifierParams, _gameEnergyCounter.energy); + _scoreController._prevMultiplierFromModifiers = totalMultiplier; } private void UpdateScore(int newScore, int? immediateMaxPossibleScore, float time) { var immediate = immediateMaxPossibleScore ?? LeaderboardUtils.OldMaxRawScoreForNumberOfNotes(CalculatePostNoteCountForTime(time)); - var multiplier = Accessors.GameplayMultiplier(ref _scoreController); + var multiplier = _scoreController._prevMultiplierFromModifiers; var newModifiedScore = ScoreModel.GetModifiedScoreForGameplayModifiersScoreMultiplier(newScore, multiplier); - Accessors.MultipliedScore(ref _scoreController) = newScore; - Accessors.ImmediateMultipliedPossible(ref _scoreController) = immediate; - Accessors.ModifiedScore(ref _scoreController) = newModifiedScore; - Accessors.ImmediateModifiedPossible(ref _scoreController) = ScoreModel.GetModifiedScoreForGameplayModifiersScoreMultiplier(immediate, multiplier); + _scoreController._multipliedScore = newScore; + _scoreController._immediateMaxPossibleMultipliedScore = immediate; + _scoreController._modifiedScore = newModifiedScore; + _scoreController._immediateMaxPossibleModifiedScore = ScoreModel.GetModifiedScoreForGameplayModifiersScoreMultiplier(immediate, multiplier); FieldAccessor>.Get(_scoreController, "scoreDidChangeEvent").Invoke(newScore, newModifiedScore); } diff --git a/ScoreSaber/Core/ReplaySystem/Recorders/MetadataRecorder.cs b/ScoreSaber/Core/ReplaySystem/Recorders/MetadataRecorder.cs index 4fa3c77..96f6091 100644 --- a/ScoreSaber/Core/ReplaySystem/Recorders/MetadataRecorder.cs +++ b/ScoreSaber/Core/ReplaySystem/Recorders/MetadataRecorder.cs @@ -47,7 +47,7 @@ public Metadata Export() { }; return new Metadata() { - Version = "3.0.0", + Version = "3.0.1", LevelID = _gameplayCoreSceneSetupData.beatmapLevel.levelID, Difficulty = BeatmapDifficultyMethods.DefaultRating(_gameplayCoreSceneSetupData.beatmapKey.difficulty), Characteristic = _gameplayCoreSceneSetupData.beatmapKey.beatmapCharacteristic.serializedName, diff --git a/ScoreSaber/Core/ReplaySystem/Recorders/ScoreEventRecorder.cs b/ScoreSaber/Core/ReplaySystem/Recorders/ScoreEventRecorder.cs index 333ebf3..78052e8 100644 --- a/ScoreSaber/Core/ReplaySystem/Recorders/ScoreEventRecorder.cs +++ b/ScoreSaber/Core/ReplaySystem/Recorders/ScoreEventRecorder.cs @@ -43,7 +43,7 @@ private void ScoreController_scoreDidChangeEvent(int rawScore, int score) { _scoreKeyframes.Add(new ScoreEvent() { Score = rawScore, Time = audioTimeSyncController.songTime, - ImmediateMaxPossibleScore = Accessors.ImmediateMultipliedPossible(ref scoreController) + ImmediateMaxPossibleScore = scoreController._immediateMaxPossibleMultipliedScore }); } diff --git a/ScoreSaber/Core/ReplaySystem/ReplayLoader.cs b/ScoreSaber/Core/ReplaySystem/ReplayLoader.cs index e21bb1b..c053aa2 100644 --- a/ScoreSaber/Core/ReplaySystem/ReplayLoader.cs +++ b/ScoreSaber/Core/ReplaySystem/ReplayLoader.cs @@ -25,7 +25,7 @@ public ReplayLoader(PlayerDataModel playerDataModel, MenuTransitionsHelper menuT _playerDataModel = playerDataModel; _menuTransitionsHelper = menuTransitionsHelper; - _standardLevelScenesTransitionSetupDataSO = Accessors.StandardLevelScenesTransitionSetupData(ref _menuTransitionsHelper); + _standardLevelScenesTransitionSetupDataSO = _menuTransitionsHelper._standardLevelScenesTransitionSetupData; _replayFileReader = new ReplayFileReader(); _environmentsListModel = environmentsListModel; } @@ -72,7 +72,8 @@ await Task.Run(async () => { beatmapKey: beatmapKey, beatmapLevel: beatmapLevel, overrideEnvironmentSettings: playerData.overrideEnvironmentSettings, - overrideColorScheme: playerData.colorSchemesSettings.GetSelectedColorScheme(), + playerOverrideColorScheme: playerData.colorSchemesSettings.GetSelectedColorScheme(), + playerOverrideLightshowColors: playerData.colorSchemesSettings.ShouldOverrideLightshowColors(), beatmapOverrideColorScheme: beatmapLevel.GetColorScheme(beatmapKey.beatmapCharacteristic, beatmapKey.difficulty), gameplayModifiers: gameplayModifiers, playerSpecificSettings: playerSettings, @@ -147,7 +148,8 @@ await Task.Run(() => { beatmapKey: beatmapKey, beatmapLevel: beatmapLevel, overrideEnvironmentSettings: playerData.overrideEnvironmentSettings, - overrideColorScheme: playerData.colorSchemesSettings.GetSelectedColorScheme(), + playerOverrideColorScheme: playerData.colorSchemesSettings.GetSelectedColorScheme(), + playerOverrideLightshowColors: playerData.colorSchemesSettings.ShouldOverrideLightshowColors(), beatmapOverrideColorScheme: beatmapLevel.GetColorScheme(beatmapKey.beatmapCharacteristic, beatmapKey.difficulty), gameplayModifiers: LeaderboardUtils.GetModifierFromStrings(replay.metadata.Modifiers.ToArray(), false).gameplayModifiers, playerSpecificSettings: playerSettings, diff --git a/ScoreSaber/Core/ReplaySystem/ReplayState.cs b/ScoreSaber/Core/ReplaySystem/ReplayState.cs index a8217ba..9119ed4 100644 --- a/ScoreSaber/Core/ReplaySystem/ReplayState.cs +++ b/ScoreSaber/Core/ReplaySystem/ReplayState.cs @@ -10,6 +10,8 @@ internal class ReplayState internal BeatmapKey CurrentBeatmapKey; internal GameplayModifiers CurrentModifiers; internal string CurrentPlayerName; + internal bool isUsersReplay; + internal bool lockPause = false; // Legacy internal bool IsLegacyReplay; diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs new file mode 100644 index 0000000..eaeb02a --- /dev/null +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopMainImberPanelView.cs @@ -0,0 +1,368 @@ +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components; +using BeatSaberMarkupLanguage.FloatingScreen; +using BeatSaberMarkupLanguage.ViewControllers; +using HMUI; +using IPA.Utilities; +using ScoreSaber.Core.Data; +using SiraUtil.Affinity; +using SiraUtil.Tools.FPFC; +using System; +using System.Collections; +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 = @"desktop-imber-panel.bsml")] + [ViewDefinition("ScoreSaber.Core.ReplaySystem.UI.desktop-imber-panel.bsml")] + internal class DesktopMainImberPanelView : BSMLAutomaticViewController, IAffinity, IDisposable { + + public event Action HandDidSwitchEvent; + public event Action DidTimeSyncChange; + public event Action DidClickPausePlay; + public event Action DidClickRestart; + public event Action DidPositionJump; + public event Action DidClickLoop; + + 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; + [Inject] private readonly PauseMenuManager _pauseMenuManager = null; + + private EventSystem originalEventSystem; + private EventSystem imberEventSystem; + + public bool didParse { get; private set; } + + public Transform Transform { + get => this.transform; + } + + public Pose defaultPosition { get; set; } + + 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; + + [UIComponent("TimeScaleGO")] + public GameObject TimeScaleGO = null; + + [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(); + } + } + + 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 + } + } + + public void DownClick() { + _audioTimeSyncController.Pause(); + } + + public void UpClick() { + _audioTimeSyncController.Resume(); + } + + [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; + + [UIComponent("container")] + public VerticalLayoutGroup _container = null; + + [UIComponent("tooltipHeader")] + public VerticalLayoutGroup tooltipHeader = null; + + private void DisableItalics(GameObject obj) { + TextMeshProUGUI[] textMeshProUGUIs = obj.GetComponentsInChildren(); + + foreach (var textMeshProUGUI in textMeshProUGUIs) { + textMeshProUGUI.fontStyle = FontStyles.Normal; + } + } + + private void UnskewImageViews(GameObject obj) { + ImageView[] imageViews = obj.GetComponentsInChildren(); + + foreach (var imageView2 in imageViews) { + imageView2.SetField("_skew", 0f); + imageView2.__Refresh(); + } + } + + public void SetupObjects() { + DisableItalics(_container.gameObject); + foreach(Transform child in tabSelector.transform) { + var x = child.gameObject.transform.Find("BG").gameObject.GetComponent(); + x.SetField("_skew", 0f); + x.__Refresh(); + var y = child.gameObject.transform.Find("Text").gameObject.GetComponent(); + y.alignment = TextAlignmentOptions.Center; + y.fontStyle = FontStyles.Normal; + y.transform.localPosition = new Vector3(0, -0.25f, 0); + } + tabSelector.transform.localScale = new Vector2(1.2f, 1.2f); + UnskewImageViews(_container.gameObject); + } + + [InjectOptional] private IFPFCSettings _fpfcSettings = null; + + [Inject] + protected void Construct() { + if (_fpfcSettings == null) return; + if (!_fpfcSettings.Enabled && !Environment.GetCommandLineArgs().Contains("fpfc")) return; // fpfcSettings is being inconsistent? + GameObject inputOBJ; + + var canvasGameObj = new GameObject(); + var canvas = 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.name = "ScoreSaberDesktopImberUI"; + + canvasGameObj.SetActive(true); + + canvas.sortingOrder = 1; + canvas.overrideSorting = true; + + canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.TexCoord1 | AdditionalCanvasShaderChannels.TexCoord2; + + 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); + + timebarActive.material = Plugin.NoGlowMatRound; + timebarbg.material = Plugin.NoGlowMatRound; + + + var contents = this.gameObject.transform.Find("Contents"); + var containerRect = contents.GetComponent(); + containerRect.anchorMax = new Vector2(Plugin.Settings.replayUIPosition.x, Plugin.Settings.replayUIPosition.y); + containerRect.anchorMin = new Vector2(Plugin.Settings.replayUIPosition.x, Plugin.Settings.replayUIPosition.y); + contents.localScale = new Vector2(Plugin.Settings.replayUISize, Plugin.Settings.replayUISize); + } + + + public void Dispose() { + if (!_fpfcSettings.Enabled) return; + //EventSystem.current = originalEventSystem; + Cursor.visible = false; + } + + + public void Setup(float initialSongTime, int targetFramerate) { + + _initialTime = initialSongTime; + _targetFPS = targetFramerate; + _timeSync = initialSongTime; + } + + protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { + + base.DidActivate(firstActivation, addedToHierarchy, screenSystemEnabling); + if (firstActivation) { + var x = timebarActive.gameObject.AddComponent(); + x.timebarBackground = timebarbg; + x.timebarActive = timebarActive; + x.upClick += UpClick; + x.downClick += DownClick; + x.OnProgressUpdated += (progress) => { + _imberScrubber.MainNode_PositionDidChange(progress); + }; + var y = timebarbg.gameObject.AddComponent(); + y.upClick += UpClick; + y.downClick += DownClick; + y.progressHandler = x; + _audioTimeSyncController.stateChangedEvent += () => { + if (_audioTimeSyncController.state == AudioTimeSyncController.State.Playing) { + playPauseText = "PAUSE"; + } else { + playPauseText = "PLAY"; + } + }; + SetupObjects(); + + + } + didParse = true; + } + + protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) { + + base.DidDeactivate(removedFromHierarchy, screenSystemDisabling); + } + + public void SwitchHand(XRNode xrNode) { + + HandDidSwitchEvent?.Invoke(xrNode); + } + + [UIAction("pause-play")] + protected void PausePlay() { + if(Plugin.ReplayState.lockPause) return; + DidClickPausePlay?.Invoke(); + } + + [UIAction("restart")] + protected void Restart() { + + DidClickRestart?.Invoke(); + } + + [UIAction("loop")] + protected void Loop() { + + DidClickLoop?.Invoke(); + } + + [UIAction("format-time-percent")] + protected string FormatTimePercent(float value) { + + return value.ToString("P0"); + } + + [UIAction("jump")] + protected void Jump() { + + DidPositionJump?.Invoke(); + } + + [UIAction("exit-replay")] + protected void ExitReplay() { + _shouldReturnToMenu = true; + + _pauseMenuManager.MenuButtonPressed(); + } + + private string FloatToTimeStamp(float timeInSeconds) { + int minutes = (int)timeInSeconds / 60; + int seconds = (int)timeInSeconds % 60; + + return $"{minutes:D2}:{seconds:D2}"; + } + + public void FixedUpdate() { + + 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); + } + + private bool _shouldReturnToMenu = false; + + [AffinityPrefix, AffinityPatch(typeof(GameSongController), nameof(GameSongController.SendSongDidFinishEvent))] + private bool StartLevelFinished() { + if (_shouldReturnToMenu) { + _shouldReturnToMenu = false; + return true; + } + return false; + } + } +} \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopUtils/ProgressHandler.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopUtils/ProgressHandler.cs new file mode 100644 index 0000000..f145b76 --- /dev/null +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopUtils/ProgressHandler.cs @@ -0,0 +1,99 @@ +using HMUI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine.EventSystems; +using UnityEngine; + +namespace ScoreSaber.Core.ReplaySystem.UI { + public class ProgressHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { + + public ImageView timebarActive; + public ImageView timebarBackground; + + public event Action OnProgressUpdated; + + public delegate void upClickEvent(); + public event upClickEvent upClick; + + public delegate void downClickEvent(); + public event downClickEvent downClick; + + private bool isDragging = false; + private float minX = -19f; + private float maxX = 19f; + + private Vector3 originalScale; + private Vector3 hoverScale; + private float scaleSpeed = 0.1f; + + public void UpdateProgress(float progress) { + OnProgressUpdated?.Invoke(progress); + } + + private void Start() { + originalScale = timebarActive.transform.localScale; + hoverScale = new Vector3(originalScale.x, originalScale.y * 1.2f, originalScale.z); + } + + public void OnPointerClick(PointerEventData eventData) { + UpdateTimebarPosition(eventData); + } + + public void OnPointerDown(PointerEventData eventData) { + isDragging = true; + downClick?.Invoke(); + UpdateTimebarPosition(eventData); + } + + public void OnPointerUp(PointerEventData eventData) { + isDragging = false; + upClick?.Invoke(); + UpdateTimebarPosition(eventData); + } + + public void OnDrag(PointerEventData eventData) { + if (isDragging) { + UpdateTimebarPosition(eventData); + } + } + + public void OnPointerEnter(PointerEventData eventData) { + StopAllCoroutines(); + StartCoroutine(SmoothScale(timebarActive.transform, hoverScale)); + } + + public void OnPointerExit(PointerEventData eventData) { + StopAllCoroutines(); + StartCoroutine(SmoothScale(timebarActive.transform, originalScale)); + } + + 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); + } + } + + private IEnumerator SmoothScale(Transform target, Vector3 targetScale) + { + while (Vector3.Distance(target.localScale, targetScale) > 0.01f) + { + target.localScale = Vector3.Lerp(target.localScale, targetScale, scaleSpeed * Time.deltaTime); + yield return new WaitForFixedUpdate(); + } + target.localScale = targetScale; + } + } +} diff --git a/ScoreSaber/Core/ReplaySystem/UI/DesktopUtils/TimebarBackgroundHandler.cs b/ScoreSaber/Core/ReplaySystem/UI/DesktopUtils/TimebarBackgroundHandler.cs new file mode 100644 index 0000000..a064455 --- /dev/null +++ b/ScoreSaber/Core/ReplaySystem/UI/DesktopUtils/TimebarBackgroundHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine.EventSystems; +using UnityEngine; + +namespace ScoreSaber.Core.ReplaySystem.UI { + public class TimebarBackgroundHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler { + + public ProgressHandler progressHandler; + + private float minX = -19f; + private float maxX = 19f; + + public delegate void upClickEvent(); + public event upClickEvent upClick; + + public delegate void downClickEvent(); + public event downClickEvent downClick; + + public void OnDrag(PointerEventData eventData) { + OnPointerDown(eventData); + } + + public void OnPointerDown(PointerEventData eventData) { + RectTransform timebarRect = GetComponent(); + Vector2 localPoint; + + if (RectTransformUtility.ScreenPointToLocalPointInRectangle(timebarRect, eventData.position, eventData.pressEventCamera, out localPoint)) { + float clampedX = Mathf.Clamp(localPoint.x, minX, maxX); + + progressHandler.timebarActive.rectTransform.anchoredPosition = new Vector2(clampedX, 0); + + float progress = Mathf.InverseLerp(minX, maxX, clampedX); + + progressHandler.UpdateProgress(progress); + } + downClick?.Invoke(); + } + + public void OnPointerUp(PointerEventData eventData) { + upClick?.Invoke(); + } + } +} diff --git a/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs b/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs index ae77cd0..91dbf0c 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ImberManager.cs @@ -3,6 +3,8 @@ using ScoreSaber.Core.Data; using ScoreSaber.Core.ReplaySystem.Data; using ScoreSaber.Core.ReplaySystem.Playback; +using ScoreSaber.Core.Services; +using ScoreSaber.Core.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -12,8 +14,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; @@ -24,11 +25,14 @@ internal class ImberManager : IInitializable, IDisposable private readonly AudioTimeSyncController _audioTimeSyncController; private readonly ReplayTimeSyncController _replayTimeSyncController; private readonly ImberUIPositionController _imberUIPositionController; + private readonly DesktopMainImberPanelView _desktopMainImberPanelView; + private readonly TweeningUtils _tweeningUtils; 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, + TweeningUtils tweeningUtils){ _gamePause = gamePause; _posePlayer = posePlayer; @@ -39,10 +43,14 @@ public ImberManager(ReplayFile file, IGamePause gamePause, ImberScrubber imberSc _audioTimeSyncController = audioTimeSyncController; _replayTimeSyncController = replayTimeSyncController; _imberUIPositionController = imberUIPositionController; + _desktopMainImberPanelView = desktopMainImberPanelView; + _tweeningUtils = tweeningUtils; + _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.Setup(1f, 90); } public void Initialize() { @@ -57,6 +65,14 @@ 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.HandDidSwitchEvent += MainImberPanelView_DidHandSwitchEvent; + _spectateAreaController.DidUpdatePlayerSpectatorPose += SpectateAreaController_DidUpdatePlayerSpectatorPose; _imberScrubber.DidCalculateNewTime += ImberScrubber_DidCalculateNewTime; _imberSpecsReporter.DidReport += ImberSpecsReporter_DidReport; @@ -64,6 +80,10 @@ public void Initialize() { if (!Plugin.Settings.hasOpenedReplayUI) { CreateWatermark(); } + + if(Plugin.Settings.startReplayUIHidden) { + _desktopMainImberPanelView.gameObject.SetActive(false); + } } private void MainImberPanelView_DidHandSwitchEvent(XRNode hand) { @@ -92,6 +112,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 +187,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 +227,24 @@ public void Dispose() { _mainImberPanelView.DidClickRestart -= MainImberPanelView_DidClickRestart; _mainImberPanelView.DidPositionJump -= MainImberPanelView_DidPositionJump; _mainImberPanelView.DidClickLoop -= MainImberPanelView_DidClickLoop; + + + _desktopMainImberPanelView.HandDidSwitchEvent -= MainImberPanelView_DidHandSwitchEvent; + _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.C)) { + Cursor.visible = !Cursor.visible; + Cursor.lockState = Cursor.visible ? CursorLockMode.None : CursorLockMode.Locked; + } + if (Input.GetKeyDown(KeyCode.I)) { + _tweeningUtils.FadeLayoutGroup(_desktopMainImberPanelView._container, !_desktopMainImberPanelView._container.gameObject.activeSelf, 0.1f, _desktopMainImberPanelView.tooltipHeader.gameObject); + } } } } \ No newline at end of file diff --git a/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs b/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs index 999c920..8206317 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ImberScrubber.cs @@ -25,8 +25,7 @@ public bool loopMode { _loopNode.gameObject.SetActive(value); _bar.AssignNodeToPercent(_loopNode, Mathf.Min(_maxPercent, 1f)); MainNode_PositionDidChange(_bar.GetNodePercent(_mainNode)); - - _mainNode.max = _maxPercent; // uwu owo owo uwu EVENTUALLY REPLACE WITH LEVEL FAILED TIME YEA YEA + _mainNode.max = _levelFailTime; // uwu owo owo uwu EVENTUALLY REPLACE WITH LEVEL FAILED TIME YEA YEA // i did it } } @@ -110,7 +109,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/ImberUIPositionController.cs b/ScoreSaber/Core/ReplaySystem/UI/ImberUIPositionController.cs index 3577efc..20337d7 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ImberUIPositionController.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ImberUIPositionController.cs @@ -1,7 +1,9 @@ using HMUI; using ScoreSaber.Core.Data; +using SiraUtil.Tools.FPFC; using System; using System.Collections; +using System.Linq; using UnityEngine; using UnityEngine.XR; using VRUIControls; @@ -33,6 +35,7 @@ internal class ImberUIPositionController : IInitializable, ITickable, IDisposabl private readonly Canvas _canvas; private Vector3 _controllerOffset; + [Inject] private readonly IFPFCSettings _fpfcSettings = null; public ImberUIPositionController(IGamePause gamePause, ImberScrubber imberScrubber, PauseMenuManager pauseMenuManager, MainImberPanelView mainImberPanelView, VRControllerAccessor vrControllerAccessor) { @@ -48,7 +51,6 @@ public ImberUIPositionController(IGamePause gamePause, ImberScrubber imberScrubb _curve = _canvas.GetComponent(); _controllerOffset = new Vector3(0f, 0f, -2f); } - public void Initialize() { _gamePause.didPauseEvent += GamePause_didPauseEvent; @@ -78,7 +80,9 @@ private void GamePause_didPauseEvent() { } public void Tick() { - + if(_fpfcSettings.Enabled) { + return; + } VRController controller = _handTrack == XRNode.LeftHand ? _vrControllerAccessor.leftController : _vrControllerAccessor.rightController; // Detect Trigger Double Click diff --git a/ScoreSaber/Core/ReplaySystem/UI/Legacy/GameReplayUI.cs b/ScoreSaber/Core/ReplaySystem/UI/Legacy/GameReplayUI.cs index 95ce219..868a52e 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/Legacy/GameReplayUI.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/Legacy/GameReplayUI.cs @@ -1,4 +1,5 @@ using HMUI; +using ScoreSaber.Core.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -14,15 +15,15 @@ internal class GameReplayUI : MonoBehaviour { [Inject] private readonly GameplayCoreSceneSetupData _gameplayCoreSceneSetupData = null; public void Start() { - + if(Plugin.Settings.hideWatermarkIfUsersReplay && Plugin.ReplayState.isUsersReplay) { + return; + } CreateReplayUI(); } private void CreateReplayUI() { - - string replayText = string.Format("REPLAY MODE - Watching {0} play {1} - {2} ({3})", Plugin.ReplayState.CurrentPlayerName, - Plugin.ReplayState.CurrentBeatmapLevel.songAuthorName, Plugin.ReplayState.CurrentBeatmapLevel.songName, - Enum.GetName(typeof(BeatmapDifficulty), Plugin.ReplayState.CurrentBeatmapKey.difficulty).Replace("ExpertPlus", "Expert+")); + string replayTextTitle = Plugin.ReplayState.IsLegacyReplay ? "LEGACY REPLAY" : "REPLAY"; + string replayText = $"{replayTextTitle} {Plugin.ReplayState.CurrentPlayerName} playing ({Plugin.ReplayState.CurrentBeatmapLevel.songAuthorName} - {Plugin.ReplayState.CurrentBeatmapLevel.songName}) [{BeatmapUtils.FriendlyLevelAuthorName(Plugin.ReplayState.CurrentBeatmapLevel.allMappers, Plugin.ReplayState.CurrentBeatmapLevel.allLighters)}]"; float timeScale = 1f; if (!Plugin.ReplayState.IsLegacyReplay) { @@ -35,7 +36,7 @@ private void CreateReplayUI() { } string friendlyMods = GetFriendlyModifiers(Plugin.ReplayState.CurrentModifiers); if (friendlyMods != string.Empty) { - replayText += string.Format(" [{0}]", friendlyMods); + replayText += string.Format(" [{0}]", friendlyMods); } GameObject _watermarkCanvas = new GameObject("InGameReplayUI"); @@ -75,6 +76,8 @@ public TextMeshProUGUI CreateText(RectTransform parent, string text, Vector2 anc return textMeshProUGUI; } + + public string GetFriendlyModifiers(GameplayModifiers gameplayModifiers) { if (gameplayModifiers == null) return string.Empty; diff --git a/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs b/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs index 8bc9858..67f1d86 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/MainImberPanelView.cs @@ -3,7 +3,7 @@ using BeatSaberMarkupLanguage.FloatingScreen; using BeatSaberMarkupLanguage.ViewControllers; using HMUI; -using ScoreSaber.Core.Data; +using SiraUtil.Tools.FPFC; using System; using System.Collections.Generic; using UnityEngine; @@ -128,8 +128,8 @@ public float rightSaberSpeed { protected readonly CurvedTextMeshPro rightSpeedText = null; [Inject] - protected void Construct() { - + protected void Construct(IFPFCSettings fPFCSettings) { + if(fPFCSettings.Enabled) return; _floatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(60f, 45f), false, defaultPosition.position, defaultPosition.rotation); _floatingScreen.GetComponent().sortingOrder = 31; _floatingScreen.name = "Imber Replay Panel (Screen)"; @@ -178,6 +178,7 @@ public void SwitchHand(XRNode xrNode) { [UIAction("pause-play")] protected void PausePlay() { + if(Plugin.ReplayState.lockPause) return; DidClickPausePlay?.Invoke(); } 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); - } - } - } -} diff --git a/ScoreSaber/Core/ReplaySystem/UI/ResultsViewReplayButtonController.cs b/ScoreSaber/Core/ReplaySystem/UI/ResultsViewReplayButtonController.cs index e293e04..3d1cc8a 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/ResultsViewReplayButtonController.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/ResultsViewReplayButtonController.cs @@ -90,7 +90,7 @@ public void Dispose() { [UIAction("replay-click")] protected void ClickedReplayButton() { - + Plugin.ReplayState.isUsersReplay = true; _replayLoader.Load(_serializedReplay, _beatmapLevel, _beatmapKey, _levelCompletionResults.gameplayModifiers, _playerService.localPlayerInfo.playerName).RunTask(); watchReplayButton.interactable = false; diff --git a/ScoreSaber/Core/ReplaySystem/UI/SpectateAreaController.cs b/ScoreSaber/Core/ReplaySystem/UI/SpectateAreaController.cs index c101204..fd1e63f 100644 --- a/ScoreSaber/Core/ReplaySystem/UI/SpectateAreaController.cs +++ b/ScoreSaber/Core/ReplaySystem/UI/SpectateAreaController.cs @@ -41,11 +41,11 @@ public void AnimateTo(string poseID) { _activeNote.noteTransform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.Euler(45f, 45f, 45f)); ColorNoteVisuals visuals = _activeNote.GetComponent(); - Accessors.SetCircleVisibility(ref visuals, false); - Accessors.SetArrowVisibility(ref visuals, false); - var color = Accessors.NoteColor(ref visuals) = Color.cyan.ColorWithAlpha(3f); + visuals.showCircle = false; + visuals.showArrow = false; + var color = visuals._noteColor = Color.cyan.ColorWithAlpha(3f); - foreach (var block in Accessors.NoteMaterialBlocks(ref visuals)) { + foreach (var block in visuals._materialPropertyBlockControllers) { block.materialPropertyBlock.SetColor(_colorID, color); block.ApplyChanges(); } 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..53383b8 --- /dev/null +++ b/ScoreSaber/Core/ReplaySystem/UI/desktop-imber-panel.bsml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + +