public bool Load(string replayFile, string beatmapFile) { Debug.Print("Loading Replay file..."); Replay = replayFile == null?LoadReplay() : new Replay(replayFile); if (Replay == null) { return(false); } Debug.Print("Loaded replay {0}", Replay.Filename); Debug.Print("Loading Beatmap file..."); Beatmap = beatmapFile == null?LoadBeatmap(Replay) : new Beatmap(beatmapFile); if (Beatmap == null) { return(false); } Debug.Print("Loaded beatmap {0}", Beatmap.Filename); Debug.Print("Analyzing... "); Debug.Print(Replay.ReplayFrames.Count.ToString()); ReplayAnalyzer = new ReplayAnalyzer(Beatmap, Replay); if (ReplayAnalyzer.misses.Count == 0) { return(false); } return(true); }
public async Task <string> Load() { Debug.Print("Loading Replay file..."); Replay = ReplayFile == null ? await LoadReplay() : new Replay(ReplayFile); if (Replay == null) { return("Couldn't find replay"); } Debug.Print("Loaded replay {0}", Replay.Filename); Debug.Print("Loading Beatmap file..."); Beatmap ??= BeatmapFile == null ? await LoadBeatmap(Replay) : new Beatmap(BeatmapFile); if (Beatmap == null) { return("Couldn't find beatmap"); } Debug.Print("Loaded beatmap {0}", Beatmap.Filename); Debug.Print("Analyzing... "); ReplayAnalyzer = new ReplayAnalyzer(Beatmap, Replay); if (ReplayAnalyzer.misses.Count == 0) { return("No misses found"); } return(null); }
void ReadFile() { ReplayAnalyzer replayAnalyzer = new ReplayAnalyzer(File.OpenRead("demo.dem")); replayAnalyzer.Start(); bool breakpoint = true; //Set Breakpoint to Read Replay Analyze Class }
public string SaveText(Beatmap map = null) { StringBuilder sb = new StringBuilder(); sb.AppendLine(ToString()); sb.AppendLine("Count 300: " + Count300); sb.AppendLine("Count 100: " + Count100); sb.AppendLine("Count 50: " + Count50); sb.AppendLine("Count Geki: " + CountGeki); sb.AppendLine("Count Katu: " + CountKatu); sb.AppendLine("Count Miss: " + CountMiss); sb.AppendLine("Total Score: " + TotalScore); sb.AppendLine("Max Combo: " + MaxCombo); sb.AppendLine("Is fullcombo: " + IsPerfect); sb.AppendLine("Mods: " + Mods.ToString()); List <HitFrame> hits = null; List <HitFrame> attemptedHits = null; if (!ReferenceEquals(map, null)) { var analyzer = new ReplayAnalyzer(map, this); hits = analyzer.hits; attemptedHits = analyzer.attemptedHits; } int hitIndex = 0; int attemptedHitIndex = 0; for (int i = 0; i < ReplayFrames.Count; i++) { if (!ReferenceEquals(hits, null) && hitIndex < hits.Count && hits[hitIndex].frame.Time == ReplayFrames[i].Time) { sb.AppendLine(ReplayFrames[i].ToString() + " " + hits[hitIndex].ToString()); ++hitIndex; continue; } if (!ReferenceEquals(attemptedHits, null) && attemptedHitIndex < attemptedHits.Count && attemptedHits[attemptedHitIndex].frame.Time == ReplayFrames[i].Time) { sb.AppendLine(ReplayFrames[i].ToString() + " " + attemptedHits[attemptedHitIndex].note.ToString()); ++attemptedHitIndex; continue; } sb.AppendLine(ReplayFrames[i].ToString()); } return(sb.ToString()); }
/// <summary> /// Draws the miss. /// </summary> /// <returns>A Bitmap containing the drawing</returns> /// <param name="num">Index of the miss as it shows up in r.misses.</param> public Image DrawHitObject(int num, Rectangle area) { Image img = new Image <Rgba32>(area.Width, area.Height, ColorScheme.BackgroundColor); img.Mutate(g => { bool hr = Replay.Mods.HasFlag(Mods.HardRock); CircleObject hitObject; if (drawAllHitObjects) { hitObject = Beatmap.HitObjects[num]; } else { hitObject = ReplayAnalyzer.misses[num]; } bool isMiss = !drawAllHitObjects || ReplayAnalyzer.misses.Contains(hitObject); float radius = (float)hitObject.Radius; Func <Color, Pen> circlePen = color => new Pen(color, radius * 2 / scale) { EndCapStyle = EndCapStyle.Round, JointStyle = JointStyle.Round, }; Func <Color, Pen> linePen = color => new Pen(color, 1.5f); RectangleF bounds = new RectangleF(PointF.Subtract(hitObject.Location.ToPointF(), Scale(area.Size, scale / 2)), Scale(area.Size, scale)); int replayFramesStart, replayFramesEnd, hitObjectsStart, hitObjectsEnd; for (hitObjectsStart = Beatmap.HitObjects.Count(x => x.StartTime <= hitObject.StartTime) - 1; hitObjectsStart >= 0 && bounds.Contains(Beatmap.HitObjects[hitObjectsStart].Location.ToPointF()) && hitObject.StartTime - Beatmap.HitObjects[hitObjectsStart].StartTime < maxTime; hitObjectsStart--) { ; } for (hitObjectsEnd = Beatmap.HitObjects.Count(x => x.StartTime <= hitObject.StartTime) - 1; hitObjectsEnd < Beatmap.HitObjects.Count && bounds.Contains(Beatmap.HitObjects[hitObjectsEnd].Location.ToPointF()) && Beatmap.HitObjects[hitObjectsEnd].StartTime - hitObject.StartTime < maxTime; hitObjectsEnd++) { ; } for (replayFramesStart = Replay.ReplayFrames.Count(x => x.Time <= Beatmap.HitObjects[hitObjectsStart + 1].StartTime); replayFramesStart > 1 && replayFramesStart < Replay.ReplayFrames.Count && bounds.Contains(Replay.ReplayFrames[replayFramesStart].GetPointF()) && hitObject.StartTime - Replay.ReplayFrames[replayFramesStart].Time < maxTime; replayFramesStart--) { ; } for (replayFramesEnd = Replay.ReplayFrames.Count(x => x.Time <= Beatmap.HitObjects[hitObjectsEnd - 1].StartTime); replayFramesEnd < Replay.ReplayFrames.Count - 1 && (replayFramesEnd < 2 || bounds.Contains(Replay.ReplayFrames[replayFramesEnd - 2].GetPointF())) && Replay.ReplayFrames[replayFramesEnd].Time - hitObject.StartTime < maxTime; replayFramesEnd++) { ; } g.Draw(linePen(ColorScheme.PlayfieldColor), Rectangle.Round(ScaleToRect(new RectangleF(pSub(new PointF(0, hr? 384 : 0), bounds, hr), new SizeF(512, 384)), bounds, area))); for (int q = hitObjectsEnd - 1; q > hitObjectsStart; q--) { if (Beatmap.HitObjects[q].Type.HasFlag(HitObjectType.Slider)) { SliderObject slider = (SliderObject)Beatmap.HitObjects[q]; PointF[] pt = slider.Curves.SelectMany(curve => curve.CurveSnapshots) .Select(c => c.point + slider.StackOffset.ToVector2()) .Select(s => ScaleToRect(pSub(s.ToPointF(), bounds, hr), bounds, area)).ToArray(); if (pt.Length > 1) { g.DrawLines(circlePen(ColorScheme.SliderColor.WithAlpha(80 / 255f)), pt); } } var color = ColorScheme.GetCircleColor(Math.Abs(Beatmap.HitObjects[q].StartTime - hitObject.StartTime) / maxTime); var circleRect = ScaleToRect(new RectangleF(PointF.Subtract( pSub(Beatmap.HitObjects[q].Location.ToPointF(), bounds, hr), (Size) new SizeF(radius, radius)), new SizeF(radius * 2, radius * 2)), bounds, area); var circle = new EllipsePolygon(RectangleF.Center(circleRect), circleRect.Size); if (HitCircleOutlines) { g.Draw(linePen(color), circle); } else { g.Fill(color, circle); } } float distance = 10.0001f; float?closestHit = null; float closestDistance = 0; string verdict = null; for (int k = replayFramesStart; k < replayFramesEnd - 2; k++) { PointF p1 = pSub(Replay.ReplayFrames[k].GetPointF(), bounds, hr); PointF p2 = pSub(Replay.ReplayFrames[k + 1].GetPointF(), bounds, hr); float hitAcc = Replay.ReplayFrames[k].Time - hitObject.StartTime; var pen = linePen(GetHitColor(Beatmap.OverallDifficulty, hitAcc) ?? ColorScheme.LineColor); g.DrawLines(pen, ScaleToRect(p1, bounds, area), ScaleToRect(p2, bounds, area)); if (distance > 10 && Math.Abs(hitObject.StartTime - Replay.ReplayFrames[k + 1].Time) > 50) { Point2 v1 = new Point2(p1.X - p2.X, p1.Y - p2.Y); if (v1.Length > 0) { v1.Normalize(); v1 *= (float)(Math.Sqrt(2) * arrowLength / 2); PointF p3 = PointF.Add(p2, new SizeF(v1.X + v1.Y, v1.Y - v1.X)); PointF p4 = PointF.Add(p2, new SizeF(v1.X - v1.Y, v1.X + v1.Y)); p2 = ScaleToRect(p2, bounds, area); p3 = ScaleToRect(p3, bounds, area); p4 = ScaleToRect(p4, bounds, area); g.DrawLines(pen, p2, p3); g.DrawLines(pen, p2, p4); } distance = 0; } else { distance += new Point2(p1.X - p2.X, p1.Y - p2.Y).Length; } if (Replay.ReplayFrames[k].Time <= hitObject.StartTime && Replay.ReplayFrames[k + 1].Time > hitObject.StartTime) { var lerp = (hitObject.StartTime - Replay.ReplayFrames[k].Time) / (Replay.ReplayFrames[k + 1].Time - Replay.ReplayFrames[k].Time); var p3 = new PointF(p1.X + (p2.X - p1.X) * lerp, p1.Y + (p2.Y - p1.Y) * lerp); EllipsePolygon circle = new EllipsePolygon(ScaleToRect(p3, bounds, area), ScaleToRect(new SizeF(2, 2), bounds, area)); g.Draw(linePen(ColorScheme.MidpointColor), circle); if (hitObject.ContainsPoint(Replay.ReplayFrames[k].GetPoint2())) { verdict = "Misclick"; } else { verdict = "Misaim"; } } if (ReplayAnalyzer.getKey(k == 0 ? Keys.None : Replay.ReplayFrames[k - 1].Keys, Replay.ReplayFrames[k].Keys) > 0) { EllipsePolygon circle = new EllipsePolygon(ScaleToRect(p1, bounds, area), ScaleToRect(new SizeF(6, 6), bounds, area)); g.Draw(pen, circle); if (Math.Abs(hitAcc) < GetHitWindow(Beatmap.OverallDifficulty, 50) && (!closestHit.HasValue || Math.Abs(closestHit.Value) > Math.Abs(hitAcc))) { closestHit = hitAcc; closestDistance = hitObject.DistanceToPoint(Replay.ReplayFrames[k].GetPoint2()); } } } if (closestDistance <= 0) { verdict = "Notelock"; closestDistance = 0; } int textSize = 16; int textPadding = 3; Font f = new Font(SystemFonts.Get("Segoe UI"), textSize); var opts = new TextOptions(f) { WrappingLength = area.Width - 2 * textPadding, Origin = new System.Numerics.Vector2(textPadding, textPadding), }; var topText = Beatmap.ToString(); if (drawAllHitObjects) { topText += $"\nObject {num + 1} of {Beatmap.HitObjects.Count}"; } else { topText += $"\nMiss {num + 1} of {MissCount}"; } g.DrawText(opts, topText, ColorScheme.TextColor); float time = hitObject.StartTime; if (Replay.Mods.HasFlag(Mods.DoubleTime)) { time /= 1.5f; } else if (Replay.Mods.HasFlag(Mods.HalfTime)) { time /= 0.75f; } TimeSpan ts = TimeSpan.FromMilliseconds(time); opts = new TextOptions(f) { VerticalAlignment = VerticalAlignment.Bottom, Origin = new System.Numerics.Vector2(textPadding, area.Height - textPadding), }; g.DrawText(opts, $"Time: {ts:mm\\:ss\\.fff}", ColorScheme.TextColor); if (closestHit.HasValue && isMiss) { opts = new TextOptions(f) { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, TextAlignment = TextAlignment.End, Origin = new System.Numerics.Vector2(area.Width - textPadding, area.Height - textPadding), }; string clickText = $"Closest click: {Math.Abs(closestHit.Value)}ms {(closestHit.Value < 0? "early":"late")}"; if (closestDistance > 0) { clickText += $", {closestDistance:N} units off"; } g.DrawText(opts, $"Verdict: {verdict}\n{clickText}", ColorScheme.TextColor); } }); return(img); }
public MissAnalyzer(Replay replay, Beatmap beatmap, ReplayAnalyzer analyzer) { Replay = replay; Beatmap = beatmap; ReplayAnalyzer = analyzer; }
public MissAnalyzer(string replayFile, string beatmap) { if (!File.Exists("options.cfg")) { File.Create("options.cfg").Close(); Console.ForegroundColor = ConsoleColor.Green; Debug.Print("\nCreating options.cfg... "); Debug.Print("- In options.cfg, you can define various settings that impact the program. "); Debug.Print("- To add these to options.cfg, add a new line formatted <Setting Name>=<Value> "); Debug.Print("- Available settings : SongsDir | Value = Specify osu!'s songs dir."); Debug.Print("- APIKey | Value = Your osu! API key (https://osu.ppy.sh/api/"); Debug.Print("- OsuDir | Value = Your osu! directory"); Console.ResetColor(); } options = new Options("options.cfg"); if (options.Settings.ContainsKey("osudir")) { database = new OsuDatabase(options, "osu!.db"); } Text = "Miss Analyzer"; Size = new Size(size, size + SystemInformation.CaptionHeight); img = new Bitmap(size, size); g = Graphics.FromImage(img); gOut = Graphics.FromHwnd(Handle); FormBorderStyle = FormBorderStyle.FixedSingle; Debug.Print("Loading Replay file..."); if (replayFile == null) { loadReplay(); if (r == null) { Environment.Exit(1); } } else { r = new Replay(replayFile, true, false); } Debug.Print("Loaded replay {0}", r.Filename); Debug.Print("Loading Beatmap file..."); if (beatmap == null) { loadBeatmap(); if (b == null) { Environment.Exit(1); } } else { b = new Beatmap(beatmap); } Debug.Print("Loaded beatmap {0}", b.Filename); Debug.Print("Analyzing... "); Debug.Print(r.ReplayFrames.Count.ToString()); re = new ReplayAnalyzer(b, r); if (re.misses.Count == 0) { Console.ForegroundColor = ConsoleColor.Red; Debug.Print("There is no miss in this replay. "); Console.ReadLine(); Environment.Exit(1); } number = 0; scale = 1; }
protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); Invalidate(); switch (e.KeyCode) { case System.Windows.Forms.Keys.Up: ScaleChange(1); break; case System.Windows.Forms.Keys.Down: ScaleChange(-1); break; case System.Windows.Forms.Keys.Right: if (number == re.misses.Count - 1) { break; } number++; break; case System.Windows.Forms.Keys.Left: if (number == 0) { break; } number--; break; case System.Windows.Forms.Keys.T: ring = !ring; break; case System.Windows.Forms.Keys.P: for (int i = 0; i < re.misses.Count; i++) { if (all) { drawMiss(b.HitObjects.IndexOf(re.misses[i])); } else { drawMiss(i); } img.Save(r.Filename.Substring(r.Filename.LastIndexOf("\\") + 1, r.Filename.Length - 5 - r.Filename.LastIndexOf("\\")) + "." + i + ".png", System.Drawing.Imaging.ImageFormat.Png); } break; case System.Windows.Forms.Keys.R: loadReplay(); loadBeatmap(); re = new ReplayAnalyzer(b, r); Invalidate(); number = 0; if (r == null || b == null) { Environment.Exit(1); } break; case System.Windows.Forms.Keys.A: if (all) { all = false; number = re.misses.Count(x => x.StartTime < b.HitObjects[number].StartTime); } else { all = true; number = b.HitObjects.IndexOf(re.misses[number]); } break; } }
public async Task <string> Load(GuildSettings guildSettings, OsuApi api, ServerReplayDb replays, ServerBeatmapDb beatmaps) { this.guildSettings = guildSettings; JToken score = null; if (Username != null && UserId == null) { UserId = await api.GetUserIdv1(Username); } if (BeatmapId != null) { _beatmap = await beatmaps.GetBeatmapFromId(BeatmapId); } if (ReplayFile != null) { _replay = new Replay(ReplayFile); } else if (ScoreId != null) { if (Mods == null || Beatmap == null) { score = await api.GetScorev2(ScoreId); } else { _replay = await replays.GetReplayFromOnlineId(ScoreId, Mods, Beatmap); } } if (_replay == null && PlayIndex.HasValue) { if (PlayIndex.Value < 0) { return("Index value must be greater than 0"); } if (UserId != null && UserScores != null) { score = await api.GetUserScoresv2(UserId, UserScores, PlayIndex.Value, FailedScores); } else if (BeatmapId != null) { score = await api.GetBeatmapScoresv2(BeatmapId, PlayIndex.Value); } } if (score != null) { if (!(bool)score["replay"]) { return("Replay not saved online"); } if ((bool)score["perfect"]) { return("No misses"); } if (_beatmap == null) { _beatmap = await beatmaps.GetBeatmapFromId((string)score["beatmap"]["id"]); } _replay = await replays.GetReplayFromScore(score, _beatmap); } if (_beatmap == null && _replay != null) { _beatmap = await beatmaps.GetBeatmap(_replay.MapHash); } if (_beatmap != null && _replay != null && _beatmap.BeatmapHash != _replay.MapHash) { _beatmap = await beatmaps.GetBeatmapFromId(_beatmap.BeatmapID.Value.ToString(), forceRedl : true); } if (_replay != null && !_replay.fullLoaded) { return("Replay does not contain any cursor data - can't analyze"); } if (_replay != null && _beatmap != null) { if (_beatmap.Mode != GameMode.osu) { return(null); } _analyzer = new ReplayAnalyzer(_beatmap, _replay); Loaded = true; return(null); } return($"Couldn't find {(_replay == null? "replay" : "beatmap")}"); }
public async Task <string> Load(OsuApi api, ServerReplayDb replays, ServerBeatmapDb beatmaps) { JToken score = null; if (Username != null && UserId == null) { UserId = await api.GetUserIdv1(Username); } if (BeatmapId != null) { _beatmap = await beatmaps.GetBeatmapFromId(BeatmapId); } if (ReplayFile != null) { _replay = new Replay(ReplayFile); } else if (ScoreId != null && Mods != null) { _replay = await replays.GetReplayFromOnlineId(ScoreId, Mods, _beatmap); } if (_replay == null && PlayIndex.HasValue) { if (UserId != null && UserScores != null) { score = await api.GetUserScoresv2(UserId, UserScores, PlayIndex.Value, FailedScores); } else if (BeatmapId != null) { score = await api.GetBeatmapScoresv2(BeatmapId, PlayIndex.Value); } if (score != null && _beatmap == null) { _beatmap = await beatmaps.GetBeatmapFromId((string)score["beatmap"]["id"]); } } if (score != null && _beatmap != null) { _replay = await replays.GetReplayFromScore(score, _beatmap); } if (_beatmap == null && _replay != null) { _beatmap = await beatmaps.GetBeatmap(_replay.MapHash); } if (_replay != null && !_replay.fullLoaded) { return("Replay does not contain any cursor data - can't analyze"); } if (_replay != null && _beatmap != null) { _analyzer = new ReplayAnalyzer(_beatmap, _replay); Loaded = true; return(null); } return($"Couldn't find {(_replay == null? "replay" : "beatmap")}"); }
/// <summary> /// Draws the miss. /// </summary> /// <returns>A Bitmap containing the drawing</returns> /// <param name="num">Index of the miss as it shows up in r.misses.</param> public Bitmap DrawHitObject(int num, Rectangle area) { Bitmap img = new Bitmap(area.Width, area.Height); Graphics g = Graphics.FromImage(img); bool hr = Replay.Mods.HasFlag(Mods.HardRock); CircleObject hitObject; if (drawAllHitObjects) { hitObject = Beatmap.HitObjects[num]; } else { hitObject = ReplayAnalyzer.misses[num]; } float radius = (float)hitObject.Radius; Pen circle = new Pen(Color.Gray, radius * 2 / scale) { StartCap = System.Drawing.Drawing2D.LineCap.Round, EndCap = System.Drawing.Drawing2D.LineCap.Round, LineJoin = System.Drawing.Drawing2D.LineJoin.Round }; Pen p = new Pen(Color.White); g.FillRectangle(p.Brush, area); RectangleF bounds = new RectangleF(PointF.Subtract(hitObject.Location.ToPointF(), MathUtils.Scale(area.Size, scale / 2)), MathUtils.Scale(area.Size, scale)); int replayFramesStart, replayFramesEnd, hitObjectsStart, hitObjectsEnd; for (hitObjectsStart = Beatmap.HitObjects.Count(x => x.StartTime <= hitObject.StartTime) - 1; hitObjectsStart >= 0 && bounds.Contains(Beatmap.HitObjects[hitObjectsStart].Location.ToPointF()) && hitObject.StartTime - Beatmap.HitObjects[hitObjectsStart].StartTime < maxTime; hitObjectsStart--) { } for (hitObjectsEnd = Beatmap.HitObjects.Count(x => x.StartTime <= hitObject.StartTime) - 1; hitObjectsEnd < Beatmap.HitObjects.Count && bounds.Contains(Beatmap.HitObjects[hitObjectsEnd].Location.ToPointF()) && Beatmap.HitObjects[hitObjectsEnd].StartTime - hitObject.StartTime < maxTime; hitObjectsEnd++) { } for (replayFramesStart = Replay.ReplayFrames.Count(x => x.Time <= Beatmap.HitObjects[hitObjectsStart + 1].StartTime); replayFramesStart > 1 && replayFramesStart < Replay.ReplayFrames.Count && bounds.Contains(Replay.ReplayFrames[replayFramesStart].GetPointF()) && hitObject.StartTime - Replay.ReplayFrames[replayFramesStart].Time < maxTime; replayFramesStart--) { } for (replayFramesEnd = Replay.ReplayFrames.Count(x => x.Time <= Beatmap.HitObjects[hitObjectsEnd - 1].StartTime); replayFramesEnd < Replay.ReplayFrames.Count - 1 && bounds.Contains(Replay.ReplayFrames[replayFramesEnd].GetPointF()) && Replay.ReplayFrames[replayFramesEnd].Time - hitObject.StartTime < maxTime; replayFramesEnd++) { } p.Color = Color.DarkGray; g.DrawRectangle(p, Rectangle.Round(ScaleToRect(new RectangleF(pSub(new PointF(0, hr? 384 : 0), bounds, hr), new SizeF(512, 384)), bounds, area))); p.Color = Color.Gray; for (int q = hitObjectsEnd - 1; q > hitObjectsStart; q--) { int c = Math.Min(255, 100 + (int)(Math.Abs(Beatmap.HitObjects[q].StartTime - hitObject.StartTime) * 100 / maxTime)); if (Beatmap.HitObjects[q].Type.HasFlag(HitObjectType.Slider)) { SliderObject slider = (SliderObject)Beatmap.HitObjects[q]; PointF[] pt = slider.Curves.SelectMany(curve => curve.CurveSnapshots).Select(s => ScaleToRect( pSub(s.point.ToPointF(), bounds, hr), bounds, area)).ToArray(); circle.Color = Color.FromArgb(80, Color.DarkGoldenrod); g.DrawLines(circle, pt); } p.Color = Color.FromArgb(c == 100 ? c + 50 : c, c, c); if (HitCircleOutlines) { g.DrawEllipse(p, ScaleToRect(new RectangleF(PointF.Subtract( pSub(Beatmap.HitObjects[q].Location.ToPointF(), bounds, hr), new SizeF(radius, radius).ToSize()), new SizeF(radius * 2, radius * 2)), bounds, area)); } else { g.FillEllipse(p.Brush, ScaleToRect(new RectangleF(PointF.Subtract( pSub(Beatmap.HitObjects[q].Location.ToPointF(), bounds, hr), new SizeF(radius, radius).ToSize()), new SizeF(radius * 2, radius * 2)), bounds, area)); } } float distance = 10.0001f; for (int k = replayFramesStart; k < replayFramesEnd - 2; k++) { PointF p1 = pSub(Replay.ReplayFrames[k].GetPointF(), bounds, hr); PointF p2 = pSub(Replay.ReplayFrames[k + 1].GetPointF(), bounds, hr); p.Color = GetHitColor(Beatmap.OverallDifficulty, (int)(hitObject.StartTime - Replay.ReplayFrames[k].Time)); g.DrawLine(p, ScaleToRect(p1, bounds, area), ScaleToRect(p2, bounds, area)); if (distance > 10 && Math.Abs(hitObject.StartTime - Replay.ReplayFrames[k + 1].Time) > 50) { Point2 v1 = new Point2(p1.X - p2.X, p1.Y - p2.Y); if (v1.Length > 0) { v1.Normalize(); v1 *= (float)(Math.Sqrt(2) * arrowLength / 2); PointF p3 = PointF.Add(p2, new SizeF(v1.X + v1.Y, v1.Y - v1.X)); PointF p4 = PointF.Add(p2, new SizeF(v1.X - v1.Y, v1.X + v1.Y)); p2 = ScaleToRect(p2, bounds, area); p3 = ScaleToRect(p3, bounds, area); p4 = ScaleToRect(p4, bounds, area); g.DrawLine(p, p2, p3); g.DrawLine(p, p2, p4); } distance = 0; } else { distance += new Point2(p1.X - p2.X, p1.Y - p2.Y).Length; } if (ReplayAnalyzer.getKey(k == 0 ? ReplayAPI.Keys.None : Replay.ReplayFrames[k - 1].Keys, Replay.ReplayFrames[k].Keys) > 0) { g.DrawEllipse(p, ScaleToRect(new RectangleF(PointF.Subtract(p1, new Size(3, 3)), new Size(6, 6)), bounds, area)); } } p.Color = Color.Black; Font f = new Font(FontFamily.GenericSansSerif, 12); g.DrawString(Beatmap.ToString(), f, p.Brush, 0, 0); if (drawAllHitObjects) { g.DrawString($"Object {num + 1} of {Beatmap.HitObjects.Count}", f, p.Brush, 0, f.Height); } else { g.DrawString($"Miss {num + 1} of {MissCount}", f, p.Brush, 0, f.Height); } float time = hitObject.StartTime; if (Replay.Mods.HasFlag(Mods.DoubleTime)) { time /= 1.5f; } else if (Replay.Mods.HasFlag(Mods.HalfTime)) { time /= 0.75f; } TimeSpan ts = TimeSpan.FromMilliseconds(time); g.DrawString($"Time: {ts:mm\\:ss\\.fff}", f, p.Brush, 0, area.Height - f.Height); return(img); }