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);
        }
Esempio n. 2
0
        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);
        }
Esempio n. 3
0
        void ReadFile()
        {
            ReplayAnalyzer replayAnalyzer = new ReplayAnalyzer(File.OpenRead("demo.dem"));

            replayAnalyzer.Start();


            bool breakpoint = true;
            //Set Breakpoint to Read Replay Analyze Class
        }
Esempio n. 4
0
        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;
 }
Esempio n. 7
0
        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;
        }
Esempio n. 8
0
        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")}");
        }
Esempio n. 10
0
        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")}");
        }
Esempio n. 11
0
        /// <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);
        }