diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs new file mode 100644 index 0000000000..57ce4c41e7 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . 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()); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 26126bca58..ad91e491ef 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -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 { - 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[] ranks; - private int hoveredIndex = -1; public readonly Bindable Statistics = new Bindable(); + private readonly OsuSpriteText placeholder; + public RankGraph() { - Padding = new MarginPadding { Vertical = padding }; - Children = new Drawable[] + Add(placeholder = new OsuSpriteText { - 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; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "No recent plays", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular) + }); } 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 ?? new[] { statistics.Ranks.Global.Value }; - ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); - - if (ranks.Length > 1) - { - placeholder.FadeOut(fade_duration, Easing.Out); - - graph.DefaultValueCount = ranks.Length; - graph.Values = ranks.Select(x => -MathF.Log(x.Value)); - } - - graph.FadeTo(ranks.Length > 1 ? 1 : 0, fade_duration, Easing.Out); - - if (IsHovered) - graph.UpdateBallPosition(lastHoverPosition); + int[] userRanks = statistics?.RankHistory?.Data; + Data = userRanks?.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); } - private float lastHoverPosition; + protected override float GetDataPointHeight(int rank) => -MathF.Log(rank); - protected override bool OnHover(HoverEvent e) + protected override void ShowGraph() { - if (ranks?.Length > 1) - { - graph.UpdateBallPosition(lastHoverPosition = e.MousePosition.X); - graph.ShowBar(); - } - - return base.OnHover(e); + base.ShowGraph(); + placeholder.FadeOut(FADE_DURATION, Easing.Out); } - protected override bool OnMouseMove(MouseMoveEvent e) + protected override void HideGraph() { - if (ranks?.Length > 1) - graph.UpdateBallPosition(e.MousePosition.X); - - return base.OnMouseMove(e); + base.HideGraph(); + placeholder.FadeIn(FADE_DURATION, Easing.Out); } - protected override void OnHoverLost(HoverLostEvent e) + protected override object GetTooltipContent(int index, int rank) { - graph.HideBar(); - base.OnHoverLost(e); + var days = ranked_days - index + 1; + + return new TooltipDisplayContent + { + Rank = $"#{rank:N0}", + Time = days == 0 ? "now" : $"{"day".ToQuantity(days)} ago" + }; } - private class RankChartLineGraph : LineGraph + protected override UserGraphTooltip GetTooltip() => new RankGraphTooltip(); + + private class RankGraphTooltip : UserGraphTooltip { - private readonly CircularContainer movingBall; - private readonly Container bar; - private readonly Box ballBg; - private readonly Box line; - - public Action 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; - - return new TooltipDisplayContent - { - Rank = $"#{ranks[hoveredIndex].Value:#,##0}", - Time = days == 0 ? "now" : $"{days} days ago" - }; - } - } - - public ITooltip GetCustomTooltip() => new RankGraphTooltip(); - - private class RankGraphTooltip : VisibilityContainer, ITooltip - { - 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 diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs new file mode 100644 index 0000000000..b1e8c8f0ca --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . 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 + { + [CanBeNull] + public UserHistoryCount[] Values + { + set => Data = value?.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); + } + + /// + /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the . + /// + 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; + } + } +} diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs new file mode 100644 index 0000000000..cdfd722d68 --- /dev/null +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -0,0 +1,294 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// Graph which is used in to present changes in user statistics over time. + /// + /// Type of data to be used for X-axis of the graph. + /// Type of data to be used for Y-axis of the graph. + public abstract class UserGraph : Container, IHasCustomTooltip + { + protected const float FADE_DURATION = 150; + + private readonly UserLineGraph graph; + private KeyValuePair[] 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); + } + + /// + /// Set of values which will be used to create a graph. + /// + [CanBeNull] + protected KeyValuePair[] 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(); + } + + /// + /// Function used to convert point to it's Y-axis position on the graph. + /// + /// Value to convert. + 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 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); + } + } +} diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 0f593db277..6f51771c12 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -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,23 +201,32 @@ 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; - // this should never happen as the server sends the user's state on watching, - // but is here as a safety measure. - if (replay == null) - return; - - foreach (var frame in data.Frames) + lock (scoreLock) { - IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); + // this should never happen as the server sends the user's state on watching, + // but is here as a safety measure. + if (score == null) + return; - var convertedFrame = (ReplayFrame)convertibleFrame; - convertedFrame.Time = frame.Time; + // rulesetInstance should be guaranteed to be in sync with the score via scoreLock. + Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset)); - replay.Frames.Add(convertedFrame); + foreach (var frame in data.Frames) + { + IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); + + var convertedFrame = (ReplayFrame)convertibleFrame; + convertedFrame.Time = frame.Time; + + 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,27 +298,28 @@ namespace osu.Game.Screens.Play return; } - replay ??= new Replay { HasReceivedAllFrames = false }; - - var scoreInfo = new ScoreInfo + lock (scoreLock) { - Beatmap = resolvedBeatmap, - User = targetUser, - Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), - Ruleset = resolvedRuleset.RulesetInfo, - }; + 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; - rulesetInstance = resolvedRuleset; + ruleset.Value = resolvedRuleset.RulesetInfo; + rulesetInstance = resolvedRuleset; - beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); - watchButton.Enabled.Value = true; + 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)