Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.FailRetryDisplay.cs
4493 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osuTK;

namespace osu.Game.Screens.SelectV2
{
    public partial class BeatmapMetadataWedge
    {
        private partial class FailRetryDisplay : CompositeDrawable
        {
            private readonly GraphDrawable retriesGraph;
            private readonly GraphDrawable failsGraph;

            public APIFailTimes Data
            {
                set
                {
                    int[] retries = value.Retries ?? Array.Empty<int>();
                    int[] fails = value.Fails ?? Array.Empty<int>();
                    int[] total = retries.Zip(fails, (r, f) => r + f).ToArray();

                    int maximum = total.DefaultIfEmpty(0).Max();

                    retriesGraph.Data = total.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
                    failsGraph.Data = fails.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
                }
            }

            public FailRetryDisplay()
            {
                RelativeSizeAxes = Axes.X;
                AutoSizeAxes = Axes.Y;

                InternalChild = new FillFlowContainer
                {
                    RelativeSizeAxes = Axes.X,
                    AutoSizeAxes = Axes.Y,
                    Direction = FillDirection.Vertical,
                    Spacing = new Vector2(0f, 4f),
                    Children = new Drawable[]
                    {
                        new OsuSpriteText
                        {
                            Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
                            Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
                            Margin = new MarginPadding { Bottom = 4f },
                        },
                        new Container
                        {
                            RelativeSizeAxes = Axes.X,
                            Height = 65f,
                            Children = new[]
                            {
                                retriesGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both, Y = -1f },
                                failsGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both },
                            },
                        },
                    },
                };
            }

            [BackgroundDependencyLoader]
            private void load(OsuColour colours)
            {
                retriesGraph.Colour = colours.Orange1;
                failsGraph.Colour = colours.DarkOrange2;
            }

            private partial class GraphDrawable : Drawable
            {
                private readonly float[] displayedData = new float[100];

                private float[] data = new float[100];

                public float[] Data
                {
                    get => data;
                    set
                    {
                        data = value;
                        Invalidate(Invalidation.DrawNode);
                    }
                }

                private IShader shader = null!;

                [BackgroundDependencyLoader]
                private void load(ShaderManager shaders)
                {
                    shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle");
                }

                protected override void Update()
                {
                    base.Update();

                    bool changed = false;

                    for (int i = 0; i < displayedData.Length; i++)
                    {
                        float before = displayedData[i];
                        float value = data.ElementAtOrDefault(i);
                        displayedData[i] = (float)Interpolation.DampContinuously(displayedData[i], value, 40, Time.Elapsed);
                        changed |= displayedData[i] != before;
                    }

                    if (changed)
                        Invalidate(Invalidation.DrawNode);
                }

                protected override DrawNode CreateDrawNode() => new GraphDrawNode(this);

                // todo: consider integrating this with BarGraph
                // this is different from BarGraph since this displays each bar with corner radii applied.
                private class GraphDrawNode : DrawNode
                {
                    private readonly GraphDrawable source;

                    private Vector2 drawSize;
                    private float[] displayedData = null!;
                    private IShader shader = null!;
                    private IVertexBatch<TexturedVertex2D>? quadBatch;

                    public GraphDrawNode(GraphDrawable source)
                        : base(source)
                    {
                        this.source = source;
                    }

                    public override void ApplyState()
                    {
                        base.ApplyState();

                        drawSize = source.DrawSize;
                        displayedData = source.displayedData;
                        shader = source.shader;
                    }

                    protected override void Draw(IRenderer renderer)
                    {
                        base.Draw(renderer);

                        const float spacing_constant = 1.5f;

                        float position = 0;
                        float barWidth = drawSize.X / displayedData.Length / spacing_constant;

                        float totalSpacing = drawSize.X - barWidth * displayedData.Length;
                        float spacing = totalSpacing / (displayedData.Length - 1);

                        quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(displayedData.Length * 4, 1);
                        shader.Bind();

                        for (int i = 0; i < displayedData.Length; i++)
                        {
                            float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth);

                            drawBar(renderer, position, barWidth, barHeight);

                            position += barWidth + spacing;
                        }

                        shader.Unbind();
                    }

                    private void drawBar(IRenderer renderer, float position, float width, float height)
                    {
                        // Since bars have corner radius, to avoid masking usage and draw all bars in a single draw call
                        // we are using FastCircle implementation.
                        // Not using FastCircle directly to minimize drawable count.

                        RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height));
                        Vector4 textureRectangle = new Vector4(0, 0, drawRectangle.Width, drawRectangle.Height);
                        Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix;

                        var blend = new Vector2(Math.Min(drawRectangle.Width, drawRectangle.Height) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height));

                        quadBatch?.AddAction(new TexturedVertex2D(renderer)
                        {
                            Position = screenSpaceDrawQuad.BottomLeft,
                            TexturePosition = new Vector2(0, drawRectangle.Height),
                            TextureRect = textureRectangle,
                            BlendRange = blend,
                            Colour = DrawColourInfo.Colour.BottomLeft.SRGB,
                        });
                        quadBatch?.AddAction(new TexturedVertex2D(renderer)
                        {
                            Position = screenSpaceDrawQuad.BottomRight,
                            TexturePosition = new Vector2(drawRectangle.Width, drawRectangle.Height),
                            TextureRect = textureRectangle,
                            BlendRange = blend,
                            Colour = DrawColourInfo.Colour.BottomRight.SRGB,
                        });
                        quadBatch?.AddAction(new TexturedVertex2D(renderer)
                        {
                            Position = screenSpaceDrawQuad.TopRight,
                            TexturePosition = new Vector2(drawRectangle.Width, 0),
                            TextureRect = textureRectangle,
                            BlendRange = blend,
                            Colour = DrawColourInfo.Colour.TopRight.SRGB,
                        });
                        quadBatch?.AddAction(new TexturedVertex2D(renderer)
                        {
                            Position = screenSpaceDrawQuad.TopLeft,
                            TexturePosition = Vector2.Zero,
                            TextureRect = textureRectangle,
                            BlendRange = blend,
                            Colour = DrawColourInfo.Colour.TopLeft.SRGB,
                        });
                    }

                    protected override void Dispose(bool isDisposing)
                    {
                        base.Dispose(isDisposing);

                        quadBatch?.Dispose();
                    }
                }
            }
        }
    }
}