From 864c33bec6eea3bba0abc51aa0bd730e61aadabc Mon Sep 17 00:00:00 2001 From: Grim Date: Wed, 18 Sep 2024 17:16:26 +0300 Subject: [PATCH] Refactor Run, extract SplitsController, use source generator --- SpeedTool/JSON/SourceGeneratorContext.cs | 5 + SpeedTool/JSON/TimeCollectionConverter.cs | 45 +++ SpeedTool/Platform/Platform.cs | 28 +- SpeedTool/Platform/Run.cs | 319 ++------------------ SpeedTool/SpeedTool.csproj | 2 +- SpeedTool/Splits/Category.cs | 50 +--- SpeedTool/Splits/Game.cs | 5 +- SpeedTool/Splits/RunCollection.cs | 2 +- SpeedTool/Splits/RunInfo.cs | 35 +-- SpeedTool/Splits/Split.cs | 49 +--- SpeedTool/Splits/SplitDisplayInfo.cs | 57 ++-- SpeedTool/Splits/SplitInfo.cs | 36 +++ SpeedTool/Splits/SplitsController.cs | 338 ++++++++++++++++++++++ SpeedTool/Splits/TimeCollection.cs | 23 -- SpeedTool/Windows/TimerUI/Classic.cs | 4 +- SpeedTool/Windows/TimerUI/SpeedTool.cs | 2 +- 16 files changed, 511 insertions(+), 489 deletions(-) create mode 100644 SpeedTool/JSON/TimeCollectionConverter.cs create mode 100644 SpeedTool/Splits/SplitInfo.cs create mode 100644 SpeedTool/Splits/SplitsController.cs diff --git a/SpeedTool/JSON/SourceGeneratorContext.cs b/SpeedTool/JSON/SourceGeneratorContext.cs index 0f2da19..40644b2 100644 --- a/SpeedTool/JSON/SourceGeneratorContext.cs +++ b/SpeedTool/JSON/SourceGeneratorContext.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using SpeedTool.Global.Definitions; +using SpeedTool.Splits; [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(ClassicUISettings))] @@ -8,4 +9,8 @@ [JsonSerializable(typeof(HotkeySettings))] [JsonSerializable(typeof(SpeedToolUISettings))] [JsonSerializable(typeof(SpeedToolUITheme))] +[JsonSerializable(typeof(SplitInfo))] +[JsonSerializable(typeof(SplitDisplayInfo))] +[JsonSerializable(typeof(Category))] +[JsonSerializable(typeof(RunInfo))] internal partial class SourceGeneratorContext : JsonSerializerContext {} diff --git a/SpeedTool/JSON/TimeCollectionConverter.cs b/SpeedTool/JSON/TimeCollectionConverter.cs new file mode 100644 index 0000000..40797fa --- /dev/null +++ b/SpeedTool/JSON/TimeCollectionConverter.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SpeedTool.Splits; +using SpeedTool.Timer; + +namespace SpeedTool.JSON; + +public sealed class TimeCollectionConverter : JsonConverter +{ + public override TimeCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException($"Invalid object"); + } + + TimeCollection tc = new(); + + for(int i = 0; i < (int)TimingMethod.Last; i++) + { + reader.Read(); + if(reader.TokenType != JsonTokenType.Number) + throw new JsonException("Expected number"); + + tc[(TimingMethod)i] = new TimeSpan(reader.GetInt64()); + } + + while (reader.TokenType != JsonTokenType.EndArray) + { + reader.Read(); + } + + return tc; + } + + public override void Write(Utf8JsonWriter writer, TimeCollection value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + for(int i = 0; i < (int)TimingMethod.Last; i++) + writer.WriteNumberValue(value[(TimingMethod)i].Ticks); + + writer.WriteEndArray(); + } +} diff --git a/SpeedTool/Platform/Platform.cs b/SpeedTool/Platform/Platform.cs index 343a5cf..a02947c 100644 --- a/SpeedTool/Platform/Platform.cs +++ b/SpeedTool/Platform/Platform.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Nodes; +using System.Text.Json; using Silk.NET.Input.Glfw; using Silk.NET.Windowing.Glfw; using SpeedTool.Injector; @@ -134,6 +134,15 @@ public void PreviousCategory() ReloadRun(); } + public TimeCollection GetCurrentTimes() + { + TimeCollection tc = new(); + for(int i = 0; i < (int)TimingMethod.Last; i++) + tc[(TimingMethod)i] = GetTimerFor((TimingMethod)i).CurrentTime; + + return tc; + } + public ITimerSource GetTimerFor(TimingMethod method) { return sources[(int)method]; @@ -185,7 +194,8 @@ public void SaveRunAsPB(RunInfo run) var fileName = game!.Name + "." + CurrentCategory!.Name; fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c, '_')); var dst = ENV.LocalFilesPath + "pbs/" + fileName + ".json"; - File.WriteAllText(dst, run.ToJson().ToString()); + var json = JsonSerializer.Serialize(run, typeof(RunInfo), SourceGeneratorContext.Default); + File.WriteAllText(dst, json); } public RunInfo? GetPBRun(Game g, Category c) @@ -195,7 +205,17 @@ public void SaveRunAsPB(RunInfo run) var dst = ENV.LocalFilesPath + "pbs/" + fileName + ".json"; if(File.Exists(dst)) - return RunInfo.FromJson(JsonNode.Parse(File.ReadAllText(dst))!.AsObject()); + { + try + { + return JsonSerializer.Deserialize(File.ReadAllText(dst), typeof(RunInfo), SourceGeneratorContext.Default) as RunInfo; + } + catch + { + File.Delete(dst); + return null; + } + } return null; } @@ -244,7 +264,7 @@ public void ReloadRun() if(injector != null) injector.Reset(); - run = new Run(game, game.GetCategories()[activeCategory].Splits, GetPBRun(game, CurrentCategory!)); + run = new Run(game, game.GetCategories()[activeCategory], GetPBRun(game, CurrentCategory!)); sources[(int)TimingMethod.RealTime] = run.Timer; } diff --git a/SpeedTool/Platform/Run.cs b/SpeedTool/Platform/Run.cs index e845f30..33dbc50 100644 --- a/SpeedTool/Platform/Run.cs +++ b/SpeedTool/Platform/Run.cs @@ -1,23 +1,16 @@ using SpeedTool.Splits; using SpeedTool.Timer; -using SpeedTool.Util; namespace SpeedTool.Platform; public class Run : ISplitsSource { - public Run(Game game, Split[] splits, RunInfo? comparisonRun) + public Run(Game game, Category cat, RunInfo? comparisonRun) { - this.splits = splits; this.game = game; - FlattenSplits(); - - if(comparisonRun != null && comparisonRun.Splits.Length == flattened.Length) - { - for(int i = 0; i < comparisonRun.Splits.Length; i++) - flattened[i].PBTimes = comparisonRun.Splits[i].Times; - comparison = comparisonRun; - } + comparison = comparisonRun; + category = cat; + controller = new SplitsController(cat, comparisonRun); } public bool Started { get; private set; } @@ -31,7 +24,7 @@ public ITimerSource Timer } } - public SplitDisplayInfo CurrentSplit => flattened[currentSplit]; + public SplitDisplayInfo CurrentSplit => controller.CurrentSplit; public void Split() { @@ -45,30 +38,24 @@ public void Split() { Platform.SharedPlatform.ReloadRun(); timer.Reset(); - currentSplit = 0; + controller = new SplitsController(category, null); IsFinished = false; return; } Started = true; - currentSplit = -1; - NextSplit(); - flattened[currentSplit].IsCurrent = true; + controller.Start(); timer.Reset(); timer.Start(); return; } - NextSplit(); - if(currentSplit >= flattened.Length) + if(!controller.NextSplit()) { timer.Stop(); Started = false; IsFinished = true; - currentSplit--; SaveRun(); - return; } - flattened[currentSplit].IsCurrent = true; } public void Pause() @@ -79,242 +66,30 @@ public void Pause() public void SkipSplit() { - if(!Started) + if(!Started || IsFinished) { return; } - flattened[currentSplit].IsCurrent = false; - NextSplitNoUpdate(); - if(currentSplit >= flattened.Length) - { - timer.Stop(); - Started = false; - currentSplit--; - return; - } - flattened[currentSplit].IsCurrent = true; - } - - private void NextSplitNoUpdate() - { - // Write split time - currentSplit++; - - // Roll over parent splitts - while(infoStack.Count > 0 && infoStack.Peek().Level >= flattened[currentSplit].Level) - { - var p = infoStack.Pop(); - } - - // Roll over to the first actual split in the tree - while(NextFlatSplit != null && CurrentFlatSplit.Level < NextFlatSplit.Value.Level) - { - infoStack.Push(flattened[currentSplit]); - currentSplit++; - } - - // If split has subsplits, go to next split - if(NextFlatSplit != null && NextFlatSplit.Value.Level > CurrentFlatSplit.Level) - { - SkipSplit(); - } + controller.SkipSplit(); } - private void FlattenSplits() - { - List f = new(); - List zero = new(); - foreach(var split in splits) - { - zero.Add(f.Count); - f.AddRange(split.Flatten()); - } - - flattened = f.ToArray(); - zeroLevelSplits = zero.ToArray(); - } - - int currentSplit = 0; - - private Split[] splits; - - private SplitDisplayInfo[] flattened = []; - private int[] zeroLevelSplits = []; - public IEnumerable GetSplits(int count) { - // Honestly, I wrote this function when I was high on sleep deprevation and eye disease, - // so it might be difficult to understand the hell is going on here. - // I'm trying to explain it as an aftermath with clear head, so don't mind me - - // This function has a `wieght` parameter to decide how to value splits from each side. - // TODO: This weight feature doesn't really work well, it should probably be changed to something else - var currentLevelSplits = GetCurrentLevelSplits(); - var posInCurrentLevel = currentLevelSplits.IndexOf(CurrentFlatSplit); - - // If on level 0, just get enough splits to display - if(CurrentFlatSplit.Level == 0) - { - return currentLevelSplits.TakeAtPosWeighted(count, posInCurrentLevel, weight); - } - - // Always display splits tree on top - var topmostSplits = GetTopmostSplits(); - var topmostCount = Math.Min(count - 1, topmostSplits.Count); - - // If we have enough tree to fill in the requested space, do that - if(topmostCount == count - 1) - return topmostSplits.TakeLast(topmostCount).Append(CurrentFlatSplit); - - var currentLevelCount = Math.Min(currentLevelSplits.Count, count - topmostCount); - - // If tree + current level fits the space, do that - if(currentLevelCount + topmostCount >= count) - { - return topmostSplits.TakeLast(topmostCount).Concat(currentLevelSplits.TakeAtPosWeighted(currentLevelCount, posInCurrentLevel, weight)); - } - - var middle = topmostSplits.TakeLast(topmostCount).Concat(currentLevelSplits.TakeAtPosWeighted(currentLevelCount, posInCurrentLevel, weight)); - - var zeroLevelCount = count - currentLevelCount - topmostCount; - - zeroLevelSplits.Select(x => flattened[x]).TakeAtPosWeighted(zeroLevelCount, 1, weight); - - // Figure out next and previous splits to fit the space - List prev = new(); - List next = new(); - int i = 0; - while(i < zeroLevelSplits.Length && zeroLevelSplits[i] < currentSplit) - { - if(zeroLevelSplits[i] == currentSplit) - { - i++; - continue; - } - prev.Add(flattened[zeroLevelSplits[i]]); - i++; - } - prev = prev.Take(prev.Count - 1).ToList(); - while(i < zeroLevelSplits.Length) - { - if(zeroLevelSplits[i] == currentSplit) - { - i++; - continue; - } - next.Add(flattened[zeroLevelSplits[i]]); - i++; - } - - // Figure out how many splits to take from the left and from the right - var wantRight = (int)(weight / 100.0 * zeroLevelCount); - wantRight = Math.Min(next.Count, wantRight); - - var wantLeft = zeroLevelCount - wantRight; - wantLeft = Math.Min(wantLeft, prev.Count); - - return prev.TakeLast(wantLeft).Concat(middle).Concat(next.Take(wantRight)); - } - - private void NextSplit() - { - // Write split time - if(currentSplit >= 0) - { - flattened[currentSplit].IsCurrent = false; - for(int i = 0; i < (int)TimingMethod.Last; i++) - { - var tm = (TimingMethod)i; - flattened[currentSplit].Times[tm] = Platform.SharedPlatform.GetTimerFor(tm).CurrentTime; - } - if(comparison != null) - flattened[currentSplit].DeltaTimes = flattened[currentSplit].Times - comparison.Splits[currentSplit].Times; - } - - currentSplit++; - - // Roll over parent splitts and write times for them - while(infoStack.Count > 0 && infoStack.Peek().Level >= flattened[currentSplit].Level) - { - var p = infoStack.Pop(); - for(int i = 0; i < (int)TimingMethod.Last; i++) - { - var tm = (TimingMethod)i; - p.Times[tm] = Platform.SharedPlatform.GetTimerFor(tm).CurrentTime; - } - if(comparison != null) - p.DeltaTimes = flattened[currentSplit].Times - comparison.Splits[currentSplit].Times; - } - - // Roll over to the first actual split in the tree - while(NextFlatSplit != null && CurrentFlatSplit.Level < NextFlatSplit.Value.Level) - { - infoStack.Push(flattened[currentSplit]); - currentSplit++; - } - - // If split has subsplits, go to next split - if(NextFlatSplit != null && NextFlatSplit.Value.Level > CurrentFlatSplit.Level) - { - NextSplit(); - } - } - - private List GetCurrentLevelSplits() - { - var begin = FirstSubsplitPos(); - var end = LastSubsplitPos(); - - List infos = new(); - - for(int i = begin; i <= end; i++) - { - if(flattened[i].Level == CurrentFlatSplit.Level) - infos.Add(flattened[i]); - } - - return infos; - } - - private List GetTopmostSplits() - { - var list = infoStack.ToList(); - list.Reverse(); - return list; - } - - private int FirstSubsplitPos() - { - for(int i = currentSplit; i >= 0; i--) - { - if(flattened[i].Level < CurrentFlatSplit.Level) - return i + 1; - } - return 0; - } - - private int LastSubsplitPos() - { - for(int i = currentSplit; i < flattened.Length; i++) - { - if(flattened[i].Level < CurrentFlatSplit.Level) - return i - 1; - } - return flattened.Length - 1; + return controller.GetSplits(count); } public RunInfo GetRunInfo() { if(game == null) - return new RunInfo("unnamed", "unnamed", flattened.Last().Times, flattened); - return new RunInfo(game!.Name, Platform.SharedPlatform.CurrentCategory!.Name, flattened.Last().Times, flattened); + return new RunInfo("unnamed", "unnamed", controller.AllSplits.Last().Times, controller.AllSplits.Select(x => x.ToSplitInfo()).ToArray()); + return new RunInfo(game!.Name, Platform.SharedPlatform.CurrentCategory!.Name, controller.AllSplits.Last().Times, controller.AllSplits.Select(x => x.ToSplitInfo()).ToArray()); } private void SaveRun() { var tm = game.DefaultTimingMethod; - if(comparison == null || flattened.Last().Times[tm] < comparison!.Times[tm]) + if(comparison == null || controller.AllSplits.Last().Times[tm] < comparison!.TotalTimes[tm]) { Platform.SharedPlatform.SaveRunAsPB(GetRunInfo()); } @@ -322,73 +97,19 @@ private void SaveRun() public void UndoSplit() { - while(PreviousFlatSplit != null) - { - flattened[currentSplit].IsCurrent = false; - currentSplit--; - flattened[currentSplit].Times.Reset(); - flattened[currentSplit].DeltaTimes.Reset(); - - if(NextFlatSplit!.Value.Level <= CurrentFlatSplit.Level) - { - break; - } - } - flattened[currentSplit].IsCurrent = true; - ResetInfoStack(); - - // Reset infoStack somehow - foreach(var i in infoStack.AsEnumerable()) - { - i.Times.Reset(); - i.DeltaTimes.Reset(); - } - } + if(!Started || IsFinished) + return; - private void ResetInfoStack() - { - var upTo = currentSplit; - currentSplit = 0; - infoStack = new(); - while(currentSplit != upTo) - { - NextSplitNoUpdate(); - } + controller.UndoSplit(); } private RunInfo? comparison; private Game game; + private Category category; - private Stack infoStack = new(); + SplitsController controller; - private SplitDisplayInfo? NextFlatSplit => currentSplit >= flattened.Length - 1 ? null : flattened[currentSplit + 1]; - private SplitDisplayInfo CurrentFlatSplit => flattened[currentSplit]; - private SplitDisplayInfo? PreviousFlatSplit => currentSplit <= 0 ? null : flattened[currentSplit - 1]; - - public SplitDisplayInfo? PreviousSplit - { - get - { - if(PreviousFlatSplit == null) - return null; - - int i = currentSplit; - while(i > 0) - { - i--; - - if(flattened[i + 1].Level <= flattened[i].Level) - { - break; - } - } - - if(i >= 0) - return flattened[i]; - - return null; - } - } + public SplitDisplayInfo? PreviousSplit => controller.PreviousSplit; const int weight = 75; diff --git a/SpeedTool/SpeedTool.csproj b/SpeedTool/SpeedTool.csproj index ad310ef..fa00fe4 100644 --- a/SpeedTool/SpeedTool.csproj +++ b/SpeedTool/SpeedTool.csproj @@ -8,7 +8,7 @@ true False true - 0.2.0 + 0.2.1 true NU1701 diff --git a/SpeedTool/Splits/Category.cs b/SpeedTool/Splits/Category.cs index 137c1c8..144e59f 100644 --- a/SpeedTool/Splits/Category.cs +++ b/SpeedTool/Splits/Category.cs @@ -1,7 +1,3 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using SpeedTool.Util; - namespace SpeedTool.Splits; public class Category @@ -12,49 +8,9 @@ public Category(string name, Split[] splits) Splits = splits; } - public JsonObject ToJson() - { - JsonObject o = new(); - o["Name"] = Name; - o["RunsCount"] = RunsCount; - o["Splits"] = SerializeSplits(); - return o; - } - - public static Category FromJson(JsonObject obj) - { - if(!obj.ContainsKey("Splits")) - throw new FormatException(); - var splits = obj["Splits"]!.AsArray(); - var spl = new Split[splits.Count]; - for(int i = 0; i < spl.Length; i++) - { - spl[i] = Split.FromJson(splits[i]!.AsObject()); - } - - return new Category(obj.EnforceGetString("Name"), spl); - } - - public string Name { get; private set; } + public string Name { get; set; } - public Split[] Splits { get; private set; } + public Split[] Splits { get; set; } - public int RunsCount { get; private set; } - - private JsonArray SerializeSplits() - { - JsonArray arr = new(); - for(int i = 0; i < Splits.Length; i++) - { - arr.Add((JsonNode)Splits[i].ToJson()); - } - - return arr; - } - - private static void EnforceField(JsonObject obj, string name) - { - if(obj[name] == null) - throw new JsonException(); - } + public int RunsCount { get; set; } } diff --git a/SpeedTool/Splits/Game.cs b/SpeedTool/Splits/Game.cs index 854a13a..9050307 100644 --- a/SpeedTool/Splits/Game.cs +++ b/SpeedTool/Splits/Game.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Text.Json; using System.Text.Json.Nodes; using SpeedTool.Timer; using SpeedTool.Util; @@ -100,7 +101,7 @@ public static Game LoadFromFile(string path) g.categories = new Category[categories.Length]; for(int i = 0; i < categories.Length; i++) { - g.categories[i] = Category.FromJson(JSONHelper.EnforceParseAsObject(categories[i].AsText())); + g.categories[i] = (JsonSerializer.Deserialize(categories[i].AsText(), typeof(Category), SourceGeneratorContext.Default) as Category)!; } g.source.Dispose(); @@ -118,7 +119,7 @@ public Category[] GetCategories() private JsonObject GetCategoryJson(int idx) { - return categories[idx].ToJson(); + return JsonSerializer.SerializeToNode(categories[idx], typeof(Category), SourceGeneratorContext.Default)!.AsObject(); } private JsonObject GetMetaJson() diff --git a/SpeedTool/Splits/RunCollection.cs b/SpeedTool/Splits/RunCollection.cs index a74f398..4c0f51c 100644 --- a/SpeedTool/Splits/RunCollection.cs +++ b/SpeedTool/Splits/RunCollection.cs @@ -11,7 +11,7 @@ public RunInfo BestRun { if(!HasRuns) throw new Exception("Collection was empty"); - return runs.OrderBy(x => x.Times[Timer.TimingMethod.RealTime]).FirstOrDefault()!; + return runs.OrderBy(x => x.TotalTimes[Timer.TimingMethod.RealTime]).FirstOrDefault()!; } } diff --git a/SpeedTool/Splits/RunInfo.cs b/SpeedTool/Splits/RunInfo.cs index 9147aac..afd502c 100644 --- a/SpeedTool/Splits/RunInfo.cs +++ b/SpeedTool/Splits/RunInfo.cs @@ -1,41 +1,18 @@ -using System.Text.Json.Nodes; -using SpeedTool.Util; - namespace SpeedTool.Splits; public class RunInfo { - public RunInfo(string name, string cat, TimeCollection times, SplitDisplayInfo[] infos) + public RunInfo(string name, string cat, TimeCollection times, SplitInfo[] infos) { CategoryName = cat; GameName = name; - Times = times; + TotalTimes = times; Splits = infos; } - public JsonObject ToJson() - { - JsonObject o = new(); - o["CategoryName"] = CategoryName; - o["Times"] = Times.ToJson(); - o["GameName"] = GameName; - - JsonArray arr = new(); - for(int i = 0; i < Splits.Length; i++) - arr.Add((JsonNode)Splits[i].ToJson()); - o["Splits"] = arr; - return o; - } - - public static RunInfo FromJson(JsonObject o) - { - RunInfo ret = new(o.EnforceGetString("GameName"), o.EnforceGetString("CategoryName"), new TimeCollection(o["Times"]!.AsObject()), SplitDisplayInfo.DeserializeJsonArray(o["Splits"]!.AsArray())); - return ret; - } - - public string CategoryName { get; private set; } - public string GameName { get; private set; } - public TimeCollection Times { get; private set; } + public string CategoryName { get; set; } + public string GameName { get; set; } + public TimeCollection TotalTimes { get; set; } - public SplitDisplayInfo[] Splits { get; private set; } + public SplitInfo[] Splits { get; set; } } diff --git a/SpeedTool/Splits/Split.cs b/SpeedTool/Splits/Split.cs index b1e7866..5df75f0 100644 --- a/SpeedTool/Splits/Split.cs +++ b/SpeedTool/Splits/Split.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using SpeedTool.JSON; using SpeedTool.Util; namespace SpeedTool.Splits; @@ -12,32 +13,14 @@ public Split(string name) Name = name; } - public JsonObject ToJson() - { - JsonObject o = new(); - o["Name"] = Name; - if(Subsplits != null && Subsplits.Length != 0) - { - o["Subsplits"] = SerializeSubsplits(); - } - - return o; - } - - public static Split FromJson(JsonObject obj) - { - Split spl = new Split(obj.EnforceGetString("Name")); - if(obj.ContainsKey("Subsplits")) - { - spl.LoadSubsplitsFromJson(obj["Subsplits"]!.AsArray()); - } - return spl; - } - + [JsonInclude] public string Name; + [JsonInclude] public Split[] Subsplits; + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] public TimeCollection SplitTimes; public SplitDisplayInfo[] Flatten() @@ -55,26 +38,6 @@ public void InsertSplit(int idx, Split split) Subsplits = Subsplits.InsertAt(idx, split); } - private void LoadSubsplitsFromJson(JsonArray array) - { - var len = array.Count; - Subsplits = new Split[len]; - for(int i = 0; i < len; i++) - { - Subsplits[i] = Split.FromJson(array[i]!.AsObject()); - } - } - - private JsonArray SerializeSubsplits() - { - JsonArray array = new(); - for(int i = 0; i < Subsplits.Length; i++) - { - array.Add((JsonNode)Subsplits[i].ToJson()); - } - return array; - } - private SplitDisplayInfo[] Flatten(int level) { List list = [new SplitDisplayInfo(Name, false, level)]; diff --git a/SpeedTool/Splits/SplitDisplayInfo.cs b/SpeedTool/Splits/SplitDisplayInfo.cs index 3a582b8..324d7da 100644 --- a/SpeedTool/Splits/SplitDisplayInfo.cs +++ b/SpeedTool/Splits/SplitDisplayInfo.cs @@ -1,9 +1,9 @@ -using System.Text.Json.Nodes; -using SpeedTool.Util; +using System.Text.Json.Serialization; +using SpeedTool.JSON; namespace SpeedTool.Splits; -public struct SplitDisplayInfo +public class SplitDisplayInfo { public SplitDisplayInfo(string name, bool active, int level) { @@ -19,30 +19,15 @@ public SplitDisplayInfo(Split s) Level = 0; } - public static SplitDisplayInfo FromJsonObject(JsonObject o) + public SplitInfo ToSplitInfo() { - SplitDisplayInfo ret = new SplitDisplayInfo(o.EnforceGetString("DisplayString"), false, (int)o["Level"]!); - ret.Times = new TimeCollection(o["Times"]!.AsObject()); - ret.DeltaTimes = new TimeCollection(o["DeltaTimes"]!.AsObject()); - return ret; - } - - public static JsonArray SerializeMany(SplitDisplayInfo[] splits) - { - JsonArray ret = new(); - for(int i = 0; i < splits.Length; i++) - ret.Add((JsonNode)splits[i].ToJson()); - - return ret; - } - - public static SplitDisplayInfo[] DeserializeJsonArray(JsonArray array) - { - var count = array.Count; - SplitDisplayInfo[] ret = new SplitDisplayInfo[count]; - for(int i = 0; i < count; i++) - ret[i] = FromJsonObject(array[i]!.AsObject()); - return ret; + return new SplitInfo() + { + DeltaTime = DeltaTimes, + Name = DisplayString, + SegmentTime = SegmentTimes, + TotalTime = Times + }; } /// @@ -57,20 +42,18 @@ public static SplitDisplayInfo[] DeserializeJsonArray(JsonArray array) public string DisplayString { get; private set; } + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] public TimeCollection DeltaTimes = new(); + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] public TimeCollection Times = new(); - public TimeCollection PBTimes = new(); + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] + public TimeCollection SegmentTimes = new(); - public JsonObject ToJson() - { - JsonObject o = new(); - o["Level"] = Level; - o["DisplayString"] = DisplayString; - o["DeltaTimes"] = DeltaTimes.ToJson(); - o["Times"] = Times.ToJson(); - - return o; - } + [JsonInclude] + public SplitInfo PBSplit = new(); } diff --git a/SpeedTool/Splits/SplitInfo.cs b/SpeedTool/Splits/SplitInfo.cs new file mode 100644 index 0000000..ce69866 --- /dev/null +++ b/SpeedTool/Splits/SplitInfo.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; +using SpeedTool.JSON; + +namespace SpeedTool.Splits; + +public struct SplitInfo +{ + public SplitInfo() { } + + [JsonInclude] + public string Name = ""; + + [JsonInclude] + public int Level = 0; + + /// + /// Time spent in this segment + /// + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] + public TimeCollection SegmentTime = new(); + + /// + /// Difference from previous run + /// + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] + public TimeCollection DeltaTime = new(); + + /// + /// Total time spent to arrive at that split + /// + [JsonInclude] + [JsonConverter(typeof(TimeCollectionConverter))] + public TimeCollection TotalTime = new(); +} diff --git a/SpeedTool/Splits/SplitsController.cs b/SpeedTool/Splits/SplitsController.cs new file mode 100644 index 0000000..1115a20 --- /dev/null +++ b/SpeedTool/Splits/SplitsController.cs @@ -0,0 +1,338 @@ +using System.Diagnostics.CodeAnalysis; +using SpeedTool.Timer; +using SpeedTool.Util; + +namespace SpeedTool.Splits; + +public sealed class SplitsController +{ + public SplitsController(Category category, RunInfo? comparisonRun) + { + FlattenSplits(category); + + if(comparisonRun != null && comparisonRun.Splits.Length == flattened.Length) + { + for(int i = 0; i < comparisonRun.Splits.Length; i++) + flattened[i].PBSplit = comparisonRun.Splits[i]; + comparison = comparisonRun; + } + } + + public SplitDisplayInfo CurrentSplit => flattened[Math.Max(0, currentSplitId)]; + + public SplitDisplayInfo[] AllSplits => flattened; + + public SplitDisplayInfo? PreviousSplit + { + get + { + if(PreviousFlatSplit == null) + return null; + + int i = currentSplitId; + while(i > 0) + { + i--; + + if(flattened[i + 1].Level <= flattened[i].Level) + { + break; + } + } + + if(i >= 0) + return flattened[i]; + + return null; + } + } + + private SplitDisplayInfo[] flattened; + + [MemberNotNull(nameof(flattened))] + [MemberNotNull(nameof(zeroLevelSplits))] + private void FlattenSplits(Category category) + { + List f = new(); + List zero = new(); + foreach(var split in category.Splits) + { + zero.Add(f.Count); + f.AddRange(split.Flatten()); + } + + flattened = f.ToArray(); + zeroLevelSplits = zero.ToArray(); + } + + public void Start() + { + currentSplitId = -1; + NextSplit(); + } + + public bool NextSplit() + { + // Write split time + if(currentSplitId >= 0) + { + flattened[currentSplitId].IsCurrent = false; + for(int i = 0; i < (int)TimingMethod.Last; i++) + { + var tm = (TimingMethod)i; + var time = Platform.Platform.SharedPlatform.GetTimerFor(tm).CurrentTime; + flattened[currentSplitId].Times[tm] = time; + // TODO: Fill segment times + } + if(comparison != null) + flattened[currentSplitId].DeltaTimes = flattened[currentSplitId].Times - comparison.Splits[currentSplitId].TotalTime; + } + + currentSplitId++; + if(currentSplitId >= flattened.Length) + { + currentSplitId--; + return false; + } + + // Roll over parent splitts and write times for them + while(infoStack.Count > 0 && infoStack.Peek().Split.Level >= flattened[currentSplitId].Level) + { + var p = infoStack.Pop(); + for(int i = 0; i < (int)TimingMethod.Last; i++) + { + var tm = (TimingMethod)i; + p.Split.Times[tm] = Platform.Platform.SharedPlatform.GetTimerFor(tm).CurrentTime; + } + if(comparison != null) + p.Split.DeltaTimes = flattened[currentSplitId].Times - comparison.Splits[currentSplitId].TotalTime; + } + + // Roll over to the first actual split in the tree + while(NextFlatSplit != null && CurrentFlatSplit.Level < NextFlatSplit.Level) + { + infoStack.Push(new(flattened[currentSplitId], Platform.Platform.SharedPlatform.GetCurrentTimes())); + currentSplitId++; + } + + // If split has subsplits, go to next split + if(NextFlatSplit != null && NextFlatSplit.Level > CurrentFlatSplit.Level) + { + if(!NextSplit()) + return false; + } + + flattened[currentSplitId].IsCurrent = true; + return true; + } + + public void UndoSplit() + { + while(PreviousFlatSplit != null) + { + flattened[currentSplitId].IsCurrent = false; + currentSplitId--; + flattened[currentSplitId].Times.Reset(); + flattened[currentSplitId].DeltaTimes.Reset(); + + if(NextFlatSplit!.Level <= CurrentFlatSplit.Level) + { + break; + } + } + flattened[currentSplitId].IsCurrent = true; + ResetInfoStack(); + + // Reset infoStack somehow + foreach(var i in infoStack.AsEnumerable()) + { + i.Split.Times.Reset(); + i.Split.DeltaTimes.Reset(); + } + } + + public void SkipSplit() + { + if(currentSplitId == flattened.Length - 1) + return; + flattened[currentSplitId].IsCurrent = false; + // Write split time + currentSplitId++; + + // Roll over parent splitts + while(infoStack.Count > 0 && infoStack.Peek().Split.Level >= flattened[currentSplitId].Level) + { + var p = infoStack.Pop(); + } + + // Roll over to the first actual split in the tree + while(NextFlatSplit != null && CurrentFlatSplit.Level < NextFlatSplit.Level) + { + infoStack.Push(new(flattened[currentSplitId], Platform.Platform.SharedPlatform.GetCurrentTimes())); + currentSplitId++; + } + + // If split has subsplits, go to next split + if(NextFlatSplit != null && NextFlatSplit.Level > CurrentFlatSplit.Level) + { + SkipSplit(); + } + + flattened[currentSplitId].IsCurrent = true; + } + + public IEnumerable GetSplits(int count) + { + // Honestly, I wrote this function when I was high on sleep deprevation and eye disease, + // so it might be difficult to understand the hell is going on here. + // I'm trying to explain it as an aftermath with clear head, so don't mind me + + // This function has a `wieght` parameter to decide how to value splits from each side. + // TODO: This weight feature doesn't really work well, it should probably be changed to something else + var currentLevelSplits = GetCurrentLevelSplits(); + var posInCurrentLevel = currentLevelSplits.IndexOf(CurrentFlatSplit); + + // If on level 0, just get enough splits to display + if(CurrentFlatSplit.Level == 0) + { + return currentLevelSplits.TakeAtPosWeighted(count, posInCurrentLevel, weight); + } + + // Always display splits tree on top + var topmostSplits = GetTopmostSplits(); + var topmostCount = Math.Min(count - 1, topmostSplits.Count); + + // If we have enough tree to fill in the requested space, do that + if(topmostCount == count - 1) + return topmostSplits.TakeLast(topmostCount).Append(CurrentFlatSplit); + + var currentLevelCount = Math.Min(currentLevelSplits.Count, count - topmostCount); + + // If tree + current level fits the space, do that + if(currentLevelCount + topmostCount >= count) + { + return topmostSplits.TakeLast(topmostCount).Concat(currentLevelSplits.TakeAtPosWeighted(currentLevelCount, posInCurrentLevel, weight)); + } + + var middle = topmostSplits.TakeLast(topmostCount).Concat(currentLevelSplits.TakeAtPosWeighted(currentLevelCount, posInCurrentLevel, weight)); + + var zeroLevelCount = count - currentLevelCount - topmostCount; + + zeroLevelSplits.Select(x => flattened[x]).TakeAtPosWeighted(zeroLevelCount, 1, weight); + + // Figure out next and previous splits to fit the space + List prev = new(); + List next = new(); + int i = 0; + while(i < zeroLevelSplits.Length && zeroLevelSplits[i] < currentSplitId) + { + if(zeroLevelSplits[i] == currentSplitId) + { + i++; + continue; + } + prev.Add(flattened[zeroLevelSplits[i]]); + i++; + } + prev = prev.Take(prev.Count - 1).ToList(); + while(i < zeroLevelSplits.Length) + { + if(zeroLevelSplits[i] == currentSplitId) + { + i++; + continue; + } + next.Add(flattened[zeroLevelSplits[i]]); + i++; + } + + // Figure out how many splits to take from the left and from the right + var wantRight = (int)(weight / 100.0 * zeroLevelCount); + wantRight = Math.Min(next.Count, wantRight); + + var wantLeft = zeroLevelCount - wantRight; + wantLeft = Math.Min(wantLeft, prev.Count); + + return prev.TakeLast(wantLeft).Concat(middle).Concat(next.Take(wantRight)); + } + + private void ResetInfoStack() + { + var upTo = currentSplitId; + currentSplitId = 0; + infoStack = new(); + while(currentSplitId != upTo) + { + SkipSplit(); + } + } + + private List GetCurrentLevelSplits() + { + var begin = FirstSubsplitPos(); + var end = LastSubsplitPos(); + + List infos = new(); + + for(int i = begin; i <= end; i++) + { + if(flattened[i].Level == CurrentFlatSplit.Level) + infos.Add(flattened[i]); + } + + return infos; + } + + private List GetTopmostSplits() + { + return infoStack.Reverse().Select(x => x.Split).ToList(); + } + + private int FirstSubsplitPos() + { + for(int i = currentSplitId; i >= 0; i--) + { + if(flattened[i].Level < CurrentFlatSplit.Level) + return i + 1; + } + return 0; + } + + private int LastSubsplitPos() + { + if(currentSplitId < 0) + return 0; + for(int i = currentSplitId; i < flattened.Length; i++) + { + if(flattened[i].Level < CurrentFlatSplit.Level) + return i - 1; + } + return flattened.Length - 1; + } + + class SplitStackInfo + { + public SplitStackInfo(SplitDisplayInfo s, TimeCollection times) + { + Split = s; + StartTimes = times; + } + + public SplitDisplayInfo Split; + public TimeCollection StartTimes; + } + + private Stack infoStack = new(); + + private SplitDisplayInfo? NextFlatSplit => currentSplitId >= flattened.Length - 1 ? null : flattened[currentSplitId + 1]; + private SplitDisplayInfo CurrentFlatSplit => flattened[Math.Max(0, currentSplitId)]; + private SplitDisplayInfo? PreviousFlatSplit => currentSplitId <= 0 ? null : flattened[currentSplitId - 1]; + + int currentSplitId = 0; + + private RunInfo? comparison; + + private int[] zeroLevelSplits; + + const int weight = 75; +} diff --git a/SpeedTool/Splits/TimeCollection.cs b/SpeedTool/Splits/TimeCollection.cs index bf1442e..2dfd0ab 100644 --- a/SpeedTool/Splits/TimeCollection.cs +++ b/SpeedTool/Splits/TimeCollection.cs @@ -1,5 +1,3 @@ -using System.Numerics; -using System.Text.Json.Nodes; using SpeedTool.Timer; namespace SpeedTool.Splits; @@ -11,14 +9,6 @@ public TimeCollection() } - public TimeCollection(JsonObject o) - { - for(int i = 0; i < TIMES_COUNT; i++) - { - spans[i] = new TimeSpan((long)o["spans"]!.AsArray()[i]!); - } - } - public TimeSpan this[TimingMethod timingMethod] { get @@ -43,19 +33,6 @@ public TimeSpan this[TimingMethod timingMethod] return res; } - public JsonObject ToJson() - { - JsonObject o = new(); - JsonArray a = new(); - for(int i = 0; i < TIMES_COUNT; i++) - { - a.Add((JsonNode)spans[i].Ticks); - } - - o["spans"] = a; - return o; - } - public void Reset() { for(int i = 0; i < TIMES_COUNT; i++) diff --git a/SpeedTool/Windows/TimerUI/Classic.cs b/SpeedTool/Windows/TimerUI/Classic.cs index 11a3ddd..4cb9894 100644 --- a/SpeedTool/Windows/TimerUI/Classic.cs +++ b/SpeedTool/Windows/TimerUI/Classic.cs @@ -101,9 +101,9 @@ public override void DoUI(ISplitsSource splits, ITimerSource source) { return (ColorsConfig.TextColor, displayInfo.Times[DisplayTimingMethod].ToSpeedToolTimerString()); } - else if(displayInfo.PBTimes[DisplayTimingMethod].Ticks != 0) + else if(displayInfo.PBSplit.TotalTime[DisplayTimingMethod].Ticks != 0) { - return (ColorsConfig.TextColor, displayInfo.PBTimes[DisplayTimingMethod].ToSpeedToolTimerString()); + return (ColorsConfig.TextColor, displayInfo.PBSplit.TotalTime[DisplayTimingMethod].ToSpeedToolTimerString()); } return (ColorsConfig.TextColor, ""); diff --git a/SpeedTool/Windows/TimerUI/SpeedTool.cs b/SpeedTool/Windows/TimerUI/SpeedTool.cs index 2533fa2..e568a63 100644 --- a/SpeedTool/Windows/TimerUI/SpeedTool.cs +++ b/SpeedTool/Windows/TimerUI/SpeedTool.cs @@ -85,7 +85,7 @@ public override void DoUI(ISplitsSource splits, ITimerSource timer) if(timer.CurrentState == TimerState.Finished) DrawDtText(splits.CurrentSplit); else if(splits.PreviousSplit != null) - DrawDtText(splits.PreviousSplit!.Value); + DrawDtText(splits.PreviousSplit!); DrawTimeText(timer); }