diff --git a/Gemfile.lock b/Gemfile.lock index a4b49af7e4..8ac863c9a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,27 +1,27 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.2) + CFPropertyList (3.0.3) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) atomos (0.1.3) aws-eventstream (1.1.0) - aws-partitions (1.354.0) - aws-sdk-core (3.104.3) + aws-partitions (1.413.0) + aws-sdk-core (3.110.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.36.0) - aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-kms (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.78.0) - aws-sdk-core (~> 3, >= 3.104.3) + aws-sdk-s3 (1.87.0) + aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.1) + aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.3) + babosa (1.0.4) claide (1.0.3) colored (1.2) colored2 (3.1.2) @@ -29,22 +29,23 @@ GEM highline (~> 1.7.2) declarative (0.0.20) declarative-option (0.1.0) - digest-crc (0.6.1) - rake (~> 13.0) + digest-crc (0.6.3) + rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.0.0) - excon (0.76.0) - faraday (1.0.1) + emoji_regex (3.2.1) + excon (0.78.1) + faraday (1.2.0) multipart-post (>= 1.2, < 3) - faraday-cookie_jar (0.0.6) - faraday (>= 0.7.4) + ruby2_keywords + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday_middleware (1.0.0) faraday (~> 1.0) - fastimage (2.2.0) - fastlane (2.156.0) + fastimage (2.2.1) + fastlane (2.170.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) aws-sdk-s3 (~> 1.0) @@ -96,17 +97,17 @@ GEM google-cloud-core (1.5.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.3.3) + google-cloud-env (1.4.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.0.1) - google-cloud-storage (1.27.0) + google-cloud-storage (1.29.2) addressable (~> 2.5) digest-crc (~> 0.4) google-api-client (~> 0.33) google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.13.1) + googleauth (0.14.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -118,10 +119,10 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.4.0) - json (2.3.1) - jwt (2.2.1) + json (2.5.1) + jwt (2.2.2) memoist (0.16.2) - mini_magick (4.10.1) + mini_magick (4.11.0) mini_mime (1.0.2) mini_portile2 (2.4.0) multi_json (1.15.0) @@ -132,14 +133,15 @@ GEM mini_portile2 (~> 2.4.0) os (1.1.1) plist (3.5.0) - public_suffix (4.0.5) - rake (13.0.1) + public_suffix (4.0.6) + rake (13.0.3) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) + ruby2_keywords (0.0.2) rubyzip (2.3.0) security (0.1.3) signet (0.14.0) @@ -168,7 +170,7 @@ GEM unf_ext (0.0.7.7) unicode-display_width (1.7.0) word_wrap (1.0.0) - xcodeproj (1.18.0) + xcodeproj (1.19.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/osu.Android.props b/osu.Android.props index fc01f9bf1d..cd2ce58c55 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 8078c7b994..975c54c3f6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -20,14 +20,17 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Online; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene + public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { + private const int users = 16; + [Cached(typeof(SpectatorStreamingClient))] - private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users); [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); @@ -47,10 +50,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { AddStep("create leaderboard", () => { + leaderboard?.Expire(); + OsuScoreProcessor scoreProcessor; Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); @@ -58,6 +63,9 @@ namespace osu.Game.Tests.Visual.Gameplay streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + Client.PlayingUsers.Clear(); + Client.PlayingUsers.AddRange(streamingClient.PlayingUsers); + Children = new Drawable[] { scoreProcessor = new OsuScoreProcessor(), @@ -81,6 +89,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); } + [Test] + public void TestUserQuit() + { + AddRepeatStep("mark user quit", () => Client.PlayingUsers.RemoveAt(0), users); + } + public class TestMultiplayerStreaming : SpectatorStreamingClient { public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index dcfe0432a8..3d65e7e4ba 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,13 +14,12 @@ namespace osu.Game.Tests.Visual.Online public class TestSceneAccountCreationOverlay : OsuTestScene { private readonly Container userPanelArea; + private readonly AccountCreationOverlay accountCreation; private IBindable localUser; public TestSceneAccountCreationOverlay() { - AccountCreationOverlay accountCreation; - Children = new Drawable[] { accountCreation = new AccountCreationOverlay(), @@ -31,8 +31,6 @@ namespace osu.Game.Tests.Visual.Online Origin = Anchor.TopRight, }, }; - - AddStep("show", () => accountCreation.Show()); } [BackgroundDependencyLoader] @@ -42,8 +40,19 @@ namespace osu.Game.Tests.Visual.Online localUser = API.LocalUser.GetBoundCopy(); localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true); + } - AddStep("logout", API.Logout); + [Test] + public void TestOverlayVisibility() + { + AddStep("start hidden", () => accountCreation.Hide()); + AddStep("log out", API.Logout); + + AddStep("show manually", () => accountCreation.Show()); + AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible); + + AddStep("log back in", () => API.Login("dummy", "password")); + AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 998e42b478..cd2c4e9346 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -1,8 +1,13 @@ // 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 Humanizer; using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Changelog; @@ -12,13 +17,61 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneChangelogOverlay : OsuTestScene { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + private readonly Dictionary streams; + private readonly Dictionary builds; + + private APIChangelogBuild requestedBuild; private TestChangelogOverlay changelog; - protected override bool UseOnlineAPI => true; + public TestSceneChangelogOverlay() + { + streams = APIUpdateStream.KNOWN_STREAMS.Keys.Select((stream, id) => new APIUpdateStream + { + Id = id + 1, + Name = stream, + DisplayName = stream.Humanize(), // not quite there, but good enough. + }).ToDictionary(stream => stream.Name); + + string version = DateTimeOffset.Now.ToString("yyyy.Mdd.0"); + builds = APIUpdateStream.KNOWN_STREAMS.Keys.Select(stream => new APIChangelogBuild + { + Version = version, + DisplayVersion = version, + UpdateStream = streams[stream], + ChangelogEntries = new List() + }).ToDictionary(build => build.UpdateStream.Name); + + foreach (var stream in streams.Values) + stream.LatestBuild = builds[stream.Name]; + } [SetUp] public void SetUp() => Schedule(() => { + requestedBuild = null; + + dummyAPI.HandleRequest = request => + { + switch (request) + { + case GetChangelogRequest changelogRequest: + var changelogResponse = new APIChangelogIndex + { + Streams = streams.Values.ToList(), + Builds = builds.Values.ToList() + }; + changelogRequest.TriggerSuccess(changelogResponse); + break; + + case GetChangelogBuildRequest buildRequest: + if (requestedBuild != null) + buildRequest.TriggerSuccess(requestedBuild); + break; + } + }; + Child = changelog = new TestChangelogOverlay(); }); @@ -41,26 +94,60 @@ namespace osu.Game.Tests.Visual.Online } [Test] - [Ignore("needs to be updated to not be so server dependent")] public void ShowWithBuild() { - AddStep(@"Show with Lazer 2018.712.0", () => + showBuild(() => new APIChangelogBuild { - changelog.ShowBuild(new APIChangelogBuild + Version = "2018.712.0", + DisplayVersion = "2018.712.0", + UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME], + ChangelogEntries = new List { - Version = "2018.712.0", - DisplayVersion = "2018.712.0", - UpdateStream = new APIUpdateStream { Id = 5, Name = OsuGameBase.CLIENT_STREAM_NAME }, - ChangelogEntries = new List + new APIChangelogEntry { - new APIChangelogEntry + Type = ChangelogEntryType.Fix, + Category = "osu!", + Title = "Fix thing", + MessageHtml = "Additional info goes here.", + Repository = "osu", + GithubPullRequestId = 11100, + GithubUser = new APIChangelogUser { - Category = "Test", - Title = "Title", - MessageHtml = "Message", + OsuUsername = "smoogipoo", + UserId = 1040328 } + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Add, + Category = "osu!", + Title = "Add thing", + Major = true, + Repository = "ppy/osu-framework", + GithubPullRequestId = 4444, + GithubUser = new APIChangelogUser + { + DisplayName = "frenzibyte", + GithubUrl = "https://github.com/frenzibyte" + } + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Misc, + Category = "Code quality", + Title = "Clean up thing", + GithubUser = new APIChangelogUser + { + DisplayName = "some dude" + } + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Misc, + Category = "Code quality", + Title = "Clean up another thing" } - }); + } }); AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0); @@ -71,35 +158,38 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestHTMLUnescaping() { - AddStep(@"Ensure HTML string unescaping", () => + showBuild(() => new APIChangelogBuild { - changelog.ShowBuild(new APIChangelogBuild + Version = "2019.920.0", + DisplayVersion = "2019.920.0", + UpdateStream = new APIUpdateStream { - Version = "2019.920.0", - DisplayVersion = "2019.920.0", - UpdateStream = new APIUpdateStream + Name = "Test", + DisplayName = "Test" + }, + ChangelogEntries = new List + { + new APIChangelogEntry { - Name = "Test", - DisplayName = "Test" - }, - ChangelogEntries = new List - { - new APIChangelogEntry + Category = "Testing HTML strings unescaping", + Title = "Ensuring HTML strings are being unescaped", + MessageHtml = """"This text should appear triple-quoted""" >_<", + GithubUser = new APIChangelogUser { - Category = "Testing HTML strings unescaping", - Title = "Ensuring HTML strings are being unescaped", - MessageHtml = """"This text should appear triple-quoted""" >_<", - GithubUser = new APIChangelogUser - { - DisplayName = "Dummy", - OsuUsername = "Dummy", - } - }, - } - }); + DisplayName = "Dummy", + OsuUsername = "Dummy", + } + }, + } }); } + private void showBuild(Func build) + { + AddStep("set up build", () => requestedBuild = build.Invoke()); + AddStep("show build", () => changelog.ShowBuild(requestedBuild)); + } + private class TestChangelogOverlay : ChangelogOverlay { public new List Streams => base.Streams; diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs index d9e48373bb..5af7d6a01c 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using Newtonsoft.Json; using osu.Framework.Graphics.Colour; using osuTK.Graphics; @@ -27,34 +28,16 @@ namespace osu.Game.Online.API.Requests.Responses public bool Equals(APIUpdateStream other) => Id == other?.Id; - public ColourInfo Colour + internal static readonly Dictionary KNOWN_STREAMS = new Dictionary { - get - { - switch (Name) - { - case "stable40": - return new Color4(102, 204, 255, 255); + ["stable40"] = new Color4(102, 204, 255, 255), + ["stable"] = new Color4(34, 153, 187, 255), + ["beta40"] = new Color4(255, 221, 85, 255), + ["cuttingedge"] = new Color4(238, 170, 0, 255), + [OsuGameBase.CLIENT_STREAM_NAME] = new Color4(237, 18, 33, 255), + ["web"] = new Color4(136, 102, 238, 255) + }; - case "stable": - return new Color4(34, 153, 187, 255); - - case "beta40": - return new Color4(255, 221, 85, 255); - - case "cuttingedge": - return new Color4(238, 170, 0, 255); - - case OsuGameBase.CLIENT_STREAM_NAME: - return new Color4(237, 18, 33, 255); - - case "web": - return new Color4(136, 102, 238, 255); - - default: - return new Color4(0, 0, 0, 255); - } - } - } + public ColourInfo Colour => KNOWN_STREAMS.TryGetValue(Name, out var colour) ? colour : new Color4(0, 0, 0, 255); } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index d8207aa8f4..5608002513 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Online.Leaderboards statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList(); - DrawableAvatar innerAvatar; + ClickableAvatar innerAvatar; Children = new Drawable[] { @@ -115,7 +115,7 @@ namespace osu.Game.Online.Leaderboards Children = new[] { avatar = new DelayedLoadWrapper( - innerAvatar = new DrawableAvatar(user) + innerAvatar = new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, CornerRadius = corner_radius, diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 58ede5502a..3084c7475a 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -93,6 +94,11 @@ namespace osu.Game.Overlays if (welcomeScreen.GetChildScreen() != null) welcomeScreen.MakeCurrent(); + + // there might be a stale scheduled hide from a previous API state change. + // cancel it here so that the overlay is not hidden again after one frame. + scheduledHide?.Cancel(); + scheduledHide = null; } protected override void PopOut() @@ -101,7 +107,9 @@ namespace osu.Game.Overlays this.FadeOut(100); } - private void apiStateChanged(ValueChangedEvent state) => Schedule(() => + private ScheduledDelegate scheduledHide; + + private void apiStateChanged(ValueChangedEvent state) { switch (state.NewValue) { @@ -113,9 +121,10 @@ namespace osu.Game.Overlays break; case APIState.Online: - Hide(); + scheduledHide?.Cancel(); + scheduledHide = Schedule(Hide); break; } - }); + } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 65ff0fef92..2d071b7345 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -9,14 +9,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using System; using System.Linq; -using System.Text.RegularExpressions; using osu.Game.Graphics.Sprites; -using osu.Game.Users; -using osuTK.Graphics; using osu.Framework.Allocation; -using System.Net; -using osuTK; -using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Overlays.Changelog { @@ -63,126 +57,7 @@ namespace osu.Game.Overlays.Changelog Margin = new MarginPadding { Top = 35, Bottom = 15 }, }); - var fontLarge = OsuFont.GetFont(size: 16); - var fontMedium = OsuFont.GetFont(size: 12); - - foreach (var entry in categoryEntries) - { - var entryColour = entry.Major ? colours.YellowLight : Color4.White; - - LinkFlowContainer title; - - var titleContainer = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Vertical = 5 }, - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreRight, - Size = new Vector2(10), - Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus, - Colour = entryColour.Opacity(0.5f), - Margin = new MarginPadding { Right = 5 }, - }, - title = new LinkFlowContainer - { - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.BottomLeft, - } - } - }; - - title.AddText(entry.Title, t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - - if (!string.IsNullOrEmpty(entry.Repository)) - { - title.AddText(" (", t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, - creationParameters: t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - title.AddText(")", t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - } - - title.AddText("by ", t => - { - t.Font = fontMedium; - t.Colour = entryColour; - t.Padding = new MarginPadding { Left = 10 }; - }); - - if (entry.GithubUser != null) - { - if (entry.GithubUser.UserId != null) - { - title.AddUserLink(new User - { - Username = entry.GithubUser.OsuUsername, - Id = entry.GithubUser.UserId.Value - }, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else if (entry.GithubUser.GithubUrl != null) - { - title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else - { - title.AddText(entry.GithubUser.DisplayName, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - } - - ChangelogEntries.Add(titleContainer); - - if (!string.IsNullOrEmpty(entry.MessageHtml)) - { - var message = new TextFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }; - - // todo: use markdown parsing once API returns markdown - message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t => - { - t.Font = fontMedium; - t.Colour = colourProvider.Foreground1; - }); - - ChangelogEntries.Add(message); - } - } + ChangelogEntries.AddRange(categoryEntries.Select(entry => new ChangelogEntry(entry))); } } diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs new file mode 100644 index 0000000000..55edb40283 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -0,0 +1,202 @@ +// 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.Net; +using System.Text.RegularExpressions; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogEntry : FillFlowContainer + { + private readonly APIChangelogEntry entry; + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + private FontUsage fontLarge; + private FontUsage fontMedium; + + public ChangelogEntry(APIChangelogEntry entry) + { + this.entry = entry; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + } + + [BackgroundDependencyLoader] + private void load() + { + fontLarge = OsuFont.GetFont(size: 16); + fontMedium = OsuFont.GetFont(size: 12); + + Children = new[] + { + createTitle(), + createMessage() + }; + } + + private Drawable createTitle() + { + var entryColour = entry.Major ? colours.YellowLight : Color4.White; + + LinkFlowContainer title; + + var titleContainer = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Vertical = 5 }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Size = new Vector2(10), + Icon = getIconForChangelogEntry(entry.Type), + Colour = entryColour.Opacity(0.5f), + Margin = new MarginPadding { Right = 5 }, + }, + title = new LinkFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.BottomLeft, + } + } + }; + + title.AddText(entry.Title, t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + + if (!string.IsNullOrEmpty(entry.Repository)) + addRepositoryReference(title, entryColour); + + if (entry.GithubUser != null) + addGithubAuthorReference(title, entryColour); + + return titleContainer; + } + + private void addRepositoryReference(LinkFlowContainer title, Color4 entryColour) + { + title.AddText(" (", t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, + t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + title.AddText(")", t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + } + + private void addGithubAuthorReference(LinkFlowContainer title, Color4 entryColour) + { + title.AddText("by ", t => + { + t.Font = fontMedium; + t.Colour = entryColour; + t.Padding = new MarginPadding { Left = 10 }; + }); + + if (entry.GithubUser.UserId != null) + { + title.AddUserLink(new User + { + Username = entry.GithubUser.OsuUsername, + Id = entry.GithubUser.UserId.Value + }, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else if (entry.GithubUser.GithubUrl != null) + { + title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else + { + title.AddText(entry.GithubUser.DisplayName, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + } + + private Drawable createMessage() + { + if (string.IsNullOrEmpty(entry.MessageHtml)) + return Empty(); + + var message = new TextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + // todo: use markdown parsing once API returns markdown + message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t => + { + t.Font = fontMedium; + t.Colour = colourProvider.Foreground1; + }); + + return message; + } + + private static IconUsage getIconForChangelogEntry(ChangelogEntryType entryType) + { + // compare: https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/_components/changelog-entry.coffee#L8-L11 + switch (entryType) + { + case ChangelogEntryType.Add: + return FontAwesome.Solid.Plus; + + case ChangelogEntryType.Fix: + return FontAwesome.Solid.Check; + + case ChangelogEntryType.Misc: + return FontAwesome.Regular.Circle; + + default: + throw new ArgumentOutOfRangeException(nameof(entryType), $"Unrecognised entry type {entryType}"); + } + } + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 509a6dabae..6bbff045b5 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -8,5 +8,11 @@ namespace osu.Game.Overlays.Changelog public class ChangelogUpdateStreamControl : OverlayStreamControl { protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); + + protected override void LoadComplete() + { + // suppress base logic of immediately selecting first item if one exists + // (we always want to start with no stream selected). + } } } diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 5b428a3825..00f46b0035 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Chat.Tabs if (value.Type != ChannelType.PM) throw new ArgumentException("Argument value needs to have the targettype user!"); - DrawableAvatar avatar; + ClickableAvatar avatar; AddRange(new Drawable[] { @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Chat.Tabs Anchor = Anchor.Centre, Origin = Anchor.Centre, Masking = true, - Child = new DelayedLoadWrapper(avatar = new DrawableAvatar(value.Users.First()) + Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First()) { RelativeSizeAxes = Axes.Both, OpenOnClick = { Value = false }, diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 58281debf1..83b70911c6 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -34,6 +34,7 @@ namespace osu.Game.Screens.Play.HUD public BindableDouble TotalScore { get; } = new BindableDouble(); public BindableDouble Accuracy { get; } = new BindableDouble(1); public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); private int? scorePosition; @@ -51,7 +52,7 @@ namespace osu.Game.Screens.Play.HUD positionText.Text = $"#{scorePosition.Value.FormatRank()}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); - updateColour(); + updateState(); } } @@ -78,6 +79,8 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { + Container avatarContainer; + InternalChildren = new Drawable[] { mainFillContainer = new Container @@ -152,7 +155,7 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(4f, 0f), Children = new Drawable[] { - new CircularContainer + avatarContainer = new CircularContainer { Masking = true, Anchor = Anchor.CentreLeft, @@ -166,11 +169,7 @@ namespace osu.Game.Screens.Play.HUD Alpha = 0.3f, RelativeSizeAxes = Axes.Both, Colour = colours.Gray4, - }, - new UpdateableAvatar(User) - { - RelativeSizeAxes = Axes.Both, - }, + } } }, usernameText = new OsuSpriteText @@ -227,23 +226,36 @@ namespace osu.Game.Screens.Play.HUD } }; + LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); + TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); + HasQuit.BindValueChanged(_ => updateState()); } protected override void LoadComplete() { base.LoadComplete(); - updateColour(); + updateState(); FinishTransforms(true); } private const double panel_transition_duration = 500; - private void updateColour() + private void updateState() { + if (HasQuit.Value) + { + // we will probably want to display this in a better way once we have a design. + // and also show states other than quit. + mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); + panelColour = Color4.Gray; + textColour = Color4.White; + return; + } + if (scorePosition == 1) { mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index bc1a03c5aa..83b6f6621b 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -10,5 +10,7 @@ namespace osu.Game.Screens.Play.HUD BindableDouble TotalScore { get; } BindableDouble Accuracy { get; } BindableInt Combo { get; } + + BindableBool HasQuit { get; } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index c10ec9e004..00e2b8bfa7 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; @@ -18,10 +21,21 @@ namespace osu.Game.Screens.Play.HUD { private readonly ScoreProcessor scoreProcessor; - private readonly int[] userIds; - private readonly Dictionary userScores = new Dictionary(); + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private Bindable scoringMode; + + private readonly BindableList playingUsers; + /// /// Construct a new leaderboard. /// @@ -33,43 +47,68 @@ namespace osu.Game.Screens.Play.HUD this.scoreProcessor = scoreProcessor; // todo: this will likely be passed in as User instances. - this.userIds = userIds; + playingUsers = new BindableList(userIds); } - [Resolved] - private SpectatorStreamingClient streamingClient { get; set; } - - [Resolved] - private UserLookupCache userLookupCache { get; set; } - - private Bindable scoringMode; - [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { streamingClient.OnNewFrames += handleIncomingFrames; - foreach (var user in userIds) + foreach (var userId in playingUsers) { - streamingClient.WatchUser(user); + streamingClient.WatchUser(userId); // probably won't be required in the final implementation. - var resolvedUser = userLookupCache.GetUserAsync(user).Result; + var resolvedUser = userLookupCache.GetUserAsync(userId).Result; var trackedUser = new TrackedUserData(); - userScores[user] = trackedUser; + userScores[userId] = trackedUser; var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); + ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit); } scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); scoringMode.BindValueChanged(updateAllScores, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. + foreach (int userId in playingUsers) + { + if (!multiplayerClient.PlayingUsers.Contains(userId)) + usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); + } + + playingUsers.BindTo(multiplayerClient.PlayingUsers); + playingUsers.BindCollectionChanged(usersChanged); + } + + private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Remove: + foreach (var userId in e.OldItems.OfType()) + { + streamingClient.StopWatchingUser(userId); + + if (userScores.TryGetValue(userId, out var trackedData)) + trackedData.MarkUserQuit(); + } + + break; + } + } + private void updateAllScores(ValueChangedEvent mode) { foreach (var trackedData in userScores.Values) @@ -91,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD if (streamingClient != null) { - foreach (var user in userIds) + foreach (var user in playingUsers) { streamingClient.StopWatchingUser(user); } @@ -114,9 +153,15 @@ namespace osu.Game.Screens.Play.HUD private readonly BindableInt currentCombo = new BindableInt(); + public IBindable UserQuit => userQuit; + + private readonly BindableBool userQuit = new BindableBool(); + [CanBeNull] public FrameHeader LastHeader; + public void MarkUserQuit() => userQuit.Value = true; + public void UpdateScore(ScoreProcessor processor, ScoringMode mode) { if (LastHeader == null) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs new file mode 100644 index 0000000000..0fca9c7c9b --- /dev/null +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Users.Drawables +{ + public class ClickableAvatar : Container + { + /// + /// Whether to open the user's profile when clicked. + /// + public readonly BindableBool OpenOnClick = new BindableBool(true); + + private readonly User user; + + [Resolved(CanBeNull = true)] + private OsuGame game { get; set; } + + /// + /// A clickable avatar for the specified user, with UI sounds included. + /// If is true, clicking will open the user's profile. + /// + /// The user. A null value will get a placeholder avatar. + public ClickableAvatar(User user = null) + { + this.user = user; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + ClickableArea clickableArea; + Add(clickableArea = new ClickableArea + { + RelativeSizeAxes = Axes.Both, + Action = openProfile + }); + + LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add); + + clickableArea.Enabled.BindTo(OpenOnClick); + } + + private void openProfile() + { + if (!OpenOnClick.Value) + return; + + if (user?.Id > 1) + game?.ShowUser(user.Id); + } + + private class ClickableArea : OsuClickableContainer + { + public override string TooltipText => Enabled.Value ? @"view profile" : null; + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + return base.OnClick(e); + } + } + } +} diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 42d2dbb1c6..3dae3afe3f 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -1,88 +1,45 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; namespace osu.Game.Users.Drawables { [LongRunningLoad] - public class DrawableAvatar : Container + public class DrawableAvatar : Sprite { - /// - /// Whether to open the user's profile when clicked. - /// - public readonly BindableBool OpenOnClick = new BindableBool(true); - private readonly User user; - [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } - /// - /// An avatar for specified user. + /// A simple, non-interactable avatar sprite for the specified user. /// /// The user. A null value will get a placeholder avatar. public DrawableAvatar(User user = null) { this.user = user; + + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (textures == null) - throw new ArgumentNullException(nameof(textures)); + if (user != null && user.Id > 1) + Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - Texture texture = null; - if (user != null && user.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - texture ??= textures.Get(@"Online/avatar-guest"); - - ClickableArea clickableArea; - Add(clickableArea = new ClickableArea - { - RelativeSizeAxes = Axes.Both, - Child = new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = texture, - FillMode = FillMode.Fit, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - Action = openProfile - }); - - clickableArea.Enabled.BindTo(OpenOnClick); + Texture ??= textures.Get(@"Online/avatar-guest"); } - private void openProfile() + protected override void LoadComplete() { - if (!OpenOnClick.Value) - return; - - if (user?.Id > 1) - game?.ShowUser(user.Id); - } - - private class ClickableArea : OsuClickableContainer - { - public override string TooltipText => Enabled.Value ? @"view profile" : null; - - protected override bool OnClick(ClickEvent e) - { - if (!Enabled.Value) - return false; - - return base.OnClick(e); - } + base.LoadComplete(); + this.FadeInFromZero(300, Easing.OutQuint); } } } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 171462f3fc..927e48cb56 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -65,12 +65,11 @@ namespace osu.Game.Users.Drawables if (user == null && !ShowGuestOnNull) return null; - var avatar = new DrawableAvatar(user) + var avatar = new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, }; - avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); avatar.OpenOnClick.BindTo(OpenOnClick); return avatar; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cbf9f6f1bd..3e1b56c29c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index adbcc0ef1c..85ba0590ea 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - +