Created
June 16, 2024 11:50
-
-
Save minisbett/843d5ecc1bd8401198c6d8c37fa74ae5 to your computer and use it in GitHub Desktop.
hold times
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using Discord; | |
| using Discord.Interactions; | |
| using huisbot.Models.Osu; | |
| using huisbot.Services; | |
| using huisbot.Utilities; | |
| using huisbot.Utilities.Discord; | |
| using OsuParsers.Decoders; | |
| using OsuParsers.Enums; | |
| using OsuParsers.Enums.Replays; | |
| using OsuParsers.Replays; | |
| using OsuParsers.Replays.Objects; | |
| using ScottPlot; | |
| using ScottPlot.Renderable; | |
| using System.Text; | |
| using Color = System.Drawing.Color; | |
| using ImageFormat = System.Drawing.Imaging.ImageFormat; | |
| namespace huisbot.Modules.Miscellaneous; | |
| public partial class HoldTimeCommandModule : ModuleBase | |
| { | |
| private readonly OsuApiService _osu; | |
| public HoldTimeCommandModule(OsuApiService osu) => _osu = osu; | |
| [SlashCommand("holdtimes", "Calculates the hold times of a replay.")] | |
| public async Task HandleHoldTimesAsync( | |
| [Summary("replay", "The replay.")] Attachment? replayAttachment = null, | |
| [Summary("score_id_kokisu", "The kokisu score ID.")] int? kokisuScoreId = null) | |
| { | |
| await DeferAsync(); | |
| if(replayAttachment is null && kokisuScoreId is null) | |
| { | |
| await FollowupAsync(embed: Embeds.Error("Either a replay or a kokisu score ID must be specified.")); | |
| return; | |
| } | |
| byte[] data = await new HttpClient().GetByteArrayAsync(replayAttachment is not null ? replayAttachment.Url : $"https://api.kokisu.moe/v1/get_replay?id={kokisuScoreId}"); | |
| Replay replay = ReplayDecoder.Decode(new MemoryStream(data)); | |
| int offset = 0; | |
| List<(int offset, bool pressed)> key1 = new List<(int offset, bool pressed)>(); | |
| List<(int offset, bool pressed)> key2 = new List<(int offset, bool pressed)>(); | |
| foreach (ReplayFrame frame in replay.ReplayFrames) | |
| { | |
| offset += frame.TimeDiff; | |
| key1.Add((offset, frame.StandardKeys.HasFlag(StandardKeys.K1))); | |
| key2.Add((offset, frame.StandardKeys.HasFlag(StandardKeys.K2))); | |
| } | |
| List<int> holdTimes1 = new List<int>(); | |
| List<int> holdTimes2 = new List<int>(); | |
| (int offset, bool pressed)? pressedFrame = null; | |
| foreach (var frame in key1) | |
| { | |
| if (pressedFrame is null) | |
| { | |
| if (frame.pressed) | |
| pressedFrame = frame; | |
| } | |
| else if (!frame.pressed) | |
| { | |
| holdTimes1.Add(frame.offset - pressedFrame.Value.offset); | |
| pressedFrame = null; | |
| } | |
| } | |
| pressedFrame = null; | |
| foreach (var frame in key2) | |
| { | |
| if (pressedFrame is null) | |
| { | |
| if (frame.pressed) | |
| pressedFrame = frame; | |
| } | |
| else if (!frame.pressed) | |
| { | |
| holdTimes2.Add(frame.offset - pressedFrame.Value.offset); | |
| pressedFrame = null; | |
| } | |
| } | |
| // Group the hold times into the amount of times a hold time occured | |
| Dictionary<int, int> holdTimeDistribution1 = new Dictionary<int, int>(); | |
| foreach (int holdTime in holdTimes1) | |
| { | |
| if (holdTimeDistribution1.ContainsKey(holdTime)) | |
| holdTimeDistribution1[holdTime]++; | |
| else | |
| holdTimeDistribution1[holdTime] = 1; | |
| } | |
| Dictionary<int, int> holdTimeDistribution2 = new Dictionary<int, int>(); | |
| foreach (int holdTime in holdTimes2) | |
| { | |
| if (holdTimeDistribution2.ContainsKey(holdTime)) | |
| holdTimeDistribution2[holdTime]++; | |
| else | |
| holdTimeDistribution2[holdTime] = 1; | |
| } | |
| // Sort the hold time distribution by the amount of times a hold time occured | |
| var sortedHoldTimeDistribution1 = holdTimeDistribution1.OrderBy(x => x.Key); | |
| var sortedHoldTimeDistribution2 = holdTimeDistribution2.OrderBy(x => x.Key); | |
| var xs1 = sortedHoldTimeDistribution1.Select(x => x.Key * 1d).ToArray(); | |
| var ys1 = sortedHoldTimeDistribution1.Select(x => x.Value * 1d).ToArray(); | |
| var xs2 = sortedHoldTimeDistribution2.Select(x => x.Key * 1d).ToArray(); | |
| var ys2 = sortedHoldTimeDistribution2.Select(x => x.Value * 1d).ToArray(); | |
| // filter out all hold times more than 100ms | |
| var filteredXs1 = new List<double>(); | |
| var filteredYs1 = new List<double>(); | |
| for (int i = 0; i < xs1.Length; i++) | |
| { | |
| if (xs1[i] < 100) | |
| { | |
| filteredXs1.Add(xs1[i]); | |
| filteredYs1.Add(ys1[i]); | |
| } | |
| } | |
| var filteredXs2 = new List<double>(); | |
| var filteredYs2 = new List<double>(); | |
| for (int i = 0; i < xs2.Length; i++) | |
| { | |
| if (xs2[i] < 100) | |
| { | |
| filteredXs2.Add(xs2[i]); | |
| filteredYs2.Add(ys2[i]); | |
| } | |
| } | |
| OsuBeatmap beatmap = (await _osu.GetBeatmapAsync(hash: replay.BeatmapMD5Hash))!; | |
| List<string> mods = new List<string>(); | |
| foreach(OsuParsers.Enums.Mods mod in Enum.GetValues<OsuParsers.Enums.Mods>()) | |
| if(replay.Mods.HasFlag(mod)) | |
| mods.Add(mod.ToString()); | |
| mods.Remove("None"); | |
| if(mods.Contains("Nightcore")) | |
| mods.Remove("DoubleTime"); | |
| Plot liveLocal = new Plot(1000, 600); | |
| liveLocal.Style(Color.White, Color.White, Color.Gray, null, null, Color.Black); | |
| liveLocal.Title($"{replay.PlayerName} on {beatmap.Title} [{beatmap.Version}] +{string.Join(", ", mods)}"); | |
| liveLocal.LeftAxis.Label("Amount"); | |
| liveLocal.BottomAxis.Label($"Milliseconds"); | |
| liveLocal.AddBar(filteredYs1.ToArray(), filteredXs1.ToArray(), Color.FromArgb(100, 121, 159, 203)); | |
| liveLocal.AddBar(filteredYs2.ToArray(), filteredXs2.ToArray(), Color.FromArgb(100, 249, 102, 94)); | |
| liveLocal.SetAxisLimits(yMin: 0); | |
| Legend legend = liveLocal.Legend(true, Alignment.UpperCenter); | |
| legend.Orientation = Orientation.Horizontal; | |
| legend.FillColor = Color.FromArgb(63, 66, 66); | |
| legend.FontColor = Color.White; | |
| legend.OutlineColor = Color.Transparent; | |
| legend.ShadowColor = Color.Transparent; | |
| // convert the sortedHoldTimeDistribution1 & 2 into a string | |
| string htd1s = string.Join("\n", sortedHoldTimeDistribution1.Select(x => $"{x.Key}ms: {x.Value}")); | |
| string htd2s = string.Join("\n", sortedHoldTimeDistribution2.Select(x => $"{x.Key}ms: {x.Value}")); | |
| string file = $"**Hold Time Distribution 1**\n{htd1s}\n\n\n**Hold Time Distribution 2**\n{htd2s}"; | |
| // Render the plot to a bitmap and send it. | |
| using MemoryStream ms = new MemoryStream(); | |
| liveLocal.Render().Save(ms, ImageFormat.Png); | |
| await FollowupWithFilesAsync(new FileAttachment[] | |
| { | |
| new FileAttachment(new MemoryStream(Encoding.Default.GetBytes(file)), "holdtimes.txt"), | |
| new FileAttachment(ms, "plot.png"), | |
| }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment