private void Start()
    {
        // Fill GridItems using the imported scriptable object data
        GridItems = new List <GridItem>();
        foreach (ScriptableObject obj in GenerationManager.Instance.scriptableObjects)
        {
            GridItem gridItem = Instantiate(gridItemPrefab, gridItemRoot.transform).GetComponent <GridItem>();
            gridItem.InitializeItem(obj as SliderDataCustomizer);
            GridItems.Add(gridItem);
        }

        // Retrieve all customizer sliders in the UI
        CustomizerSliders = new List <SliderObject>();
        foreach (Transform child in customizerEntryRoot.transform)
        {
            SliderObject customizerSlider = child.GetComponent <SliderObject>();
            if (customizerSlider != null)
            {
                CustomizerSliders.Add(customizerSlider);
            }
        }

        // Prepare the Perlin layer previewer
        perlinTexture = new Texture2D(
            (int)perlinPreviewImage.rectTransform.sizeDelta.x,
            (int)perlinPreviewImage.rectTransform.sizeDelta.y);
        perlinPreviewImage.texture = perlinTexture;

        // Select the first item for customization
        if (GridItems.Count > 0)
        {
            GridItems[0].SelectItem();
        }
    }
Пример #2
0
        protected override void OnDataInitialize(VisualObject vObject)
        {
            SliderObject sliderObject = vObject as SliderObject;

            if (sliderObject == null || (!((ResourceData)this.BackGroundData != (ResourceData)null) || sliderObject.BackGroundData.GetResourceData().Type == this.BackGroundData.Type))
            {
                return;
            }
            sliderObject.BackGroundData = (ResourceFile)null;
        }
Пример #3
0
        protected override void OnDataInitialize(VisualObject vObject)
        {
            SliderObject sliderObject = vObject as SliderObject;

            if (sliderObject != null)
            {
                if (this.BackGroundData != null && sliderObject.BackGroundData.GetResourceData().Type != this.BackGroundData.Type)
                {
                    sliderObject.BackGroundData = null;
                }
            }
        }
        public void TestSlider()
        {
            var          db       = new OsuDbAPI.OsuDbFile("/home/will/osu!/osu!.db", byId: true);
            Beatmap      b        = db.BeatmapsById[539697].Load("/home/will/A/osu!/Songs");
            Replay       r        = new Replay("/home/will/osu!/Data/r/20b064985202e1a5219432c774476b8b-132562134499563000.osr");
            MissAnalyzer analyzer = new MissAnalyzer(r, b);
            SliderObject s        = (SliderObject)b.HitObjects[465];

            Console.WriteLine(s);
            Console.WriteLine(string.Join(", ", s.Curves));
            Console.WriteLine(string.Join(", ", s.Points));
            Console.WriteLine(string.Join(", ", s.Curves.SelectMany(curve => curve.CurveSnapshots)));
        }
Пример #5
0
        /// <summary>
        /// Saves the beatmap
        /// </summary>
        /// <param name="filename">The file to save the beatmap as</param>
        public void Save(string filename)
        {
            WriteBuffer.Clear();
            SectionLength.Clear();

            CultureInfo lastCulture = Thread.CurrentThread.CurrentCulture;

            Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US", false);
            Save("", "osu file format v13");
            FieldInfo[] newFields = GetType().GetFields();
            FieldInfo[] oldFields = Info.GetType().GetFields();

            foreach (FieldInfo f1 in newFields)
            {
                foreach (FieldInfo f2 in oldFields.Where(f2 => f1.Name == f2.Name))
                {
                    switch (f1.Name)
                    {
                    case "EditorBookmarks":
                    {
                        List <int> temps = (List <int>)f1.GetValue(this);
                        if (temps.Count != 0)
                        {
                            Save("General", "EditorBookmarks:" + string.Join(",", temps.Select(t => t.ToString(CultureInfo.InvariantCulture)).ToArray()));
                        }
                    }
                    break;

                    case "Bookmarks":
                    {
                        List <int> temps = (List <int>)f1.GetValue(this);
                        if (temps.Count != 0)
                        {
                            Save("Editor", "Bookmarks:" + string.Join(",", temps.Select(t => t.ToString(CultureInfo.InvariantCulture)).ToArray()));
                        }
                    }
                    break;

                    case "Tags":
                    {
                        List <string> temps = (List <string>)f1.GetValue(this);
                        if (temps.Count != 0)
                        {
                            Save("Metadata", "Tags:" + string.Join(" ", temps.ToArray()));
                        }
                    }
                    break;

                    case "Events":
                        foreach (EventBase o in (IEnumerable <EventBase>)f1.GetValue(this))
                        {
                            if (o.GetType() == typeof(ContentEvent))
                            {
                                ContentEvent backgroundInfo = (ContentEvent)o;
                                Save("Events", "0," + o.StartTime + ",\"" + backgroundInfo.Filename + "\"");
                            }
                            else if (o.GetType() == typeof(BreakEvent))
                            {
                                BreakEvent breakInfo = (BreakEvent)o;
                                Save("Events", "2," + o.StartTime + "," + breakInfo.EndTime);
                            }
                            else if (o.GetType() == typeof(BackgroundColourEvent))
                            {
                                BackgroundColourEvent colourInfo = (BackgroundColourEvent)o;
                                Save("Events", "3," + o.StartTime + "," + colourInfo.Colour.R + "," + colourInfo.Colour.G + "," + colourInfo.Colour.B);
                            }
                        }
                        break;

                    case "TimingPoints":
                    {
                        foreach (TimingPoint o in (IEnumerable <TimingPoint>)f1.GetValue(this))
                        {
                            Save("TimingPoints", o.Time + "," + o.BpmDelay + "," + o.TimeSignature + "," + o.SampleSet + "," + o.CustomSampleSet + "," + o.VolumePercentage + "," + Convert.ToInt32(!o.InheritsBPM) + "," + (int)o.VisualOptions);
                        }
                    }
                    break;

                    case "ComboColours":
                    {
                        foreach (Combo o in (IEnumerable <Combo>)f1.GetValue(this))
                        {
                            Save("Colours", "Combo" + o.ComboNumber + ':' + o.Colour.R + "," + o.Colour.G + "," + o.Colour.B);
                        }
                    }
                    break;

                    //case "SliderBorder":
                    //    {
                    //        if (f1.GetValue(this) == f2.GetValue(Info))
                    //            continue;
                    //        Colour o = (Colour)f1.GetValue(this);
                    //        Save("Colours", "SliderBorder: " + o.R + "," + o.G + "," + o.B);
                    //    }
                    //    break;
                    case "HitObjects":
                        foreach (CircleObject obj in (IEnumerable <CircleObject>)f1.GetValue(this))
                        {
                            if (obj.GetType() == typeof(CircleObject))
                            {
                                Save("HitObjects", obj.Location.X + "," + obj.Location.Y + "," + obj.StartTime + "," + (int)obj.Type + "," + (int)obj.Effect);
                            }
                            else if (obj.GetType() == typeof(SliderObject))
                            {
                                SliderObject sliderInfo  = (SliderObject)obj;
                                string       pointString = sliderInfo.Points.Aggregate("", (current, p) => current + ("|" + p.X + ':' + p.Y));
                                Save("HitObjects", obj.Location.X + "," + obj.Location.Y + "," + obj.StartTime + "," + (int)obj.Type + "," + (int)obj.Effect + "," + sliderInfo.Type.ToString().Substring(0, 1) + pointString + "," + sliderInfo.RepeatCount + "," + sliderInfo.MaxPoints);
                            }
                            else if (obj.GetType() == typeof(SpinnerObject))
                            {
                                SpinnerObject spinnerInfo = (SpinnerObject)obj;
                                Save("HitObjects", obj.Location.X + "," + obj.Location.Y + "," + obj.StartTime + "," + (int)obj.Type + "," + (int)obj.Effect + "," + spinnerInfo.EndTime);
                            }
                        }
                        break;

                    default:
                        if (f1.Name != "Format" && f1.Name != "Filename" && f1.Name != "BeatmapHash")
                        {
                            if (f1.GetValue(this) != null)
                            {
                                if (f2.GetValue(Info) != null)
                                {
                                    if ((f1.GetValue(this).GetType() == typeof(GameMode)) || (f1.GetValue(this).GetType() == typeof(OverlayOptions)))
                                    {
                                        Save(GetSection(f1.Name), f1.Name + ':' + (int)f1.GetValue(this));
                                    }
                                    else
                                    {
                                        Save(GetSection(f1.Name), f1.Name + ':' + f1.GetValue(this));
                                    }
                                }
                                else
                                {
                                    if ((f2.GetValue(Info).GetType() == typeof(GameMode)) || (f2.GetValue(Info).GetType() == typeof(OverlayOptions)))
                                    {
                                        Save(GetSection(f2.Name), f2.Name + ':' + (int)f2.GetValue(Info));
                                    }
                                    else
                                    {
                                        Save(GetSection(f2.Name), f2.Name + ':' + f2.GetValue(Info));
                                    }
                                }
                            }
                        }
                        break;
                    }
                }
            }
            foreach (PropertyInfo f1 in GetType().GetProperties())
            {
                foreach (PropertyInfo f2 in Info.GetType().GetProperties().Where(f2 => f1.Name == f2.Name))
                {
                    if (f1.GetValue(this, null) != null)
                    {
                        if (f2.GetValue(Info, null) != null)
                        {
                            if ((f1.GetValue(this, null).GetType() == typeof(GameMode)) || (f1.GetValue(this, null).GetType() == typeof(OverlayOptions)))
                            {
                                Save(GetSection(f1.Name), f1.Name + ':' + (int)f1.GetValue(this, null));
                            }
                            else
                            {
                                Save(GetSection(f1.Name), f1.Name + ':' + f1.GetValue(this, null));
                            }
                        }
                        else
                        {
                            if ((f2.GetValue(Info, null).GetType() == typeof(GameMode)) || (f2.GetValue(Info, null).GetType() == typeof(OverlayOptions)))
                            {
                                Save(GetSection(f2.Name), f2.Name + ':' + (int)f2.GetValue(Info, null));
                            }
                            else
                            {
                                Save(GetSection(f2.Name), f2.Name + ':' + f2.GetValue(Info, null));
                            }
                        }
                    }
                }
            }
            FinishSave(filename);
            Thread.CurrentThread.CurrentCulture = lastCulture;
        }
Пример #6
0
        private void Parse(string bm)
        {
            Info.Filename    = bm;
            Info.BeatmapHash = MD5FromFile(bm);
            using (StreamReader sR = new StreamReader(bm))
            {
                string currentSection = "";

                while (sR.Peek() != -1)
                {
                    string line = sR.ReadLine();

                    //Check for section tag
                    if (line.StartsWith("["))
                    {
                        currentSection = line;
                        continue;
                    }

                    //Check for commented-out line
                    //or blank lines
                    if (line.StartsWith("//") || line.Length == 0)
                    {
                        continue;
                    }

                    //Check for version string
                    if (line.StartsWith("osu file format"))
                    {
                        Info.Format = Convert.ToInt32(line.Substring(17).Replace(Environment.NewLine, "").Replace(" ", ""));
                    }

                    //Do work for [General], [Metadata], [Difficulty] and [Editor] sections
                    if ((currentSection == "[General]") || (currentSection == "[Metadata]") || (currentSection == "[Difficulty]") || (currentSection == "[Editor]"))
                    {
                        string[] reSplit   = line.Split(':');
                        string   cProperty = reSplit[0].TrimEnd();

                        bool isValidProperty = false;
                        foreach (string k in BM_Sections.Keys)
                        {
                            if (k.Contains(cProperty))
                            {
                                isValidProperty = true;
                            }
                        }
                        if (!isValidProperty)
                        {
                            continue;
                        }

                        //Check for blank value
                        string cValue = reSplit[1].Trim();

                        //Import properties into Info
                        switch (cProperty)
                        {
                        case "EditorBookmarks":
                        {
                            string[] marks = cValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                            foreach (string m in marks.Where(m => m != ""))
                            {
                                Info.EditorBookmarks.Add(Convert.ToInt32(m));
                            }
                        }
                        break;

                        case "Bookmarks":
                        {
                            string[] marks = cValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                            foreach (string m in marks.Where(m => m != ""))
                            {
                                Info.Bookmarks.Add(Convert.ToInt32(m));
                            }
                        }
                        break;

                        case "Tags":
                            string[] tags = cValue.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                            foreach (string t in tags)
                            {
                                Info.Tags.Add(t);
                            }
                            break;

                        case "Mode":
                            Info.Mode = (GameMode)Convert.ToInt32(cValue);
                            break;

                        case "OverlayPosition":
                            Info.OverlayPosition = (OverlayOptions)Enum.Parse(typeof(OverlayOptions), cValue);
                            break;

                        case "AlwaysShowPlayfield":
                            Info.AlwaysShowPlayfield = Convert.ToBoolean(Convert.ToInt32(cValue));
                            break;

                        default:
                            FieldInfo    fi = Info.GetType().GetField(cProperty);
                            PropertyInfo pi = Info.GetType().GetProperty(cProperty);
                            if (fi != null)
                            {
                                if (fi.FieldType == typeof(float?))
                                {
                                    fi.SetValue(Info, (float?)Convert.ToDouble(cValue));
                                }
                                if (fi.FieldType == typeof(float))
                                {
                                    fi.SetValue(Info, (float)Convert.ToDouble(cValue));
                                }
                                else if ((fi.FieldType == typeof(int?)) || (fi.FieldType == typeof(int)))
                                {
                                    fi.SetValue(Info, Convert.ToInt32(cValue));
                                }
                                else if (fi.FieldType == typeof(string))
                                {
                                    fi.SetValue(Info, cValue);
                                }
                                break;
                            }
                            if (pi.PropertyType == typeof(float?))
                            {
                                pi.SetValue(Info, (float?)Convert.ToDouble(cValue), null);
                            }
                            if (pi.PropertyType == typeof(float))
                            {
                                pi.SetValue(Info, (float)Convert.ToDouble(cValue), null);
                            }
                            else if ((pi.PropertyType == typeof(int?)) || (pi.PropertyType == typeof(int)))
                            {
                                pi.SetValue(Info, Convert.ToInt32(cValue), null);
                            }
                            else if (pi.PropertyType == typeof(string))
                            {
                                pi.SetValue(Info, cValue, null);
                            }
                            break;
                        }
                        continue;
                    }

                    //The following are version-dependent, the version is stored as a numeric value inside Info.Format
                    //Do work for [Events] section
                    if (currentSection == "[Events]")
                    {
                        string[] reSplit = line.Split(',');
                        switch (reSplit[0].ToLower())
                        {
                        case "0":
                        case "1":
                        case "video":
                            Info.Events.Add(new ContentEvent
                            {
                                Type      = reSplit[0].ToLower() == "1" || reSplit[0].ToLower() == "video" ? ContentType.Video : ContentType.Image,
                                StartTime = Convert.ToInt32(reSplit[1]),
                                Filename  = reSplit[2].Replace("\"", "")
                            });
                            break;

                        case "2":
                            Info.Events.Add(new BreakEvent
                            {
                                StartTime = Convert.ToInt32(reSplit[1]),
                                EndTime   = Convert.ToInt32(reSplit[2])
                            });
                            break;

                        case "3":
                            Info.Events.Add(new BackgroundColourEvent
                            {
                                StartTime = Convert.ToInt32(reSplit[1]),
                                Colour    = new Colour
                                {
                                    R = Convert.ToInt32(reSplit[2]),
                                    G = Convert.ToInt32(reSplit[3]),
                                    B = Convert.ToInt32(reSplit[4])
                                },
                            });
                            break;
                        }
                    }

                    //Do work for [TimingPoints] section
                    if (currentSection == "[TimingPoints]")
                    {
                        TimingPoint tempTimingPoint = new TimingPoint();

                        float[]  values  = { 0, 0, 4, 0, 0, 100, 0, 0, 0 };
                        string[] reSplit = line.Split(',');
                        for (int i = 0; i < reSplit.Length; i++)
                        {
                            values[i] = (float)Convert.ToDouble(reSplit[i]);
                        }
                        tempTimingPoint.Time             = (float)Convert.ToDouble(values[0]);
                        tempTimingPoint.BpmDelay         = (float)Convert.ToDouble(values[1]);
                        tempTimingPoint.TimeSignature    = Convert.ToInt32(values[2]);
                        tempTimingPoint.SampleSet        = Convert.ToInt32(values[3]);
                        tempTimingPoint.CustomSampleSet  = Convert.ToInt32(values[4]);
                        tempTimingPoint.VolumePercentage = Convert.ToInt32(values[5]);
                        tempTimingPoint.InheritsBPM      = !Convert.ToBoolean(Convert.ToInt32(values[6]));
                        tempTimingPoint.VisualOptions    = (TimingPointOptions)Convert.ToInt32(values[7]);
                        Info.TimingPoints.Add(tempTimingPoint);
                    }

                    //Do work for [Colours] section
                    if (currentSection == "[Colours]")
                    {
                        string   property = line.Substring(0, line.IndexOf(':', 1)).Trim();
                        string   value    = line.Substring(line.IndexOf(':', 1) + 1).Trim();
                        string[] reSplit  = value.Split(',');

                        if (property.Length > 5 && property.Substring(0, 5) == "Combo")
                        {
                            Combo newCombo = new Combo
                            {
                                Colour = new Colour
                                {
                                    R = Convert.ToInt32(reSplit[0]),
                                    G = Convert.ToInt32(reSplit[1]),
                                    B = Convert.ToInt32(reSplit[2])
                                }
                            };
                            try
                            {
                                newCombo.ComboNumber = Convert.ToInt32(property.Substring(5, 1));
                            }
                            catch
                            {
                                Debug.Assert(false, "Invalid combonumber at index 5. " + line);
                                continue;
                            }
                        }
                        else if (property.Length > 5 && property == "SliderBorder")
                        {
                            Info.SliderBorder = new Colour
                            {
                                R = Convert.ToInt32(reSplit[0]),
                                G = Convert.ToInt32(reSplit[1]),
                                B = Convert.ToInt32(reSplit[2])
                            };
                        }
                    }

                    //Do work for [HitObjects] section
                    if (currentSection == "[HitObjects]")
                    {
                        string[]     reSplit   = line.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                        CircleObject newObject = new CircleObject
                        {
                            Radius    = 40 - 4 * (Info.CircleSize - 2),
                            Location  = new Point2(Convert.ToInt32(reSplit[0]), Convert.ToInt32(reSplit[1])),
                            StartTime = (float)Convert.ToDouble(reSplit[2]),
                            Type      = (HitObjectType)Convert.ToInt32(reSplit[3]),
                            Effect    = (EffectType)Convert.ToInt32(reSplit[4])
                        };
                        if ((newObject.Type & HitObjectType.Slider) > 0)
                        {
                            newObject = new SliderObject(newObject);
                            ((SliderObject)newObject).Velocity = Info.SliderMultiplier;
                            switch (reSplit[5].Substring(0, 1))
                            {
                            case "B":
                                ((SliderObject)newObject).Type = SliderType.Bezier;
                                break;

                            case "C":
                                ((SliderObject)newObject).Type = SliderType.CSpline;
                                break;

                            case "L":
                                ((SliderObject)newObject).Type = SliderType.Linear;
                                break;

                            case "P":
                                ((SliderObject)newObject).Type = SliderType.PSpline;
                                break;
                            }
                            string[] pts = reSplit[5].Split(new[] { "|" }, StringSplitOptions.None);

                            //Todo: Check this
                            if (Format <= 4)
                            {
                                ((SliderObject)newObject).Points.Add(newObject.Location);
                            }

                            //Always exclude index 1, this will contain the type
                            for (int i = 1; i <= pts.Length - 1; i++)
                            {
                                Point2 p = new Point2((float)Convert.ToDouble(pts[i].Substring(0, pts[i].IndexOf(":", StringComparison.InvariantCulture))),
                                                      (float)Convert.ToDouble(pts[i].Substring(pts[i].IndexOf(":", StringComparison.InvariantCulture) + 1)));
                                ((SliderObject)newObject).Points.Add(p);
                            }
                            ((SliderObject)newObject).RepeatCount = Convert.ToInt32(reSplit[6]);
                            float tempMaxPoints;
                            if (float.TryParse(reSplit[7], out tempMaxPoints))
                            {
                                ((SliderObject)newObject).MaxPoints = tempMaxPoints;
                            }
                        }
                        if ((newObject.Type & HitObjectType.Spinner) > 0)
                        {
                            newObject = new SpinnerObject(newObject);
                            ((SpinnerObject)newObject).EndTime = (float)Convert.ToDouble(reSplit[5]);
                        }
                        Info.HitObjects.Add(newObject);
                    }
                }
            }

            //Copy the fields/properties of Info locally
            foreach (FieldInfo fi in Info.GetType().GetFields())
            {
                FieldInfo ff = GetType().GetField(fi.Name);
                ff.SetValue(this, fi.GetValue(Info));
            }
            foreach (PropertyInfo pi in Info.GetType().GetProperties())
            {
                PropertyInfo ff = GetType().GetProperty(pi.Name);
                ff.SetValue(this, pi.GetValue(Info, null), null);
            }
        }
        /// <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);
        }
Пример #8
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>
        private Bitmap drawMiss(int num)
        {
            bool         hr = r.Mods.HasFlag(Mods.HardRock);
            CircleObject miss;

            if (all)
            {
                miss = b.HitObjects[num];
            }
            else
            {
                miss = re.misses[num];
            }
            float radius = (float)miss.Radius;
            Pen   circle = new Pen(Color.Gray, radius * 2 / scale);

            circle.StartCap = System.Drawing.Drawing2D.LineCap.Round;
            circle.EndCap   = System.Drawing.Drawing2D.LineCap.Round;
            circle.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
            Pen p = new Pen(Color.White);

            g.FillRectangle(p.Brush, 0, 0, size, size);
            RectangleF bounds = new RectangleF((miss.Location - new Point2(size * scale / 2, size * scale / 2)).ToPointF(),
                                               new SizeF(size * scale, size * scale));

            int i, j, y, z;

            for (y = b.HitObjects.Count(x => x.StartTime <= miss.StartTime) - 1;
                 y >= 0 && bounds.Contains(b.HitObjects[y].Location.ToPointF()) &&
                 miss.StartTime - b.HitObjects[y].StartTime < maxTime;
                 y--)
            {
            }
            for (z = b.HitObjects.Count(x => x.StartTime <= miss.StartTime) - 1;
                 z < b.HitObjects.Count && bounds.Contains(b.HitObjects[z].Location.ToPointF()) &&
                 b.HitObjects[z].StartTime - miss.StartTime < maxTime;
                 z++)
            {
            }
            for (i = r.ReplayFrames.Count(x => x.Time <= b.HitObjects[y + 1].StartTime);
                 i > 0 && bounds.Contains(r.ReplayFrames[i].PointF) &&
                 miss.StartTime - r.ReplayFrames[i].Time < maxTime;
                 i--)
            {
            }
            for (j = r.ReplayFrames.Count(x => x.Time <= b.HitObjects[z - 1].StartTime);
                 j < r.ReplayFrames.Count - 1 && bounds.Contains(r.ReplayFrames[j].PointF) &&
                 r.ReplayFrames[j].Time - miss.StartTime < maxTime;
                 j++)
            {
            }
            p.Color = Color.Gray;
            for (int q = z - 1; q > y; q--)
            {
                int c = Math.Min(255, 100 + (int)(Math.Abs(b.HitObjects[q].StartTime - miss.StartTime) * 100 / maxTime));
                if (b.HitObjects[q].Type == HitObjectType.Slider)
                {
                    SliderObject slider = (SliderObject)b.HitObjects[q];
                    PointF[]     pt     = new PointF[sliderGranularity];
                    for (int x = 0; x < sliderGranularity; x++)
                    {
                        pt[x] = ScaleToRect(
                            pSub(slider.PositionAtDistance(x * 1f * slider.PixelLength / sliderGranularity).toPoint(),
                                 bounds, hr), bounds);
                    }
                    circle.Color = Color.LemonChiffon;
                    g.DrawLines(circle, pt);
                }

                p.Color = Color.FromArgb(c == 100 ? c + 50 : c, c, c);
                if (ring)
                {
                    g.DrawEllipse(p, ScaleToRect(new RectangleF(PointF.Subtract(
                                                                    pSub(b.HitObjects[q].Location.ToPointF(), bounds, hr),
                                                                    new SizeF(radius, radius).ToSize()), new SizeF(radius * 2, radius * 2)), bounds));
                }
                else
                {
                    g.FillEllipse(p.Brush, ScaleToRect(new RectangleF(PointF.Subtract(
                                                                          pSub(b.HitObjects[q].Location.ToPointF(), bounds, hr),
                                                                          new SizeF(radius, radius).ToSize()), new SizeF(radius * 2, radius * 2)), bounds));
                }
            }
            float distance = 10.0001f;

            for (int k = i; k < j; k++)
            {
                PointF p1 = pSub(r.ReplayFrames[k].PointF, bounds, hr);
                PointF p2 = pSub(r.ReplayFrames[k + 1].PointF, bounds, hr);
                p.Color = getHitColor(b.OverallDifficulty, (int)(miss.StartTime - r.ReplayFrames[k].Time));
                g.DrawLine(p, ScaleToRect(p1, bounds), ScaleToRect(p2, bounds));
                if (distance > 10 && Math.Abs(miss.StartTime - r.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);
                        p3 = ScaleToRect(p3, bounds);
                        p4 = ScaleToRect(p4, bounds);
                        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 (re.getKey(k == 0 ? ReplayAPI.Keys.None : r.ReplayFrames[k - 1].Keys, r.ReplayFrames[k].Keys) > 0)
                {
                    g.DrawEllipse(p, ScaleToRect(new RectangleF(PointF.Subtract(p1, new Size(3, 3)), new Size(6, 6)),
                                                 bounds));
                }
            }

            p.Color = Color.Black;
            Font f = new Font(FontFamily.GenericSansSerif, 12);

            if (all)
            {
                g.DrawString("Object " + (num + 1) + " of " + b.HitObjects.Count, f, p.Brush, 0, 0);
            }
            else
            {
                g.DrawString("Miss " + (num + 1) + " of " + re.misses.Count, f, p.Brush, 0, 0);
            }
            TimeSpan ts = TimeSpan.FromMilliseconds(miss.StartTime);

            g.DrawString("Time: " + ts.ToString(@"mm\:ss\.fff"), f, p.Brush, 0, size - f.Height);
            return(img);
        }
Пример #9
0
        private void Parse(string bm)
        {
            Info.Filename = bm;
            Info.BeatmapHash = MD5FromFile(bm);
            using (StreamReader sR = new StreamReader(bm))
            {
                string currentSection = "";

                while (sR.Peek() != -1)
                {
                    string line = sR.ReadLine();

                    //Check for section tag
                    if (line.StartsWith("["))
                    {
                        currentSection = line;
                        continue;
                    }

                    //Check for commented-out line
                    //or blank lines
                    if (line.StartsWith("//") || line.Length == 0)
                        continue;

                    //Check for version string
                    if (line.StartsWith("osu file format"))
                        Info.Format = Convert.ToInt32(line.Substring(17).Replace(Environment.NewLine, "").Replace(" ", ""));

                    //Do work for [General], [Metadata], [Difficulty] and [Editor] sections
                    if ((currentSection == "[General]") || (currentSection == "[Metadata]") || (currentSection == "[Difficulty]") || (currentSection == "[Editor]"))
                    {
                        string[] reSplit = line.Split(':');
                        string cProperty = reSplit[0].TrimEnd();

                        bool isValidProperty = false;
                        foreach (string k in BM_Sections.Keys)
                        {
                            if (k.Contains(cProperty))
                                isValidProperty = true;
                        }
                        if (!isValidProperty)
                            continue;

                        //Check for blank value
                        string cValue = reSplit[1].Trim();

                        //Import properties into Info
                        switch (cProperty)
                        {
                            case "EditorBookmarks":
                                {
                                    string[] marks = cValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                                    foreach (string m in marks.Where(m => m != ""))
                                        Info.EditorBookmarks.Add(Convert.ToInt32(m));
                                }
                                break;
                            case "Bookmarks":
                                {
                                    string[] marks = cValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                                    foreach (string m in marks.Where(m => m != ""))
                                        Info.Bookmarks.Add(Convert.ToInt32(m));
                                }
                                break;
                            case "Tags":
                                string[] tags = cValue.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                                foreach (string t in tags)
                                    Info.Tags.Add(t);
                                break;
                            case "Mode":
                                Info.Mode = (GameMode)Convert.ToInt32(cValue);
                                break;
                            case "OverlayPosition":
                                Info.OverlayPosition = (OverlayOptions)Enum.Parse(typeof(OverlayOptions), cValue);
                                break;
                            case "AlwaysShowPlayfield":
                                Info.AlwaysShowPlayfield = Convert.ToBoolean(Convert.ToInt32(cValue));
                                break;
                            default:
                                FieldInfo fi = Info.GetType().GetField(cProperty);
                                PropertyInfo pi = Info.GetType().GetProperty(cProperty);
                                if (fi != null)
                                {
                                    if (fi.FieldType == typeof(float?))
                                        fi.SetValue(Info, (float?)Convert.ToDouble(cValue));
                                    if (fi.FieldType == typeof(float))
                                        fi.SetValue(Info, (float)Convert.ToDouble(cValue));
                                    else if ((fi.FieldType == typeof(int?)) || (fi.FieldType == typeof(int)))
                                        fi.SetValue(Info, Convert.ToInt32(cValue));
                                    else if (fi.FieldType == typeof(string))
                                        fi.SetValue(Info, cValue);
                                    break;
                                }
                                if (pi.PropertyType == typeof(float?))
                                    pi.SetValue(Info, (float?)Convert.ToDouble(cValue), null);
                                if (pi.PropertyType == typeof(float))
                                    pi.SetValue(Info, (float)Convert.ToDouble(cValue), null);
                                else if ((pi.PropertyType == typeof(int?)) || (pi.PropertyType == typeof(int)))
                                    pi.SetValue(Info, Convert.ToInt32(cValue), null);
                                else if (pi.PropertyType == typeof(string))
                                    pi.SetValue(Info, cValue, null);
                                break;
                        }
                        continue;
                    }

                    //The following are version-dependent, the version is stored as a numeric value inside Info.Format
                    //Do work for [Events] section
                    if (currentSection == "[Events]")
                    {
                        string[] reSplit = line.Split(',');
                        switch (reSplit[0].ToLower())
                        {
                            case "0":
                            case "1":
                            case "video":
                                Info.Events.Add(new ContentEvent
                                {
                                    Type = reSplit[0].ToLower() == "1" || reSplit[0].ToLower() == "video" ? ContentType.Video : ContentType.Image,
                                    StartTime = Convert.ToInt32(reSplit[1]),
                                    Filename = reSplit[2].Replace("\"", "")
                                });
                                break;
                            case "2":
                                Info.Events.Add(new BreakEvent
                                {
                                    StartTime = Convert.ToInt32(reSplit[1]),
                                    EndTime = Convert.ToInt32(reSplit[2])
                                });
                                break;
                            case "3":
                                Info.Events.Add(new BackgroundColourEvent
                                {
                                    StartTime = Convert.ToInt32(reSplit[1]),
                                    Colour = new Colour
                                    {
                                        R = Convert.ToInt32(reSplit[2]),
                                        G = Convert.ToInt32(reSplit[3]),
                                        B = Convert.ToInt32(reSplit[4])
                                    },
                                });
                                break;
                        }
                    }

                    //Do work for [TimingPoints] section
                    if (currentSection == "[TimingPoints]")
                    {
                        TimingPoint tempTimingPoint = new TimingPoint();

                        float[] values = { 0, 0, 4, 0, 0, 100, 0, 0, 0 };
                        string[] reSplit = line.Split(',');
                        for (int i = 0; i < reSplit.Length; i++)
                            values[i] = (float)Convert.ToDouble(reSplit[i]);
                        tempTimingPoint.Time = (float)Convert.ToDouble(values[0]);
                        tempTimingPoint.BpmDelay = (float)Convert.ToDouble(values[1]);
                        tempTimingPoint.TimeSignature = Convert.ToInt32(values[2]);
                        tempTimingPoint.SampleSet = Convert.ToInt32(values[3]);
                        tempTimingPoint.CustomSampleSet = Convert.ToInt32(values[4]);
                        tempTimingPoint.VolumePercentage = Convert.ToInt32(values[5]);
                        tempTimingPoint.InheritsBPM = !Convert.ToBoolean(Convert.ToInt32(values[6]));
                        tempTimingPoint.VisualOptions = (TimingPointOptions)Convert.ToInt32(values[7]);
                        Info.TimingPoints.Add(tempTimingPoint);
                    }

                    //Do work for [Colours] section
                    if (currentSection == "[Colours]")
                    {
                        string property = line.Substring(0, line.IndexOf(':', 1)).Trim();
                        string value = line.Substring(line.IndexOf(':', 1) + 1).Trim();
                        string[] reSplit = value.Split(',');

                        if (property.Length > 5 && property.Substring(0, 5) == "Combo")
                        {
                            Combo newCombo = new Combo
                            {
                                Colour = new Colour
                                {
                                    R = Convert.ToInt32(reSplit[0]),
                                    G = Convert.ToInt32(reSplit[1]),
                                    B = Convert.ToInt32(reSplit[2])
                                }
                            };
                            try
                            {
                                newCombo.ComboNumber = Convert.ToInt32(property.Substring(5, 1));
                            }
                            catch
                            {
                                Debug.Assert(false, "Invalid combonumber at index 5. " + line);
                                continue;
                            }
                        }
                        else if (property.Length > 5 && property == "SliderBorder")
                        {
                            Info.SliderBorder = new Colour
                            {
                                R = Convert.ToInt32(reSplit[0]),
                                G = Convert.ToInt32(reSplit[1]),
                                B = Convert.ToInt32(reSplit[2])
                            };
                        }
                    }

                    //Do work for [HitObjects] section
                    if (currentSection == "[HitObjects]")
                    {
                        string[] reSplit = line.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                        CircleObject newObject = new CircleObject
                        {
                            Radius = 40 - 4 * (Info.CircleSize - 2),
                            Location = new Point2(Convert.ToInt32(reSplit[0]), Convert.ToInt32(reSplit[1])),
                            StartTime = (float)Convert.ToDouble(reSplit[2]),
                            Type = (HitObjectType)Convert.ToInt32(reSplit[3]),
                            Effect = (EffectType)Convert.ToInt32(reSplit[4])
                        };
                        if ((newObject.Type & HitObjectType.Slider) > 0)
                        {
                            newObject = new SliderObject(newObject);
                            ((SliderObject)newObject).Velocity = Info.SliderMultiplier;
                            switch (reSplit[5].Substring(0, 1))
                            {
                                case "B":
                                    ((SliderObject)newObject).Type = SliderType.Bezier;
                                    break;
                                case "C":
                                    ((SliderObject)newObject).Type = SliderType.CSpline;
                                    break;
                                case "L":
                                    ((SliderObject)newObject).Type = SliderType.Linear;
                                    break;
                                case "P":
                                    ((SliderObject)newObject).Type = SliderType.PSpline;
                                    break;
                            }
                            string[] pts = reSplit[5].Split(new[] { "|" }, StringSplitOptions.None);

                            //Todo: Check this
                            if (Format <= 4)
                                ((SliderObject)newObject).Points.Add(newObject.Location);

                            //Always exclude index 1, this will contain the type
                            for (int i = 1; i <= pts.Length - 1; i++)
                            {
                                Point2 p = new Point2((float)Convert.ToDouble(pts[i].Substring(0, pts[i].IndexOf(":", StringComparison.InvariantCulture))),
                                                            (float)Convert.ToDouble(pts[i].Substring(pts[i].IndexOf(":", StringComparison.InvariantCulture) + 1)));
                                ((SliderObject)newObject).Points.Add(p);
                            }
                            ((SliderObject)newObject).RepeatCount = Convert.ToInt32(reSplit[6]);
                            float tempMaxPoints;
                            if (float.TryParse(reSplit[7], out tempMaxPoints))
                                ((SliderObject)newObject).MaxPoints = tempMaxPoints;
                        }
                        if ((newObject.Type & HitObjectType.Spinner) > 0)
                        {
                            newObject = new SpinnerObject(newObject);
                            ((SpinnerObject)newObject).EndTime = (float)Convert.ToDouble(reSplit[5]);
                        }
                        Info.HitObjects.Add(newObject);
                    }
                }
            }

            //Copy the fields/properties of Info locally
            foreach (FieldInfo fi in Info.GetType().GetFields())
            {
                FieldInfo ff = GetType().GetField(fi.Name);
                ff.SetValue(this, fi.GetValue(Info));
            }
            foreach (PropertyInfo pi in Info.GetType().GetProperties())
            {
                PropertyInfo ff = GetType().GetProperty(pi.Name);
                ff.SetValue(this, pi.GetValue(Info, null), null);
            }
        }
Пример #10
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);
        }