diff --git a/scripts/Rhythia.cs b/scripts/Rhythia.cs index d78fc3b..20b07e9 100644 --- a/scripts/Rhythia.cs +++ b/scripts/Rhythia.cs @@ -40,41 +40,8 @@ public override async void _Ready() } // Stats - - if (!File.Exists($"{Constants.USER_FOLDER}/stats")) - { - Logger.Log("Stats file not found"); - File.WriteAllText($"{Constants.USER_FOLDER}/stats", ""); - Stats.Save(); - } - - try - { - Stats.Load(); - } - catch - { - Stats.GamePlaytime = 0; - Stats.TotalPlaytime = 0; - Stats.GamesOpened = 0; - Stats.TotalDistance = 0; - Stats.NotesHit = 0; - Stats.NotesMissed = 0; - Stats.HighestCombo = 0; - Stats.Attempts = 0; - Stats.Passes = 0; - Stats.FullCombos = 0; - Stats.HighestScore = 0; - Stats.TotalScore = 0; - Stats.RageQuits = 0; - Stats.PassAccuracies = []; - Stats.FavoriteMaps = []; - - Stats.Save(); - } - - Stats.GamesOpened++; - + Stats.Initialize(); + Stats.Instance.GamesOpened++; // marking sspms for importing can be done with an one liner, kept the following block of code in case we need to loop over every valid map file for some reason @@ -180,12 +147,12 @@ public static void Quit() LegacyRunner.CurrentAttempt.Stop(); } - Stats.TotalPlaytime += (Time.GetTicksUsec() - Constants.STARTED) / 1000000; + Stats.Instance.TotalPlaytime += (Time.GetTicksUsec() - Constants.STARTED) / 1000000; if (loaded) { SettingsManager.Save(); - Stats.Save(); + Stats.Instance.Save(); } Discord.Client.Dispose(); @@ -200,7 +167,7 @@ public override void _Notification(int what) { if (SceneManager.Scene != null && SceneManager.Scene is LegacyRunner) { - Stats.RageQuits++; + Stats.Instance.RageQuits++; } Quit(); diff --git a/scripts/database/Stats.cs b/scripts/database/Stats.cs index 3fe3a06..342c224 100644 --- a/scripts/database/Stats.cs +++ b/scripts/database/Stats.cs @@ -3,122 +3,62 @@ using System.Security.Cryptography; using Godot; using Godot.Collections; +using SQLite; -[GlobalClass] -public partial class Stats : Node -{ - public static ulong GamePlaytime = 0; - public static ulong TotalPlaytime = 0; - public static ulong GamesOpened = 0; - public static ulong TotalDistance = 0; - public static ulong NotesHit = 0; - public static ulong NotesMissed = 0; - public static ulong HighestCombo = 0; - public static ulong Attempts = 0; - public static ulong Passes = 0; - public static ulong FullCombos = 0; - public static ulong HighestScore = 0; - public static ulong TotalScore = 0; - public static ulong RageQuits = 0; - public static Array PassAccuracies = []; - public static Godot.Collections.Dictionary FavoriteMaps = []; - - public static void Save() - { - File.SetAttributes($"{Constants.USER_FOLDER}/stats", FileAttributes.None); - Godot.FileAccess file = Godot.FileAccess.Open($"{Constants.USER_FOLDER}/stats", Godot.FileAccess.ModeFlags.Write); - string accuraciesJson = Json.Stringify(PassAccuracies); - string mapsJson = Json.Stringify(FavoriteMaps); - - file.Store8(1); - file.Store64(GamePlaytime); - file.Store64(TotalPlaytime); - file.Store64(GamesOpened); - file.Store64(TotalDistance); - file.Store64(NotesHit); - file.Store64(NotesMissed); - file.Store64(HighestCombo); - file.Store64(Attempts); - file.Store64(Passes); - file.Store64(FullCombos); - file.Store64(HighestScore); - file.Store64(TotalScore); - file.Store64(RageQuits); - file.Store32((uint)accuraciesJson.Length); - file.StoreString(accuraciesJson); - file.Store32((uint)mapsJson.Length); - file.StoreString(mapsJson); - file.Close(); - - byte[] bytes = File.ReadAllBytes($"{Constants.USER_FOLDER}/stats"); - byte[] hash = new byte[32]; - - SHA256.HashData(bytes, hash); +#nullable enable - file = Godot.FileAccess.Open($"{Constants.USER_FOLDER}/stats", Godot.FileAccess.ModeFlags.Write); - file.StoreBuffer(bytes); - file.StoreBuffer(hash); - file.Close(); +public class Stats +{ + private const string id = "_STATS"; - File.SetAttributes($"{Constants.USER_FOLDER}/stats", FileAttributes.Hidden); - Logger.Log("Saved stats"); - } + public static Stats Instance { get; private set; } = new(); - public static void Load() + public static void Initialize() { - try - { - FileParser file = new($"{Constants.USER_FOLDER}/stats"); - - byte[] bytes = file.Get((int)file.Length - 32); + DatabaseService.Connection.CreateTable(); + SQLiteConnection connection = DatabaseService.Connection; - file.Seek(0); + Stats? result = connection.Find(id); - byte version = file.Get(1)[0]; - - switch (version) - { - case 1: - { - GamePlaytime = file.GetUInt64(); - TotalPlaytime = file.GetUInt64(); - GamesOpened = file.GetUInt64(); - TotalDistance = file.GetUInt64(); - NotesHit = file.GetUInt64(); - NotesMissed = file.GetUInt64(); - HighestCombo = file.GetUInt64(); - Attempts = file.GetUInt64(); - Passes = file.GetUInt64(); - FullCombos = file.GetUInt64(); - HighestScore = file.GetUInt64(); - TotalScore = file.GetUInt64(); - RageQuits = file.GetUInt64(); - PassAccuracies = (Array)Json.ParseString(file.GetString((int)file.GetUInt32())); - FavoriteMaps = (Godot.Collections.Dictionary)Json.ParseString(file.GetString((int)file.GetUInt32())); - - byte[] hash = file.Get(32); - byte[] newHash = new byte[32]; - - SHA256.HashData(bytes, newHash); - - for (int i = 0; i < 32; i++) - { - if (hash[i] != newHash[i]) - { - throw new("Wrong hash lol"); - } - } - - break; - } - } - } - catch (Exception exception) + if (result == null) { - ToastNotification.Notify("Stats file corrupt or modified", 2); - Logger.Error(exception); + result = new(); + connection.Insert(result); } - Logger.Log("Loaded stats"); + Instance = result; + } + + [PrimaryKey] + public string Id { get; set; } = id; + + public ulong GamePlaytime { get; set; } + public ulong TotalPlaytime { get; set; } + public ulong GamesOpened { get; set; } + public ulong TotalDistance { get; set; } + public ulong NotesHit { get; set; } + public ulong NotesMissed { get; set; } + public ulong HighestCombo { get; set; } + public ulong Attempts { get; set; } + public ulong Passes { get; set; } + public ulong FullCombos { get; set; } + public ulong HighestScore { get; set; } + public ulong TotalScore { get; set; } + public ulong RageQuits { get; set; } + public double AverageAccuracy { get; set; } + + public event Action? StatsUpdated; + + + /// + /// Forces a sync for stats + /// + public void ForceUpdate() => StatsUpdated?.Invoke(this); + + public void Save() + { + SQLiteConnection connection = DatabaseService.Connection; + connection.InsertOrReplace(Instance); + Logger.Log("Saved stats"); } } diff --git a/scripts/map/Map.cs b/scripts/map/Map.cs index 30f246f..4baa45c 100644 --- a/scripts/map/Map.cs +++ b/scripts/map/Map.cs @@ -61,6 +61,8 @@ public partial class Map : RefCounted public string AudioExt { get; set; } = string.Empty; + public int PlayCount { get; set; } = 0; + private Texture2D cover = DefaultCover; [Ignore] diff --git a/scripts/scenes/LegacyRunner.cs b/scripts/scenes/LegacyRunner.cs index cfc28c1..f55f539 100644 --- a/scripts/scenes/LegacyRunner.cs +++ b/scripts/scenes/LegacyRunner.cs @@ -270,11 +270,11 @@ public void Hit(int index) if (!IsReplay) { - Stats.NotesHit++; + Stats.Instance.NotesHit++; - if (Combo > Stats.HighestCombo) + if (Combo > Stats.Instance.HighestCombo) { - Stats.HighestCombo = Combo; + Stats.Instance.HighestCombo = Combo; } HitsInfo[index] = lateness; @@ -356,7 +356,7 @@ public void Miss(int index) if (!IsReplay) { HitsInfo[index] = -1; - Stats.NotesMissed++; + Stats.Instance.NotesMissed++; } //if (Health - HealthStep <= 0) @@ -1355,7 +1355,7 @@ public override void Load() public static void Play(Map map, double speed = 1, double startFrom = 0, Dictionary mods = null, string[] players = null, Replay[] replays = null) { - map = MapParser.Decode(map.FilePath); + Map parsed_map = MapParser.Decode(map.FilePath); SceneManager.Root.GetViewport().GuiGetFocusOwner()?.ReleaseFocus(); @@ -1366,7 +1366,7 @@ public static void Play(Map map, double speed = 1, double startFrom = 0, Diction Stop(); } - CurrentAttempt = new(map, speed, startFrom, mods ?? [], players, replays); + CurrentAttempt = new(parsed_map, speed, startFrom, mods ?? [], players, replays); SoundManager.BeginGameplayScope(CurrentAttempt.Map); Playing = true; stopQueued = false; @@ -1375,16 +1375,9 @@ public static void Play(Map map, double speed = 1, double startFrom = 0, Diction if (!CurrentAttempt.IsReplay) { - Stats.Attempts++; - - if (!Stats.FavoriteMaps.ContainsKey(map.Name)) - { - Stats.FavoriteMaps[map.Name] = 1; - } - else - { - Stats.FavoriteMaps[map.Name]++; - } + Stats.Instance.Attempts++; + map.PlayCount++; + MapManager.Update(map); } SceneManager.Load("res://scenes/game.tscn"); @@ -1453,8 +1446,8 @@ public static void Stop(bool results = true) if (!CurrentAttempt.IsReplay) { - Stats.GamePlaytime += (Time.GetTicksUsec() - Started) / 1000000; - Stats.TotalDistance += (ulong)CurrentAttempt.DistanceMM; + Stats.Instance.GamePlaytime += (Time.GetTicksUsec() - Started) / 1000000; + Stats.Instance.TotalDistance += (ulong)CurrentAttempt.DistanceMM; if (CurrentAttempt.StartFrom == 0) { @@ -1472,22 +1465,25 @@ public static void Stop(bool results = true) if (CurrentAttempt.Qualifies) { - Stats.Passes++; - Stats.TotalScore += CurrentAttempt.Score; + Stats.Instance.Passes++; + Stats.Instance.TotalScore += CurrentAttempt.Score; if (CurrentAttempt.Accuracy == 100) { - Stats.FullCombos++; + Stats.Instance.FullCombos++; } - if (CurrentAttempt.Score > Stats.HighestScore) + if (CurrentAttempt.Score > Stats.Instance.HighestScore) { - Stats.HighestScore = CurrentAttempt.Score; + Stats.Instance.HighestScore = CurrentAttempt.Score; } - Stats.PassAccuracies.Add(CurrentAttempt.Accuracy); + // TEST: test if this is good + Stats.Instance.AverageAccuracy = (Stats.Instance.AverageAccuracy + CurrentAttempt.Accuracy) / Stats.Instance.Passes; } } + + Stats.Instance.ForceUpdate(); } if (results) diff --git a/scripts/ui/menu/extras/StatsPanel.cs b/scripts/ui/menu/extras/StatsPanel.cs index 75ba45f..28e5987 100644 --- a/scripts/ui/menu/extras/StatsPanel.cs +++ b/scripts/ui/menu/extras/StatsPanel.cs @@ -1,17 +1,89 @@ using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; using Godot; public partial class StatsPanel : ExtrasPanel { + private bool initialized = false; + private Label gamePlaytime { get; set; } + private Label totalPlaytime { get; set; } + private Label gamesOpened { get; set; } + private Label totalDistance { get; set; } + private Label notesHit { get; set; } + private Label notesMissed { get; set; } + private Label highestCombo { get; set; } + private Label attempts { get; set; } + private Label passes { get; set; } + private Label fullCombos { get; set; } + private Label highestScore { get; set; } + private Label totalScore { get; set; } + private Label averageAccuracy { get; set; } + private Label rageQuits { get; set; } + private Label favouriteMap { get; set; } + public override void _Ready() { base._Ready(); + Node vbox = GetNode("ScrollContainer/VBoxContainer"); + gamePlaytime = vbox.GetNode