1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Merge branch 'master' into add-random-skin-hotkey

This commit is contained in:
smoogipoo 2020-11-11 15:48:12 +09:00
commit 7df9484458
5 changed files with 495 additions and 286 deletions

View File

@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Overlays.Profile.Sections.Historical;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using static osu.Game.Users.User;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneUserHistoryGraph : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
public TestSceneUserHistoryGraph()
{
UserHistoryGraph graph;
Add(graph = new UserHistoryGraph
{
RelativeSizeAxes = Axes.X,
Height = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TooltipCounterName = "Test"
});
var values = new[]
{
new UserHistoryCount { Date = new DateTime(2000, 1, 1), Count = 10 },
new UserHistoryCount { Date = new DateTime(2000, 2, 1), Count = 20 },
new UserHistoryCount { Date = new DateTime(2000, 3, 1), Count = 100 },
new UserHistoryCount { Date = new DateTime(2000, 4, 1), Count = 15 },
new UserHistoryCount { Date = new DateTime(2000, 5, 1), Count = 30 }
};
var moreValues = new[]
{
new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 },
new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 },
new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 },
new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 },
new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 },
new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 },
new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 }
};
AddStep("Set fake values", () => graph.Values = values);
AddStep("Set more values", () => graph.Values = moreValues);
AddStep("Set null values", () => graph.Values = null);
AddStep("Set empty values", () => graph.Values = Array.Empty<UserHistoryCount>());
}
}
}

View File

@ -4,309 +4,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
{
public class RankGraph : Container, IHasCustomTooltip
public class RankGraph : UserGraph<int, int>
{
private const float secondary_textsize = 13;
private const float padding = 10;
private const float fade_duration = 150;
private const int ranked_days = 88;
private readonly RankChartLineGraph graph;
private readonly OsuSpriteText placeholder;
private KeyValuePair<int, int>[] ranks;
private int hoveredIndex = -1;
public readonly Bindable<UserStatistics> Statistics = new Bindable<UserStatistics>();
private readonly OsuSpriteText placeholder;
public RankGraph()
{
Padding = new MarginPadding { Vertical = padding };
Children = new Drawable[]
{
placeholder = new OsuSpriteText
Add(placeholder = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "No recent plays",
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular)
},
graph = new RankChartLineGraph
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Y = -secondary_textsize,
Alpha = 0,
}
};
graph.OnBallMove += i => hoveredIndex = i;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
graph.LineColour = colours.Yellow;
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Statistics.BindValueChanged(statistics => updateStatistics(statistics.NewValue), true);
}
private void updateStatistics(UserStatistics statistics)
{
placeholder.FadeIn(fade_duration, Easing.Out);
hoveredIndex = -1;
if (statistics?.Ranks.Global == null)
{
graph.FadeOut(fade_duration, Easing.Out);
ranks = null;
return;
int[] userRanks = statistics?.RankHistory?.Data;
Data = userRanks?.Select((x, index) => new KeyValuePair<int, int>(index, x)).Where(x => x.Value != 0).ToArray();
}
int[] userRanks = statistics.RankHistory?.Data ?? new[] { statistics.Ranks.Global.Value };
ranks = userRanks.Select((x, index) => new KeyValuePair<int, int>(index, x)).Where(x => x.Value != 0).ToArray();
protected override float GetDataPointHeight(int rank) => -MathF.Log(rank);
if (ranks.Length > 1)
protected override void ShowGraph()
{
placeholder.FadeOut(fade_duration, Easing.Out);
graph.DefaultValueCount = ranks.Length;
graph.Values = ranks.Select(x => -MathF.Log(x.Value));
base.ShowGraph();
placeholder.FadeOut(FADE_DURATION, Easing.Out);
}
graph.FadeTo(ranks.Length > 1 ? 1 : 0, fade_duration, Easing.Out);
if (IsHovered)
graph.UpdateBallPosition(lastHoverPosition);
protected override void HideGraph()
{
base.HideGraph();
placeholder.FadeIn(FADE_DURATION, Easing.Out);
}
private float lastHoverPosition;
protected override bool OnHover(HoverEvent e)
protected override object GetTooltipContent(int index, int rank)
{
if (ranks?.Length > 1)
{
graph.UpdateBallPosition(lastHoverPosition = e.MousePosition.X);
graph.ShowBar();
}
return base.OnHover(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (ranks?.Length > 1)
graph.UpdateBallPosition(e.MousePosition.X);
return base.OnMouseMove(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
graph.HideBar();
base.OnHoverLost(e);
}
private class RankChartLineGraph : LineGraph
{
private readonly CircularContainer movingBall;
private readonly Container bar;
private readonly Box ballBg;
private readonly Box line;
public Action<int> OnBallMove;
public RankChartLineGraph()
{
Add(bar = new Container
{
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Alpha = 0,
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
line = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 1.5f,
},
movingBall = new CircularContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Size = new Vector2(18),
Masking = true,
BorderThickness = 4,
RelativePositionAxes = Axes.Y,
Child = ballBg = new Box { RelativeSizeAxes = Axes.Both }
}
}
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
ballBg.Colour = colourProvider.Background5;
movingBall.BorderColour = line.Colour = colours.Yellow;
}
public void UpdateBallPosition(float mouseXPosition)
{
const int duration = 200;
int index = calculateIndex(mouseXPosition);
Vector2 position = calculateBallPosition(index);
movingBall.MoveToY(position.Y, duration, Easing.OutQuint);
bar.MoveToX(position.X, duration, Easing.OutQuint);
OnBallMove.Invoke(index);
}
public void ShowBar() => bar.FadeIn(fade_duration);
public void HideBar() => bar.FadeOut(fade_duration);
private int calculateIndex(float mouseXPosition) => (int)Math.Clamp(MathF.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)), 0, DefaultValueCount - 1);
private Vector2 calculateBallPosition(int index)
{
float y = GetYPosition(Values.ElementAt(index));
return new Vector2(index / (float)(DefaultValueCount - 1), y);
}
}
public object TooltipContent
{
get
{
if (ranks == null || hoveredIndex == -1)
return null;
var days = ranked_days - ranks[hoveredIndex].Key + 1;
var days = ranked_days - index + 1;
return new TooltipDisplayContent
{
Rank = $"#{ranks[hoveredIndex].Value:#,##0}",
Time = days == 0 ? "now" : $"{days} days ago"
Rank = $"#{rank:N0}",
Time = days == 0 ? "now" : $"{"day".ToQuantity(days)} ago"
};
}
}
public ITooltip GetCustomTooltip() => new RankGraphTooltip();
protected override UserGraphTooltip GetTooltip() => new RankGraphTooltip();
private class RankGraphTooltip : VisibilityContainer, ITooltip
private class RankGraphTooltip : UserGraphTooltip
{
private readonly OsuSpriteText globalRankingText, timeText;
private readonly Box background;
public RankGraphTooltip()
: base("Global Ranking")
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 10;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(10),
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = "Global Ranking "
},
globalRankingText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
}
},
timeText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
// Temporary colour since it's currently impossible to change it without bugs (see https://github.com/ppy/osu-framework/issues/3231)
// If above is fixed, this should use OverlayColourProvider
background.Colour = colours.Gray1;
}
public bool SetContent(object content)
public override bool SetContent(object content)
{
if (!(content is TooltipDisplayContent info))
return false;
globalRankingText.Text = info.Rank;
timeText.Text = info.Time;
Counter.Text = info.Rank;
BottomText.Text = info.Time;
return true;
}
private bool instantMove = true;
public void Move(Vector2 pos)
{
if (instantMove)
{
Position = pos;
instantMove = false;
}
else
this.MoveTo(pos, 200, Easing.OutQuint);
}
protected override void PopIn()
{
instantMove |= !IsPresent;
this.FadeIn(200, Easing.OutQuint);
}
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
}
private class TooltipDisplayContent

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class UserHistoryGraph : UserGraph<DateTime, long>
{
[CanBeNull]
public UserHistoryCount[] Values
{
set => Data = value?.Select(v => new KeyValuePair<DateTime, long>(v.Date, v.Count)).ToArray();
}
/// <summary>
/// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the <see cref="HistoryGraphTooltip"/>.
/// </summary>
public string TooltipCounterName { get; set; } = "Plays";
protected override float GetDataPointHeight(long playCount) => playCount;
protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(TooltipCounterName);
protected override object GetTooltipContent(DateTime date, long playCount)
{
return new TooltipDisplayContent
{
Count = playCount.ToString("N0"),
Date = date.ToString("MMMM yyyy")
};
}
protected class HistoryGraphTooltip : UserGraphTooltip
{
public HistoryGraphTooltip(string tooltipCounterName)
: base(tooltipCounterName)
{
}
public override bool SetContent(object content)
{
if (!(content is TooltipDisplayContent info))
return false;
Counter.Text = info.Count;
BottomText.Text = info.Date;
return true;
}
}
private class TooltipDisplayContent
{
public string Count;
public string Date;
}
}
}

View File

@ -0,0 +1,294 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Overlays.Profile
{
/// <summary>
/// Graph which is used in <see cref="UserProfileOverlay"/> to present changes in user statistics over time.
/// </summary>
/// <typeparam name="TKey">Type of data to be used for X-axis of the graph.</typeparam>
/// <typeparam name="TValue">Type of data to be used for Y-axis of the graph.</typeparam>
public abstract class UserGraph<TKey, TValue> : Container, IHasCustomTooltip
{
protected const float FADE_DURATION = 150;
private readonly UserLineGraph graph;
private KeyValuePair<TKey, TValue>[] data;
private int hoveredIndex = -1;
protected UserGraph()
{
Add(graph = new UserLineGraph
{
RelativeSizeAxes = Axes.Both,
Alpha = 0
});
graph.OnBallMove += i => hoveredIndex = i;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
graph.LineColour = colours.Yellow;
}
private float lastHoverPosition;
protected override bool OnHover(HoverEvent e)
{
if (data?.Length > 1)
{
graph.UpdateBallPosition(lastHoverPosition = e.MousePosition.X);
graph.ShowBar();
return true;
}
return base.OnHover(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (data?.Length > 1)
graph.UpdateBallPosition(e.MousePosition.X);
return base.OnMouseMove(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
graph.HideBar();
base.OnHoverLost(e);
}
/// <summary>
/// Set of values which will be used to create a graph.
/// </summary>
[CanBeNull]
protected KeyValuePair<TKey, TValue>[] Data
{
set
{
data = value;
redrawGraph();
}
}
private void redrawGraph()
{
hoveredIndex = -1;
if (data?.Length > 1)
{
graph.DefaultValueCount = data.Length;
graph.Values = data.Select(pair => GetDataPointHeight(pair.Value)).ToArray();
ShowGraph();
if (IsHovered)
graph.UpdateBallPosition(lastHoverPosition);
return;
}
HideGraph();
}
/// <summary>
/// Function used to convert <see cref="Data"/> point to it's Y-axis position on the graph.
/// </summary>
/// <param name="value">Value to convert.</param>
protected abstract float GetDataPointHeight(TValue value);
protected virtual void ShowGraph() => graph.FadeIn(FADE_DURATION, Easing.Out);
protected virtual void HideGraph() => graph.FadeOut(FADE_DURATION, Easing.Out);
public ITooltip GetCustomTooltip() => GetTooltip();
protected abstract UserGraphTooltip GetTooltip();
public object TooltipContent
{
get
{
if (data == null || hoveredIndex == -1)
return null;
var (key, value) = data[hoveredIndex];
return GetTooltipContent(key, value);
}
}
protected abstract object GetTooltipContent(TKey key, TValue value);
protected class UserLineGraph : LineGraph
{
private readonly CircularContainer movingBall;
private readonly Container bar;
private readonly Box ballBg;
private readonly Box line;
public Action<int> OnBallMove;
public UserLineGraph()
{
Add(bar = new Container
{
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Alpha = 0,
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
line = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
movingBall = new CircularContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Size = new Vector2(20),
Masking = true,
BorderThickness = 4,
RelativePositionAxes = Axes.Y,
Child = ballBg = new Box { RelativeSizeAxes = Axes.Both }
}
}
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
ballBg.Colour = colourProvider.Background5;
movingBall.BorderColour = line.Colour = colours.Yellow;
}
public void UpdateBallPosition(float mouseXPosition)
{
const int duration = 200;
int index = calculateIndex(mouseXPosition);
Vector2 position = calculateBallPosition(index);
movingBall.MoveToY(position.Y, duration, Easing.OutQuint);
bar.MoveToX(position.X, duration, Easing.OutQuint);
OnBallMove.Invoke(index);
}
public void ShowBar() => bar.FadeIn(FADE_DURATION);
public void HideBar() => bar.FadeOut(FADE_DURATION);
private int calculateIndex(float mouseXPosition) => (int)Math.Clamp(MathF.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)), 0, DefaultValueCount - 1);
private Vector2 calculateBallPosition(int index)
{
float y = GetYPosition(Values.ElementAt(index));
return new Vector2(index / (float)(DefaultValueCount - 1), y);
}
}
protected abstract class UserGraphTooltip : VisibilityContainer, ITooltip
{
protected readonly OsuSpriteText Counter, BottomText;
private readonly Box background;
protected UserGraphTooltip(string tooltipCounterName)
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 10;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(10),
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(3, 0),
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = tooltipCounterName
},
Counter = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
}
},
BottomText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
// Temporary colour since it's currently impossible to change it without bugs (see https://github.com/ppy/osu-framework/issues/3231)
// If above is fixed, this should use OverlayColourProvider
background.Colour = colours.Gray1;
}
public abstract bool SetContent(object content);
private bool instantMove = true;
public void Move(Vector2 pos)
{
if (instantMove)
{
Position = pos;
instantMove = false;
}
else
this.MoveTo(pos, 200, Easing.OutQuint);
}
protected override void PopIn()
{
instantMove |= !IsPresent;
this.FadeIn(200, Easing.OutQuint);
}
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -61,7 +62,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private RulesetStore rulesets { get; set; }
private Replay replay;
private Score score;
private readonly object scoreLock = new object();
private Container beatmapPanelContainer;
@ -198,14 +201,22 @@ namespace osu.Game.Screens.Play
private void userSentFrames(int userId, FrameDataBundle data)
{
// this is not scheduled as it handles propagation of frames even when in a child screen (at which point we are not alive).
// probably not the safest way to handle this.
if (userId != targetUser.Id)
return;
lock (scoreLock)
{
// this should never happen as the server sends the user's state on watching,
// but is here as a safety measure.
if (replay == null)
if (score == null)
return;
// rulesetInstance should be guaranteed to be in sync with the score via scoreLock.
Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset));
foreach (var frame in data.Frames)
{
IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame();
@ -214,7 +225,8 @@ namespace osu.Game.Screens.Play
var convertedFrame = (ReplayFrame)convertibleFrame;
convertedFrame.Time = frame.Time;
replay.Frames.Add(convertedFrame);
score.Replay.Frames.Add(convertedFrame);
}
}
}
@ -247,10 +259,13 @@ namespace osu.Game.Screens.Play
if (userId != targetUser.Id)
return;
if (replay != null)
lock (scoreLock)
{
replay.HasReceivedAllFrames = true;
replay = null;
if (score != null)
{
score.Replay.HasReceivedAllFrames = true;
score = null;
}
}
Schedule(clearDisplay);
@ -283,14 +298,18 @@ namespace osu.Game.Screens.Play
return;
}
replay ??= new Replay { HasReceivedAllFrames = false };
var scoreInfo = new ScoreInfo
lock (scoreLock)
{
score = new Score
{
ScoreInfo = new ScoreInfo
{
Beatmap = resolvedBeatmap,
User = targetUser,
Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(),
Ruleset = resolvedRuleset.RulesetInfo,
},
Replay = new Replay { HasReceivedAllFrames = false },
};
ruleset.Value = resolvedRuleset.RulesetInfo;
@ -299,11 +318,8 @@ namespace osu.Game.Screens.Play
beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap);
watchButton.Enabled.Value = true;
this.Push(new SpectatorPlayerLoader(new Score
{
ScoreInfo = scoreInfo,
Replay = replay,
}));
this.Push(new SpectatorPlayerLoader(score));
}
}
private void showBeatmapPanel(SpectatorState state)