Skip to content

Instantly share code, notes, and snippets.

@minisbett
Created June 16, 2024 11:50
Show Gist options
  • Select an option

  • Save minisbett/843d5ecc1bd8401198c6d8c37fa74ae5 to your computer and use it in GitHub Desktop.

Select an option

Save minisbett/843d5ecc1bd8401198c6d8c37fa74ae5 to your computer and use it in GitHub Desktop.
hold times
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