diff --git a/BeatRecorder/Entities/BeatSaber/DataPuller/DataPullerData.cs b/BeatRecorder/Entities/BeatSaber/DataPuller/DataPullerData.cs new file mode 100644 index 0000000..10c1800 --- /dev/null +++ b/BeatRecorder/Entities/BeatSaber/DataPuller/DataPullerData.cs @@ -0,0 +1,29 @@ +namespace BeatRecorder.Entities; + +public class DataPullerData +{ + public int Score { get; set; } + public int ScoreWithMultipliers { get; set; } + public int MaxScore { get; set; } + public int MaxScoreWithMultipliers { get; set; } + public string Rank { get; set; } + public bool FullCombo { get; set; } + public int NotesSpawned { get; set; } + public int Combo { get; set; } + public int Misses { get; set; } + public float Accuracy { get; set; } + public Blockhitscore BlockHitScore { get; set; } + public float PlayerHealth { get; set; } + public int ColorType { get; set; } + public int TimeElapsed { get; set; } + public int EventTrigger { get; set; } + public long UnixTimestamp { get; set; } + + public class Blockhitscore + { + public int PreSwing { get; set; } + public int PostSwing { get; set; } + public int CenterSwing { get; set; } + } + +} diff --git a/BeatRecorder/Entities/BeatSaber/DataPuller/DataPullerMain.cs b/BeatRecorder/Entities/BeatSaber/DataPuller/DataPullerMain.cs new file mode 100644 index 0000000..6ead7e5 --- /dev/null +++ b/BeatRecorder/Entities/BeatSaber/DataPuller/DataPullerMain.cs @@ -0,0 +1,62 @@ +namespace BeatRecorder.Entities; + +public class DataPullerMain +{ + public string GameVersion { get; set; } + public string PluginVersion { get; set; } + public bool InLevel { get; set; } + public bool LevelPaused { get; set; } + public bool LevelFinished { get; set; } + public bool LevelFailed { get; set; } + public bool LevelQuit { get; set; } + public string Hash { get; set; } + public string SongName { get; set; } + public string SongSubName { get; set; } + public string SongAuthor { get; set; } + public string Mapper { get; set; } + public string BSRKey { get; set; } + public string CoverImage { get; set; } + public int Duration { get; set; } + public string MapType { get; set; } + public string Difficulty { get; set; } + public string CustomDifficultyLabel { get; set; } + public int BPM { get; set; } + public float NJS { get; set; } + public ModifierObject Modifiers { get; set; } + public float ModifiersMultiplier { get; set; } + public bool PracticeMode { get; set; } + public Practicemodemodifiers PracticeModeModifiers { get; set; } + public float PP { get; set; } + public float Star { get; set; } + public bool IsMultiplayer { get; set; } + public int PreviousRecord { get; set; } + public string PreviousBSR { get; set; } + public long UnixTimestamp { get; set; } + + public class ModifierObject + { + public bool NoFailOn0Energy { get; set; } + public bool OneLife { get; set; } + public bool FourLives { get; set; } + public bool NoBombs { get; set; } + public bool NoWalls { get; set; } + public bool NoArrows { get; set; } + public bool GhostNotes { get; set; } + public bool DisappearingArrows { get; set; } + public bool SmallNotes { get; set; } + public bool ProMode { get; set; } + public bool StrictAngles { get; set; } + public bool ZenMode { get; set; } + public bool SlowerSong { get; set; } + public bool FasterSong { get; set; } + public bool SuperFastSong { get; set; } + } + + public class Practicemodemodifiers + { + public float SongSpeedMul { get; set; } + public bool StartInAdvanceAndClearNotes { get; set; } + public float SongStartTime { get; set; } + } + +} diff --git a/BeatRecorder/Entities/BeatSaber/DataPullerData.cs b/BeatRecorder/Entities/BeatSaber/DataPuller/Legacy/DataPullerData.cs similarity index 93% rename from BeatRecorder/Entities/BeatSaber/DataPullerData.cs rename to BeatRecorder/Entities/BeatSaber/DataPuller/Legacy/DataPullerData.cs index fd251a2..e8daaae 100644 --- a/BeatRecorder/Entities/BeatSaber/DataPullerData.cs +++ b/BeatRecorder/Entities/BeatSaber/DataPuller/Legacy/DataPullerData.cs @@ -1,4 +1,4 @@ -namespace BeatRecorder.Entities; +namespace BeatRecorder.Entities.Legacy; public class DataPullerData { diff --git a/BeatRecorder/Entities/BeatSaber/DataPullerMain.cs b/BeatRecorder/Entities/BeatSaber/DataPuller/Legacy/DataPullerMain.cs similarity index 98% rename from BeatRecorder/Entities/BeatSaber/DataPullerMain.cs rename to BeatRecorder/Entities/BeatSaber/DataPuller/Legacy/DataPullerMain.cs index f609014..6a8a2fa 100644 --- a/BeatRecorder/Entities/BeatSaber/DataPullerMain.cs +++ b/BeatRecorder/Entities/BeatSaber/DataPuller/Legacy/DataPullerMain.cs @@ -1,4 +1,4 @@ -namespace BeatRecorder.Entities; +namespace BeatRecorder.Entities.Legacy; public class DataPullerMain { diff --git a/BeatRecorder/Entities/BeatSaber/SharedStatus.cs b/BeatRecorder/Entities/BeatSaber/SharedStatus.cs index ec27da3..f24135e 100644 --- a/BeatRecorder/Entities/BeatSaber/SharedStatus.cs +++ b/BeatRecorder/Entities/BeatSaber/SharedStatus.cs @@ -158,11 +158,54 @@ public SharedStatus(DataPullerMain main, DataPullerData data, int MaxCombo, Base GameVersion = main?.GameVersion, }; + if (!baseBeatSaberHandler.ImageCache.ContainsKey(main?.CoverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg")) + baseBeatSaberHandler.ImageCache.TryAdd(main?.CoverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg", Bitmap.FromStream(new HttpClient().GetStreamAsync(main?.CoverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg").Result)); + + Bitmap image = (Bitmap)baseBeatSaberHandler.ImageCache[main?.CoverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg"]; + + BeatmapInfo = new() + { + Name = main?.SongName, + SubName = main?.SongSubName, + Author = main?.SongAuthor, + Creator = main?.Mapper, + Cover = (Bitmap)image, + IdOrHash = main?.Hash, + Bpm = main?.BPM, + NoteJumpSpeed = main?.NJS, + Difficulty = main?.Difficulty, + CustomDifficulty = main?.CustomDifficultyLabel + }; + + PerformanceInfo = new() + { + RawScore = data?.Score, + Score = data?.ScoreWithMultipliers, + Accuracy = (double)Math.Round(data?.Accuracy ?? 0, 2), + Rank = data?.Rank, + MissedNoteCount = data?.Misses, + Failed = main?.LevelFailed, + Finished = main?.LevelFinished, + MaxCombo = MaxCombo, + Combo = data?.Combo, + SoftFailed = ((main?.LevelFailed ?? false) || (data?.PlayerHealth ?? 0) <= 0) && (main?.Modifiers.NoFailOn0Energy ?? false) + }; + } + + public SharedStatus(Entities.Legacy.DataPullerMain main, Entities.Legacy.DataPullerData data, int MaxCombo, BaseBeatSaberHandler baseBeatSaberHandler) + { + GameInfo = new() + { + ModUsed = Mod.Datapuller, + ModVersion = main?.PluginVersion, + GameVersion = main?.GameVersion, + }; + if (!baseBeatSaberHandler.ImageCache.ContainsKey(main?.coverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg")) baseBeatSaberHandler.ImageCache.TryAdd(main?.coverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg", Bitmap.FromStream(new HttpClient().GetStreamAsync(main?.coverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg").Result)); Bitmap image = (Bitmap)baseBeatSaberHandler.ImageCache[main?.coverImage ?? "https://raw.githubusercontent.com/TheXorog/BeatRecorder/main/BeatRecorder/Assets/BeatSaberIcon.jpg"]; - + BeatmapInfo = new() { Name = main?.SongName, diff --git a/BeatRecorder/Entities/Config.cs b/BeatRecorder/Entities/Config.cs index 55b6349..96bc271 100644 --- a/BeatRecorder/Entities/Config.cs +++ b/BeatRecorder/Entities/Config.cs @@ -54,6 +54,12 @@ internal class Config /// public string BeatSaberPort { get; set; } = "6557"; + /// + /// Whether to use the legacy handler of the selected mod, if supported. + /// Currently supports: DataPuller. + /// + public bool BeatSaberUseLegacyIfAvailable { get; set; } = false; + /// /// The websocket server to connect to for obs /// @@ -139,7 +145,6 @@ internal class Config /// public string OBSPauseScene { get; set; } = ""; - /// /// Migration value for deserializering diff --git a/BeatRecorder/Program.cs b/BeatRecorder/Program.cs index f1c8980..f83d33f 100644 --- a/BeatRecorder/Program.cs +++ b/BeatRecorder/Program.cs @@ -8,7 +8,7 @@ namespace BeatRecorder; public class Program { - public static string Version = "2.0.1"; + public static string Version = "2.1.0"; public bool RunningPrerelease = false; @@ -208,7 +208,10 @@ async Task UseModernSocket() } case "datapuller": { - BeatSaberClient = new DataPullerHandler().Initialize(this); // 2946 + if (LoadedConfig.BeatSaberUseLegacyIfAvailable) + BeatSaberClient = new DataPullerLegacyHandler().Initialize(this); // 2946 + else + BeatSaberClient = new DataPullerHandler().Initialize(this); break; } case "beatsaberplus": diff --git a/BeatRecorder/Util/BeatSaber/DataPullerHandler.cs b/BeatRecorder/Util/BeatSaber/DataPuller/DataPullerHandler.cs similarity index 99% rename from BeatRecorder/Util/BeatSaber/DataPullerHandler.cs rename to BeatRecorder/Util/BeatSaber/DataPuller/DataPullerHandler.cs index e7c2594..8a46096 100644 --- a/BeatRecorder/Util/BeatSaber/DataPullerHandler.cs +++ b/BeatRecorder/Util/BeatSaber/DataPuller/DataPullerHandler.cs @@ -181,7 +181,7 @@ private void DataMessageRecieved(string text) return; } - if (InLevel && (CurrentData?.unixTimestamp ?? 0) < _status.unixTimestamp) + if (InLevel && (CurrentData?.UnixTimestamp ?? 0) < _status.UnixTimestamp) CurrentData = _status; if (CurrentMaxCombo < _status.Combo) diff --git a/BeatRecorder/Util/BeatSaber/DataPuller/DataPullerLegacyHandler.cs b/BeatRecorder/Util/BeatSaber/DataPuller/DataPullerLegacyHandler.cs new file mode 100644 index 0000000..664f536 --- /dev/null +++ b/BeatRecorder/Util/BeatSaber/DataPuller/DataPullerLegacyHandler.cs @@ -0,0 +1,271 @@ +using BeatRecorder.Entities; +using BeatRecorder.Entities.Legacy; +using BeatRecorder.Enums; +using DataPullerData = BeatRecorder.Entities.Legacy.DataPullerData; +using DataPullerMain = BeatRecorder.Entities.Legacy.DataPullerMain; + +namespace BeatRecorder.Util.BeatSaber; + +internal class DataPullerLegacyHandler : BaseBeatSaberHandler +{ + private Program Program = null; + + private WebsocketClient mainSocket { get; set; } + private WebsocketClient dataSocket { get; set; } + + internal SharedStatus CurrentStatus => new(CurrentMain, CurrentData, CurrentMaxCombo, this); + internal SharedStatus LastCompletedStatus { get; set; } + + private DataPullerMain CurrentMain = null; + private DataPullerData CurrentData = null; + + ConnectionTypeWarning LastWarning = ConnectionTypeWarning.Connected; + + int CurrentMaxCombo = 0; + private bool InLevel = false; + private bool IsPaused = false; + + public override BaseBeatSaberHandler Initialize(Program program) + { + _logger.LogInfo("Initializing Connection to Beat Saber via DataPuller.."); + + this.Program = program; + + var factory = new Func(() => new ClientWebSocket + { + Options = + { + KeepAliveInterval = TimeSpan.FromSeconds(5) + } + }); + + Task mainConn = Task.Run(() => + { + mainSocket = new WebsocketClient(new Uri($"ws://{Program.LoadedConfig.BeatSaberUrl}:{Program.LoadedConfig.BeatSaberPort}/BSDataPuller/MapData"), factory) + { + ReconnectTimeout = null, + ErrorReconnectTimeout = TimeSpan.FromSeconds(3) + }; + + mainSocket.MessageReceived.Subscribe(msg => { MainMessageRecieved(msg.Text); }); + mainSocket.ReconnectionHappened.Subscribe(type => { Reconnected(type); }); + mainSocket.DisconnectionHappened.Subscribe(type => { Disconnected(type); }); + + mainSocket.Start().Wait(); + + while (!mainSocket.IsRunning) + Thread.Sleep(50); + + _logger.LogInfo($"Connected to Beat Saber via DataPuller Main Socket"); + }); + + Task dataConn = Task.Run(() => + { + dataSocket = new WebsocketClient(new Uri($"ws://{Program.LoadedConfig.BeatSaberUrl}:{Program.LoadedConfig.BeatSaberPort}/BSDataPuller/LiveData"), factory) + { + ReconnectTimeout = null, + ErrorReconnectTimeout = TimeSpan.FromSeconds(3) + }; + + dataSocket.MessageReceived.Subscribe(msg => { DataMessageRecieved(msg.Text); }); + dataSocket.ReconnectionHappened.Subscribe(type => { Reconnected(type); }); + dataSocket.DisconnectionHappened.Subscribe(type => { Disconnected(type); }); + + dataSocket.Start().Wait(); + + while (!dataSocket.IsRunning) + Thread.Sleep(50); + + _logger.LogInfo($"Connected to Beat Saber via DataPuller Data Socket"); + }); + + while (!mainConn.IsCompleted || !dataConn.IsCompleted) + Thread.Sleep(50); + + return this; + } + + public override SharedStatus GetCurrentStatus() => CurrentStatus; + public override SharedStatus GetLastCompletedStatus() => LastCompletedStatus; + + private void MainMessageRecieved(string text) + { + DataPullerMain _status; + + try + { + _status = JsonConvert.DeserializeObject(text); + } + catch (Exception ex) + { + _logger.LogFatal($"Unable to convert message into object", ex); + return; + } + + if (InLevel != _status.InLevel) + { + if (!InLevel && _status.InLevel) + { + InLevel = true; + _logger.LogInfo($"Started playing \"{_status.SongName}\" by \"{_status.SongAuthor}\""); + + CurrentMain = _status; + CurrentMaxCombo = 0; + + if (!Program.LoadedConfig.OBSIngameScene.IsNullOrWhiteSpace()) + Program.ObsClient.SetCurrentScene(Program.LoadedConfig.OBSIngameScene); + + _ = Program.ObsClient.StartRecording(); + } + else if (InLevel && !_status.InLevel) + { + Thread.Sleep(500); + InLevel = false; + IsPaused = false; + + _logger.LogInfo($"Stopped playing \"{_status.SongName}\" by \"{_status.SongAuthor}\""); + + CurrentMain = _status; + + CurrentStatus.Update(new SharedStatus(CurrentMain, CurrentData, CurrentMaxCombo, this)); + LastCompletedStatus = CurrentStatus.Clone(); + + _ = Program.ObsClient.StopRecording(); + + if (!Program.LoadedConfig.OBSMenuScene.IsNullOrWhiteSpace()) + Program.ObsClient.SetCurrentScene(Program.LoadedConfig.OBSMenuScene); + + if (Program.LoadedConfig.PauseRecordingOnIngamePause) + Program.ObsClient.ResumeRecording(); + } + } + + if (_status.InLevel) + { + if (IsPaused != _status.LevelPaused) + { + if (IsPaused && _status.LevelPaused) + { + IsPaused = true; + _logger.LogInfo("Song paused."); + + if (Program.LoadedConfig.PauseRecordingOnIngamePause) + Program.ObsClient.PauseRecording(); + + if (!Program.LoadedConfig.OBSPauseScene.IsNullOrWhiteSpace()) + Program.ObsClient.SetCurrentScene(Program.LoadedConfig.OBSPauseScene); + } + else if (IsPaused && !_status.LevelPaused) + { + IsPaused = false; + _logger.LogInfo("Song resumed."); + + if (Program.LoadedConfig.PauseRecordingOnIngamePause) + Program.ObsClient.ResumeRecording(); + + if (!Program.LoadedConfig.OBSIngameScene.IsNullOrWhiteSpace()) + Program.ObsClient.SetCurrentScene(Program.LoadedConfig.OBSIngameScene); + } + } + } + } + + private void DataMessageRecieved(string text) + { + DataPullerData _status; + + try + { + _status = JsonConvert.DeserializeObject(text); + } + catch (Exception ex) + { + _logger.LogFatal($"Unable to convert message into object", ex); + return; + } + + if (InLevel && (CurrentData?.unixTimestamp ?? 0) < _status.unixTimestamp) + CurrentData = _status; + + if (CurrentMaxCombo < _status.Combo) + CurrentMaxCombo = _status.Combo; + } + + private void Disconnected(DisconnectionInfo msg) + { + try + { + Process[] processCollection = Process.GetProcesses(); + + if (!processCollection.Any(x => x.ProcessName.ToLower().Replace(" ", "").StartsWith("beatsaber"))) + { + if (LastWarning != ConnectionTypeWarning.NoProcess) + { + _logger.LogWarn($"Couldn't find a BeatSaber process, is BeatSaber started? ({msg.Type})"); + Program.steamNotifications?.SendNotification("Disconnected from Beat Saber", 1000, MessageType.ERROR); + } + LastWarning = ConnectionTypeWarning.NoProcess; + } + else + { + bool FoundWebSocketDll = false; + + string InstallationDirectory = processCollection.First(x => x.ProcessName.ToLower().Replace(" ", "").StartsWith("beatsaber")).MainModule.FileName; + InstallationDirectory = InstallationDirectory.Remove(InstallationDirectory.LastIndexOf("\\"), InstallationDirectory.Length - InstallationDirectory.LastIndexOf("\\")); + + if (Directory.GetDirectories(InstallationDirectory).Any(x => x.ToLower().EndsWith("plugins"))) + { + if (Directory.GetFiles($"{InstallationDirectory}\\Plugins").Any(x => x.Contains("DataPuller") && x.EndsWith(".dll"))) + { + FoundWebSocketDll = true; + } + } + else + { + if (LastWarning != ConnectionTypeWarning.NotModded) + { + _logger.LogFatal($"Beat Saber seems to be running but the BSDataPuller modifaction doesn't seem to be installed. Is your game even modded? (If haven't modded it, please do it: https://bit.ly/2TAvenk. If already modded, install BSDataPuller: https://bit.ly/3mcvC7g) ({msg.Type})"); + Program.steamNotifications?.SendNotification("Disconnected from Beat Saber", 1000, MessageType.ERROR); + } + LastWarning = ConnectionTypeWarning.NotModded; + return; + } + + if (FoundWebSocketDll) + { + if (LastWarning != ConnectionTypeWarning.ModInstalled) + { + _logger.LogFatal($"Beat Saber seems to be running and the BSDataPuller modifaction seems to be installed. Please make sure you put in the right port and you installed all of BSDataPuller' dependiencies! (If not installed, please install it: https://bit.ly/3mcvC7g) ({msg.Type})"); + Program.steamNotifications?.SendNotification("Disconnected from Beat Saber", 1000, MessageType.ERROR); + } + LastWarning = ConnectionTypeWarning.ModInstalled; + } + else + { + if (LastWarning != ConnectionTypeWarning.ModNotInstalled) + { + _logger.LogFatal($"Beat Saber seems to be running but the BSDataPuller modifaction doesn't seem to be installed. Please make sure to install BSDataPuller! (If not installed, please install it: https://bit.ly/3mcvC7g) ({msg.Type})"); + Program.steamNotifications?.SendNotification("Disconnected from Beat Saber", 1000, MessageType.ERROR); + } + LastWarning = ConnectionTypeWarning.ModNotInstalled; + } + } + } + catch (Exception ex) + { + _logger.LogError($"Failed to check if BSDataPuller is installed: (Disconnect Reason: {msg.Type}) {ex}"); + } + } + + private void Reconnected(ReconnectionInfo msg) + { + Program.steamNotifications?.SendNotification("Connected to Beat Saber", 1000, MessageType.INFO); + + if (msg.Type != ReconnectionType.Initial) + _logger.LogInfo($"Beat Saber Connection via DataPuller re-established: {msg.Type}"); + + LastWarning = ConnectionTypeWarning.Connected; + } + + internal override bool GetIsRunning() => (mainSocket?.IsRunning ?? false) && (dataSocket?.IsRunning ?? false); +}