From f148fbcc94774816b607a33307e7627279e12631 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Wed, 29 Sep 2021 00:59:08 +0200 Subject: [PATCH 001/170] Cap LoopCount to at least 1 --- osu.Game/Storyboards/CommandLoop.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index c22ca0d8c0..c17436d813 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.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 System; using System.Collections.Generic; namespace osu.Game.Storyboards @@ -16,7 +17,7 @@ namespace osu.Game.Storyboards public CommandLoop(double startTime, int loopCount) { LoopStartTime = startTime; - LoopCount = loopCount; + LoopCount = Math.Max(1, loopCount); } public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) From 73ee82ee2b95fcde64fe18168d3f705efe089414 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 19:15:58 +0900 Subject: [PATCH 002/170] Rename RecentParticipantsList -> DrawableRoomParticipantsList --- .../Multiplayer/TestSceneDrawableRoom.cs | 2 +- ... TestSceneDrawableRoomParticipantsList.cs} | 30 +++++++++---------- .../Lounge/Components/DrawableRoom.cs | 8 ++--- ...ist.cs => DrawableRoomParticipantsList.cs} | 4 +-- 4 files changed, 22 insertions(+), 22 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRecentParticipantsList.cs => TestSceneDrawableRoomParticipantsList.cs} (82%) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{RecentParticipantsList.cs => DrawableRoomParticipantsList.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index b1f5781f6f..0d4b14f90b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, })); - AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); + AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs similarity index 82% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index 50ec2bf3ac..ea75898946 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -13,16 +13,16 @@ using osu.Game.Users.Drawables; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRecentParticipantsList : OnlinePlayTestScene + public class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene { - private RecentParticipantsList list; + private DrawableRoomParticipantsList list; [SetUp] public new void Setup() => Schedule(() => { SelectedRoom.Value = new Room { Name = { Value = "test room" } }; - Child = list = new RecentParticipantsList + Child = list = new DrawableRoomParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -40,19 +40,19 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("set 8 circles", () => list.NumberOfCircles = 8); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddStep("add one more user", () => addUser(9)); - AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); AddStep("remove first user", () => removeUserAt(0)); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddStep("add one more user", () => addUser(9)); - AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); AddStep("remove last user", () => removeUserAt(8)); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); } [Test] @@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set 3 circles", () => list.NumberOfCircles = 3); AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2); - AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); + AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); AddStep("set 10 circles", () => list.NumberOfCircles = 10); AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9); - AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); + AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); } [Test] @@ -105,20 +105,20 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("remove from start", () => removeUserAt(0)); AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); + AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1)); AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); + AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); AddRepeatStep("remove 45 users", () => removeUserAt(0), 45); AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); - AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); AddStep("remove another user", () => removeUserAt(0)); AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2); AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any()); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 80070aa6ba..f2cd9d1410 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly Bindable roomCategory = new Bindable(); private readonly Bindable hasPassword = new Bindable(); - private RecentParticipantsList recentParticipantsList; + private DrawableRoomParticipantsList drawableRoomParticipantsList; private RoomSpecialCategoryPill specialCategoryPill; private PasswordProtectedIcon passwordIcon; private EndDateInfo endDateInfo; @@ -217,7 +217,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Children = new Drawable[] { ButtonsContainer, - recentParticipantsList = new RecentParticipantsList + drawableRoomParticipantsList = new DrawableRoomParticipantsList { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -280,8 +280,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { numberOfAvatars = value; - if (recentParticipantsList != null) - recentParticipantsList.NumberOfCircles = value; + if (drawableRoomParticipantsList != null) + drawableRoomParticipantsList.NumberOfCircles = value; } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index bc658f45e4..961ab276dc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -17,7 +17,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RecentParticipantsList : OnlinePlayComposite + public class DrawableRoomParticipantsList : OnlinePlayComposite { private const float avatar_size = 36; @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private HiddenUserCount hiddenUsers; private OsuSpriteText totalCount; - public RecentParticipantsList() + public DrawableRoomParticipantsList() { AutoSizeAxes = Axes.X; Height = 60; From d89577b2e7d3056631b082418be5488775363183 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 19:34:40 +0900 Subject: [PATCH 003/170] Add host to DrawableRoomParticipantsList --- .../TestSceneDrawableRoomParticipantsList.cs | 13 +- .../DrawableRoomParticipantsList.cs | 133 ++++++++++++++---- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index ea75898946..6909bc3f97 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -20,7 +20,18 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { - SelectedRoom.Value = new Room { Name = { Value = "test room" } }; + SelectedRoom.Value = new Room + { + Name = { Value = "test room" }, + Host = + { + Value = new User + { + Id = 2, + Username = "peppy", + } + } + }; Child = list = new DrawableRoomParticipantsList { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 961ab276dc..3d366a4c92 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -4,11 +4,13 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Users; @@ -23,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private FillFlowContainer avatarFlow; + private CircularAvatar hostAvatar; + private LinkFlowContainer hostText; private HiddenUserCount hiddenUsers; private OsuSpriteText totalCount; @@ -51,42 +55,99 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new FillFlowContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Padding = new MarginPadding { Right = 16 }, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SpriteIcon + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(16), - Margin = new MarginPadding { Left = 8 }, - Icon = FontAwesome.Solid.User, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Spacing = new Vector2(8), + Padding = new MarginPadding + { + Left = 8, + Right = 16 + }, + Children = new Drawable[] + { + hostAvatar = new CircularAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + hostText = new LinkFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Text = "hosted by smoogipoo" + } + } }, - totalCount = new OsuSpriteText + new Container { - Font = OsuFont.Default.With(weight: FontWeight.Bold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Shear = new Vector2(0.2f, 0), + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background3, + } + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Padding = new MarginPadding + { + Left = 8, + Right = 16 + }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(16), + Icon = FontAwesome.Solid.User, + }, + totalCount = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + avatarFlow = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Margin = new MarginPadding { Left = 4 }, + }, + hiddenUsers = new HiddenUserCount + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + } }, - avatarFlow = new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Margin = new MarginPadding { Left = 4 }, - }, - hiddenUsers = new HiddenUserCount - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } } } }; @@ -102,6 +163,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components updateHiddenUsers(); totalCount.Text = ParticipantCount.Value.ToString(); }, true); + + Host.BindValueChanged(onHostChanged, true); } private int numberOfCircles = 4; @@ -194,6 +257,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onHostChanged(ValueChangedEvent host) + { + hostAvatar.User = host.NewValue; + hostText.Clear(); + + hostText.AddText(@"hosted by "); + if (host.NewValue != null) + hostText.AddUserLink(host.NewValue); + } + private class CircularAvatar : CompositeDrawable { public User User From 5f921c7836d8be698a3d3f66bc67a27251c36cd3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:24:32 +0900 Subject: [PATCH 004/170] Change SelectedItem to show the last item by default --- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 24b3b4ec94..722a2a0b94 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay protected Bindable Duration { get; private set; } /// - /// The currently selected item in the , or the first item from + /// The currently selected item in the , or the last item from /// if this is not within a . /// protected readonly Bindable SelectedItem = new Bindable(); @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay protected virtual void UpdateSelectedItem() { - SelectedItem.Value = Playlist.FirstOrDefault(); + SelectedItem.Value = Playlist.LastOrDefault(); } } } From 67d847fbd3493c30fb1821345e38dfec4299487c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:24:49 +0900 Subject: [PATCH 005/170] Add room status text to DrawableRoom --- .../Multiplayer/TestSceneDrawableRoom.cs | 21 ++++----- .../Lounge/Components/DrawableRoom.cs | 43 +++++++++++++------ 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 0d4b14f90b..22ff2b98ce 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -43,11 +43,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(10), Children = new Drawable[] { - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Flyte's Trash Playlist" }, + Name = { Value = "Multiplayer room" }, Status = { Value = new RoomStatusOpen() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, + Type = { Value = MatchType.HeadToHead }, Playlist = { new PlaylistItem @@ -65,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 2" }, + Name = { Value = "Playlist room with multiple beatmaps" }, Status = { Value = new RoomStatusPlaying() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, Playlist = @@ -100,15 +101,15 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 3" }, + Name = { Value = "Finished room" }, Status = { Value = new RoomStatusEnded() }, EndDate = { Value = DateTimeOffset.Now }, }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 4 (spotlight)" }, + Name = { Value = "Spotlight room" }, Status = { Value = new RoomStatusOpen() }, Category = { Value = RoomCategory.Spotlight }, }), @@ -123,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer DrawableRoom drawableRoom = null; Room room = null; - AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room + AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room { Name = { Value = "Room with password" }, Status = { Value = new RoomStatusOpen() }, @@ -141,7 +142,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); } - private DrawableRoom createDrawableRoom(Room room) + private DrawableRoom createLoungeRoom(Room room) { room.Host.Value ??= new User { Username = "peppy", Id = 2 }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f2cd9d1410..df356e64bd 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -172,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Children = new Drawable[] { new RoomNameText(), - new RoomHostText(), + new RoomStatusText() } } }, @@ -304,11 +305,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - private class RoomHostText : OnlinePlayComposite + private class RoomStatusText : OnlinePlayComposite { - private LinkFlowContainer hostText; + [Resolved] + private OsuColour colours { get; set; } - public RoomHostText() + private LinkFlowContainer linkFlow; + + public RoomStatusText() { AutoSizeAxes = Axes.Both; } @@ -316,26 +320,37 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [BackgroundDependencyLoader] private void load() { - InternalChild = hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 16)) + InternalChild = linkFlow = new LinkFlowContainer(s => { - AutoSizeAxes = Axes.Both + s.Font = OsuFont.Default.With(size: 16); + s.Colour = colours.Lime1; + }) + { + AutoSizeAxes = Axes.Both, }; } protected override void LoadComplete() { base.LoadComplete(); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); + } - Host.BindValueChanged(host => + private void onSelectedItemChanged(ValueChangedEvent item) + { + if (Type.Value == MatchType.Playlists) { - hostText.Clear(); + linkFlow.Text = "Waiting for players"; + return; + } - if (host.NewValue != null) - { - hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue); - } - }, true); + linkFlow.Clear(); + + if (item.NewValue?.Beatmap.Value != null) + { + linkFlow.AddText("Currently playing "); + linkFlow.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString()); + } } } From c9c2d205447b5ddd5ade0787da36f88b79c20f5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:44:38 +0900 Subject: [PATCH 006/170] Limit max size --- .../Lounge/Components/DrawableRoom.cs | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index df356e64bd..9e693b0b0c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -137,7 +137,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -167,7 +168,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Top = 3 }, Direction = FillDirection.Vertical, Children = new Drawable[] @@ -310,23 +312,47 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private OsuColour colours { get; set; } - private LinkFlowContainer linkFlow; + private SpriteText statusText; + private LinkFlowContainer beatmapText; public RoomStatusText() { - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; } [BackgroundDependencyLoader] private void load() { - InternalChild = linkFlow = new LinkFlowContainer(s => + InternalChild = new GridContainer { - s.Font = OsuFont.Default.With(size: 16); - s.Colour = colours.Lime1; - }) - { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + statusText = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 16), + Colour = colours.Lime1 + }, + beatmapText = new LinkFlowContainer(s => + { + s.Font = OsuFont.Default.With(size: 16); + s.Colour = colours.Lime1; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } }; } @@ -338,18 +364,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void onSelectedItemChanged(ValueChangedEvent item) { + beatmapText.Clear(); + if (Type.Value == MatchType.Playlists) { - linkFlow.Text = "Waiting for players"; + statusText.Text = "Waiting for players"; return; } - linkFlow.Clear(); - if (item.NewValue?.Beatmap.Value != null) { - linkFlow.AddText("Currently playing "); - linkFlow.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString()); + statusText.Text = "Currently playing "; + beatmapText.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), + LinkAction.OpenBeatmap, + item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString(), + creationParameters: s => + { + s.Truncate = true; + s.RelativeSizeAxes = Axes.X; + }); } } } From c83dd7d2b62117c09ab942fb5bd8aeaa790f624e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:52:03 +0900 Subject: [PATCH 007/170] Merge OnlinePlayComposite and RoomSubScreenComposite --- .../Match/BeatmapSelectionControl.cs | 2 +- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 10 +++-- .../OnlinePlay/RoomSubScreenComposite.cs | 38 ------------------- 3 files changed, 8 insertions(+), 42 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 6f1817a77c..35f30edf65 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class BeatmapSelectionControl : RoomSubScreenComposite + public class BeatmapSelectionControl : OnlinePlayComposite { [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 722a2a0b94..aa971864ef 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -65,6 +65,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } + [Resolved(CanBeNull = true)] + private IBindable subScreenSelectedItem { get; set; } + /// /// The currently selected item in the , or the last item from /// if this is not within a . @@ -75,12 +78,13 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); + subScreenSelectedItem?.BindValueChanged(_ => UpdateSelectedItem()); Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); } protected virtual void UpdateSelectedItem() - { - SelectedItem.Value = Playlist.LastOrDefault(); - } + => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null + ? Playlist.LastOrDefault() + : subScreenSelectedItem.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs deleted file mode 100644 index 4cfd881aa3..0000000000 --- a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Match; - -namespace osu.Game.Screens.OnlinePlay -{ - /// - /// An with additional logic tracking the currently-selected inside a . - /// - public class RoomSubScreenComposite : OnlinePlayComposite - { - [Resolved] - private IBindable subScreenSelectedItem { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true); - } - - protected override void UpdateSelectedItem() - { - if (RoomID.Value == null) - { - // If the room hasn't been created yet, fall-back to the base logic. - base.UpdateSelectedItem(); - return; - } - - SelectedItem.Value = subScreenSelectedItem.Value; - } - } -} From 56b3c8aa9a72be245240de2eea652b8a77a5a64d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 11:52:14 +0900 Subject: [PATCH 008/170] Remove forgotten text --- .../Lounge/Components/DrawableRoomParticipantsList.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 3d366a4c92..db1686ef4a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -80,8 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Text = "hosted by smoogipoo" + AutoSizeAxes = Axes.Both } } }, From 619a907c47df793c6c03e1c6bb111423acff02b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:01:26 +0900 Subject: [PATCH 009/170] Fix zero height grid --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 9e693b0b0c..f2af38c2df 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -333,6 +333,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new Dimension(GridSizeMode.AutoSize), }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, Content = new[] { new Drawable[] From 18ab6747f73306610a607bec265bacffa009d16a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:01:28 +0900 Subject: [PATCH 010/170] Fix tests --- .../TestSceneDrawableRoomParticipantsList.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index 6909bc3f97..982dfc5cd9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -80,9 +80,9 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 8; i++) { AddStep("remove user", () => removeUserAt(0)); - int remainingUsers = 7 - i; + int remainingUsers = 8 - i; - int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers; + int displayedUsers = remainingUsers > 4 ? 3 : remainingUsers; AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType().Count() == displayedUsers); } } @@ -97,11 +97,11 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("set 3 circles", () => list.NumberOfCircles = 3); - AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2); + AddAssert("3 users displayed", () => list.ChildrenOfType().Count() == 3); AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); AddStep("set 10 circles", () => list.NumberOfCircles = 10); - AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9); + AddAssert("10 users displayed", () => list.ChildrenOfType().Count() == 10); AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); } @@ -115,24 +115,24 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("remove from start", () => removeUserAt(0)); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1)); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); AddRepeatStep("remove 45 users", () => removeUserAt(0), 45); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); AddStep("remove another user", () => removeUserAt(0)); - AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2); + AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2); - AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any()); + AddAssert("1 circle displayed", () => list.ChildrenOfType().Count() == 1); } private void addUser(int id) From ea30445efc41a508c4fe865956ccabee13b7d9b3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:03:34 +0900 Subject: [PATCH 011/170] Remove verbatim string --- .../Lounge/Components/DrawableRoomParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index db1686ef4a..e9be805d44 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -261,7 +261,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components hostAvatar.User = host.NewValue; hostText.Clear(); - hostText.AddText(@"hosted by "); + hostText.AddText("hosted by "); if (host.NewValue != null) hostText.AddUserLink(host.NewValue); } From 202a602d2f8f249aa0bfc33ff79cc4f1e91d4226 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:03:44 +0900 Subject: [PATCH 012/170] Change default status to "ready to play" --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f2af38c2df..03d13c353a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Type.Value == MatchType.Playlists) { - statusText.Text = "Waiting for players"; + statusText.Text = "Ready to play"; return; } From 816018edb71b81dcee304f0e850216cd4abd5486 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:04:30 +0900 Subject: [PATCH 013/170] Move hosted by text into nullcheck --- .../Lounge/Components/DrawableRoomParticipantsList.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index e9be805d44..31eb5db9bc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -261,9 +261,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components hostAvatar.User = host.NewValue; hostText.Clear(); - hostText.AddText("hosted by "); if (host.NewValue != null) + { + hostText.AddText("hosted by "); hostText.AddUserLink(host.NewValue); + } } private class CircularAvatar : CompositeDrawable From 6ffd9fdcfa20476f80a45fbbd60602782323148d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 14:46:01 +0900 Subject: [PATCH 014/170] Split out `BeatmapOnlineLookupQueue` from `BeatmapManager` --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 25 +- ...BeatmapManager_BeatmapOnlineLookupQueue.cs | 215 ------------------ osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 215 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 9 +- 5 files changed, 234 insertions(+), 234 deletions(-) delete mode 100644 osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs create mode 100644 osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 7e7e5ebc45..a7d34fadbe 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -161,8 +161,8 @@ namespace osu.Game.Tests.Online protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => new TestDownloadRequest(set); - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap, performOnlineLookups) + public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bd85017d58..a2f9740779 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable, IBeatmapResourceProvider + public partial class BeatmapManager : DownloadableArchiveModelManager, IBeatmapResourceProvider { /// /// Fired when a single difficulty has been hidden. @@ -54,6 +54,12 @@ namespace osu.Game.Beatmaps /// public IBindable> BeatmapRestored => beatmapRestored; + /// + /// A function which populates online information during the import process. + /// It is run as the final step of import. + /// + public Func PopulateOnlineInformation; + private readonly Bindable> beatmapRestored = new Bindable>(); /// @@ -79,11 +85,8 @@ namespace osu.Game.Beatmaps [CanBeNull] private readonly GameHost host; - [CanBeNull] - private readonly BeatmapOnlineLookupQueue onlineLookupQueue; - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, - WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) + WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; @@ -99,9 +102,6 @@ namespace osu.Game.Beatmaps beatmaps.ItemRemoved += removeWorkingCache; beatmaps.ItemUpdated += removeWorkingCache; - if (performOnlineLookups) - onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); trackStore = audioManager.GetTrackStore(Files.Store); } @@ -156,8 +156,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - if (onlineLookupQueue != null) - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); + if (PopulateOnlineInformation != null) + await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) @@ -533,11 +533,6 @@ namespace osu.Game.Beatmaps } } - public void Dispose() - { - onlineLookupQueue?.Dispose(); - } - #region IResourceStorageProvider TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs deleted file mode 100644 index 3dd34f6c2f..0000000000 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ /dev/null @@ -1,215 +0,0 @@ -// 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.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Sqlite; -using osu.Framework.Development; -using osu.Framework.IO.Network; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; - -namespace osu.Game.Beatmaps -{ - public partial class BeatmapManager - { - [ExcludeFromDynamicCompile] - private class BeatmapOnlineLookupQueue : IDisposable - { - private readonly IAPIProvider api; - private readonly Storage storage; - - private const int update_queue_request_concurrency = 4; - - private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); - - private FileWebRequest cacheDownloadRequest; - - private const string cache_database_name = "online.db"; - - public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) - { - this.api = api; - this.storage = storage; - - // avoid downloading / using cache for unit tests. - if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) - prepareLocalCache(); - } - - public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) - { - return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); - } - - // todo: expose this when we need to do individual difficulty lookups. - protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); - - private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) - { - if (checkLocalCache(set, beatmap)) - return; - - if (api?.State.Value != APIState.Online) - return; - - var req = new GetBeatmapRequest(beatmap); - - req.Failure += fail; - - try - { - // intentionally blocking to limit web request concurrency - api.Perform(req); - - var res = req.Result; - - if (res != null) - { - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = res.AuthorID; - - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; - - LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); - } - } - catch (Exception e) - { - fail(e); - } - - void fail(Exception e) - { - beatmap.OnlineBeatmapID = null; - LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); - } - } - - private void prepareLocalCache() - { - string cacheFilePath = storage.GetFullPath(cache_database_name); - string compressedCacheFilePath = $"{cacheFilePath}.bz2"; - - cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); - - cacheDownloadRequest.Failed += ex => - { - File.Delete(compressedCacheFilePath); - File.Delete(cacheFilePath); - - Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); - }; - - cacheDownloadRequest.Finished += () => - { - try - { - using (var stream = File.OpenRead(cacheDownloadRequest.Filename)) - using (var outStream = File.OpenWrite(cacheFilePath)) - using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) - bz2.CopyTo(outStream); - - // set to null on completion to allow lookups to begin using the new source - cacheDownloadRequest = null; - } - catch (Exception ex) - { - Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); - File.Delete(cacheFilePath); - } - finally - { - File.Delete(compressedCacheFilePath); - } - }; - - cacheDownloadRequest.PerformAsync(); - } - - private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) - { - // download is in progress (or was, and failed). - if (cacheDownloadRequest != null) - return false; - - // database is unavailable. - if (!storage.Exists(cache_database_name)) - return false; - - if (string.IsNullOrEmpty(beatmap.MD5Hash) - && string.IsNullOrEmpty(beatmap.Path) - && beatmap.OnlineBeatmapID == null) - return false; - - try - { - using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) - { - db.Open(); - - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; - - cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); - cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - var status = (BeatmapSetOnlineStatus)reader.GetByte(2); - - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); - beatmap.OnlineBeatmapID = reader.GetInt32(1); - - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = reader.GetInt32(3); - - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); - - LogForModel(set, $"Cached local retrieval for {beatmap}."); - return true; - } - } - } - } - } - catch (Exception ex) - { - LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); - } - - return false; - } - - public void Dispose() - { - cacheDownloadRequest?.Dispose(); - updateScheduler?.Dispose(); - } - } - } -} diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs new file mode 100644 index 0000000000..bbac30f2bb --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -0,0 +1,215 @@ +// 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.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using osu.Framework.Development; +using osu.Framework.IO.Network; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using SharpCompress.Compressors; +using SharpCompress.Compressors.BZip2; + +namespace osu.Game.Beatmaps +{ + [ExcludeFromDynamicCompile] + public class BeatmapOnlineLookupQueue : IDisposable + { + private readonly IAPIProvider api; + private readonly Storage storage; + + private const int update_queue_request_concurrency = 4; + + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); + + private FileWebRequest cacheDownloadRequest; + + private const string cache_database_name = "online.db"; + + public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) + { + this.api = api; + this.storage = storage; + + // avoid downloading / using cache for unit tests. + if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) + prepareLocalCache(); + } + + public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + { + return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); + } + + // todo: expose this when we need to do individual difficulty lookups. + protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + + private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) + { + if (checkLocalCache(set, beatmap)) + return; + + if (api?.State.Value != APIState.Online) + return; + + var req = new GetBeatmapRequest(beatmap); + + req.Failure += fail; + + try + { + // intentionally blocking to limit web request concurrency + api.Perform(req); + + var res = req.Result; + + if (res != null) + { + beatmap.Status = res.Status; + beatmap.BeatmapSet.Status = res.BeatmapSet.Status; + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + + if (beatmap.Metadata != null) + beatmap.Metadata.AuthorID = res.AuthorID; + + if (beatmap.BeatmapSet.Metadata != null) + beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; + + logForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + } + } + catch (Exception e) + { + fail(e); + } + + void fail(Exception e) + { + beatmap.OnlineBeatmapID = null; + logForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); + } + } + + private void prepareLocalCache() + { + string cacheFilePath = storage.GetFullPath(cache_database_name); + string compressedCacheFilePath = $"{cacheFilePath}.bz2"; + + cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); + + cacheDownloadRequest.Failed += ex => + { + File.Delete(compressedCacheFilePath); + File.Delete(cacheFilePath); + + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); + }; + + cacheDownloadRequest.Finished += () => + { + try + { + using (var stream = File.OpenRead(cacheDownloadRequest.Filename)) + using (var outStream = File.OpenWrite(cacheFilePath)) + using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) + bz2.CopyTo(outStream); + + // set to null on completion to allow lookups to begin using the new source + cacheDownloadRequest = null; + } + catch (Exception ex) + { + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + File.Delete(cacheFilePath); + } + finally + { + File.Delete(compressedCacheFilePath); + } + }; + + cacheDownloadRequest.PerformAsync(); + } + + private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) + { + // download is in progress (or was, and failed). + if (cacheDownloadRequest != null) + return false; + + // database is unavailable. + if (!storage.Exists(cache_database_name)) + return false; + + if (string.IsNullOrEmpty(beatmap.MD5Hash) + && string.IsNullOrEmpty(beatmap.Path) + && beatmap.OnlineBeatmapID == null) + return false; + + try + { + using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) + { + db.Open(); + + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; + + cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); + cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var status = (BeatmapSetOnlineStatus)reader.GetByte(2); + + beatmap.Status = status; + beatmap.BeatmapSet.Status = status; + beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); + beatmap.OnlineBeatmapID = reader.GetInt32(1); + + if (beatmap.Metadata != null) + beatmap.Metadata.AuthorID = reader.GetInt32(3); + + if (beatmap.BeatmapSet.Metadata != null) + beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); + + logForModel(set, $"Cached local retrieval for {beatmap}."); + return true; + } + } + } + } + } + catch (Exception ex) + { + logForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); + } + + return false; + } + + private void logForModel(BeatmapSetInfo set, string message) => + ArchiveModelManager.LogForModel(set, $"{nameof(BeatmapOnlineLookupQueue)}] {message}"); + + public void Dispose() + { + cacheDownloadRequest?.Dispose(); + updateScheduler?.Dispose(); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..8263e26dec 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,6 +138,8 @@ namespace osu.Game private UserLookupCache userCache; + private BeatmapOnlineLookupQueue onlineBeatmapLookupCache; + private FileStore fileStore; private RulesetConfigCache rulesetConfigCache; @@ -242,7 +244,11 @@ namespace osu.Game // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); + + onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(API, Storage); + + BeatmapManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -524,7 +530,6 @@ namespace osu.Game base.Dispose(isDisposing); RulesetStore?.Dispose(); - BeatmapManager?.Dispose(); LocalConfig?.Dispose(); contextFactory?.FlushConnections(); From 8a6501fa58f67f49aa71df20dbb76a5caba92516 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 14:46:07 +0900 Subject: [PATCH 015/170] Add basic component level xmldoc --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index bbac30f2bb..19f02c82ec 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -21,6 +21,13 @@ using SharpCompress.Compressors.BZip2; namespace osu.Game.Beatmaps { + /// + /// A component which handles population of online IDs for beatmaps using a two part lookup procedure. + /// + /// + /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ). + /// This will always be checked before doing a second online query to get required metadata. + /// [ExcludeFromDynamicCompile] public class BeatmapOnlineLookupQueue : IDisposable { From e7e04733234cf2de40abf9241548c786f64d8ed6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 15:40:41 +0900 Subject: [PATCH 016/170] Split out `WorkingBeatmapCache` from `BeatmapManager` --- osu.Game/Beatmaps/BeatmapManager.cs | 134 +-------- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 147 --------- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 279 ++++++++++++++++++ 3 files changed, 289 insertions(+), 271 deletions(-) delete mode 100644 osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs create mode 100644 osu.Game/Beatmaps/WorkingBeatmapCache.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a2f9740779..1c0e7dc319 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,18 +9,13 @@ using System.Linq.Expressions; using System.Text; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; -using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; @@ -31,7 +26,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; -using osu.Game.Users; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -40,7 +34,7 @@ namespace osu.Game.Beatmaps /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IBeatmapResourceProvider + public class BeatmapManager : DownloadableArchiveModelManager { /// /// Fired when a single difficulty has been hidden. @@ -60,12 +54,12 @@ namespace osu.Game.Beatmaps /// public Func PopulateOnlineInformation; - private readonly Bindable> beatmapRestored = new Bindable>(); - /// - /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// The game working beatmap cache, used to invalidate entries on changes. /// - public readonly WorkingBeatmap DefaultBeatmap; + public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } + + private readonly Bindable> beatmapRestored = new Bindable>(); public override IEnumerable HandledExtensions => new[] { ".osz" }; @@ -75,35 +69,19 @@ namespace osu.Game.Beatmaps protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; - private readonly AudioManager audioManager; - private readonly IResourceStore resources; - private readonly LargeTextureStore largeTextureStore; - private readonly ITrackStore trackStore; + private readonly RulesetStore rulesets; - [CanBeNull] - private readonly GameHost host; - - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, - WorkingBeatmap defaultBeatmap = null) + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; - this.audioManager = audioManager; - this.resources = resources; - this.host = host; - - DefaultBeatmap = defaultBeatmap; beatmaps = (BeatmapStore)ModelStore; beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); - beatmaps.ItemRemoved += removeWorkingCache; - beatmaps.ItemUpdated += removeWorkingCache; - - largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); - trackStore = audioManager.GetTrackStore(Files.Store); + beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); + beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); } protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => @@ -111,33 +89,6 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) - { - var metadata = new BeatmapMetadata - { - Author = user, - }; - - var set = new BeatmapSetInfo - { - Metadata = metadata, - Beatmaps = new List - { - new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - Ruleset = ruleset, - Metadata = metadata, - WidescreenStoryboard = true, - SamplesMatchPlaybackRate = true, - } - } - }; - - var working = Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); - } - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) @@ -278,43 +229,7 @@ namespace osu.Game.Beatmaps } } - removeWorkingCache(info); - } - - private readonly WeakList workingCache = new WeakList(); - - /// - /// Retrieve a instance for the provided - /// - /// The beatmap to lookup. - /// A instance correlating to the provided . - public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) - { - // if there are no files, presume the full beatmap info has not yet been fetched from the database. - if (beatmapInfo?.BeatmapSet?.Files.Count == 0) - { - int lookupId = beatmapInfo.ID; - beatmapInfo = QueryBeatmap(b => b.ID == lookupId); - } - - if (beatmapInfo?.BeatmapSet == null) - return DefaultBeatmap; - - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); - if (working != null) - return working; - - beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); - - // best effort; may be higher than expected. - GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); - - return working; - } + WorkingBeatmapCache?.Invalidate(info); } /// @@ -515,35 +430,6 @@ namespace osu.Game.Beatmaps return endTime - startTime; } - private void removeWorkingCache(BeatmapSetInfo info) - { - if (info.Beatmaps == null) return; - - foreach (var b in info.Beatmaps) - removeWorkingCache(b); - } - - private void removeWorkingCache(BeatmapInfo info) - { - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); - if (working != null) - workingCache.Remove(working); - } - } - - #region IResourceStorageProvider - - TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; - ITrackStore IBeatmapResourceProvider.Tracks => trackStore; - AudioManager IStorageResourceProvider.AudioManager => audioManager; - IResourceStore IStorageResourceProvider.Files => Files.Store; - IResourceStore IStorageResourceProvider.Resources => resources; - IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); - - #endregion - /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs deleted file mode 100644 index 45112ae74c..0000000000 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ /dev/null @@ -1,147 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using System.IO; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Framework.Logging; -using osu.Framework.Testing; -using osu.Game.Beatmaps.Formats; -using osu.Game.IO; -using osu.Game.Skinning; -using osu.Game.Storyboards; - -namespace osu.Game.Beatmaps -{ - public partial class BeatmapManager - { - [ExcludeFromDynamicCompile] - private class BeatmapManagerWorkingBeatmap : WorkingBeatmap - { - [NotNull] - private readonly IBeatmapResourceProvider resources; - - public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources) - : base(beatmapInfo, resources.AudioManager) - { - this.resources = resources; - } - - protected override IBeatmap GetBeatmap() - { - if (BeatmapInfo.Path == null) - return new Beatmap { BeatmapInfo = BeatmapInfo }; - - try - { - using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) - return Decoder.GetDecoder(stream).Decode(stream); - } - catch (Exception e) - { - Logger.Error(e, "Beatmap failed to load"); - return null; - } - } - - protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. - - protected override Texture GetBackground() - { - if (Metadata?.BackgroundFile == null) - return null; - - try - { - return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile)); - } - catch (Exception e) - { - Logger.Error(e, "Background failed to load"); - return null; - } - } - - protected override Track GetBeatmapTrack() - { - if (Metadata?.AudioFile == null) - return null; - - try - { - return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); - } - catch (Exception e) - { - Logger.Error(e, "Track failed to load"); - return null; - } - } - - protected override Waveform GetWaveform() - { - if (Metadata?.AudioFile == null) - return null; - - try - { - var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); - return trackData == null ? null : new Waveform(trackData); - } - catch (Exception e) - { - Logger.Error(e, "Waveform failed to load"); - return null; - } - } - - protected override Storyboard GetStoryboard() - { - Storyboard storyboard; - - try - { - using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) - { - var decoder = Decoder.GetDecoder(stream); - - // todo: support loading from both set-wide storyboard *and* beatmap specific. - if (BeatmapSetInfo?.StoryboardFile == null) - storyboard = decoder.Decode(stream); - else - { - using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) - storyboard = decoder.Decode(stream, secondaryStream); - } - } - } - catch (Exception e) - { - Logger.Error(e, "Storyboard failed to load"); - storyboard = new Storyboard(); - } - - storyboard.BeatmapInfo = BeatmapInfo; - - return storyboard; - } - - protected internal override ISkin GetSkin() - { - try - { - return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); - } - catch (Exception e) - { - Logger.Error(e, "Skin failed to load"); - return null; - } - } - - public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath); - } - } -} diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs new file mode 100644 index 0000000000..9f40eb4898 --- /dev/null +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -0,0 +1,279 @@ +// 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.IO; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Lists; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Users; + +namespace osu.Game.Beatmaps +{ + public class WorkingBeatmapCache : IBeatmapResourceProvider + { + private readonly WeakList workingCache = new WeakList(); + + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public readonly WorkingBeatmap DefaultBeatmap; + + public BeatmapManager BeatmapManager { private get; set; } + + private readonly AudioManager audioManager; + private readonly IResourceStore resources; + private readonly LargeTextureStore largeTextureStore; + private readonly ITrackStore trackStore; + private readonly IResourceStore files; + + [CanBeNull] + private readonly GameHost host; + + public WorkingBeatmapCache([NotNull] AudioManager audioManager, IResourceStore resources, IResourceStore files, WorkingBeatmap defaultBeatmap = null, GameHost host = null) + { + DefaultBeatmap = defaultBeatmap; + + this.audioManager = audioManager; + this.resources = resources; + this.host = host; + this.files = files; + largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files)); + trackStore = audioManager.GetTrackStore(files); + } + + public void Invalidate(BeatmapSetInfo info) + { + if (info.Beatmaps == null) return; + + foreach (var b in info.Beatmaps) + Invalidate(b); + } + + public void Invalidate(BeatmapInfo info) + { + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); + if (working != null) + workingCache.Remove(working); + } + } + + /// + /// Create a new . + /// + public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) + { + var metadata = new BeatmapMetadata + { + Author = user, + }; + + var set = new BeatmapSetInfo + { + Metadata = metadata, + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + Metadata = metadata, + WidescreenStoryboard = true, + SamplesMatchPlaybackRate = true, + } + } + }; + + var working = BeatmapManager.Import(set).Result; + return GetWorkingBeatmap(working.Beatmaps.First()); + } + + /// + /// Retrieve a instance for the provided + /// + /// The beatmap to lookup. + /// A instance correlating to the provided . + public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + { + // if there are no files, presume the full beatmap info has not yet been fetched from the database. + if (beatmapInfo?.BeatmapSet?.Files.Count == 0) + { + int lookupId = beatmapInfo.ID; + beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId); + } + + if (beatmapInfo?.BeatmapSet == null) + return DefaultBeatmap; + + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; + + beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; + + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); + + // best effort; may be higher than expected. + GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); + + return working; + } + } + + #region IResourceStorageProvider + + TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + ITrackStore IBeatmapResourceProvider.Tracks => trackStore; + AudioManager IStorageResourceProvider.AudioManager => audioManager; + IResourceStore IStorageResourceProvider.Files => files; + IResourceStore IStorageResourceProvider.Resources => resources; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); + + #endregion + + [ExcludeFromDynamicCompile] + private class BeatmapManagerWorkingBeatmap : WorkingBeatmap + { + [NotNull] + private readonly IBeatmapResourceProvider resources; + + public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources) + : base(beatmapInfo, resources.AudioManager) + { + this.resources = resources; + } + + protected override IBeatmap GetBeatmap() + { + if (BeatmapInfo.Path == null) + return new Beatmap { BeatmapInfo = BeatmapInfo }; + + try + { + using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) + return Decoder.GetDecoder(stream).Decode(stream); + } + catch (Exception e) + { + Logger.Error(e, "Beatmap failed to load"); + return null; + } + } + + protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. + + protected override Texture GetBackground() + { + if (Metadata?.BackgroundFile == null) + return null; + + try + { + return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile)); + } + catch (Exception e) + { + Logger.Error(e, "Background failed to load"); + return null; + } + } + + protected override Track GetBeatmapTrack() + { + if (Metadata?.AudioFile == null) + return null; + + try + { + return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); + } + catch (Exception e) + { + Logger.Error(e, "Track failed to load"); + return null; + } + } + + protected override Waveform GetWaveform() + { + if (Metadata?.AudioFile == null) + return null; + + try + { + var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new Waveform(trackData); + } + catch (Exception e) + { + Logger.Error(e, "Waveform failed to load"); + return null; + } + } + + protected override Storyboard GetStoryboard() + { + Storyboard storyboard; + + try + { + using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) + { + var decoder = Decoder.GetDecoder(stream); + + // todo: support loading from both set-wide storyboard *and* beatmap specific. + if (BeatmapSetInfo?.StoryboardFile == null) + storyboard = decoder.Decode(stream); + else + { + using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) + storyboard = decoder.Decode(stream, secondaryStream); + } + } + } + catch (Exception e) + { + Logger.Error(e, "Storyboard failed to load"); + storyboard = new Storyboard(); + } + + storyboard.BeatmapInfo = BeatmapInfo; + + return storyboard; + } + + protected internal override ISkin GetSkin() + { + try + { + return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); + } + catch (Exception e) + { + Logger.Error(e, "Skin failed to load"); + return null; + } + } + + public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath); + } + } +} From d21139b03efb77e7f5aeeea2d8236320d0e0d693 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 15:43:49 +0900 Subject: [PATCH 017/170] Split out database portion from `BeatmapManager` --- osu.Game/Beatmaps/BeatmapManager.cs | 466 +--------------------- osu.Game/Beatmaps/BeatmapModelManager.cs | 479 +++++++++++++++++++++++ 2 files changed, 483 insertions(+), 462 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapModelManager.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1c0e7dc319..c445925a90 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -1,479 +1,21 @@ // 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.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio.Track; -using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; -using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Testing; -using osu.Game.Beatmaps.Formats; -using osu.Game.Database; -using osu.Game.IO; -using osu.Game.IO.Archives; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Skinning; -using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps { /// - /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. + /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : DownloadableArchiveModelManager + public class BeatmapManager { - /// - /// Fired when a single difficulty has been hidden. - /// - public IBindable> BeatmapHidden => beatmapHidden; - - private readonly Bindable> beatmapHidden = new Bindable>(); - - /// - /// Fired when a single difficulty has been restored. - /// - public IBindable> BeatmapRestored => beatmapRestored; - - /// - /// A function which populates online information during the import process. - /// It is run as the final step of import. - /// - public Func PopulateOnlineInformation; - - /// - /// The game working beatmap cache, used to invalidate entries on changes. - /// - public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } - - private readonly Bindable> beatmapRestored = new Bindable>(); - - public override IEnumerable HandledExtensions => new[] { ".osz" }; - - protected override string[] HashableFileTypes => new[] { ".osu" }; - - protected override string ImportFromStablePath => "."; - - protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - - private readonly BeatmapStore beatmaps; - private readonly RulesetStore rulesets; - - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) - : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) + public BeatmapManager() { - this.rulesets = rulesets; - - beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); - beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); - beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); - beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); + beatmapModelManager = new BeatmapModelManager() } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => - new DownloadBeatmapSetRequest(set, minimiseDownloadSize); - - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) - { - if (archive != null) - beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); - - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - { - // remove metadata from difficulties where it matches the set - if (beatmapSet.Metadata.Equals(b.Metadata)) - b.Metadata = null; - - b.BeatmapSet = beatmapSet; - } - - validateOnlineIds(beatmapSet); - - bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - - if (PopulateOnlineInformation != null) - await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); - - // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. - if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) - { - if (beatmapSet.OnlineBeatmapSetID != null) - { - beatmapSet.OnlineBeatmapSetID = null; - LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); - } - } - } - - protected override void PreImport(BeatmapSetInfo beatmapSet) - { - if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) - throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); - - // check if a set already exists with the same online id, delete if it does. - if (beatmapSet.OnlineBeatmapSetID != null) - { - var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); - - if (existingOnlineId != null) - { - Delete(existingOnlineId); - - // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. - existingOnlineId.OnlineBeatmapSetID = null; - foreach (var b in existingOnlineId.Beatmaps) - b.OnlineBeatmapID = null; - - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); - } - } - } - - private void validateOnlineIds(BeatmapSetInfo beatmapSet) - { - var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - - // ensure all IDs are unique - if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) - { - LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); - resetIds(); - return; - } - - // find any existing beatmaps in the database that have matching online ids - var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); - - if (existingBeatmaps.Count > 0) - { - // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. - // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. - var existing = CheckForExisting(beatmapSet); - - if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) - { - LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); - resetIds(); - } - } - - void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); - } - - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); - - /// - /// Delete a beatmap difficulty. - /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); - - /// - /// Restore a beatmap difficulty. - /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); - - /// - /// Saves an file against a given . - /// - /// The to save the content against. The file referenced by will be replaced. - /// The content to write. - /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) - { - var setInfo = info.BeatmapSet; - - using (var stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - using (ContextFactory.GetForWrite()) - { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); - var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; - - // grab the original file (or create a new one if not found). - var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); - - // metadata may have changed; update the path with the standard format. - beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - - // update existing or populate new file's filename. - fileInfo.Filename = beatmapInfo.Path; - - stream.Seek(0, SeekOrigin.Begin); - ReplaceFile(setInfo, fileInfo, stream); - } - } - - WorkingBeatmapCache?.Invalidate(info); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanSkipImport(existing, import)) - return false; - - return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); - } - - protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanReuseExisting(existing, import)) - return false; - - var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - - // force re-import if we are not in a sane state. - return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); - } - - /// - /// Returns a list of all usable s. - /// - /// A list of available . - public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => - GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); - - /// - /// Returns a list of all usable s. Note that files are not populated. - /// - /// The level of detail to include in the returned objects. - /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. - /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) - { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY - // clause which causes queries to take 5-10x longer. - // TODO: remove if upgrading to EF core 3.x. - return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The level of detail to include in the returned objects. - /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) - { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - return queryable.AsNoTracking().Where(query); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The first result for the provided query, or null if no results were found. - public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - - protected override string HumanisedModelName => "beatmap"; - - protected override BeatmapSetInfo CreateModel(ArchiveReader reader) - { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); - - if (string.IsNullOrEmpty(mapName)) - { - Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); - return null; - } - - Beatmap beatmap; - using (var stream = new LineBufferedReader(reader.GetStream(mapName))) - beatmap = Decoder.GetDecoder(stream).Decode(stream); - - return new BeatmapSetInfo - { - OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, - Beatmaps = new List(), - Metadata = beatmap.Metadata, - DateAdded = DateTimeOffset.UtcNow - }; - } - - /// - /// Create all required s for the provided archive. - /// - private List createBeatmapDifficulties(List files) - { - var beatmapInfos = new List(); - - foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) - { - using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) - using (var ms = new MemoryStream()) // we need a memory stream so we can seek - using (var sr = new LineBufferedReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; - - var decoder = Decoder.GetDecoder(sr); - IBeatmap beatmap = decoder.Decode(sr); - - string hash = ms.ComputeSHA2Hash(); - - if (beatmapInfos.Any(b => b.Hash == hash)) - continue; - - beatmap.BeatmapInfo.Path = file.Filename; - beatmap.BeatmapInfo.Hash = hash; - beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - - var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - beatmap.BeatmapInfo.Ruleset = ruleset; - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; - beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - - beatmapInfos.Add(beatmap.BeatmapInfo); - } - } - - return beatmapInfos; - } - - private double calculateLength(IBeatmap b) - { - if (!b.HitObjects.Any()) - return 0; - - var lastObject = b.HitObjects.Last(); - - //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). - double endTime = lastObject.GetEndTime(); - double startTime = b.HitObjects.First().StartTime; - - return endTime - startTime; - } - - /// - /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. - /// - private class DummyConversionBeatmap : WorkingBeatmap - { - private readonly IBeatmap beatmap; - - public DummyConversionBeatmap(IBeatmap beatmap) - : base(beatmap.BeatmapInfo, null) - { - this.beatmap = beatmap; - } - - protected override IBeatmap GetBeatmap() => beatmap; - protected override Texture GetBackground() => null; - protected override Track GetBeatmapTrack() => null; - protected internal override ISkin GetSkin() => null; - public override Stream GetStream(string storagePath) => null; - } } - /// - /// The level of detail to include in database results. - /// - public enum IncludedDetails - { - /// - /// Only include beatmap difficulties and set level metadata. - /// - Minimal, - - /// - /// Include all difficulties, rulesets, difficulty metadata but no files. - /// - AllButFiles, - - /// - /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. - /// - AllButRuleset, - - /// - /// Include everything. - /// - All - } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs new file mode 100644 index 0000000000..be3adc412c --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -0,0 +1,479 @@ +// 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.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Formats; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Beatmaps +{ + /// + /// Handles ef-core storage of beatmaps. + /// + [ExcludeFromDynamicCompile] + public class BeatmapModelManager : DownloadableArchiveModelManager + { + /// + /// Fired when a single difficulty has been hidden. + /// + public IBindable> BeatmapHidden => beatmapHidden; + + private readonly Bindable> beatmapHidden = new Bindable>(); + + /// + /// Fired when a single difficulty has been restored. + /// + public IBindable> BeatmapRestored => beatmapRestored; + + /// + /// A function which populates online information during the import process. + /// It is run as the final step of import. + /// + public Func PopulateOnlineInformation; + + /// + /// The game working beatmap cache, used to invalidate entries on changes. + /// + public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } + + private readonly Bindable> beatmapRestored = new Bindable>(); + + public override IEnumerable HandledExtensions => new[] { ".osz" }; + + protected override string[] HashableFileTypes => new[] { ".osu" }; + + protected override string ImportFromStablePath => "."; + + protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); + + private readonly BeatmapStore beatmaps; + private readonly RulesetStore rulesets; + + public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) + : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) + { + this.rulesets = rulesets; + + beatmaps = (BeatmapStore)ModelStore; + beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); + beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); + beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); + beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => + new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + + protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + { + if (archive != null) + beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); + + foreach (BeatmapInfo b in beatmapSet.Beatmaps) + { + // remove metadata from difficulties where it matches the set + if (beatmapSet.Metadata.Equals(b.Metadata)) + b.Metadata = null; + + b.BeatmapSet = beatmapSet; + } + + validateOnlineIds(beatmapSet); + + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); + + if (PopulateOnlineInformation != null) + await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); + + // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. + if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + { + if (beatmapSet.OnlineBeatmapSetID != null) + { + beatmapSet.OnlineBeatmapSetID = null; + LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); + } + } + } + + protected override void PreImport(BeatmapSetInfo beatmapSet) + { + if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) + throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); + + // check if a set already exists with the same online id, delete if it does. + if (beatmapSet.OnlineBeatmapSetID != null) + { + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); + + if (existingOnlineId != null) + { + Delete(existingOnlineId); + + // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. + existingOnlineId.OnlineBeatmapSetID = null; + foreach (var b in existingOnlineId.Beatmaps) + b.OnlineBeatmapID = null; + + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); + } + } + } + + private void validateOnlineIds(BeatmapSetInfo beatmapSet) + { + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); + + // ensure all IDs are unique + if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) + { + LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); + resetIds(); + return; + } + + // find any existing beatmaps in the database that have matching online ids + var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); + + if (existingBeatmaps.Count > 0) + { + // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. + // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. + var existing = CheckForExisting(beatmapSet); + + if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) + { + LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); + resetIds(); + } + } + + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); + } + + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); + + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + + /// + /// Saves an file against a given . + /// + /// The to save the content against. The file referenced by will be replaced. + /// The content to write. + /// The beatmap content to write, null if to be omitted. + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) + { + var setInfo = info.BeatmapSet; + + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + using (ContextFactory.GetForWrite()) + { + var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; + + // grab the original file (or create a new one if not found). + var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); + + // metadata may have changed; update the path with the standard format. + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + + // update existing or populate new file's filename. + fileInfo.Filename = beatmapInfo.Path; + + stream.Seek(0, SeekOrigin.Begin); + ReplaceFile(setInfo, fileInfo, stream); + } + } + + WorkingBeatmapCache?.Invalidate(info); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanSkipImport(existing, import)) + return false; + + return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); + } + + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + + // force re-import if we are not in a sane state. + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); + } + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => + GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); + + /// + /// Returns a list of all usable s. Note that files are not populated. + /// + /// The level of detail to include in the returned objects. + /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. + /// A list of available . + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY + // clause which causes queries to take 5-10x longer. + // TODO: remove if upgrading to EF core 3.x. + return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The level of detail to include in the returned objects. + /// Results from the provided query. + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + return queryable.AsNoTracking().Where(query); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + + protected override string HumanisedModelName => "beatmap"; + + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrEmpty(mapName)) + { + Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); + return null; + } + + Beatmap beatmap; + using (var stream = new LineBufferedReader(reader.GetStream(mapName))) + beatmap = Decoder.GetDecoder(stream).Decode(stream); + + return new BeatmapSetInfo + { + OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, + Beatmaps = new List(), + Metadata = beatmap.Metadata, + DateAdded = DateTimeOffset.UtcNow + }; + } + + /// + /// Create all required s for the provided archive. + /// + private List createBeatmapDifficulties(List files) + { + var beatmapInfos = new List(); + + foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + { + using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) + using (var ms = new MemoryStream()) // we need a memory stream so we can seek + using (var sr = new LineBufferedReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = Decoder.GetDecoder(sr); + IBeatmap beatmap = decoder.Decode(sr); + + string hash = ms.ComputeSHA2Hash(); + + if (beatmapInfos.Any(b => b.Hash == hash)) + continue; + + beatmap.BeatmapInfo.Path = file.Filename; + beatmap.BeatmapInfo.Hash = hash; + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); + beatmap.BeatmapInfo.Ruleset = ruleset; + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; + beatmap.BeatmapInfo.Length = calculateLength(beatmap); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + + beatmapInfos.Add(beatmap.BeatmapInfo); + } + } + + return beatmapInfos; + } + + private double calculateLength(IBeatmap b) + { + if (!b.HitObjects.Any()) + return 0; + + var lastObject = b.HitObjects.Last(); + + //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). + double endTime = lastObject.GetEndTime(); + double startTime = b.HitObjects.First().StartTime; + + return endTime - startTime; + } + + /// + /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. + /// + private class DummyConversionBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public DummyConversionBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => null; + protected override Track GetBeatmapTrack() => null; + protected internal override ISkin GetSkin() => null; + public override Stream GetStream(string storagePath) => null; + } + } + + /// + /// The level of detail to include in database results. + /// + public enum IncludedDetails + { + /// + /// Only include beatmap difficulties and set level metadata. + /// + Minimal, + + /// + /// Include all difficulties, rulesets, difficulty metadata but no files. + /// + AllButFiles, + + /// + /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. + /// + AllButRuleset, + + /// + /// Include everything. + /// + All + } +} From 5618c9933bfd61b5587160a1821248bf7b1fb214 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 16:44:39 +0900 Subject: [PATCH 018/170] Expose more pieces of `ArchiveModelManager` via interfaces --- osu.Game/Database/ArchiveModelManager.cs | 11 +- .../DownloadableArchiveModelManager.cs | 6 -- osu.Game/Database/IModelFileManager.cs | 36 +++++++ osu.Game/Database/IModelManager.cs | 101 +++++++++++++++++- osu.Game/Scoring/ScoreManager.cs | 2 +- 5 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Database/IModelFileManager.cs diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ddd2bc5d1e..fc217d3058 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -135,7 +135,7 @@ namespace osu.Game.Database return Import(notification, tasks); } - protected async Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public async Task> Import(ProgressNotification notification, params ImportTask[] tasks) { if (tasks.Length == 0) { @@ -227,7 +227,7 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - internal async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -479,7 +479,7 @@ namespace osu.Game.Database /// /// The item to export. /// The output stream to export to. - protected virtual void ExportModelTo(TModel model, Stream outputStream) + public virtual void ExportModelTo(TModel model, Stream outputStream) { using (var archive = ZipArchive.Create()) { @@ -745,9 +745,6 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// public Task ImportFromStableAsync(StableStorage stableStorage) { var storage = PrepareStableStorage(stableStorage); diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index da3144e8d0..e6d5b44b65 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -54,12 +54,6 @@ namespace osu.Game.Database /// The request object. protected abstract ArchiveDownloadRequest CreateDownloadRequest(TModel model, bool minimiseDownloadSize); - /// - /// Begin a download for the requested . - /// - /// The to be downloaded. - /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. - /// Whether the download was started. public bool Download(TModel model, bool minimiseDownloadSize = false) { if (!canDownload(model)) return false; diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs new file mode 100644 index 0000000000..c74b945eb7 --- /dev/null +++ b/osu.Game/Database/IModelFileManager.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; + +namespace osu.Game.Database +{ + public interface IModelFileManager + where TModel : class + where TFileModel : class + { + /// + /// Replace an existing file with a new version. + /// + /// The item to operate on. + /// The existing file to be replaced. + /// The new file contents. + /// An optional filename for the new file. Will use the previous filename if not specified. + void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null); + + /// + /// Delete an existing file. + /// + /// The item to operate on. + /// The existing file to be deleted. + void DeleteFile(TModel model, TFileModel file); + + /// + /// Add a new file. + /// + /// The item to operate on. + /// The new file contents. + /// The filename for the new file. + void AddFile(TModel model, Stream contents, string filename); + } +} diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 8c314f1617..8f0c6e1561 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -1,8 +1,15 @@ -// 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 System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; namespace osu.Game.Database { @@ -24,5 +31,97 @@ namespace osu.Game.Database /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// IBindable> ItemRemoved { get; } + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + Task ImportFromStableAsync(StableStorage stableStorage); + + /// + /// Exports an item to a legacy (.zip based) package. + /// + /// The item to export. + void Export(TModel item); + + /// + /// Exports an item to the given output stream. + /// + /// The item to export. + /// The output stream to export to. + void ExportModelTo(TModel model, Stream outputStream); + + /// + /// Perform an update of the specified item. + /// TODO: Support file additions/removals. + /// + /// The item to update. + void Update(TModel item); + + /// + /// Delete an item from the manager. + /// Is a no-op for already deleted items. + /// + /// The item to delete. + /// false if no operation was performed + bool Delete(TModel item); + + /// + /// Delete multiple items. + /// This will post notifications tracking progress. + /// + void Delete(List items, bool silent = false); + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + void Undelete(List items, bool silent = false); + + /// + /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set. + /// + /// The item to restore + void Undelete(TModel item); + + /// + /// Import one or more items from filesystem . + /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + Task Import(params string[] paths); + + Task Import(params ImportTask[] tasks); + + Task> Import(ProgressNotification notification, params ImportTask[] tasks); + + /// + /// Import one from the filesystem and delete the file on success. + /// Note that this bypasses the UI flow and should only be used for special cases or testing. + /// + /// The containing data about the to import. + /// Whether this is a low priority import. + /// An optional cancellation token. + /// The imported model, if successful. + Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from an . + /// + /// The archive to be imported. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from a . + /// + /// The model to be imported. + /// An optional archive to use for model population. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 81e701f001..56c346d177 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -78,7 +78,7 @@ namespace osu.Game.Scoring protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask; - protected override void ExportModelTo(ScoreInfo model, Stream outputStream) + public override void ExportModelTo(ScoreInfo model, Stream outputStream) { var file = model.Files.SingleOrDefault(); if (file == null) From 84bddf0885893d700addf2cbc00900dc408a0f8e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:00:15 +0900 Subject: [PATCH 019/170] Initial PP counter implementation --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- .../Difficulty/DifficultyCalculator.cs | 126 +++++++++++++++--- .../HUD/DefaultPerformancePointsCounter.cs | 108 +++++++++++++++ osu.Game/Screens/Play/Player.cs | 8 +- 4 files changed, 218 insertions(+), 26 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 61760e69b0..c4c5c89f28 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps /// The applicable . protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); - public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) + public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) { using (var cancellationSource = createCancellationTokenSource(timeout)) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 224c9178ae..f38949d982 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -7,6 +7,8 @@ using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; @@ -19,6 +21,10 @@ namespace osu.Game.Rulesets.Difficulty private readonly Ruleset ruleset; private readonly WorkingBeatmap beatmap; + private IBeatmap playableBeatmap; + private Mod[] playableMods; + private double clockRate; + protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) { this.ruleset = ruleset; @@ -32,14 +38,41 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate(params Mod[] mods) { - mods = mods.Select(m => m.DeepClone()).ToArray(); + preProcess(mods); - IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + var skills = CreateSkills(playableBeatmap, playableMods, clockRate); - var track = new TrackVirtual(10000); - mods.OfType().ForEach(m => m.ApplyToTrack(track)); + if (!playableBeatmap.HitObjects.Any()) + return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); - return calculate(playableBeatmap, mods, track.Rate); + foreach (var hitObject in getDifficultyHitObjects()) + { + foreach (var skill in skills) + skill.ProcessInternal(hitObject); + } + + return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); + } + + public IEnumerable CalculateTimed(params Mod[] mods) + { + preProcess(mods); + + if (!playableBeatmap.HitObjects.Any()) + yield break; + + var skills = CreateSkills(playableBeatmap, playableMods, clockRate); + var progressiveBeatmap = new ProgressiveCalculationBeatmap(playableBeatmap); + + foreach (var hitObject in getDifficultyHitObjects()) + { + progressiveBeatmap.HitObjects.Add(hitObject.BaseObject); + + foreach (var skill in skills) + skill.ProcessInternal(hitObject); + + yield return new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)); + } } /// @@ -57,24 +90,23 @@ namespace osu.Game.Rulesets.Difficulty } } - private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) + /// + /// Retrieves the s to calculate against. + /// + private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(playableBeatmap, clockRate)); + + /// + /// Performs required tasks before every calculation. + /// + /// The original list of s. + private void preProcess(Mod[] mods) { - var skills = CreateSkills(beatmap, mods, clockRate); + playableMods = mods.Select(m => m.DeepClone()).ToArray(); + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - if (!beatmap.HitObjects.Any()) - return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); - - var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList(); - - foreach (var hitObject in difficultyHitObjects) - { - foreach (var skill in skills) - { - skill.ProcessInternal(hitObject); - } - } - - return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); + var track = new TrackVirtual(10000); + mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; } /// @@ -183,5 +215,57 @@ namespace osu.Game.Rulesets.Difficulty /// Clockrate to calculate difficulty with. /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); + + public class TimedDifficultyAttributes : IComparable + { + public readonly double Time; + public readonly DifficultyAttributes Attributes; + + public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) + { + Time = time; + Attributes = attributes; + } + + public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); + } + + private class ProgressiveCalculationBeatmap : IBeatmap + { + private readonly IBeatmap baseBeatmap; + + public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap) + { + this.baseBeatmap = baseBeatmap; + } + + public BeatmapInfo BeatmapInfo + { + get => baseBeatmap.BeatmapInfo; + set => baseBeatmap.BeatmapInfo = value; + } + + public BeatmapMetadata Metadata => baseBeatmap.Metadata; + + public ControlPointInfo ControlPointInfo + { + get => baseBeatmap.ControlPointInfo; + set => baseBeatmap.ControlPointInfo = value; + } + + public List Breaks => baseBeatmap.Breaks; + + public double TotalBreakTime => baseBeatmap.TotalBreakTime; + + public readonly List HitObjects = new List(); + + IReadOnlyList IBeatmap.HitObjects => HitObjects; + + public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); + + public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); + + public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); + } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs new file mode 100644 index 0000000000..18bb621dd1 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -0,0 +1,108 @@ +// 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.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + public class DefaultPerformancePointsCounter : RollingCounter, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } + + [Resolved] + private Player player { get; set; } + + private DifficultyCalculator.TimedDifficultyAttributes[] timedAttributes; + private Ruleset gameplayRuleset; + + public DefaultPerformancePointsCounter() + { + Current.Value = DisplayedCount = 0; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + + gameplayRuleset = player.GameplayRuleset; + timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + scoreProcessor.NewJudgement += onNewJudgement; + } + + private void onNewJudgement(JudgementResult judgement) + { + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new DifficultyCalculator.TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + if (attribIndex < 0) + attribIndex = ~attribIndex - 1; + attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); + + var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, player.Score.ScoreInfo); + Current.Value = (int)(ppProcessor?.Calculate() ?? 0); + } + + protected override LocalisableString FormatCount(int count) => $@"{count}pp"; + + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (scoreProcessor != null) + scoreProcessor.NewJudgement -= onNewJudgement; + } + + private class GameplayWorkingBeatmap : WorkingBeatmap + { + private readonly GameplayBeatmap gameplayBeatmap; + + public GameplayWorkingBeatmap(GameplayBeatmap gameplayBeatmap) + : base(gameplayBeatmap.BeatmapInfo, null) + { + this.gameplayBeatmap = gameplayBeatmap; + } + + public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) + => gameplayBeatmap; + + protected override IBeatmap GetBeatmap() => gameplayBeatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); + + protected internal override ISkin GetSkin() => throw new NotImplementedException(); + + public override Stream GetStream(string storagePath) => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9927467bd6..4018650093 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -93,9 +93,9 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } - protected Ruleset GameplayRuleset { get; private set; } + public Ruleset GameplayRuleset { get; private set; } - protected GameplayBeatmap GameplayBeatmap { get; private set; } + public GameplayBeatmap GameplayBeatmap { get; private set; } private Sample sampleRestart; @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Play [Cached] [Cached(Type = typeof(IBindable>))] - protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); + public new readonly Bindable> Mods = new Bindable>(Array.Empty()); /// /// Whether failing should be allowed. @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - protected Score Score { get; private set; } + public Score Score { get; private set; } /// /// Create a new player instance. From 90225f20820ed74fe6269a7c1c4105d2c3e4866a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 16:45:32 +0900 Subject: [PATCH 020/170] Hook up all required interfaces to new `BeatmapManager` --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 26 +- osu.Game/Beatmaps/BeatmapManager.cs | 297 +++++++++++++++++- osu.Game/Beatmaps/IWorkingBeatmapCache.cs | 15 + osu.Game/Beatmaps/WorkingBeatmapCache.cs | 42 +-- osu.Game/OsuGameBase.cs | 6 - osu.Game/Tests/Visual/EditorTestScene.cs | 37 ++- 6 files changed, 364 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Beatmaps/IWorkingBeatmapCache.cs diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index a7d34fadbe..1a3f9e414d 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -158,18 +158,34 @@ namespace osu.Game.Tests.Online public Task CurrentImportTask { get; private set; } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) - => new TestDownloadRequest(set); + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + { + return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); + } public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + internal class TestBeatmapModelManager : BeatmapModelManager { - await AllowImport.Task.ConfigureAwait(false); - return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + private readonly TestBeatmapManager testBeatmapManager; + + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) + : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + { + this.testBeatmapManager = testBeatmapManager; + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); + + public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); + return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c445925a90..18513945e5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -1,7 +1,27 @@ // 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.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Beatmaps { @@ -9,13 +29,282 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager + public class BeatmapManager : IModelDownloader, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache { - public BeatmapManager() + private readonly BeatmapModelManager beatmapModelManager; + private readonly WorkingBeatmapCache workingBeatmapCache; + + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, + WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { - beatmapModelManager = new BeatmapModelManager() + beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); + workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); + + workingBeatmapCache.BeatmapManager = beatmapModelManager; + + var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); + + beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; } - } + protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => + new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host); + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => + new BeatmapModelManager(storage, contextFactory, rulesets, api, host); + + /// + /// Create a new . + /// + public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) + { + var metadata = new BeatmapMetadata + { + Author = user, + }; + + var set = new BeatmapSetInfo + { + Metadata = metadata, + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + Metadata = metadata, + WidescreenStoryboard = true, + SamplesMatchPlaybackRate = true, + } + } + }; + + var working = beatmapModelManager.Import(set).Result; + return GetWorkingBeatmap(working.Beatmaps.First()); + } + + #region Delegation to BeatmapModelManager (methods which previously existed locally). + + /// + /// Fired when a single difficulty has been hidden. + /// + public IBindable> BeatmapHidden => beatmapModelManager.BeatmapHidden; + + /// + /// Fired when a single difficulty has been restored. + /// + public IBindable> BeatmapRestored => beatmapModelManager.BeatmapRestored; + + /// + /// Saves an file against a given . + /// + /// The to save the content against. The file referenced by will be replaced. + /// The content to write. + /// The beatmap content to write, null if to be omitted. + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin); + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected); + + /// + /// Returns a list of all usable s. Note that files are not populated. + /// + /// The level of detail to include in the returned objects. + /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. + /// A list of available . + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The level of detail to include in the returned objects. + /// Results from the provided query. + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public IQueryable QueryBeatmaps(Expression> query) => beatmapModelManager.QueryBeatmaps(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); + + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public WorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; + + /// + /// Fired when a notification should be presented to the user. + /// + public Action PostNotification { set => beatmapModelManager.PostNotification = value; } + + /// + /// Fired when the user requests to view the resulting import. + /// + public Action> PresentImport { set => beatmapModelManager.PresentImport = value; } + + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmap) => beatmapModelManager.Hide(beatmap); + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmap) => beatmapModelManager.Restore(beatmap); + + #endregion + + #region Implementation of IModelManager + + public IBindable> ItemUpdated => beatmapModelManager.ItemUpdated; + + public IBindable> ItemRemoved => beatmapModelManager.ItemRemoved; + + public Task ImportFromStableAsync(StableStorage stableStorage) + { + return beatmapModelManager.ImportFromStableAsync(stableStorage); + } + + public void Export(BeatmapSetInfo item) + { + beatmapModelManager.Export(item); + } + + public void ExportModelTo(BeatmapSetInfo model, Stream outputStream) + { + beatmapModelManager.ExportModelTo(model, outputStream); + } + + public void Update(BeatmapSetInfo item) + { + beatmapModelManager.Update(item); + } + + public bool Delete(BeatmapSetInfo item) + { + return beatmapModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + beatmapModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + beatmapModelManager.Undelete(items, silent); + } + + public void Undelete(BeatmapSetInfo item) + { + beatmapModelManager.Undelete(item); + } + + #endregion + + #region Implementation of IModelDownloader + + public IBindable>> DownloadBegan => beatmapModelManager.DownloadBegan; + + public IBindable>> DownloadFailed => beatmapModelManager.DownloadFailed; + + public bool IsAvailableLocally(BeatmapSetInfo model) + { + return beatmapModelManager.IsAvailableLocally(model); + } + + public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false) + { + return beatmapModelManager.Download(model, minimiseDownloadSize); + } + + public ArchiveDownloadRequest GetExistingDownload(BeatmapSetInfo model) + { + return beatmapModelManager.GetExistingDownload(model); + } + + #endregion + + #region Implementation of ICanAcceptFiles + + public Task Import(params string[] paths) + { + return beatmapModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return beatmapModelManager.Import(tasks); + } + + public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return beatmapModelManager.Import(notification, tasks); + } + + public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + public IEnumerable HandledExtensions => beatmapModelManager.HandledExtensions; + + #endregion + + #region Implementation of IWorkingBeatmapCache + + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + + #endregion + + #region Implementation of IModelFileManager + + public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents, string filename = null) + { + beatmapModelManager.ReplaceFile(model, file, contents, filename); + } + + public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file) + { + beatmapModelManager.DeleteFile(model, file); + } + + public void AddFile(BeatmapSetInfo model, Stream contents, string filename) + { + beatmapModelManager.AddFile(model, contents, filename); + } + + #endregion + } } diff --git a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs new file mode 100644 index 0000000000..881e734292 --- /dev/null +++ b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + public interface IWorkingBeatmapCache + { + /// + /// Retrieve a instance for the provided + /// + /// The beatmap to lookup. + /// A instance correlating to the provided . + WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo); + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 9f40eb4898..e117f1b82f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using JetBrains.Annotations; @@ -17,14 +16,12 @@ using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Storyboards; -using osu.Game.Users; namespace osu.Game.Beatmaps { - public class WorkingBeatmapCache : IBeatmapResourceProvider + public class WorkingBeatmapCache : IBeatmapResourceProvider, IWorkingBeatmapCache { private readonly WeakList workingCache = new WeakList(); @@ -33,7 +30,7 @@ namespace osu.Game.Beatmaps /// public readonly WorkingBeatmap DefaultBeatmap; - public BeatmapManager BeatmapManager { private get; set; } + public BeatmapModelManager BeatmapManager { private get; set; } private readonly AudioManager audioManager; private readonly IResourceStore resources; @@ -74,41 +71,6 @@ namespace osu.Game.Beatmaps } } - /// - /// Create a new . - /// - public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) - { - var metadata = new BeatmapMetadata - { - Author = user, - }; - - var set = new BeatmapSetInfo - { - Metadata = metadata, - Beatmaps = new List - { - new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - Ruleset = ruleset, - Metadata = metadata, - WidescreenStoryboard = true, - SamplesMatchPlaybackRate = true, - } - } - }; - - var working = BeatmapManager.Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); - } - - /// - /// Retrieve a instance for the provided - /// - /// The beatmap to lookup. - /// A instance correlating to the provided . public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) { // if there are no files, presume the full beatmap info has not yet been fetched from the database. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8263e26dec..dc1cb7a850 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,8 +138,6 @@ namespace osu.Game private UserLookupCache userCache; - private BeatmapOnlineLookupQueue onlineBeatmapLookupCache; - private FileStore fileStore; private RulesetConfigCache rulesetConfigCache; @@ -246,10 +244,6 @@ namespace osu.Game dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); - onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(API, Storage); - - BeatmapManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; - // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to // allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete. diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 1e26036116..ac8773a840 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -123,11 +123,40 @@ namespace osu.Game.Tests.Visual this.testBeatmap = testBeatmap; } - protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) - => string.Empty; + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + { + return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host); + } - public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) - => testBeatmap; + protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) + { + return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); + } + + private class TestWorkingBeatmapCache : WorkingBeatmapCache + { + private readonly TestBeatmapManager testBeatmapManager; + + public TestWorkingBeatmapCache(TestBeatmapManager testBeatmapManager, AudioManager audioManager, IResourceStore resourceStore, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost gameHost) + : base(audioManager, resourceStore, storage, defaultBeatmap, gameHost) + { + this.testBeatmapManager = testBeatmapManager; + } + + public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + => testBeatmapManager.testBeatmap; + } + + internal class TestBeatmapModelManager : BeatmapModelManager + { + public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) + : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + { + } + + protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) + => string.Empty; + } public override void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) { From 7a72747d886cc95e70c4abc94867527f2a6002e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 17:14:35 +0900 Subject: [PATCH 021/170] Add back optional online lookups --- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++++--- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 18513945e5..6ffdfa24b5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -42,9 +42,11 @@ namespace osu.Game.Beatmaps workingBeatmapCache.BeatmapManager = beatmapModelManager; - var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); - - beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + if (performOnlineLookups) + { + var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + } } protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dc1cb7a850..e76436a75b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -242,7 +242,7 @@ namespace osu.Game // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to From d2a8f35b4c5428118d306174cb72503dd2ff66c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:54:52 +0900 Subject: [PATCH 022/170] Update styling --- .../Graphics/UserInterface/RollingCounter.cs | 8 ++-- .../HUD/DefaultPerformancePointsCounter.cs | 43 +++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 244658b75e..f03287e7de 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface set => current.Current = value; } - private SpriteText displayedCountSpriteText; + private IHasText displayedCountSpriteText; /// /// If true, the roll-up duration will be proportional to change in value. @@ -72,10 +72,10 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load() { - displayedCountSpriteText = CreateSpriteText(); + displayedCountSpriteText = CreateText(); UpdateDisplay(); - Child = displayedCountSpriteText; + Child = (Drawable)displayedCountSpriteText; } protected void UpdateDisplay() @@ -160,6 +160,8 @@ namespace osu.Game.Graphics.UserInterface this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } + protected virtual IHasText CreateText() => CreateSpriteText(); + protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40f), diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index 18bb621dd1..a7651187c2 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -68,10 +68,9 @@ namespace osu.Game.Screens.Play.HUD Current.Value = (int)(ppProcessor?.Calculate() ?? 0); } - protected override LocalisableString FormatCount(int count) => $@"{count}pp"; + protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); - protected override OsuSpriteText CreateSpriteText() - => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); + protected override IHasText CreateText() => new TextComponent(); protected override void Dispose(bool isDisposing) { @@ -81,6 +80,44 @@ namespace osu.Game.Screens.Play.HUD scoreProcessor.NewJudgement -= onNewJudgement; } + private class TextComponent : CompositeDrawable, IHasText + { + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + private readonly OsuSpriteText text; + + public TextComponent() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(2), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Numeric.With(size: 16) + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = @"pp", + Font = OsuFont.Numeric.With(size: 8) + } + } + }; + } + } + private class GameplayWorkingBeatmap : WorkingBeatmap { private readonly GameplayBeatmap gameplayBeatmap; From 4d8418e0720a43f620381142c6b05a4e1ba95a69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:54:56 +0900 Subject: [PATCH 023/170] Fix possible nullrefs --- .../HUD/DefaultPerformancePointsCounter.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index a7651187c2..563032b4ea 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -5,9 +5,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -28,10 +32,12 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - [Resolved] + [CanBeNull] + [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } - [Resolved] + [CanBeNull] + [Resolved(CanBeNull = true)] private Player player { get; set; } private DifficultyCalculator.TimedDifficultyAttributes[] timedAttributes; @@ -47,18 +53,26 @@ namespace osu.Game.Screens.Play.HUD { Colour = colours.BlueLighter; - gameplayRuleset = player.GameplayRuleset; - timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + if (player != null) + { + gameplayRuleset = player.GameplayRuleset; + timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + } } protected override void LoadComplete() { base.LoadComplete(); - scoreProcessor.NewJudgement += onNewJudgement; + + if (scoreProcessor != null) + scoreProcessor.NewJudgement += onNewJudgement; } private void onNewJudgement(JudgementResult judgement) { + if (player == null) + return; + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new DifficultyCalculator.TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; From fab0d531bec9bf64687e927b937f27e861b4b7ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:55:00 +0900 Subject: [PATCH 024/170] Add counter to HUD --- osu.Game/Skinning/DefaultSkin.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index d3adae5c8c..41b7875cd1 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -68,6 +68,7 @@ namespace osu.Game.Skinning var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); + var ppCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -81,6 +82,13 @@ namespace osu.Game.Skinning score.Position = new Vector2(0, vertical_offset); + if (ppCounter != null) + { + ppCounter.Y = score.Position.Y + score.ScreenSpaceDrawQuad.Size.Y; + ppCounter.Origin = Anchor.TopCentre; + ppCounter.Anchor = Anchor.TopCentre; + } + if (accuracy != null) { accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5); @@ -123,6 +131,7 @@ namespace osu.Game.Skinning new SongProgress(), new BarHitErrorMeter(), new BarHitErrorMeter(), + new DefaultPerformancePointsCounter() } }; From fd13142a158b50f89caab9a516617137970e3388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 18:20:20 +0900 Subject: [PATCH 025/170] Add missing interface to `BeatmapManager` --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6ffdfa24b5..c72d1e8dec 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelDownloader, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache + public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache { private readonly BeatmapModelManager beatmapModelManager; private readonly WorkingBeatmapCache workingBeatmapCache; From 0a00bc779542a3ce86c77a9970e14c8cfab06f34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 17:42:12 +0900 Subject: [PATCH 026/170] Split out `IPostNotifications` into an interface --- osu.Game/Collections/CollectionManager.cs | 6 ++---- osu.Game/Database/ArchiveModelManager.cs | 3 --- osu.Game/Database/IModelManager.cs | 2 +- osu.Game/Database/IPostNotifications.cs | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Database/IPostNotifications.cs diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index fe04c70d62..6f9d9cd8a8 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; @@ -27,7 +28,7 @@ namespace osu.Game.Collections /// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the /// database backing the game. Going forward writing should be done in a similar way to other model stores. /// - public class CollectionManager : Component + public class CollectionManager : Component, IPostNotifications { /// /// Database version in stable-compatible YYYYMMDD format. @@ -106,9 +107,6 @@ namespace osu.Game.Collections backgroundSave(); }); - /// - /// Set an endpoint for notifications to be posted to. - /// public Action PostNotification { protected get; set; } /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index fc217d3058..018b41ebc1 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -57,9 +57,6 @@ namespace osu.Game.Database /// private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); - /// - /// Set an endpoint for notifications to be posted to. - /// public Action PostNotification { protected get; set; } /// diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 8f0c6e1561..721de8f3a0 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -17,7 +17,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager + public interface IModelManager : IPostNotifications where TModel : class { /// diff --git a/osu.Game/Database/IPostNotifications.cs b/osu.Game/Database/IPostNotifications.cs new file mode 100644 index 0000000000..d4fd64e79e --- /dev/null +++ b/osu.Game/Database/IPostNotifications.cs @@ -0,0 +1,16 @@ +// 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.Notifications; + +namespace osu.Game.Database +{ + public interface IPostNotifications + { + /// + /// And action which will be fired when a notification should be presented to the user. + /// + public Action PostNotification { set; } + } +} From 3e3b9bc963583fa5f6ad3b822ad3e6e3818aa06c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 18:21:16 +0900 Subject: [PATCH 027/170] Split out `IModelDownloader` and also split apart `ScoreManager` --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 25 +- osu.Game/Beatmaps/BeatmapManager.cs | 37 ++- osu.Game/Beatmaps/BeatmapModelDownloader.cs | 21 ++ osu.Game/Beatmaps/BeatmapModelManager.cs | 21 +- osu.Game/Database/ArchiveModelManager.cs | 20 +- osu.Game/Database/IModelDownloader.cs | 9 +- osu.Game/Database/IModelManager.cs | 12 + osu.Game/Database/IPresentImports.cs | 17 ++ ...hiveModelManager.cs => ModelDownloader.cs} | 47 ++-- osu.Game/Online/DownloadTrackingComposite.cs | 10 +- osu.Game/Scoring/ScoreManager.cs | 222 ++++++++++++------ osu.Game/Scoring/ScoreModelDownloader.cs | 20 ++ osu.Game/Scoring/ScoreModelManager.cs | 88 +++++++ .../Screens/Ranking/ReplayDownloadButton.cs | 2 +- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 15 files changed, 403 insertions(+), 150 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapModelDownloader.cs create mode 100644 osu.Game/Database/IPresentImports.cs rename osu.Game/Database/{DownloadableArchiveModelManager.cs => ModelDownloader.cs} (68%) create mode 100644 osu.Game/Scoring/ScoreModelDownloader.cs create mode 100644 osu.Game/Scoring/ScoreModelManager.cs diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 1a3f9e414d..d38294aba9 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -158,14 +158,30 @@ namespace osu.Game.Tests.Online public Task CurrentImportTask { get; private set; } + public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + { + } + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) { return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); } - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) { + return new TestBeatmapModelDownloader(modelManager, api, host); + } + + internal class TestBeatmapModelDownloader : BeatmapModelDownloader + { + public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) + : base(modelManager, apiProvider, gameHost) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); } internal class TestBeatmapModelManager : BeatmapModelManager @@ -173,14 +189,11 @@ namespace osu.Game.Tests.Online private readonly TestBeatmapManager testBeatmapManager; public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) - : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + : base(storage, databaseContextFactory, rulesetStore, gameHost) { this.testBeatmapManager = testBeatmapManager; } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) - => new TestDownloadRequest(set); - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c72d1e8dec..8dfd895987 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -32,12 +32,15 @@ namespace osu.Game.Beatmaps public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache { private readonly BeatmapModelManager beatmapModelManager; + private readonly BeatmapModelDownloader beatmapModelDownloader; + private readonly WorkingBeatmapCache workingBeatmapCache; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); + beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; @@ -49,11 +52,16 @@ namespace osu.Game.Beatmaps } } + protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + { + return new BeatmapModelDownloader(modelManager, api, host); + } + protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host); protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => - new BeatmapModelManager(storage, contextFactory, rulesets, api, host); + new BeatmapModelManager(storage, contextFactory, rulesets, host); /// /// Create a new . @@ -156,7 +164,14 @@ namespace osu.Game.Beatmaps /// /// Fired when a notification should be presented to the user. /// - public Action PostNotification { set => beatmapModelManager.PostNotification = value; } + public Action PostNotification + { + set + { + beatmapModelManager.PostNotification = value; + beatmapModelDownloader.PostNotification = value; + } + } /// /// Fired when the user requests to view the resulting import. @@ -179,6 +194,11 @@ namespace osu.Game.Beatmaps #region Implementation of IModelManager + public bool IsAvailableLocally(BeatmapSetInfo model) + { + return beatmapModelManager.IsAvailableLocally(model); + } + public IBindable> ItemUpdated => beatmapModelManager.ItemUpdated; public IBindable> ItemRemoved => beatmapModelManager.ItemRemoved; @@ -227,23 +247,18 @@ namespace osu.Game.Beatmaps #region Implementation of IModelDownloader - public IBindable>> DownloadBegan => beatmapModelManager.DownloadBegan; + public IBindable>> DownloadBegan => beatmapModelDownloader.DownloadBegan; - public IBindable>> DownloadFailed => beatmapModelManager.DownloadFailed; - - public bool IsAvailableLocally(BeatmapSetInfo model) - { - return beatmapModelManager.IsAvailableLocally(model); - } + public IBindable>> DownloadFailed => beatmapModelDownloader.DownloadFailed; public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false) { - return beatmapModelManager.Download(model, minimiseDownloadSize); + return beatmapModelDownloader.Download(model, minimiseDownloadSize); } public ArchiveDownloadRequest GetExistingDownload(BeatmapSetInfo model) { - return beatmapModelManager.GetExistingDownload(model); + return beatmapModelDownloader.GetExistingDownload(model); } #endregion diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs new file mode 100644 index 0000000000..ae482eeafd --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs @@ -0,0 +1,21 @@ +// 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.Platform; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Beatmaps +{ + public class BeatmapModelDownloader : ModelDownloader + { + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => + new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + + public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) + : base(beatmapModelManager, api, host) + { + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index be3adc412c..1b6694b1b4 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -21,8 +21,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; @@ -34,7 +32,7 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : DownloadableArchiveModelManager + public class BeatmapModelManager : ArchiveModelManager { /// /// Fired when a single difficulty has been hidden. @@ -72,8 +70,8 @@ namespace osu.Game.Beatmaps private readonly BeatmapStore beatmaps; private readonly RulesetStore rulesets; - public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) - : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) + public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, GameHost host = null) + : base(storage, contextFactory, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; @@ -84,9 +82,6 @@ namespace osu.Game.Beatmaps beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => - new DownloadBeatmapSetRequest(set, minimiseDownloadSize); - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) @@ -176,10 +171,6 @@ namespace osu.Game.Beatmaps void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); } - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); - /// /// Delete a beatmap difficulty. /// @@ -347,7 +338,11 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - protected override string HumanisedModelName => "beatmap"; + public override string HumanisedModelName => "beatmap"; + + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); protected override BeatmapSetInfo CreateModel(ArchiveReader reader) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 018b41ebc1..0c309bbddb 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPresentImports where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -249,10 +249,7 @@ namespace osu.Game.Database return import; } - /// - /// Fired when the user requests to view the resulting import. - /// - public Action> PresentImport; + public Action> PresentImport { protected get; set; } /// /// Silently import an item from an . @@ -799,6 +796,17 @@ namespace osu.Game.Database /// An existing model which matches the criteria to skip importing, else null. protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, ModelStore.ConsumableItems.Where(m => !m.DeletePending)); + + /// + /// Performs implementation specific comparisons to determine whether a given model is present in the local store. + /// + /// The whose existence needs to be checked. + /// The usable items present in the store. + /// Whether the exists. + protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) + => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); + /// /// Whether import can be skipped after finding an existing import early in the process. /// Only valid when is not overridden. @@ -835,7 +843,7 @@ namespace osu.Game.Database private DbSet queryModel() => ContextFactory.Get().Set(); - protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; #region Event handling / delaying diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs index 0cb633280e..a5573b2190 100644 --- a/osu.Game/Database/IModelDownloader.cs +++ b/osu.Game/Database/IModelDownloader.cs @@ -11,7 +11,7 @@ namespace osu.Game.Database /// Represents a that can download new models from an external source. /// /// The model type. - public interface IModelDownloader : IModelManager + public interface IModelDownloader : IPostNotifications where TModel : class { /// @@ -26,13 +26,6 @@ namespace osu.Game.Database /// IBindable>> DownloadFailed { get; } - /// - /// Checks whether a given is already available in the local store. - /// - /// The whose existence needs to be checked. - /// Whether the exists. - bool IsAvailableLocally(TModel model); - /// /// Begin a download for the requested . /// diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 721de8f3a0..7bfc8dbee3 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -123,5 +123,17 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Checks whether a given is already available in the local store. + /// + /// The whose existence needs to be checked. + /// Whether the exists. + bool IsAvailableLocally(TModel model); + + /// + /// A user displayable name for the model type associated with this manager. + /// + string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; } } diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPresentImports.cs new file mode 100644 index 0000000000..39b495ebd5 --- /dev/null +++ b/osu.Game/Database/IPresentImports.cs @@ -0,0 +1,17 @@ +// 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; + +namespace osu.Game.Database +{ + public interface IPresentImports + where TModel : class + { + /// + /// Fired when the user requests to view the resulting import. + /// + public Action> PresentImport { set; } + } +} diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/ModelDownloader.cs similarity index 68% rename from osu.Game/Database/DownloadableArchiveModelManager.cs rename to osu.Game/Database/ModelDownloader.cs index e6d5b44b65..e613b39b6b 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -1,29 +1,24 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using Humanizer; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Humanizer; using osu.Framework.Bindables; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; namespace osu.Game.Database { - /// - /// An that has the ability to download models using an and - /// import them into the store. - /// - /// The model type. - /// The associated file join type. - public abstract class DownloadableArchiveModelManager : ArchiveModelManager, IModelDownloader - where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable - where TFileModel : class, INamedFileInfo, new() + public abstract class ModelDownloader : IModelDownloader + where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable { + public Action PostNotification { protected get; set; } + public IBindable>> DownloadBegan => downloadBegan; private readonly Bindable>> downloadBegan = new Bindable>>(); @@ -32,18 +27,15 @@ namespace osu.Game.Database private readonly Bindable>> downloadFailed = new Bindable>>(); + private readonly IModelManager modelManager; private readonly IAPIProvider api; private readonly List> currentDownloads = new List>(); - private readonly MutableDatabaseBackedStoreWithFileIncludes modelStore; - - protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, - IIpcHost importHost = null) - : base(storage, contextFactory, modelStore, importHost) + protected ModelDownloader(IModelManager modelManager, IAPIProvider api, IIpcHost importHost = null) { + this.modelManager = modelManager; this.api = api; - this.modelStore = modelStore; } /// @@ -76,7 +68,7 @@ namespace osu.Game.Database Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false); + var imported = await modelManager.Import(notification, new ImportTask(filename)).ConfigureAwait(false); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) @@ -111,21 +103,10 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Cancelled; if (!(error is OperationCanceledException)) - Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!"); + Logger.Error(error, $"{modelManager.HumanisedModelName.Titleize()} download failed!"); } } - public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); - - /// - /// Performs implementation specific comparisons to determine whether a given model is present in the local store. - /// - /// The whose existence needs to be checked. - /// The usable items present in the store. - /// Whether the exists. - protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) - => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); - public ArchiveDownloadRequest GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null; diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index d9599481e7..2a96051427 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online /// public abstract class DownloadTrackingComposite : CompositeDrawable where TModel : class, IEquatable - where TModelManager : class, IModelDownloader + where TModelManager : class, IModelDownloader, IModelManager { protected readonly Bindable Model = new Bindable(); @@ -35,7 +35,7 @@ namespace osu.Game.Online Model.Value = model; } - private IBindable> managedUpdated; + private IBindable> managerUpdated; private IBindable> managerRemoved; private IBindable>> managerDownloadBegan; private IBindable>> managerDownloadFailed; @@ -60,8 +60,8 @@ namespace osu.Game.Online managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed.BindValueChanged(downloadFailed); - managedUpdated = Manager.ItemUpdated.GetBoundCopy(); - managedUpdated.BindValueChanged(itemUpdated); + managerUpdated = Manager.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(itemUpdated); managerRemoved = Manager.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(itemRemoved); } @@ -77,7 +77,7 @@ namespace osu.Game.Online /// /// Whether the given model is available in the database. - /// By default, this calls , + /// By default, this calls , /// but can be overriden to add additional checks for verifying the model in database. /// protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 56c346d177..d83b4e3f1d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -9,102 +9,48 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring { - public class ScoreManager : DownloadableArchiveModelManager + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPresentImports { - public override IEnumerable HandledExtensions => new[] { ".osr" }; - - protected override string[] HashableFileTypes => new[] { ".osr" }; - - protected override string ImportFromStablePath => Path.Combine("Data", "r"); - - private readonly RulesetStore rulesets; - private readonly Func beatmaps; private readonly Scheduler scheduler; - - [CanBeNull] private readonly Func difficulties; - - [CanBeNull] private readonly OsuConfigManager configManager; + private readonly ScoreModelManager scoreModelManager; + private readonly ScoreModelDownloader scoreModelDownloader; public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) - : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { - this.rulesets = rulesets; - this.beatmaps = beatmaps; this.scheduler = scheduler; this.difficulties = difficulties; this.configManager = configManager; + + scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost); + scoreModelDownloader = new ScoreModelDownloader(scoreModelManager, api, importHost); } - protected override ScoreInfo CreateModel(ArchiveReader archive) - { - if (archive == null) - return null; + public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); - using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) - { - try - { - return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; - } - catch (LegacyScoreDecoder.BeatmapNotFoundException e) - { - Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); - return null; - } - } - } + public List GetAllUsableScores() => scoreModelManager.GetAllUsableScores(); - protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) - => Task.CompletedTask; + public IEnumerable QueryScores(Expression> query) => scoreModelManager.QueryScores(query); - public override void ExportModelTo(ScoreInfo model, Stream outputStream) - { - var file = model.Files.SingleOrDefault(); - if (file == null) - return; - - using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath)) - inputStream.CopyTo(outputStream); - } - - protected override IEnumerable GetStableImportPaths(Storage storage) - => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => storage.GetFullPath(path)); - - public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); - - public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); - - public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); - - public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); - - protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + public ScoreInfo Query(Expression> query) => scoreModelManager.Query(query); /// /// Orders an array of s by total score. @@ -281,5 +227,149 @@ namespace osu.Game.Scoring this.totalScore.BindValueChanged(v => Value = v.NewValue.ToString("N0"), true); } } + + #region Implementation of IPostNotifications + + public Action PostNotification + { + set + { + scoreModelManager.PostNotification = value; + scoreModelDownloader.PostNotification = value; + } + } + + #endregion + + #region Implementation of IModelManager + + public IBindable> ItemUpdated => scoreModelManager.ItemUpdated; + + public IBindable> ItemRemoved => scoreModelManager.ItemRemoved; + + public Task ImportFromStableAsync(StableStorage stableStorage) + { + return scoreModelManager.ImportFromStableAsync(stableStorage); + } + + public void Export(ScoreInfo item) + { + scoreModelManager.Export(item); + } + + public void ExportModelTo(ScoreInfo model, Stream outputStream) + { + scoreModelManager.ExportModelTo(model, outputStream); + } + + public void Update(ScoreInfo item) + { + scoreModelManager.Update(item); + } + + public bool Delete(ScoreInfo item) + { + return scoreModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + scoreModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + scoreModelManager.Undelete(items, silent); + } + + public void Undelete(ScoreInfo item) + { + scoreModelManager.Undelete(item); + } + + public Task Import(params string[] paths) + { + return scoreModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return scoreModelManager.Import(tasks); + } + + public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; + + public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return scoreModelManager.Import(notification, tasks); + } + + public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + public bool IsAvailableLocally(ScoreInfo model) + { + return scoreModelManager.IsAvailableLocally(model); + } + + #endregion + + #region Implementation of IModelFileManager + + public void ReplaceFile(ScoreInfo model, ScoreFileInfo file, Stream contents, string filename = null) + { + scoreModelManager.ReplaceFile(model, file, contents, filename); + } + + public void DeleteFile(ScoreInfo model, ScoreFileInfo file) + { + scoreModelManager.DeleteFile(model, file); + } + + public void AddFile(ScoreInfo model, Stream contents, string filename) + { + scoreModelManager.AddFile(model, contents, filename); + } + + #endregion + + #region Implementation of IModelDownloader + + public IBindable>> DownloadBegan => scoreModelDownloader.DownloadBegan; + + public IBindable>> DownloadFailed => scoreModelDownloader.DownloadFailed; + + public bool Download(ScoreInfo model, bool minimiseDownloadSize) + { + return scoreModelDownloader.Download(model, minimiseDownloadSize); + } + + public ArchiveDownloadRequest GetExistingDownload(ScoreInfo model) + { + return scoreModelDownloader.GetExistingDownload(model); + } + + #endregion + + #region Implementation of IPresentImports + + public Action> PresentImport + { + set => scoreModelManager.PresentImport = value; + } + + #endregion } } diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs new file mode 100644 index 0000000000..b3c1e2928a --- /dev/null +++ b/osu.Game/Scoring/ScoreModelDownloader.cs @@ -0,0 +1,20 @@ +// 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.Platform; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Scoring +{ + public class ScoreModelDownloader : ModelDownloader + { + public ScoreModelDownloader(ScoreModelManager scoreManager, IAPIProvider api, IIpcHost importHost = null) + : base(scoreManager, api, importHost) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); + } +} diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs new file mode 100644 index 0000000000..c65a6acdfb --- /dev/null +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -0,0 +1,88 @@ +// 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.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Rulesets; +using osu.Game.Scoring.Legacy; + +namespace osu.Game.Scoring +{ + public class ScoreModelManager : ArchiveModelManager + { + public override IEnumerable HandledExtensions => new[] { ".osr" }; + + protected override string[] HashableFileTypes => new[] { ".osr" }; + + protected override string ImportFromStablePath => Path.Combine("Data", "r"); + + private readonly RulesetStore rulesets; + private readonly Func beatmaps; + + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) + : base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + protected override ScoreInfo CreateModel(ArchiveReader archive) + { + if (archive == null) + return null; + + using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) + { + try + { + return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; + } + catch (LegacyScoreDecoder.BeatmapNotFoundException e) + { + Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); + return null; + } + } + } + + public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); + + public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + + public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); + + public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + + public override void ExportModelTo(ScoreInfo model, Stream outputStream) + { + var file = model.Files.SingleOrDefault(); + if (file == null) + return; + + using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath)) + inputStream.CopyTo(outputStream); + } + + protected override IEnumerable GetStableImportPaths(Storage storage) + => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); + } +} diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 18b8649a59..d96b6989b4 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreManager scores) + private void load(OsuGame game, ScoreModelDownloader scores) { InternalChild = shakeContainer = new ShakeContainer { diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index ac8773a840..798b0d01ee 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual internal class TestBeatmapModelManager : BeatmapModelManager { public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) - : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + : base(storage, databaseContextFactory, rulesetStore, gameHost) { } From c05a8fc4a2ec1f598a6e317974d9293c34d780b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 18:52:09 +0900 Subject: [PATCH 028/170] Split importer interface out of `IModelManager` --- osu.Game/Database/IModelImporter.cs | 65 +++++++++++++++++++++++++++++ osu.Game/Database/IModelManager.cs | 51 +--------------------- 2 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 osu.Game/Database/IModelImporter.cs diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs new file mode 100644 index 0000000000..fa3b4d9152 --- /dev/null +++ b/osu.Game/Database/IModelImporter.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + /// + /// A class which handles importing of asociated models to the game store. + /// + /// The model type. + public interface IModelImporter : IPostNotifications + where TModel : class + { + /// + /// Import one or more items from filesystem . + /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + Task Import(params string[] paths); + + Task Import(params ImportTask[] tasks); + + Task> Import(ProgressNotification notification, params ImportTask[] tasks); + + /// + /// Import one from the filesystem and delete the file on success. + /// Note that this bypasses the UI flow and should only be used for special cases or testing. + /// + /// The containing data about the to import. + /// Whether this is a low priority import. + /// An optional cancellation token. + /// The imported model, if successful. + Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from an . + /// + /// The archive to be imported. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from a . + /// + /// The model to be imported. + /// An optional archive to use for model population. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// A user displayable name for the model type associated with this manager. + /// + string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + } +} diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 7bfc8dbee3..2b1e574176 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -4,12 +4,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.IO; -using osu.Game.IO.Archives; -using osu.Game.Overlays.Notifications; namespace osu.Game.Database { @@ -17,7 +14,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager : IPostNotifications + public interface IModelManager : IModelImporter, IPostNotifications where TModel : class { /// @@ -83,57 +80,11 @@ namespace osu.Game.Database /// The item to restore void Undelete(TModel item); - /// - /// Import one or more items from filesystem . - /// - /// - /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. - /// This will post notifications tracking progress. - /// - /// One or more archive locations on disk. - Task Import(params string[] paths); - - Task Import(params ImportTask[] tasks); - - Task> Import(ProgressNotification notification, params ImportTask[] tasks); - - /// - /// Import one from the filesystem and delete the file on success. - /// Note that this bypasses the UI flow and should only be used for special cases or testing. - /// - /// The containing data about the to import. - /// Whether this is a low priority import. - /// An optional cancellation token. - /// The imported model, if successful. - Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); - - /// - /// Silently import an item from an . - /// - /// The archive to be imported. - /// Whether this is a low priority import. - /// An optional cancellation token. - Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); - - /// - /// Silently import an item from a . - /// - /// The model to be imported. - /// An optional archive to use for model population. - /// Whether this is a low priority import. - /// An optional cancellation token. - Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); - /// /// Checks whether a given is already available in the local store. /// /// The whose existence needs to be checked. /// Whether the exists. bool IsAvailableLocally(TModel model); - - /// - /// A user displayable name for the model type associated with this manager. - /// - string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; } } From 66409147dc3ff3cc3eac9afa10841ef35e6eef98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 19:25:08 +0900 Subject: [PATCH 029/170] Remove duplicate interface specification --- osu.Game/Database/IModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 2b1e574176..f5e401cdfb 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -14,7 +14,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager : IModelImporter, IPostNotifications + public interface IModelManager : IModelImporter where TModel : class { /// From a2e61883e3b00dc205ecefc5870d9d2343ced7bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 19:33:12 +0900 Subject: [PATCH 030/170] Initial push to use `ILive` in import process --- .../Beatmaps/IO/ImportBeatmapTest.cs | 34 +++++++-------- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 2 +- .../Skins/TestSceneBeatmapSkinResources.cs | 2 +- .../Skins/TestSceneSkinResources.cs | 2 +- .../Menus/TestSceneMusicActionHandling.cs | 2 +- .../Navigation/TestScenePresentBeatmap.cs | 2 +- .../Navigation/TestScenePresentScore.cs | 4 +- .../TestScenePlaylistsRoomSubScreen.cs | 2 +- .../TestSceneBeatmapRecommendations.cs | 2 +- .../SongSelect/TestScenePlaySongSelect.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 15 +++---- osu.Game/Database/ArchiveModelManager.cs | 26 ++++++------ osu.Game/Database/EntityFrameworkLive.cs | 34 +++++++++++++++ .../Database/EntityFrameworkLiveExtensions.cs | 14 +++++++ osu.Game/Database/ILive.cs | 42 +++++++++++++++++++ osu.Game/Database/IModelImporter.cs | 8 ++-- osu.Game/Database/IPresentImports.cs | 2 +- osu.Game/FodyWeavers.xml | 3 ++ osu.Game/OsuGame.cs | 4 +- osu.Game/Scoring/ScoreManager.cs | 10 ++--- osu.Game/Screens/Menu/IntroScreen.cs | 8 +++- osu.Game/Skinning/SkinManager.cs | 2 +- 24 files changed, 164 insertions(+), 66 deletions(-) create mode 100644 osu.Game/Database/EntityFrameworkLive.cs create mode 100644 osu.Game/Database/EntityFrameworkLiveExtensions.cs create mode 100644 osu.Game/Database/ILive.cs create mode 100644 osu.Game/FodyWeavers.xml diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cba7f34ede..fc2a3792cb 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - BeatmapSetInfo importedSet; + ILive importedSet; using (var stream = File.OpenRead(tempPath)) { @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); File.Delete(tempPath); - var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); deleteBeatmapSet(imported, osu); } @@ -172,8 +172,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // but contents doesn't, so existing should still be used. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID == importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -226,8 +226,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -278,8 +278,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -329,8 +329,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -570,8 +570,8 @@ namespace osu.Game.Tests.Beatmaps.IO var imported = await manager.Import(toImport); Assert.NotNull(imported); - Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID); - Assert.AreEqual(null, imported.Beatmaps[1].OnlineBeatmapID); + Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineBeatmapID); + Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineBeatmapID); } finally { @@ -706,7 +706,7 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); } finally { @@ -759,8 +759,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); } finally { @@ -915,7 +915,7 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); } public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) @@ -930,7 +930,7 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); } private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index d38294aba9..79767bc671 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); - public Task CurrentImportTask { get; private set; } + public Task> CurrentImportTask { get; private set; } public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Online this.testBeatmapManager = testBeatmapManager; } - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public override async Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 7a9fc20426..b2600bb887 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Skins.IO private async Task loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); - return await skinManager.Import(archive); + return (await skinManager.Import(archive)).Value; } } } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index eff430ac25..f03cda1489 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; - beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + beatmap = beatmaps.GetWorkingBeatmap(imported.Value.Beatmaps[0]); beatmap.LoadTrack(); } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 107a96292f..10f1ab31df 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result; - skin = skins.GetSkin(imported); + skin = skins.GetSkin(imported.Value); } [Test] diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9037338e23..79dfe79299 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("import beatmap with track", () => { var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result; - Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); + Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Value.Beatmaps.First()); }); AddStep("bind to track change", () => diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index f0ddefa51d..5f5ebfccfb 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, } - }).Result; + }).Result.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 52b577b402..2ea765a1a9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = new OsuRuleset().RulesetInfo }, } - }).Result; + }).Result.Value; }); } @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Navigation OnlineScoreID = i, Beatmap = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo - }).Result; + }).Result.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 9051c71fc6..d8ec89a94e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists { beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; - importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result; + importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value; }); AddStep("load room", () => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 53cb628bb3..c22b6a54e9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect }).ToList() }; - return Game.BeatmapManager.Import(beatmapSet).Result; + return Game.BeatmapManager.Import(beatmapSet).Result.Value; } private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 102e5ee425..19aa91a38f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -751,7 +751,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result; + imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result.Value; }); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 2e30ed9827..3c69db032e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; + beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Value.Beatmaps[0]; for (int i = 0; i < 50; i++) { @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.UserInterface User = new User { Username = "TestUser" }, }; - importedScores.Add(scoreManager.Import(score).Result); + importedScores.Add(scoreManager.Import(score).Result.Value); } return dependencies; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8dfd895987..1bf4feb6a3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -89,8 +89,9 @@ namespace osu.Game.Beatmaps } }; - var working = beatmapModelManager.Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); + var imported = beatmapModelManager.Import(set).Result.Value; + + return GetWorkingBeatmap(imported.Beatmaps.First()); } #region Delegation to BeatmapModelManager (methods which previously existed locally). @@ -176,7 +177,7 @@ namespace osu.Game.Beatmaps /// /// Fired when the user requests to view the resulting import. /// - public Action> PresentImport { set => beatmapModelManager.PresentImport = value; } + public Action>> PresentImport { set => beatmapModelManager.PresentImport = value; } /// /// Delete a beatmap difficulty. @@ -275,22 +276,22 @@ namespace osu.Game.Beatmaps return beatmapModelManager.Import(tasks); } - public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return beatmapModelManager.Import(notification, tasks); } - public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(task, lowPriority, cancellationToken); } - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } - public Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 0c309bbddb..403bfdf621 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -132,13 +132,13 @@ namespace osu.Game.Database return Import(notification, tasks); } - public async Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { if (tasks.Length == 0) { notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; notification.State = ProgressNotificationState.Completed; - return Enumerable.Empty(); + return Enumerable.Empty>(); } notification.Progress = 0; @@ -146,7 +146,7 @@ namespace osu.Game.Database int current = 0; - var imported = new List(); + var imported = new List>(); bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; @@ -224,11 +224,11 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - public async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public async Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - TModel import; + ILive import; using (ArchiveReader reader = task.GetReader()) import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); @@ -243,13 +243,13 @@ namespace osu.Game.Database } catch (Exception e) { - LogForModel(import, $@"Could not delete original file after import ({task})", e); + LogForModel(import?.Value, $@"Could not delete original file after import ({task})", e); } return import; } - public Action> PresentImport { protected get; set; } + public Action>> PresentImport { protected get; set; } /// /// Silently import an item from an . @@ -257,7 +257,7 @@ namespace osu.Game.Database /// The archive to be imported. /// Whether this is a low priority import. /// An optional cancellation token. - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -268,7 +268,7 @@ namespace osu.Game.Database model = CreateModel(archive); if (model == null) - return Task.FromResult(null); + return Task.FromResult>(new EntityFrameworkLive(null)); } catch (TaskCanceledException) { @@ -343,7 +343,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual async Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => + public virtual async Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); @@ -369,7 +369,7 @@ namespace osu.Game.Database { LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); Undelete(existing); - return existing; + return existing.ToEntityFrameworkLive(); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -415,7 +415,7 @@ namespace osu.Game.Database // existing item will be used; rollback new import and exit early. rollback(); flushEvents(true); - return existing; + return existing.ToEntityFrameworkLive(); } LogForModel(item, @"Found existing but failed re-use check."); @@ -448,7 +448,7 @@ namespace osu.Game.Database } flushEvents(true); - return item; + return item.ToEntityFrameworkLive(); }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); /// diff --git a/osu.Game/Database/EntityFrameworkLive.cs b/osu.Game/Database/EntityFrameworkLive.cs new file mode 100644 index 0000000000..1d7b53911a --- /dev/null +++ b/osu.Game/Database/EntityFrameworkLive.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Database +{ + public class EntityFrameworkLive : ILive where T : class + { + public EntityFrameworkLive(T item) + { + Value = item; + } + + public Guid ID => throw new InvalidOperationException(); + + public void PerformRead(Action perform) + { + perform(Value); + } + + public TReturn PerformRead(Func perform) + { + return perform(Value); + } + + public void PerformWrite(Action perform) + { + perform(Value); + } + + public T Value { get; } + } +} diff --git a/osu.Game/Database/EntityFrameworkLiveExtensions.cs b/osu.Game/Database/EntityFrameworkLiveExtensions.cs new file mode 100644 index 0000000000..cd0673675e --- /dev/null +++ b/osu.Game/Database/EntityFrameworkLiveExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Database +{ + public static class EntityFrameworkLiveExtensions + { + public static ILive ToEntityFrameworkLive(this T item) + where T : class + { + return new EntityFrameworkLive(item); + } + } +} diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs new file mode 100644 index 0000000000..29e5756dba --- /dev/null +++ b/osu.Game/Database/ILive.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Database +{ + /// + /// A wrapper to provide access to database backed classes in a thread-safe manner. + /// + /// The databased type. + public interface ILive where T : class + { + Guid ID { get; } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + void PerformRead(Action perform); + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + TReturn PerformRead(Func perform); + + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. + void PerformWrite(Action perform); + + /// + /// Resolve the value of this instance on the current thread's context. + /// + /// + /// After resolving the data should not be passed between threads. + /// + T Value { get; } + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index fa3b4d9152..e94af01772 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Database Task Import(params ImportTask[] tasks); - Task> Import(ProgressNotification notification, params ImportTask[] tasks); + Task>> Import(ProgressNotification notification, params ImportTask[] tasks); /// /// Import one from the filesystem and delete the file on success. @@ -38,7 +38,7 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// Silently import an item from an . @@ -46,7 +46,7 @@ namespace osu.Game.Database /// The archive to be imported. /// Whether this is a low priority import. /// An optional cancellation token. - Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// Silently import an item from a . @@ -55,7 +55,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// A user displayable name for the model type associated with this manager. diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPresentImports.cs index 39b495ebd5..6aa29a5083 100644 --- a/osu.Game/Database/IPresentImports.cs +++ b/osu.Game/Database/IPresentImports.cs @@ -12,6 +12,6 @@ namespace osu.Game.Database /// /// Fired when the user requests to view the resulting import. /// - public Action> PresentImport { set; } + public Action>> PresentImport { set; } } } diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 99925bb1fb..35ec213755 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -624,10 +624,10 @@ namespace osu.Game SkinManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n); - BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); + BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); - ScoreManager.PresentImport = items => PresentScore(items.First()); + ScoreManager.PresentImport = items => PresentScore(items.First().Value); // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d83b4e3f1d..aa0ee4bbbb 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -299,22 +299,22 @@ namespace osu.Game.Scoring public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; - public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return scoreModelManager.Import(notification, tasks); } - public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(task, lowPriority, cancellationToken); } - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(archive, lowPriority, cancellationToken); } - public Task Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -365,7 +365,7 @@ namespace osu.Game.Scoring #region Implementation of IPresentImports - public Action> PresentImport + public Action>> PresentImport { set => scoreModelManager.PresentImport = value; } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index cfe14eab92..fbd33cad67 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -101,8 +101,12 @@ namespace osu.Game.Screens.Menu // if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state. // this could happen if a user has nuked their files store. for now, reimport to repair this. var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).Result; - import.Protected = true; - beatmaps.Update(import); + + import.PerformWrite(b => + { + b.Protected = true; + beatmaps.Update(b); + }); loadThemedIntro(); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index edeb17cbad..3842acab74 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -207,7 +207,7 @@ namespace osu.Game.Skinning Name = skin.SkinInfo.Name + " (modified)", Creator = skin.SkinInfo.Creator, InstantiationInfo = skin.SkinInfo.InstantiationInfo, - }).Result; + }).Result.Value; } public void Save(Skin skin) From 9fa901f6aa28feb7183cba972930a99378c40daf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:42:40 +0900 Subject: [PATCH 031/170] Refine `RealmContext` implementation API --- .../Database/TestRealmKeyBindingStore.cs | 20 +- osu.Game/Database/IRealmFactory.cs | 12 +- osu.Game/Database/RealmContextFactory.cs | 257 ++++++------------ osu.Game/Database/RealmExtensions.cs | 45 +-- osu.Game/Database/RealmObjectExtensions.cs | 51 ++++ osu.Game/Input/RealmKeyBindingStore.cs | 20 +- osu.Game/OsuGameBase.cs | 11 +- .../Settings/Sections/Input/KeyBindingRow.cs | 8 +- .../Sections/Input/KeyBindingsSubsection.cs | 4 +- 9 files changed, 174 insertions(+), 254 deletions(-) create mode 100644 osu.Game/Database/RealmObjectExtensions.cs diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 8be74f1a7c..f10b11733e 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database storage = new NativeStorage(directory.FullName); - realmContextFactory = new RealmContextFactory(storage); + realmContextFactory = new RealmContextFactory(storage, "test"); keyBindingStore = new RealmKeyBindingStore(realmContextFactory); } @@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - using (var usage = realmContextFactory.GetForRead()) + using (var realm = realmContextFactory.CreateContext()) { - var results = usage.Realm.All(); + var results = realm.All(); if (match.HasValue) results = results.Where(k => k.ActionInt == (int)match.Value); return results.Count(); @@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer, Enumerable.Empty()); - using (var primaryUsage = realmContextFactory.GetForRead()) + using (var primaryRealm = realmContextFactory.CreateContext()) { - var backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + var backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); var tsr = ThreadSafeReference.Create(backBinding); - using (var usage = realmContextFactory.GetForWrite()) + using (var threadedContext = realmContextFactory.CreateContext()) { - var binding = usage.Realm.ResolveReference(tsr); - binding.KeyCombination = new KeyCombination(InputKey.BackSpace); - - usage.Commit(); + var binding = threadedContext.ResolveReference(tsr); + threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); } Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } } diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 0e93e5bf4f..3b206d80eb 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -9,20 +9,12 @@ namespace osu.Game.Database { /// /// The main realm context, bound to the update thread. - /// If querying from a non-update thread is needed, use or to receive a context instead. /// Realm Context { get; } /// - /// Get a fresh context for read usage. + /// Create a new realm context for use on an arbitrary thread. /// - RealmContextFactory.RealmUsage GetForRead(); - - /// - /// Request a context for write usage. - /// This method may block if a write is already active on a different thread. - /// - /// A usage containing a usable context. - RealmContextFactory.RealmWriteUsage GetForWrite(); + Realm CreateContext(); } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ed3dc01f15..c51ac095bb 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; @@ -10,80 +9,115 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Input.Bindings; using Realms; +#nullable enable + namespace osu.Game.Database { + /// + /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. + /// public class RealmContextFactory : Component, IRealmFactory { private readonly Storage storage; - private const string database_name = @"client"; + /// + /// The filename of this realm. + /// + public readonly string Filename; private const int schema_version = 6; /// - /// Lock object which is held for the duration of a write operation (via ). + /// Lock object which is held during sections, blocking context creation during blocking periods. /// - private readonly object writeLock = new object(); + private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - /// - /// Lock object which is held during sections. - /// - private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1); - - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); - private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); - private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); - private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); - private readonly object updateContextLock = new object(); - - private Realm context; + private Realm? context; public Realm Context { get { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException($"Use {nameof(GetForRead)} or {nameof(GetForWrite)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); - lock (updateContextLock) + if (context == null) { - if (context == null) - { - context = createContext(); - Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); - } - - // creating a context will ensure our schema is up-to-date and migrated. - - return context; + context = createContext(); + Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); } + + // creating a context will ensure our schema is up-to-date and migrated. + return context; } } - public RealmContextFactory(Storage storage) + public RealmContextFactory(Storage storage, string filename) { this.storage = storage; + + Filename = filename; + + const string realm_extension = ".realm"; + + if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) + Filename += realm_extension; } - public RealmUsage GetForRead() + public Realm CreateContext() { - reads.Value++; - return new RealmUsage(createContext()); + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + + return createContext(); } - public RealmWriteUsage GetForWrite() - { - writes.Value++; - pending_writes.Value++; + /// + /// Compact this realm. + /// + /// + public bool Compact() => Realm.Compact(getConfiguration()); - Monitor.Enter(writeLock); - return new RealmWriteUsage(createContext(), writeComplete); + protected override void Update() + { + base.Update(); + + if (context?.Refresh() == true) + refreshes.Value++; + } + + private Realm createContext() + { + try + { + contextCreationLock.Wait(); + + contexts_created.Value++; + + return Realm.GetInstance(getConfiguration()); + } + finally + { + contextCreationLock.Release(); + } + } + + private RealmConfiguration getConfiguration() + { + return new RealmConfiguration(storage.GetFullPath(Filename, true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }; + } + + private void onMigration(Migration migration, ulong lastSchemaVersion) + { } /// @@ -101,163 +135,32 @@ namespace osu.Game.Database Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - blockingLock.Wait(); - flushContexts(); + contextCreationLock.Wait(); + + context?.Dispose(); + context = null; return new InvokeOnDisposal(this, endBlockingSection); static void endBlockingSection(RealmContextFactory factory) { - factory.blockingLock.Release(); + factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); } } - protected override void Update() - { - base.Update(); - - lock (updateContextLock) - { - if (context?.Refresh() == true) - refreshes.Value++; - } - } - - private Realm createContext() - { - try - { - if (IsDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); - - blockingLock.Wait(); - - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) - { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); - } - finally - { - blockingLock.Release(); - } - } - - private void writeComplete() - { - Monitor.Exit(writeLock); - pending_writes.Value--; - } - - private void onMigration(Migration migration, ulong lastSchemaVersion) - { - switch (lastSchemaVersion) - { - case 5: - // let's keep things simple. changing the type of the primary key is a bit involved. - migration.NewRealm.RemoveAll(); - break; - } - } - - private void flushContexts() - { - Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database); - Debug.Assert(blockingLock.CurrentCount == 0); - - Realm previousContext; - - lock (updateContextLock) - { - previousContext = context; - context = null; - } - - // wait for all threaded usages to finish - while (active_usages.Value > 0) - Thread.Sleep(50); - - previousContext?.Dispose(); - - Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database); - } - protected override void Dispose(bool isDisposing) { + context?.Dispose(); + if (!IsDisposed) { // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. BlockAllOperations(); - blockingLock?.Dispose(); + contextCreationLock.Dispose(); } base.Dispose(isDisposing); } - - /// - /// A usage of realm from an arbitrary thread. - /// - public class RealmUsage : IDisposable - { - public readonly Realm Realm; - - internal RealmUsage(Realm context) - { - active_usages.Value++; - Realm = context; - } - - /// - /// Disposes this instance, calling the initially captured action. - /// - public virtual void Dispose() - { - Realm?.Dispose(); - active_usages.Value--; - } - } - - /// - /// A transaction used for making changes to realm data. - /// - public class RealmWriteUsage : RealmUsage - { - private readonly Action onWriteComplete; - private readonly Transaction transaction; - - internal RealmWriteUsage(Realm context, Action onWriteComplete) - : base(context) - { - this.onWriteComplete = onWriteComplete; - transaction = Realm.BeginWrite(); - } - - /// - /// Commit all changes made in this transaction. - /// - public void Commit() => transaction.Commit(); - - /// - /// Revert all changes made in this transaction. - /// - public void Rollback() => transaction.Rollback(); - - /// - /// Disposes this instance, calling the initially captured action. - /// - public override void Dispose() - { - // rollback if not explicitly committed. - transaction?.Dispose(); - - base.Dispose(); - - onWriteComplete(); - } - } } } diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index aee36e81c5..e6f3dba39f 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -1,51 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using AutoMapper; -using osu.Game.Input.Bindings; +using System; using Realms; namespace osu.Game.Database { public static class RealmExtensions { - private static readonly IMapper mapper = new MapperConfiguration(c => + public static void Write(this Realm realm, Action function) { - c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - - c.CreateMap(); - }).CreateMapper(); - - /// - /// Create a detached copy of the each item in the collection. - /// - /// A list of managed s to detach. - /// The type of object. - /// A list containing non-managed copies of provided items. - public static List Detach(this IEnumerable items) where T : RealmObject - { - var list = new List(); - - foreach (var obj in items) - list.Add(obj.Detach()); - - return list; + using var transaction = realm.BeginWrite(); + function(realm); + transaction.Commit(); } - /// - /// Create a detached copy of the item. - /// - /// The managed to detach. - /// The type of object. - /// A non-managed copy of provided item. Will return the provided item if already detached. - public static T Detach(this T item) where T : RealmObject + public static T Write(this Realm realm, Func function) { - if (!item.IsManaged) - return item; - - return mapper.Map(item); + using var transaction = realm.BeginWrite(); + var result = function(realm); + transaction.Commit(); + return result; } } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs new file mode 100644 index 0000000000..c5aa1399a3 --- /dev/null +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using AutoMapper; +using osu.Game.Input.Bindings; +using Realms; + +namespace osu.Game.Database +{ + public static class RealmObjectExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + }).CreateMapper(); + + /// + /// Create a detached copy of the each item in the collection. + /// + /// A list of managed s to detach. + /// The type of object. + /// A list containing non-managed copies of provided items. + public static List Detach(this IEnumerable items) where T : RealmObject + { + var list = new List(); + + foreach (var obj in items) + list.Add(obj.Detach()); + + return list; + } + + /// + /// Create a detached copy of the item. + /// + /// The managed to detach. + /// The type of object. + /// A non-managed copy of provided item. Will return the provided item if already detached. + public static T Detach(this T item) where T : RealmObject + { + if (!item.IsManaged) + return item; + + return mapper.Map(item); + } + } +} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 03cb4031ca..5fa3ccdeb9 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; +using Realms; #nullable enable @@ -30,9 +31,9 @@ namespace osu.Game.Input { List combinations = new List(); - using (var context = realmFactory.GetForRead()) + using (var context = realmFactory.CreateContext()) { - foreach (var action in context.Realm.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in context.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -52,26 +53,27 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. // this is much faster as a result. - var existingBindings = usage.Realm.All().ToList(); + var existingBindings = realm.All().ToList(); - insertDefaults(usage, existingBindings, container.DefaultKeyBindings); + insertDefaults(realm, existingBindings, container.DefaultKeyBindings); foreach (var ruleset in rulesets) { var instance = ruleset.CreateInstance(); foreach (var variant in instance.AvailableVariants) - insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); + insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } - usage.Commit(); + transaction.Commit(); } } - private void insertDefaults(RealmContextFactory.RealmUsage usage, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(Realm realm, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) { // compare counts in database vs defaults for each action type. foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) @@ -83,7 +85,7 @@ namespace osu.Game.Input continue; // insert any defaults which are missing. - usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding + realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding { KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..f8f39029d2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,7 +187,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); updateThreadState = Host.UpdateThread.State.GetBoundCopy(); updateThreadState.BindValueChanged(updateThreadStateChanged); @@ -448,19 +448,20 @@ namespace osu.Game private void migrateDataToRealm() { using (var db = contextFactory.GetForWrite()) - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { // migrate ruleset settings. can be removed 20220315. var existingSettings = db.Context.DatabasedSetting; // only migrate data if the realm database is empty. - if (!usage.Realm.All().Any()) + if (!realm.All().Any()) { foreach (var dkb in existingSettings) { if (dkb.RulesetID == null) continue; - usage.Realm.Add(new RealmRulesetSetting + realm.Add(new RealmRulesetSetting { Key = dkb.Key, Value = dkb.StringValue, @@ -472,7 +473,7 @@ namespace osu.Game db.Context.RemoveRange(existingSettings); - usage.Commit(); + transaction.Commit(); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 85d88c96f8..cf8adf2785 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -368,12 +368,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; - - usage.Commit(); + var binding = realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index fae0318359..0e8e10c086 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input List bindings; - using (var usage = realmFactory.GetForRead()) - bindings = usage.Realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + using (var realm = realmFactory.CreateContext()) + bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { From 38cd383aaf474f91fa7668718de9923f4fd087cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 00:27:54 +0900 Subject: [PATCH 032/170] Remove local handling of realm when switching thread modes --- osu.Game/OsuGameBase.cs | 37 +++++++------------------------------ 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..d8cf8c729e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -13,24 +13,23 @@ using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.IO.Stores; -using osu.Framework.Platform; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Cursor; -using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.IO.Stores; using osu.Framework.Logging; -using osu.Framework.Threading; +using osu.Framework.Platform; using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -160,8 +159,6 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); - private IBindable updateThreadState; - public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -189,9 +186,6 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); - updateThreadState = Host.UpdateThread.State.GetBoundCopy(); - updateThreadState.BindValueChanged(updateThreadStateChanged); - AddInternal(realmFactory); dependencies.CacheAs(Storage); @@ -367,23 +361,6 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Black"); } - private IDisposable blocking; - - private void updateThreadStateChanged(ValueChangedEvent state) - { - switch (state.NewValue) - { - case GameThreadState.Running: - blocking?.Dispose(); - blocking = null; - break; - - case GameThreadState.Paused: - blocking = realmFactory.BlockAllOperations(); - break; - } - } - protected override void LoadComplete() { base.LoadComplete(); From ca7346e01fc5280ebe53106146f4434958c75c4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 00:34:09 +0900 Subject: [PATCH 033/170] Add test coverage --- osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index e2baa82ba0..7327d4053a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; @@ -123,6 +124,13 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("ruleset unchanged", () => ReferenceEquals(Ruleset.Value, ruleset)); } + [Test] + public void TestSwitchThreadExecutionMode() + { + AddStep("Change thread mode to multi threaded", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); }); + AddStep("Change thread mode to single thread", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); }); + } + [Test] public void TestUnavailableRulesetHandled() { From 9c0abae2b0836dd7cb9e3584be85ccf34ad03615 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:59:26 +0900 Subject: [PATCH 034/170] Add failing test coverage of realm blocking behaviour --- osu.Game.Tests/Database/GeneralUsageTests.cs | 64 ++++++++++++++++++ osu.Game.Tests/Database/RealmTest.cs | 70 ++++++++++++++++++++ osu.Game.Tests/osu.Game.Tests.csproj | 1 + 3 files changed, 135 insertions(+) create mode 100644 osu.Game.Tests/Database/GeneralUsageTests.cs create mode 100644 osu.Game.Tests/Database/RealmTest.cs diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs new file mode 100644 index 0000000000..245981cd9b --- /dev/null +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class GeneralUsageTests : RealmTest + { + /// + /// Just test the construction of a new database works. + /// + [Test] + public void TestConstructRealm() + { + RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); }); + } + + [Test] + public void TestBlockOperations() + { + RunTestWithRealm((realmFactory, _) => + { + using (realmFactory.BlockAllOperations()) + { + } + }); + } + + [Test] + public void TestBlockOperationsWithContention() + { + RunTestWithRealm((realmFactory, _) => + { + ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim(); + ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim(); + + Task.Factory.StartNew(() => + { + using (realmFactory.CreateContext()) + { + hasThreadedUsage.Set(); + + stopThreadedUsage.Wait(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); + + hasThreadedUsage.Wait(); + + Assert.Throws(() => + { + using (realmFactory.BlockAllOperations()) + { + } + }); + + stopThreadedUsage.Set(); + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs new file mode 100644 index 0000000000..2f4838cb67 --- /dev/null +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -0,0 +1,70 @@ +// 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.Runtime.CompilerServices; +using System.Threading.Tasks; +using Nito.AsyncEx; +using NUnit.Framework; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Database; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public abstract class RealmTest + { + private static readonly TemporaryNativeStorage storage; + + static RealmTest() + { + storage = new TemporaryNativeStorage("realm-test"); + storage.DeleteDirectory(string.Empty); + } + + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + { + AsyncContext.Run(() => + { + var testStorage = storage.GetStorageForDirectory(caller); + + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + testAction(realmFactory, testStorage); + + realmFactory.Dispose(); + Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + }); + } + + protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") + { + AsyncContext.Run(async () => + { + var testStorage = storage.GetStorageForDirectory(caller); + + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + + await testAction(realmFactory, testStorage); + + realmFactory.Dispose(); + Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + }); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 696f930467..cd56cb51ae 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -4,6 +4,7 @@ + From cfd3bdf888fc24df4bc8eeb0f8def24471352bbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:32:28 +0900 Subject: [PATCH 035/170] Ensure realm blocks until all threaded usages are completed --- osu.Game/Database/RealmContextFactory.cs | 39 +++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c51ac095bb..e3b0764721 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -133,20 +133,43 @@ namespace osu.Game.Database if (IsDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); + // TODO: this can be added for safety once we figure how to bypass in test + // if (!ThreadSafety.IsUpdateThread) + // throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); + Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - contextCreationLock.Wait(); + try + { + contextCreationLock.Wait(); - context?.Dispose(); - context = null; + const int sleep_length = 200; + int timeout = 5000; - return new InvokeOnDisposal(this, endBlockingSection); + context?.Dispose(); + context = null; - static void endBlockingSection(RealmContextFactory factory) + // see https://github.com/realm/realm-dotnet/discussions/2657 + while (!Compact()) + { + Thread.Sleep(sleep_length); + timeout -= sleep_length; + + if (timeout < 0) + throw new TimeoutException("Took too long to acquire lock"); + } + } + catch + { + contextCreationLock.Release(); + throw; + } + + return new InvokeOnDisposal(this, factory => { factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); - } + }); } protected override void Dispose(bool isDisposing) @@ -155,8 +178,8 @@ namespace osu.Game.Database if (!IsDisposed) { - // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. - BlockAllOperations(); + // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. + contextCreationLock.Wait(); contextCreationLock.Dispose(); } From dde19f2e81ca08df3328799d6d244ae4e62ed9cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:37:51 +0900 Subject: [PATCH 036/170] Fix unbalanced brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 19f02c82ec..bc86c6be5d 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -211,7 +211,7 @@ namespace osu.Game.Beatmaps } private void logForModel(BeatmapSetInfo set, string message) => - ArchiveModelManager.LogForModel(set, $"{nameof(BeatmapOnlineLookupQueue)}] {message}"); + ArchiveModelManager.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}"); public void Dispose() { From 27c4f2b06ee70adab63aeabfa03d42cdcbfbeefc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:38:50 +0900 Subject: [PATCH 037/170] Add missing disposal --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8263e26dec..f239119e40 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -531,6 +531,7 @@ namespace osu.Game RulesetStore?.Dispose(); LocalConfig?.Dispose(); + onlineBeatmapLookupCache?.Dispose(); contextFactory?.FlushConnections(); } From 428c7830d958d731398bcb18193160461418a670 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:43:57 +0900 Subject: [PATCH 038/170] Pass online lookup queue in as a whole, rather than function --- osu.Game/Beatmaps/BeatmapManager.cs | 16 +++++++++++++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 9 ++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c72d1e8dec..1946e3f93f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,10 +29,11 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache + public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable { private readonly BeatmapModelManager beatmapModelManager; private readonly WorkingBeatmapCache workingBeatmapCache; + private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) @@ -44,8 +45,8 @@ namespace osu.Game.Beatmaps if (performOnlineLookups) { - var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); - beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue; } } @@ -308,5 +309,14 @@ namespace osu.Game.Beatmaps } #endregion + + #region Implementation of IDisposable + + public void Dispose() + { + onlineBetamapLookupQueue?.Dispose(); + } + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index be3adc412c..72df1f37ee 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -49,10 +49,9 @@ namespace osu.Game.Beatmaps public IBindable> BeatmapRestored => beatmapRestored; /// - /// A function which populates online information during the import process. - /// It is run as the final step of import. + /// An online lookup queue component which handles populating online beatmap metadata. /// - public Func PopulateOnlineInformation; + public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; } /// /// The game working beatmap cache, used to invalidate entries on changes. @@ -107,8 +106,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - if (PopulateOnlineInformation != null) - await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); + if (OnlineLookupQueue != null) + await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) From 2ed28f625a6ce106ad76c86b2772b808daf975dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:46:37 +0900 Subject: [PATCH 039/170] Pass whole queue in rather than function --- osu.Game/Beatmaps/BeatmapManager.cs | 9 ++++----- osu.Game/OsuGameBase.cs | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a2f9740779..1fc7aa3146 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -55,10 +55,9 @@ namespace osu.Game.Beatmaps public IBindable> BeatmapRestored => beatmapRestored; /// - /// A function which populates online information during the import process. - /// It is run as the final step of import. + /// An online lookup queue component which handles populating online beatmap metadata. /// - public Func PopulateOnlineInformation; + public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; } private readonly Bindable> beatmapRestored = new Bindable>(); @@ -156,8 +155,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - if (PopulateOnlineInformation != null) - await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); + if (OnlineLookupQueue != null) + await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f239119e40..7772d5dfd8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,7 +138,7 @@ namespace osu.Game private UserLookupCache userCache; - private BeatmapOnlineLookupQueue onlineBeatmapLookupCache; + private BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; private FileStore fileStore; @@ -246,9 +246,9 @@ namespace osu.Game dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); - onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(API, Storage); + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(API, Storage); - BeatmapManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + BeatmapManager.OnlineLookupQueue = onlineBeatmapLookupQueue; // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -531,7 +531,7 @@ namespace osu.Game RulesetStore?.Dispose(); LocalConfig?.Dispose(); - onlineBeatmapLookupCache?.Dispose(); + onlineBeatmapLookupQueue?.Dispose(); contextFactory?.FlushConnections(); } From c71cf1e2200bcbefb2d71400782f90ba50918bd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:51:29 +0900 Subject: [PATCH 040/170] Fix incomplete xmldoc --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index bc86c6be5d..55164e2442 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps /// A component which handles population of online IDs for beatmaps using a two part lookup procedure. /// /// - /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ). + /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ) will be downloaded if not already present locally. /// This will always be checked before doing a second online query to get required metadata. /// [ExcludeFromDynamicCompile] From 8557530cd5e744110b13a167ad30306c7224823e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 03:45:00 +0900 Subject: [PATCH 041/170] Add back main context locking --- osu.Game/Database/RealmContextFactory.cs | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c51ac095bb..0e18b68276 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -37,6 +37,7 @@ namespace osu.Game.Database private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); + private readonly object contextLock = new object(); private Realm? context; public Realm Context @@ -46,14 +47,17 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); - if (context == null) + lock (contextLock) { - context = createContext(); - Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); - } + if (context == null) + { + context = createContext(); + Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); + } - // creating a context will ensure our schema is up-to-date and migrated. - return context; + // creating a context will ensure our schema is up-to-date and migrated. + return context; + } } } @@ -87,8 +91,11 @@ namespace osu.Game.Database { base.Update(); - if (context?.Refresh() == true) - refreshes.Value++; + lock (contextLock) + { + if (context?.Refresh() == true) + refreshes.Value++; + } } private Realm createContext() @@ -137,8 +144,11 @@ namespace osu.Game.Database contextCreationLock.Wait(); - context?.Dispose(); - context = null; + lock (contextLock) + { + context?.Dispose(); + context = null; + } return new InvokeOnDisposal(this, endBlockingSection); From b51fd00ba34a8c201e79310834ca8e9ded9aeb4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 03:46:53 +0900 Subject: [PATCH 042/170] Guard against disposal in all context retrievals --- osu.Game/Database/RealmContextFactory.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0e18b68276..bf7feebdbf 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -51,7 +51,7 @@ namespace osu.Game.Database { if (context == null) { - context = createContext(); + context = CreateContext(); Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); } @@ -73,14 +73,6 @@ namespace osu.Game.Database Filename += realm_extension; } - public Realm CreateContext() - { - if (IsDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); - - return createContext(); - } - /// /// Compact this realm. /// @@ -98,8 +90,11 @@ namespace osu.Game.Database } } - private Realm createContext() + public Realm CreateContext() { + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + try { contextCreationLock.Wait(); @@ -161,7 +156,10 @@ namespace osu.Game.Database protected override void Dispose(bool isDisposing) { - context?.Dispose(); + lock (contextLock) + { + context?.Dispose(); + } if (!IsDisposed) { From b5345235cae7edb8430f31e97139f1bbe016ee95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 10:40:55 +0900 Subject: [PATCH 043/170] Handle window file access errors --- osu.Game.Tests/Database/RealmTest.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 2f4838cb67..b7658d6408 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -38,10 +38,26 @@ namespace osu.Game.Tests.Database testAction(realmFactory, testStorage); realmFactory.Dispose(); - Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + try + { + Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + catch + { + // windows runs may error due to file still being open. + } realmFactory.Compact(); - Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + try + { + Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + catch + { + // windows runs may error due to file still being open. + } } }); } From 619dfe06907d74d3054dbdb3f23c9d0444b505d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:34:11 +0900 Subject: [PATCH 044/170] Add new interface base types for models --- osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs | 90 +++++++++++++++ osu.Game/Beatmaps/IBeatmapInfo.cs | 74 +++++++++++++ osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 117 ++++++++++++++++++++ osu.Game/Beatmaps/IBeatmapSetInfo.cs | 57 ++++++++++ osu.Game/Beatmaps/IFileInfo.cs | 18 +++ osu.Game/Beatmaps/IHasOnlineID.cs | 15 +++ osu.Game/Beatmaps/INamedFileUsage.cs | 23 ++++ osu.Game/Beatmaps/IRulesetInfo.cs | 46 ++++++++ 8 files changed, 440 insertions(+) create mode 100644 osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs create mode 100644 osu.Game/Beatmaps/IBeatmapInfo.cs create mode 100644 osu.Game/Beatmaps/IBeatmapMetadataInfo.cs create mode 100644 osu.Game/Beatmaps/IBeatmapSetInfo.cs create mode 100644 osu.Game/Beatmaps/IFileInfo.cs create mode 100644 osu.Game/Beatmaps/IHasOnlineID.cs create mode 100644 osu.Game/Beatmaps/INamedFileUsage.cs create mode 100644 osu.Game/Beatmaps/IRulesetInfo.cs diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs new file mode 100644 index 0000000000..6d9fcfcb06 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of all top-level difficulty settings for a beatmap. + /// + public interface IBeatmapDifficultyInfo + { + /// + /// The default value used for all difficulty settings except and . + /// + const float DEFAULT_DIFFICULTY = 5; + + /// + /// The drain rate of the associated beatmap. + /// + float DrainRate { get; } + + /// + /// The circle size of the associated beatmap. + /// + float CircleSize { get; } + + /// + /// The overall difficulty of the associated beatmap. + /// + float OverallDifficulty { get; } + + /// + /// The approach rate of the associated beatmap. + /// + float ApproachRate { get; } + + /// + /// The slider multiplier of the associated beatmap. + /// + double SliderMultiplier { get; } + + /// + /// The slider tick rate of the associated beatmap. + /// + double SliderTickRate { get; } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// Value to which the difficulty value maps in the specified range. + static double DifficultyRange(double difficulty, double min, double mid, double max) + { + if (difficulty > 5) + return mid + (max - mid) * (difficulty - 5) / 5; + if (difficulty < 5) + return mid - (mid - min) * (5 - difficulty) / 5; + + return mid; + } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// The values that define the two linear ranges. + /// + /// + /// od0 + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// + /// + /// od5 + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// + /// + /// od10 + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// + /// + /// + /// Value to which the difficulty value maps in the specified range. + public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) + => DifficultyRange(difficulty, range.od0, range.od5, range.od10); + } +} diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs new file mode 100644 index 0000000000..72a04621f2 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -0,0 +1,74 @@ +// 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.Localisation; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A single beatmap difficulty. + /// + public interface IBeatmapInfo : IHasOnlineID + { + /// + /// The user-specified name given to this beatmap. + /// + string DifficultyName { get; } + + /// + /// The metadata representing this beatmap. May be shared between multiple beatmaps. + /// + IBeatmapMetadataInfo Metadata { get; } + + /// + /// The difficulty settings for this beatmap. + /// + IBeatmapDifficultyInfo Difficulty { get; } + + /// + /// The playable length in milliseconds of this beatmap. + /// + double Length { get; } + + /// + /// The most common BPM of this beatmap. + /// + double BPM { get; } + + /// + /// The SHA-256 hash representing this beatmap's contents. + /// + string Hash { get; } + + /// + /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). + /// + string MD5Hash { get; } + + /// + /// The ruleset this beatmap was made for. + /// + IRulesetInfo Ruleset { get; } + + /// + /// The basic star rating for this beatmap (with no mods applied). + /// + double StarRating { get; } + + string DisplayTitle => $"{Metadata} {versionString}".Trim(); + + RomanisableString DisplayTitleRomanisable + { + get + { + var metadata = Metadata.DisplayTitleRomanisable; + + return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + } + } + + private string versionString => string.IsNullOrEmpty(DifficultyName) ? string.Empty : $"[{DifficultyName}]"; + } +} diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs new file mode 100644 index 0000000000..18dd38b404 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -0,0 +1,117 @@ +// 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.Linq; +using osu.Framework.Localisation; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// Metadata representing a beatmap. May be shared between multiple beatmap difficulties. + /// + public interface IBeatmapMetadataInfo : IEquatable + { + /// + /// The romanised title of this beatmap. + /// + string Title { get; } + + /// + /// The unicode title of this beatmap. + /// + string TitleUnicode { get; } + + /// + /// The romanised artist of this beatmap. + /// + string Artist { get; } + + /// + /// The unicode artist of this beatmap. + /// + string ArtistUnicode { get; } + + /// + /// The author of this beatmap. + /// + string Author { get; } // eventually should be linked to a persisted User. + + /// + /// The source of this beatmap. + /// + string Source { get; } + + /// + /// The tags of this beatmap. + /// + string Tags { get; } + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + int PreviewTime { get; } + + /// + /// The filename of the audio file consumed by this beatmap. + /// + string AudioFile { get; } + + /// + /// The filename of the background image file consumed by this beatmap. + /// + string BackgroundFile { get; } + + string DisplayTitle + { + get + { + string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; + return $"{Artist} - {Title} {author}".Trim(); + } + } + + RomanisableString DisplayTitleRomanisable + { + get + { + string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; + var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; + var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; + + return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); + } + } + + string[] SearchableTerms => new[] + { + Author, + Artist, + ArtistUnicode, + Title, + TitleUnicode, + Source, + Tags + }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + bool IEquatable.Equals(IBeatmapMetadataInfo? other) + { + if (other == null) + return false; + + return Title == other.Title + && TitleUnicode == other.TitleUnicode + && Artist == other.Artist + && ArtistUnicode == other.ArtistUnicode + && Author == other.Author + && Source == other.Source + && Tags == other.Tags + && PreviewTime == other.PreviewTime + && AudioFile == other.AudioFile + && BackgroundFile == other.BackgroundFile; + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs new file mode 100644 index 0000000000..f22115e08f --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.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 System.Collections.Generic; +using System.Linq; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive. + /// + public interface IBeatmapSetInfo : IHasOnlineID + { + /// + /// The date when this beatmap was imported. + /// + DateTimeOffset DateAdded { get; } + + /// + /// The best-effort metadata representing this set. In the case metadata differs between contained beatmaps, one is arbitrarily chosen. + /// + IBeatmapMetadataInfo? Metadata { get; } + + /// + /// All beatmaps contained in this set. + /// + IEnumerable Beatmaps { get; } + + /// + /// All files used by this set. + /// + IEnumerable Files { get; } + + /// + /// The maximum star difficulty of all beatmaps in this set. + /// + double MaxStarDifficulty { get; } + + /// + /// The maximum playable length in milliseconds of all beatmaps in this set. + /// + double MaxLength { get; } + + /// + /// The maximum BPM of all beatmaps in this set. + /// + double MaxBPM { get; } + + /// + /// The filename for the storyboard. + /// + string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; + } +} diff --git a/osu.Game/Beatmaps/IFileInfo.cs b/osu.Game/Beatmaps/IFileInfo.cs new file mode 100644 index 0000000000..50eb223fc4 --- /dev/null +++ b/osu.Game/Beatmaps/IFileInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a tracked file. + /// + public interface IFileInfo + { + /// + /// SHA-256 hash of the file content. + /// + string Hash { get; } + } +} diff --git a/osu.Game/Beatmaps/IHasOnlineID.cs b/osu.Game/Beatmaps/IHasOnlineID.cs new file mode 100644 index 0000000000..dc2793afe5 --- /dev/null +++ b/osu.Game/Beatmaps/IHasOnlineID.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + public interface IHasOnlineID + { + /// + /// The server-side ID representing this instance, if one exists. + /// + int? OnlineID { get; } + } +} diff --git a/osu.Game/Beatmaps/INamedFileUsage.cs b/osu.Game/Beatmaps/INamedFileUsage.cs new file mode 100644 index 0000000000..aa7a3852a7 --- /dev/null +++ b/osu.Game/Beatmaps/INamedFileUsage.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A usage of a file, with a local filename attached. + /// + public interface INamedFileUsage + { + /// + /// The underlying file on disk. + /// + IFileInfo File { get; } + + /// + /// The filename for this usage. + /// + string Filename { get; } + } +} diff --git a/osu.Game/Beatmaps/IRulesetInfo.cs b/osu.Game/Beatmaps/IRulesetInfo.cs new file mode 100644 index 0000000000..b31ebdfbfd --- /dev/null +++ b/osu.Game/Beatmaps/IRulesetInfo.cs @@ -0,0 +1,46 @@ +// 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.Rulesets; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a ruleset's metadata. + /// + public interface IRulesetInfo : IHasOnlineID + { + /// + /// The user-exposed name of this ruleset. + /// + string Name { get; } + + /// + /// An acronym defined by the ruleset that can be used as a permanent identifier. + /// + string ShortName { get; } + + /// + /// A string representation of this ruleset, to be used with reflection to instantiate the ruleset represented by this metadata. + /// + string InstantiationInfo { get; } + + public Ruleset? CreateInstance() + { + var type = Type.GetType(InstantiationInfo); + + if (type == null) + return null; + + var ruleset = Activator.CreateInstance(type) as Ruleset; + + // overwrite the pre-populated RulesetInfo with a potentially database attached copy. + // ruleset.RulesetInfo = this; + + return ruleset; + } + } +} From d30963646077df2eb6f1d2377d7bd56585071e8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 16:31:11 +0900 Subject: [PATCH 045/170] Update all EF based models to implement new read only interfaces --- osu.Game/Beatmaps/BeatmapInfo.cs | 19 ++++++++++++++++++- osu.Game/Beatmaps/BeatmapMetadata.cs | 4 +++- osu.Game/Beatmaps/BeatmapSetFileInfo.cs | 4 +++- osu.Game/Beatmaps/BeatmapSetInfo.cs | 16 +++++++++++++++- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 ++ osu.Game/Beatmaps/IBeatmapSetInfo.cs | 1 + .../{Beatmaps => Database}/IHasOnlineID.cs | 2 +- .../{Beatmaps => Database}/INamedFileUsage.cs | 4 +++- osu.Game/IO/FileInfo.cs | 2 +- osu.Game/{Beatmaps => IO}/IFileInfo.cs | 2 +- .../{Beatmaps => Rulesets}/IRulesetInfo.cs | 4 ++-- osu.Game/Rulesets/RulesetInfo.cs | 8 +++++++- 12 files changed, 57 insertions(+), 11 deletions(-) rename osu.Game/{Beatmaps => Database}/IHasOnlineID.cs (92%) rename osu.Game/{Beatmaps => Database}/INamedFileUsage.cs (92%) rename osu.Game/{Beatmaps => IO}/IFileInfo.cs (93%) rename osu.Game/{Beatmaps => Rulesets}/IRulesetInfo.cs (96%) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8cb5da8083..d2b47ef1a4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -17,13 +17,14 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapInfo : IEquatable, IHasPrimaryKey + public class BeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo { public int ID { get; set; } public int BeatmapVersion; private int? onlineBeatmapID; + private IRulesetInfo ruleset; [JsonProperty("id")] public int? OnlineBeatmapID @@ -187,5 +188,21 @@ namespace osu.Game.Beatmaps /// Returns a shallow-clone of this . /// public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion + + #region Implementation of IBeatmapInfo + + public string DifficultyName => Version; + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; + public IBeatmapDifficultyInfo Difficulty => BaseDifficulty; + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + public double StarRating => StarDifficulty; + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 713f80d1fe..fbd47d2614 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapMetadata : IEquatable, IHasPrimaryKey + public class BeatmapMetadata : IEquatable, IHasPrimaryKey, IBeatmapMetadataInfo { public int ID { get; set; } @@ -128,5 +128,7 @@ namespace osu.Game.Beatmaps && AudioFile == other.AudioFile && BackgroundFile == other.BackgroundFile; } + + string IBeatmapMetadataInfo.Author => AuthorString; } } diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index 3a55dc1577..ce50463f05 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -7,7 +7,7 @@ using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey + public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage { public int ID { get; set; } @@ -19,5 +19,7 @@ namespace osu.Game.Beatmaps [Required] public string Filename { get; set; } + + public IFileInfo File => FileInfo; } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 3b1ff4ced0..739acb9a8d 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -12,7 +12,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo { public int ID { get; set; } @@ -90,5 +90,19 @@ namespace osu.Game.Beatmaps return ReferenceEquals(this, other); } + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion + + #region Implementation of IBeatmapSetInfo + + IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata; + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + IEnumerable IBeatmapSetInfo.Files => Files; + + #endregion } } diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 72a04621f2..8ba8f316ed 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Rulesets; #nullable enable diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index f22115e08f..548a48367c 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Database; #nullable enable diff --git a/osu.Game/Beatmaps/IHasOnlineID.cs b/osu.Game/Database/IHasOnlineID.cs similarity index 92% rename from osu.Game/Beatmaps/IHasOnlineID.cs rename to osu.Game/Database/IHasOnlineID.cs index dc2793afe5..c55c461d2d 100644 --- a/osu.Game/Beatmaps/IHasOnlineID.cs +++ b/osu.Game/Database/IHasOnlineID.cs @@ -3,7 +3,7 @@ #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.Database { public interface IHasOnlineID { diff --git a/osu.Game/Beatmaps/INamedFileUsage.cs b/osu.Game/Database/INamedFileUsage.cs similarity index 92% rename from osu.Game/Beatmaps/INamedFileUsage.cs rename to osu.Game/Database/INamedFileUsage.cs index aa7a3852a7..e558ffe0fb 100644 --- a/osu.Game/Beatmaps/INamedFileUsage.cs +++ b/osu.Game/Database/INamedFileUsage.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.IO; + #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.Database { /// /// A usage of a file, with a local filename attached. diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs index e04bfb46cc..331546f9f8 100644 --- a/osu.Game/IO/FileInfo.cs +++ b/osu.Game/IO/FileInfo.cs @@ -6,7 +6,7 @@ using osu.Game.Database; namespace osu.Game.IO { - public class FileInfo : IHasPrimaryKey + public class FileInfo : IHasPrimaryKey, IFileInfo { public int ID { get; set; } diff --git a/osu.Game/Beatmaps/IFileInfo.cs b/osu.Game/IO/IFileInfo.cs similarity index 93% rename from osu.Game/Beatmaps/IFileInfo.cs rename to osu.Game/IO/IFileInfo.cs index 50eb223fc4..080d8e57f5 100644 --- a/osu.Game/Beatmaps/IFileInfo.cs +++ b/osu.Game/IO/IFileInfo.cs @@ -3,7 +3,7 @@ #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.IO { /// /// A representation of a tracked file. diff --git a/osu.Game/Beatmaps/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs similarity index 96% rename from osu.Game/Beatmaps/IRulesetInfo.cs rename to osu.Game/Rulesets/IRulesetInfo.cs index b31ebdfbfd..d4dec0de64 100644 --- a/osu.Game/Beatmaps/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Game.Rulesets; +using osu.Game.Database; #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.Rulesets { /// /// A representation of a ruleset's metadata. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 59ec9cdd7e..ca6a083a58 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -10,7 +10,7 @@ using osu.Framework.Testing; namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] - public class RulesetInfo : IEquatable + public class RulesetInfo : IEquatable, IRulesetInfo { public int? ID { get; set; } @@ -54,5 +54,11 @@ namespace osu.Game.Rulesets } public override string ToString() => Name ?? $"{Name} ({ShortName}) ID: {ID}"; + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion } } From 8595eb2d11d8540b01587efe5bd2ec0494639c34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:38:30 +0900 Subject: [PATCH 046/170] Switch `BeatmapDifficulty` usages to use interface type --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 45 +------------------ osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs | 2 +- .../Scoring/DrainingHealthProcessor.cs | 3 +- osu.Game/Rulesets/Scoring/HitWindows.cs | 4 +- 4 files changed, 7 insertions(+), 47 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 1844b193f2..0443422b31 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; +using osu.Game.Models.Interfaces; namespace osu.Game.Beatmaps { - public class BeatmapDifficulty : IHasPrimaryKey + public class BeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo { /// /// The default value used for all difficulty settings except and . @@ -49,47 +50,5 @@ namespace osu.Game.Beatmaps difficulty.SliderMultiplier = SliderMultiplier; difficulty.SliderTickRate = SliderTickRate; } - - /// - /// Maps a difficulty value [0, 10] to a two-piece linear range of values. - /// - /// The difficulty value to be mapped. - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. - /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, double min, double mid, double max) - { - if (difficulty > 5) - return mid + (max - mid) * (difficulty - 5) / 5; - if (difficulty < 5) - return mid - (mid - min) * (5 - difficulty) / 5; - - return mid; - } - - /// - /// Maps a difficulty value [0, 10] to a two-piece linear range of values. - /// - /// The difficulty value to be mapped. - /// The values that define the two linear ranges. - /// - /// - /// od0 - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// - /// - /// od5 - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// - /// - /// od10 - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. - /// - /// - /// - /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) - => DifficultyRange(difficulty, range.od0, range.od5, range.od10); } } diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 6d9fcfcb06..339364d442 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -84,7 +84,7 @@ namespace osu.Game.Beatmaps /// /// /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) + static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) => DifficultyRange(difficulty, range.od0, range.od5, range.od10); } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index cae41e22f4..785a729c6d 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Utils; @@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Scoring .First() ))); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); // Add back a portion of the amount of HP to be drained, depending on the lenience requested. targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 410614de07..71a2af03fc 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Beatmaps; +using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Scoring { foreach (var range in GetRanges()) { - var value = BeatmapDifficulty.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); + var value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); switch (range.Result) { From a92d499d7a33f938cbeb91a1d174ff6d669c1cd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:56:42 +0900 Subject: [PATCH 047/170] Convert usages of `BeatmapDifficulty` to `IBeatmapDifficultyInfo` --- .../TestSceneCatcher.cs | 4 ++-- .../TestSceneCatcherArea.cs | 4 ++-- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- .../Edit/CatchEditorPlayfield.cs | 2 +- .../Objects/CatchHitObject.cs | 4 ++-- .../Objects/JuiceStream.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 4 ++-- osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 +++--- .../UI/DrawableCatchRuleset.cs | 2 +- .../Beatmaps/ManiaBeatmapConverter.cs | 2 +- .../Patterns/Legacy/PatternGenerator.cs | 2 +- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- .../Checks/CheckTooShortSpinnersTest.cs | 12 +++++------ .../TestSceneObjectOrderedHitPolicy.cs | 2 +- .../TestSceneStartTimeOrderedHitPolicy.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Objects/SliderEndCircle.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 4 ++-- .../Beatmaps/TaikoBeatmapConverter.cs | 7 ++++--- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Scoring/TaikoHealthProcessor.cs | 4 ++-- osu.Game.Tournament/Components/SongBar.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficulty.cs | 21 ++++++++++++++++++- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModHardRock.cs | 2 +- osu.Game/Rulesets/Objects/HitObject.cs | 4 ++-- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 2 +- .../Scoring/DrainingHealthProcessor.cs | 1 - osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- .../Screens/Select/Details/AdvancedStats.cs | 4 ++-- osu.Game/osu.Game.csproj | 3 +++ 36 files changed, 75 insertions(+), 53 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 540f02580f..f291bfed13 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public IEnumerable CaughtObjects => this.ChildrenOfType(); - public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) + public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty) : base(droppedObjectTarget, difficulty) { } @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestKiaiFruit : Fruit { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 6abfbdbe21..7cae9b18b9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests private ScheduledDelegate addManyFruit; - private BeatmapDifficulty beatmapDifficulty; + private IBeatmapDifficultyInfo beatmapDifficulty; public TestSceneCatcherArea() { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) + public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty) { var droppedObjectContainer = new DroppedObjectContainer(); Add(droppedObjectContainer); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 82d76252d2..5b1f613f8d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs index 8c9f292aa9..046ba0ebce 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit public class CatchEditorPlayfield : CatchPlayfield { // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen. - public CatchEditorPlayfield(BeatmapDifficulty difficulty) + public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty) : base(difficulty) { } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d43e6f1c8b..ee10cf9711 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects /// public int RandomSeed => (int)StartTime; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index a8ad34fcbe..0d6925a83d 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects /// public double SpanDuration => Duration / this.SpanCount(); - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 1e20643a08..df32d917ce 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI internal CatcherArea CatcherArea { get; private set; } - private readonly BeatmapDifficulty difficulty; + private readonly IBeatmapDifficultyInfo difficulty; - public CatchPlayfield(BeatmapDifficulty difficulty) + public CatchPlayfield(IBeatmapDifficultyInfo difficulty) { this.difficulty = difficulty; } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 5cd85aac56..3745099010 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool caughtBananaPool; private readonly DrawablePool caughtDropletPool; - public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null) { this.droppedObjectTarget = droppedObjectTarget; @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. /// - private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); /// /// Calculates the width of the area used for attempting catches in gameplay. @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Catch.UI /// Calculates the width of the area used for attempting catches in gameplay. /// /// The beatmap difficulty. - public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); + public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// /// Determine if this catcher can catch a in the current position. diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 8b6a074426..ba6e9224c9 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Down; - TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); + TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 26393c8edb..9745285b38 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty; int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); Random = new FastRandom(seed); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index e643b82271..d65e78bb49 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (drainTime == 0) drainTime = 10000; - BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 43e876b7aa..c1937af7e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// private double tickSpacing = 50; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs index 6a3f168ee1..787807a8ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks public class CheckTooShortSpinnersTest { private CheckTooShortSpinners check; - private BeatmapDifficulty difficulty; + private IBeatmapDifficultyInfo difficulty; [SetUp] public void Setup() @@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks assertTooShort(new List { spinnerHighOd }, difficultyHighOd); } - private void assertOk(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertOk(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty); } - private void assertVeryShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertVeryShort(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort); } - private void assertTooShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertTooShort(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); @@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort); } - private BeatmapVerifierContext getContext(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private BeatmapVerifierContext getContext(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var beatmap = new Beatmap { HitObjects = hitObjects, - BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty } + BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) } }; return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 77a68b714b..cfce80a2b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestSpinner : Spinner { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); SpinsRequired = 1; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 177a4f50a1..1b85e0efde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestSpinner : Spinner { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); SpinsRequired = 1; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..a8f10f44dc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 210d5e0403..b0c655b106 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Mods #region Reduce AR (IApplicableToDifficulty) - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 36629fa41e..7c45b2bc07 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Objects }); } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index c4420b1e87..1d2666f46b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects Path.Version.ValueChanged += _ => updateNestedPositions(); } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index a6aed2c00e..f893559548 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double SpanDuration => slider.SpanDuration; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 725dbe81fb..e7e64954e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects public int SpanIndex { get; set; } public double SpanStartTime { get; set; } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 194aa640f9..f85dc0d391 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int MaximumBonusSpins { get; protected set; } = 1; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9b73e644c5..3b5b972c01 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps case IHasDuration endTimeData: { - double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { @@ -193,9 +193,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty { - public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty) + public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) { - difficulty.CopyTo(this); + CopyFrom(difficulty); + SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index b0634295d0..0318e32991 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index f7a1d130eb..94cd411d7b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..7bb01ddc6d 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tournament.Components if ((mods & LegacyMods.DoubleTime) > 0) { // temporary local calculation (taken from OsuDifficultyCalculator) - double preempt = (int)BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / 1.5; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5; ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5); bpm *= 1.5f; diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 0443422b31..2bb0787b4c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; -using osu.Game.Models.Interfaces; namespace osu.Game.Beatmaps { @@ -21,6 +20,15 @@ namespace osu.Game.Beatmaps private float? approachRate; + public BeatmapDifficulty() + { + } + + public BeatmapDifficulty(IBeatmapDifficultyInfo source) + { + CopyFrom(source); + } + public float ApproachRate { get => approachRate ?? OverallDifficulty; @@ -40,6 +48,17 @@ namespace osu.Game.Beatmaps return diff; } + public void CopyFrom(IBeatmapDifficultyInfo difficulty) + { + ApproachRate = difficulty.ApproachRate; + DrainRate = difficulty.DrainRate; + CircleSize = difficulty.CircleSize; + OverallDifficulty = difficulty.OverallDifficulty; + + SliderMultiplier = difficulty.SliderMultiplier; + SliderTickRate = difficulty.SliderTickRate; + } + public void CopyTo(BeatmapDifficulty difficulty) { difficulty.ApproachRate = ApproachRate; diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 82e90399c9..b7529f39ca 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit } /// - /// Invokes , + /// Invokes , /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index b78c30e8a5..eefa1531c4 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mods } } - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 4edcb0b074..da838f9ea6 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ae0cb895bc..0b159819d4 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Objects /// The control points. /// The difficulty settings to use. /// The cancellation token. - public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default) + public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Objects DefaultsApplied?.Invoke(this); } - protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index df569b91c1..e1de82ade7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Velocity = 1; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 785a729c6d..85693abb93 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Utils; diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 71a2af03fc..3ffd1eb66b 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Models.Interfaces; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 53e30fd9ca..a855322e57 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -106,12 +106,12 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + IBeatmapDifficultyInfo baseDifficulty = Beatmap?.BaseDifficulty; BeatmapDifficulty adjustedDifficulty = null; if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) { - adjustedDifficulty = baseDifficulty.Clone(); + adjustedDifficulty = new BeatmapDifficulty(baseDifficulty); foreach (var mod in mods.Value.OfType()) mod.ApplyToDifficulty(adjustedDifficulty); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..c8914353f8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -43,4 +43,7 @@ + + + From 05996cc2e9232e7c54e1892ca2c3a253e7783513 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:03:08 +0900 Subject: [PATCH 048/170] Add changes that got forgotted in branch surgery --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 2 +- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 2 +- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs | 4 ++-- osu.Game.Tournament/Components/SongBar.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficulty.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 1 - osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 1 - osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- 13 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 82d76252d2..5b1f613f8d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d43e6f1c8b..32fdc9f62d 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Catch.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 8b6a074426..ba6e9224c9 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Down; - TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); + TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..a8f10f44dc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 36629fa41e..7015c5bce2 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 194aa640f9..d5f08f049c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9b73e644c5..7068e469d2 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps case IHasDuration endTimeData: { - double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index f7a1d130eb..94cd411d7b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..7bb01ddc6d 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tournament.Components if ((mods & LegacyMods.DoubleTime) > 0) { // temporary local calculation (taken from OsuDifficultyCalculator) - double preempt = (int)BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / 1.5; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5; ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5); bpm *= 1.5f; diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 0443422b31..1bb1c86c1f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; -using osu.Game.Models.Interfaces; namespace osu.Game.Beatmaps { diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index d2b47ef1a4..d6f3bf0de4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -24,7 +24,6 @@ namespace osu.Game.Beatmaps public int BeatmapVersion; private int? onlineBeatmapID; - private IRulesetInfo ruleset; [JsonProperty("id")] public int? OnlineBeatmapID diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 785a729c6d..85693abb93 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Utils; diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 71a2af03fc..3ffd1eb66b 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Models.Interfaces; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring From 00e33a1da75496db67ccea0269e7f5d174b02f3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:06:09 +0900 Subject: [PATCH 049/170] Fix incorrect `OnlineID` mappings --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index d6f3bf0de4..1b3e67e2e7 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -190,7 +190,7 @@ namespace osu.Game.Beatmaps #region Implementation of IHasOnlineID - public int? OnlineID => ID; + public int? OnlineID => OnlineBeatmapID; #endregion diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 739acb9a8d..7e26b154a9 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps #region Implementation of IHasOnlineID - public int? OnlineID => ID; + public int? OnlineID => OnlineBeatmapSetID; #endregion From 9dae92e78cf9db6caf0b1e659d17be04017630d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:22:25 +0900 Subject: [PATCH 050/170] Add missing backlink to `BeatmapSet` from `Beatmap` and fix non-explicit implementations --- osu.Game/Beatmaps/BeatmapInfo.cs | 7 ++++--- osu.Game/Beatmaps/IBeatmapInfo.cs | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 1b3e67e2e7..83e547218b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -196,11 +196,12 @@ namespace osu.Game.Beatmaps #region Implementation of IBeatmapInfo - public string DifficultyName => Version; + string IBeatmapInfo.DifficultyName => Version; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; - public IBeatmapDifficultyInfo Difficulty => BaseDifficulty; + IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty; + IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; - public double StarRating => StarDifficulty; + double IBeatmapInfo.StarRating => StarDifficulty; #endregion } diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 8ba8f316ed..fa2ce2949b 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -29,6 +29,11 @@ namespace osu.Game.Beatmaps /// IBeatmapDifficultyInfo Difficulty { get; } + /// + /// The beatmap set this beatmap is part of. + /// + IBeatmapSetInfo BeatmapSet { get; } + /// /// The playable length in milliseconds of this beatmap. /// From d6618a99a3b885e0163043c76b011d648c5367af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:31:27 +0900 Subject: [PATCH 051/170] Redirect more methods to interface implementations --- osu.Game/Beatmaps/BeatmapInfo.cs | 8 ++--- osu.Game/Beatmaps/BeatmapMetadata.cs | 46 +++------------------------- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- 3 files changed, 8 insertions(+), 48 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 83e547218b..09f237a5de 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -157,13 +157,9 @@ namespace osu.Game.Beatmaps Version }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); - public override string ToString() => $"{Metadata ?? BeatmapSet?.Metadata} {versionString}".Trim(); + public override string ToString() => ((IBeatmapInfo)this).DisplayTitle; - public RomanisableString ToRomanisableString() - { - var metadata = (Metadata ?? BeatmapSet?.Metadata)?.ToRomanisableString() ?? new RomanisableString(null, null); - return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); - } + public RomanisableString ToRomanisableString() => ((IBeatmapInfo)this).DisplayTitleRomanisable; public bool Equals(BeatmapInfo other) { diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index fbd47d2614..3da80580cb 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -83,51 +82,16 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } + public string BackgroundFile { get; set; } - public override string ToString() - { - string author = Author == null ? string.Empty : $"({Author})"; - return $"{Artist} - {Title} {author}".Trim(); - } + public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); - public RomanisableString ToRomanisableString() - { - string author = Author == null ? string.Empty : $"({Author})"; - var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; - var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; + public override string ToString() => ((IBeatmapMetadataInfo)this).DisplayTitle; - return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); - } + public RomanisableString ToRomanisableString() => ((IBeatmapMetadataInfo)this).DisplayTitleRomanisable; - [JsonIgnore] - public string[] SearchableTerms => new[] - { - Author?.Username, - Artist, - ArtistUnicode, - Title, - TitleUnicode, - Source, - Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - public bool Equals(BeatmapMetadata other) - { - if (other == null) - return false; - - return Title == other.Title - && TitleUnicode == other.TitleUnicode - && Artist == other.Artist - && ArtistUnicode == other.ArtistUnicode - && AuthorString == other.AuthorString - && Source == other.Source - && Tags == other.Tags - && PreviewTime == other.PreviewTime - && AudioFile == other.AudioFile - && BackgroundFile == other.BackgroundFile; - } + public IEnumerable SearchableTerms => ((IBeatmapMetadataInfo)this).SearchableTerms; string IBeatmapMetadataInfo.Author => AuthorString; } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 7e26b154a9..4804c7032c 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } - public string StoryboardFile => Files.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; + public string StoryboardFile => ((IBeatmapSetInfo)this).StoryboardFile; /// /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. From 0daf8937e3a954f0ea595e5a2c4131bdb616e0a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:33:57 +0900 Subject: [PATCH 052/170] Add missing xmldoc --- osu.Game/Beatmaps/IBeatmapInfo.cs | 6 ++++++ osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index fa2ce2949b..6153a0af08 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -64,8 +64,14 @@ namespace osu.Game.Beatmaps /// double StarRating { get; } + /// + /// A user-presentable display title representing this metadata. + /// string DisplayTitle => $"{Metadata} {versionString}".Trim(); + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// RomanisableString DisplayTitleRomanisable { get diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs index 18dd38b404..d0dae296a0 100644 --- a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -65,6 +65,9 @@ namespace osu.Game.Beatmaps /// string BackgroundFile { get; } + /// + /// A user-presentable display title representing this metadata. + /// string DisplayTitle { get @@ -74,6 +77,9 @@ namespace osu.Game.Beatmaps } } + /// + /// A user-presentable display title representing this metadata, with localisation handling for potentially romanisable fields. + /// RomanisableString DisplayTitleRomanisable { get @@ -86,6 +92,9 @@ namespace osu.Game.Beatmaps } } + /// + /// An array of all searchable terms provided in contained metadata. + /// string[] SearchableTerms => new[] { Author, From 3faafd7200576bee1016ba6c75d2643e1d7af751 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 18:24:46 +0900 Subject: [PATCH 053/170] Rename parameter to `repeatCount` and add guards --- .../Formats/LegacyStoryboardDecoder.cs | 4 ++-- osu.Game/Storyboards/CommandLoop.cs | 23 ++++++++++++++----- osu.Game/Storyboards/StoryboardSprite.cs | 10 ++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6301c42deb..5b03212da4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -176,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats case "L": { var startTime = Parsing.ParseDouble(split[1]); - var loopCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + var repeatCount = Parsing.ParseInt(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount)); break; } diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index c17436d813..66db965803 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -9,20 +9,31 @@ namespace osu.Game.Storyboards public class CommandLoop : CommandTimelineGroup { public double LoopStartTime; - public int LoopCount; + + /// + /// The total number of times this loop is played back. Always greater than zero. + /// + public readonly int TotalIterations; public override double StartTime => LoopStartTime + CommandsStartTime; - public override double EndTime => StartTime + CommandsDuration * LoopCount; + public override double EndTime => StartTime + CommandsDuration * TotalIterations; - public CommandLoop(double startTime, int loopCount) + /// + /// Construct a new command loop. + /// + /// The start time of the loop. + /// The number of times the loop should repeat. Should be greater than zero. Zero means a single playback. + public CommandLoop(double startTime, int repeatCount) { + if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); + LoopStartTime = startTime; - LoopCount = Math.Max(1, loopCount); + TotalIterations = repeatCount + 1; } public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { - for (var loop = 0; loop < LoopCount; loop++) + for (var loop = 0; loop < TotalIterations; loop++) { var loopOffset = LoopStartTime + loop * CommandsDuration; foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset)) @@ -31,6 +42,6 @@ namespace osu.Game.Storyboards } public override string ToString() - => $"{LoopStartTime} x{LoopCount}"; + => $"{LoopStartTime} x{TotalIterations}"; } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index bf87e7d10e..6fb2f5994b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,13 +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 osuTK; -using osu.Framework.Graphics; -using osu.Game.Storyboards.Drawables; using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Game.Storyboards.Drawables; +using osuTK; namespace osu.Game.Storyboards { @@ -78,9 +78,9 @@ namespace osu.Game.Storyboards InitialPosition = initialPosition; } - public CommandLoop AddLoop(double startTime, int loopCount) + public CommandLoop AddLoop(double startTime, int repeatCount) { - var loop = new CommandLoop(startTime, loopCount); + var loop = new CommandLoop(startTime, repeatCount); loops.Add(loop); return loop; } From 4c28749d7310a658e9a8329f39064afb03306311 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 19:05:08 +0900 Subject: [PATCH 054/170] Fix incorrect legacy decoder usage --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 5b03212da4..0f15e28c00 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Formats { var startTime = Parsing.ParseDouble(split[1]); var repeatCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount)); + timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1)); break; } From 5820a7165268fcd9776e9fd1b33c27648884349d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 19:57:45 +0900 Subject: [PATCH 055/170] Fix mania difficulty calculator crashing --- .../Difficulty/ManiaDifficultyCalculator.cs | 8 ++-- .../Difficulty/DifficultyCalculator.cs | 38 +++++++++++-------- .../HUD/DefaultPerformancePointsCounter.cs | 4 +- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index a7a6677b68..fc29eadedc 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), - ScoreMultiplier = getScoreMultiplier(beatmap, mods), + ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills }; @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { - new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) + new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods) + private double getScoreMultiplier(Mod[] mods) { double scoreMultiplier = 1; @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - var maniaBeatmap = (ManiaBeatmap)beatmap; + var maniaBeatmap = (ManiaBeatmap)Beatmap; int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns; if (diff > 0) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f38949d982..780c2ad491 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -18,13 +18,17 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { - private readonly Ruleset ruleset; - private readonly WorkingBeatmap beatmap; + /// + /// The beatmap for which difficulty will be calculated. + /// + protected IBeatmap Beatmap { get; private set; } - private IBeatmap playableBeatmap; private Mod[] playableMods; private double clockRate; + private readonly Ruleset ruleset; + private readonly WorkingBeatmap beatmap; + protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) { this.ruleset = ruleset; @@ -40,10 +44,10 @@ namespace osu.Game.Rulesets.Difficulty { preProcess(mods); - var skills = CreateSkills(playableBeatmap, playableMods, clockRate); + var skills = CreateSkills(Beatmap, playableMods, clockRate); - if (!playableBeatmap.HitObjects.Any()) - return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); + if (!Beatmap.HitObjects.Any()) + return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); foreach (var hitObject in getDifficultyHitObjects()) { @@ -51,18 +55,18 @@ namespace osu.Game.Rulesets.Difficulty skill.ProcessInternal(hitObject); } - return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); + return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); } public IEnumerable CalculateTimed(params Mod[] mods) { preProcess(mods); - if (!playableBeatmap.HitObjects.Any()) + if (!Beatmap.HitObjects.Any()) yield break; - var skills = CreateSkills(playableBeatmap, playableMods, clockRate); - var progressiveBeatmap = new ProgressiveCalculationBeatmap(playableBeatmap); + var skills = CreateSkills(Beatmap, playableMods, clockRate); + var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); foreach (var hitObject in getDifficultyHitObjects()) { @@ -93,7 +97,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// Retrieves the s to calculate against. /// - private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(playableBeatmap, clockRate)); + private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate)); /// /// Performs required tasks before every calculation. @@ -102,7 +106,7 @@ namespace osu.Game.Rulesets.Difficulty private void preProcess(Mod[] mods) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); var track = new TrackVirtual(10000); mods.OfType().ForEach(m => m.ApplyToTrack(track)); @@ -118,7 +122,7 @@ namespace osu.Game.Rulesets.Difficulty => input.OrderBy(h => h.BaseObject.StartTime); /// - /// Creates all combinations which adjust the difficulty. + /// Creates all combinations which adjust the difficulty. /// public Mod[] CreateDifficultyAdjustmentModCombinations() { @@ -186,14 +190,15 @@ namespace osu.Game.Rulesets.Difficulty } /// - /// Retrieves all s which adjust the difficulty. + /// Retrieves all s which adjust the difficulty. /// protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); /// /// Creates to describe beatmap's calculated difficulty. /// - /// The whose difficulty was calculated. + /// The whose difficulty was calculated. + /// This may differ from in the case of timed calculation. /// The s that difficulty was calculated with. /// The skills which processed the beatmap. /// The rate at which the gameplay clock is run at. @@ -210,7 +215,8 @@ namespace osu.Game.Rulesets.Difficulty /// /// Creates the s to calculate the difficulty of an . /// - /// The whose difficulty will be calculated. + /// The whose difficulty will be calculated. + /// This may differ from in the case of timed calculation. /// Mods to calculate difficulty with. /// Clockrate to calculate difficulty with. /// The s. diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index 563032b4ea..ff0c628aa6 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -143,9 +143,9 @@ namespace osu.Game.Screens.Play.HUD } public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) - => gameplayBeatmap; + => gameplayBeatmap.PlayableBeatmap; - protected override IBeatmap GetBeatmap() => gameplayBeatmap; + protected override IBeatmap GetBeatmap() => gameplayBeatmap.PlayableBeatmap; protected override Texture GetBackground() => throw new NotImplementedException(); From 98badd644ff6b46d26500f48b125c65b2100410a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 20:09:39 +0900 Subject: [PATCH 056/170] Add xmldocs --- osu.Game/Graphics/UserInterface/RollingCounter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index f03287e7de..67b0b6a06b 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -160,8 +160,15 @@ namespace osu.Game.Graphics.UserInterface this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } + /// + /// Creates the text. Delegates to by default. + /// protected virtual IHasText CreateText() => CreateSpriteText(); + /// + /// Creates an which may be used to display this counter's text. + /// May not be called if is overridden. + /// protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40f), From d0081908c5af3f22aa3ef532b982e94390704910 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 20:52:48 +0900 Subject: [PATCH 057/170] Make Score internal --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4018650093..6b1186c5ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - public Score Score { get; private set; } + internal Score Score { get; private set; } /// /// Create a new player instance. From a1f880a36aede7bb087a9c25c0e618b17813e39a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 20:56:03 +0900 Subject: [PATCH 058/170] Split classes --- .../Difficulty/DifficultyCalculator.cs | 17 +++---------- .../Difficulty/TimedDifficultyAttributes.cs | 25 +++++++++++++++++++ .../HUD/DefaultPerformancePointsCounter.cs | 4 +-- 3 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 780c2ad491..3d90cc59f4 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -222,20 +222,9 @@ namespace osu.Game.Rulesets.Difficulty /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); - public class TimedDifficultyAttributes : IComparable - { - public readonly double Time; - public readonly DifficultyAttributes Attributes; - - public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) - { - Time = time; - Attributes = attributes; - } - - public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); - } - + /// + /// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time. + /// private class ProgressiveCalculationBeatmap : IBeatmap { private readonly IBeatmap baseBeatmap; diff --git a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs new file mode 100644 index 0000000000..973b2dacb2 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Wraps a object and adds a time value for which the attribute is valid. + /// Output by . + /// + public class TimedDifficultyAttributes : IComparable + { + public readonly double Time; + public readonly DifficultyAttributes Attributes; + + public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) + { + Time = time; + Attributes = attributes; + } + + public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); + } +} diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index ff0c628aa6..2bc9f78548 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(CanBeNull = true)] private Player player { get; set; } - private DifficultyCalculator.TimedDifficultyAttributes[] timedAttributes; + private TimedDifficultyAttributes[] timedAttributes; private Ruleset gameplayRuleset; public DefaultPerformancePointsCounter() @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Play.HUD if (player == null) return; - var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new DifficultyCalculator.TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); From 0ee148b53f4bf72441f879950be6b76d610d9c8a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 21:31:26 +0900 Subject: [PATCH 059/170] Extra guard against no attributes --- osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index 2bc9f78548..d93d626c72 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (player == null) + if (player == null || timedAttributes.Length == 0) return; var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); From adff418fd26c7abdff11663eb49389ec9b400268 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 22:15:10 +0900 Subject: [PATCH 060/170] Guard against exception in skin deserialisation --- osu.Game/Skinning/Skin.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index b6cb8fc7a4..92441f40da 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.IO; using osu.Game.Screens.Play.HUD; @@ -55,13 +56,20 @@ namespace osu.Game.Skinning if (bytes == null) continue; - string jsonContent = Encoding.UTF8.GetString(bytes); - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + try + { + string jsonContent = Encoding.UTF8.GetString(bytes); + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); - if (deserializedContent == null) - continue; + if (deserializedContent == null) + continue; - DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to load skin configuration."); + } } } From a32f5d44e279f617ad2933b28f26c0d0250882d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 22:23:51 +0900 Subject: [PATCH 061/170] Improve clarity of xmldoc Co-authored-by: Dan Balasescu --- osu.Game/Database/IRealmFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 3b206d80eb..a957424584 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -13,7 +13,7 @@ namespace osu.Game.Database Realm Context { get; } /// - /// Create a new realm context for use on an arbitrary thread. + /// Create a new realm context for use on the current thread. /// Realm CreateContext(); } From 4e3d9da22d4422f2eb3b3e1f0c513ac95af45d4a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 22:58:40 +0900 Subject: [PATCH 062/170] Increase test lenience --- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index e560c81fb2..7d4673c901 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -84,18 +84,18 @@ namespace osu.Game.Tests.Visual.Gameplay Remove(expectedComponentsAdjustmentContainer); return almostEqual(actualInfo, expectedInfo); - - static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => - other != null - && info.Type == other.Type - && info.Anchor == other.Anchor - && info.Origin == other.Origin - && Precision.AlmostEquals(info.Position, other.Position) - && Precision.AlmostEquals(info.Scale, other.Scale) - && Precision.AlmostEquals(info.Rotation, other.Rotation) - && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); } + private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => + other != null + && info.Type == other.Type + && info.Anchor == other.Anchor + && info.Origin == other.Origin + && Precision.AlmostEquals(info.Position, other.Position, 1) + && Precision.AlmostEquals(info.Scale, other.Scale) + && Precision.AlmostEquals(info.Rotation, other.Rotation) + && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); From 1eb67dc5941322db6b8b9ab5d8bdf87f10f1b579 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:01:28 +0000 Subject: [PATCH 063/170] Bump Microsoft.AspNetCore.SignalR.Client from 5.0.9 to 5.0.10 Bumps [Microsoft.AspNetCore.SignalR.Client](https://github.com/dotnet/aspnetcore) from 5.0.9 to 5.0.10. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.9...v5.0.10) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.SignalR.Client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..9087e77a46 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + From 323a9a748dae5f5c133be297f6f4ae33c5cbe07b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:01:32 +0000 Subject: [PATCH 064/170] Bump HtmlAgilityPack from 1.11.36 to 1.11.37 Bumps [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack) from 1.11.36 to 1.11.37. - [Release notes](https://github.com/zzzprojects/html-agility-pack/releases) - [Commits](https://github.com/zzzprojects/html-agility-pack/compare/v1.11.36...v1.11.37) --- updated-dependencies: - dependency-name: HtmlAgilityPack dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..850cce2d29 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + From 6de4e981ddb4d4fb7ef2cd6546a4b518312498f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:01:37 +0000 Subject: [PATCH 065/170] Bump Realm from 10.5.0 to 10.6.0 Bumps [Realm](https://github.com/realm/realm-dotnet) from 10.5.0 to 10.6.0. - [Release notes](https://github.com/realm/realm-dotnet/releases) - [Changelog](https://github.com/realm/realm-dotnet/blob/master/CHANGELOG.md) - [Commits](https://github.com/realm/realm-dotnet/compare/10.5.0...10.6.0) --- updated-dependencies: - dependency-name: Realm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8fad10d247..b84f1730ac 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..b1654655a2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 37931d0c38..8597a06c03 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -99,6 +99,6 @@ - + From 05ca3aec4f7ed7f5ce56ab7a0e414bbcfb4d7afa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 02:08:56 +0900 Subject: [PATCH 066/170] Rename `GameplayState` to `SpectatorGameplayState` --- .../Spectate/MultiSpectatorScreen.cs | 4 ++-- osu.Game/Screens/Play/SoloSpectator.cs | 22 +++++++++---------- ...playState.cs => SpectatorGameplayState.cs} | 6 ++--- osu.Game/Screens/Spectate/SpectatorScreen.cs | 8 +++---- 4 files changed, 20 insertions(+), 20 deletions(-) rename osu.Game/Screens/Spectate/{GameplayState.cs => SpectatorGameplayState.cs} (81%) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index c45e3a79da..7bf8ce0e1a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -213,8 +213,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - protected override void StartGameplay(int userId, GameplayState gameplayState) - => instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score); + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); protected override void EndGameplay(int userId) { diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 4520e2e825..9d4dad8bdc 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. /// - private GameplayState immediateGameplayState; + private SpectatorGameplayState immediateSpectatorGameplayState; private GetBeatmapSetRequest onlineBeatmapRequest; @@ -146,7 +146,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = () => scheduleStart(immediateGameplayState), + Action = () => scheduleStart(immediateSpectatorGameplayState), Enabled = { Value = false } } } @@ -167,18 +167,18 @@ namespace osu.Game.Screens.Play showBeatmapPanel(spectatorState); } - protected override void StartGameplay(int userId, GameplayState gameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) { - immediateGameplayState = gameplayState; + immediateSpectatorGameplayState = spectatorGameplayState; watchButton.Enabled.Value = true; - scheduleStart(gameplayState); + scheduleStart(spectatorGameplayState); } protected override void EndGameplay(int userId) { scheduledStart?.Cancel(); - immediateGameplayState = null; + immediateSpectatorGameplayState = null; watchButton.Enabled.Value = false; clearDisplay(); @@ -194,7 +194,7 @@ namespace osu.Game.Screens.Play private ScheduledDelegate scheduledStart; - private void scheduleStart(GameplayState gameplayState) + private void scheduleStart(SpectatorGameplayState spectatorGameplayState) { // This function may be called multiple times in quick succession once the screen becomes current again. scheduledStart?.Cancel(); @@ -203,15 +203,15 @@ namespace osu.Game.Screens.Play if (this.IsCurrentScreen()) start(); else - scheduleStart(gameplayState); + scheduleStart(spectatorGameplayState); }); void start() { - Beatmap.Value = gameplayState.Beatmap; - Ruleset.Value = gameplayState.Ruleset.RulesetInfo; + Beatmap.Value = spectatorGameplayState.Beatmap; + Ruleset.Value = spectatorGameplayState.Ruleset.RulesetInfo; - this.Push(new SpectatorPlayerLoader(gameplayState.Score, () => new SoloSpectatorPlayer(gameplayState.Score))); + this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, () => new SoloSpectatorPlayer(spectatorGameplayState.Score))); } } diff --git a/osu.Game/Screens/Spectate/GameplayState.cs b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs similarity index 81% rename from osu.Game/Screens/Spectate/GameplayState.cs rename to osu.Game/Screens/Spectate/SpectatorGameplayState.cs index 4579b9c07c..6ca1ac9a0a 100644 --- a/osu.Game/Screens/Spectate/GameplayState.cs +++ b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs @@ -8,9 +8,9 @@ using osu.Game.Scoring; namespace osu.Game.Screens.Spectate { /// - /// The gameplay state of a spectated user. This class is immutable. + /// An immutable spectator gameplay state. /// - public class GameplayState + public class SpectatorGameplayState { /// /// The score which the user is playing. @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Spectate /// public readonly WorkingBeatmap Beatmap; - public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) + public SpectatorGameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) { Score = score; Ruleset = ruleset; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index f0a68ea078..71bcc336f3 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Spectate private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); - private readonly Dictionary gameplayStates = new Dictionary(); + private readonly Dictionary gameplayStates = new Dictionary(); private IBindable> managerUpdated; @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Spectate Replay = new Replay { HasReceivedAllFrames = false }, }; - var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); + var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; Schedule(() => StartGameplay(userId, gameplayState)); @@ -190,8 +190,8 @@ namespace osu.Game.Screens.Spectate /// Starts gameplay for a user. /// /// The user to start gameplay for. - /// The gameplay state. - protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState); + /// The gameplay state. + protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); /// /// Ends gameplay for a user. From 32afd3f4267df99a69b5f11909d37d5e39a78525 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 02:22:23 +0900 Subject: [PATCH 067/170] Replace all basic usages --- .../Mods/TestSceneOsuModHidden.cs | 4 +- .../TestSceneGameplayCursor.cs | 10 ++-- .../Skinning/Legacy/LegacyCursorParticles.cs | 6 +- .../UI/Cursor/OsuCursorContainer.cs | 6 +- .../Skinning/Legacy/LegacyTaikoScroller.cs | 6 +- .../UI/DrawableTaikoMascot.cs | 6 +- .../Gameplay/TestSceneReplayRecorder.cs | 7 ++- .../Gameplay/TestSceneReplayRecording.cs | 7 ++- .../Gameplay/TestSceneSpectatorPlayback.cs | 4 +- osu.Game/Online/Spectator/SpectatorClient.cs | 4 +- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 +- osu.Game/Screens/Play/GameplayBeatmap.cs | 56 ------------------- osu.Game/Screens/Play/GameplayState.cs | 39 +++++++++++++ osu.Game/Screens/Play/Player.cs | 29 +++++----- osu.Game/Screens/Play/ReplayPlayer.cs | 4 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 4 +- osu.Game/Tests/Visual/TestPlayer.cs | 2 +- 17 files changed, 94 insertions(+), 104 deletions(-) delete mode 100644 osu.Game/Screens/Play/GameplayBeatmap.cs create mode 100644 osu.Game/Screens/Play/GameplayState.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 1ac3ad9194..af64be78f8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -4,13 +4,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool objectWithIncreasedVisibilityHasIndex(int index) - => Player.Mods.Value.OfType().Single().FirstObject == Player.ChildrenOfType().Single().HitObjects[index]; + => Player.Mods.Value.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; private class TestOsuModHidden : OsuModHidden { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index f9dc9abd75..41d9bf7132 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -17,6 +17,7 @@ using osu.Framework.Testing.Input; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneGameplayCursor : OsuSkinnableTestScene { [Cached] - private GameplayBeatmap gameplayBeatmap; + private GameplayState gameplayState; private OsuCursorContainer lastContainer; @@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneGameplayCursor() { - gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + var ruleset = new OsuRuleset(); + gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty()); AddStep("change background colour", () => { @@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); - gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; + gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; Scheduler.AddOnce(() => loadContent(false)); }); @@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSizing(int circleSize, float userScale) { AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); + AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); AddStep("load content", () => loadContent()); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs index c2db5f3f82..611ddd08eb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private OsuPlayfield playfield { get; set; } [Resolved(canBeNull: true)] - private GameplayBeatmap gameplayBeatmap { get; set; } + private GameplayState gameplayState { get; set; } [BackgroundDependencyLoader] private void load(ISkinSource skin, OsuColour colours) @@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void Update() { - if (playfield == null || gameplayBeatmap == null) return; + if (playfield == null || gameplayState == null) return; DrawableHitObject kiaiHitObject = null; // Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary. - if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) + if (gameplayState.Beatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking); kiaiSpewer.Active.Value = kiaiHitObject != null; diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 83bcc88e5f..cfe83d0106 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [Resolved(canBeNull: true)] - private GameplayBeatmap beatmap { get; set; } + private GameplayState state { get; set; } [Resolved] private OsuConfigManager config { get; set; } @@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { float scale = userCursorScale.Value; - if (autoCursorScale.Value && beatmap != null) + if (autoCursorScale.Value && state != null) { // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize); } cursorScale.Value = scale; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs index 6fc59ea0e8..fa49242675 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs @@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader(true)] - private void load(GameplayBeatmap gameplayBeatmap) + private void load(GameplayState gameplayState) { - if (gameplayBeatmap != null) - ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); + if (gameplayState != null) + ((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult); } private bool passing; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 6a16f311bf..e1063e1071 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI } [BackgroundDependencyLoader(true)] - private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap) + private void load(TextureStore textures, GameplayState gameplayState) { InternalChildren = new[] { @@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Taiko.UI animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; - if (gameplayBeatmap != null) - ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); + if (gameplayState != null) + ((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult); } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 0a3fedaf8e..d89fd322d1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.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 System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -17,6 +18,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -38,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); [SetUp] public void SetUp() => Schedule(() => @@ -57,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo } + ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index dfd5e2dc58..07514ad51a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.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 System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -13,6 +14,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -30,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly TestRulesetInputManager recordingManager; [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); public TestSceneReplayRecording() { @@ -48,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo } + ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 6f5f774758..07ff35f77b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -25,6 +25,8 @@ using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; @@ -62,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay private SpectatorClient spectatorClient { get; set; } [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 8c617784b9..d55ad45ff5 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying(GameplayBeatmap beatmap, Score score) + public void BeginPlaying(GameplayState state, Score score) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -148,7 +148,7 @@ namespace osu.Game.Online.Spectator currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - currentBeatmap = beatmap.PlayableBeatmap; + currentBeatmap = state.Beatmap; currentScore = score; BeginPlayingInternal(currentState); diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index b57c224059..976f95cef8 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.UI private SpectatorClient spectatorClient { get; set; } [Resolved] - private GameplayBeatmap gameplayBeatmap { get; set; } + private GameplayState gameplayState { get; set; } protected ReplayRecorder(Score target) { @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorClient?.BeginPlaying(gameplayBeatmap, target); + spectatorClient?.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs deleted file mode 100644 index 74fbe540fa..0000000000 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Screens.Play -{ - public class GameplayBeatmap : Component, IBeatmap - { - public readonly IBeatmap PlayableBeatmap; - - public GameplayBeatmap(IBeatmap playableBeatmap) - { - PlayableBeatmap = playableBeatmap; - } - - public BeatmapInfo BeatmapInfo - { - get => PlayableBeatmap.BeatmapInfo; - set => PlayableBeatmap.BeatmapInfo = value; - } - - public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - - public ControlPointInfo ControlPointInfo - { - get => PlayableBeatmap.ControlPointInfo; - set => PlayableBeatmap.ControlPointInfo = value; - } - - public List Breaks => PlayableBeatmap.Breaks; - - public double TotalBreakTime => PlayableBeatmap.TotalBreakTime; - - public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects; - - public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); - - public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); - - public IBeatmap Clone() => PlayableBeatmap.Clone(); - - private readonly Bindable lastJudgementResult = new Bindable(); - - public IBindable LastJudgementResult => lastJudgementResult; - - public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; - } -} diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs new file mode 100644 index 0000000000..4944d5b8e2 --- /dev/null +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; + +#nullable enable + +namespace osu.Game.Screens.Play +{ + public class GameplayState + { + /// + /// The final post-convert post-mod-application beatmap. + /// + public readonly IBeatmap Beatmap; + + public readonly Ruleset Ruleset; + + public IReadOnlyList Mods; + + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList mods) + { + Beatmap = beatmap; + Ruleset = ruleset; + Mods = mods; + } + + private readonly Bindable lastJudgementResult = new Bindable(); + + public IBindable LastJudgementResult => lastJudgementResult; + + public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9927467bd6..a05a8f5056 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -93,9 +93,9 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } - protected Ruleset GameplayRuleset { get; private set; } + public GameplayState GameplayState { get; private set; } - protected GameplayBeatmap GameplayBeatmap { get; private set; } + private Ruleset ruleset; private Sample sampleRestart; @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; - Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo; + Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = Mods.Value.ToArray(); PrepareReplay(); @@ -206,16 +206,16 @@ namespace osu.Game.Screens.Play if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); dependencies.CacheAs(DrawableRuleset); - ScoreProcessor = GameplayRuleset.CreateScoreProcessor(); + ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); dependencies.CacheAs(ScoreProcessor); - HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor); @@ -225,12 +225,11 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); - AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - dependencies.CacheAs(GameplayBeatmap); - - var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); + var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. @@ -280,7 +279,7 @@ namespace osu.Game.Screens.Play { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); - GameplayBeatmap.ApplyResult(r); + GameplayState.ApplyResult(r); }; DrawableRuleset.RevertResult += r => @@ -478,17 +477,17 @@ namespace osu.Game.Screens.Play throw new InvalidOperationException("Beatmap was not loaded"); var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; - GameplayRuleset = rulesetInfo.CreateInstance(); + ruleset = rulesetInfo.CreateInstance(); try { - playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); } catch (BeatmapInvalidForRulesetException) { // A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; - GameplayRuleset = rulesetInfo.CreateInstance(); + ruleset = rulesetInfo.CreateInstance(); playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); } @@ -1010,7 +1009,7 @@ namespace osu.Game.Screens.Play using (var stream = new MemoryStream()) { - new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream); + new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0c6f1ed911..eefea737cf 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); + protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play void keyboardSeek(int direction) { - double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayBeatmap.HitObjects.Last().GetEndTime()); + double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.HitObjects.Last().GetEndTime()); Seek(target); } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index d7e42a9cd1..fbb4fb5699 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -66,8 +66,8 @@ namespace osu.Game.Screens.Play foreach (var frame in bundle.Frames) { - IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, GameplayBeatmap.PlayableBeatmap); + IConvertibleReplayFrame convertibleFrame = GameplayState.Ruleset.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, GameplayState.Beatmap); var convertedFrame = (ReplayFrame)convertibleFrame; convertedFrame.Time = frame.Time; diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 5e5f20b307..d68984b144 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual if (autoplayMod != null) { - DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); + DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayState.Beatmap, Mods.Value)); return; } From 7e009f616845718cc124c68b900ad4c57382a7fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 02:28:35 +0900 Subject: [PATCH 068/170] Add full xmldoc --- osu.Game/Screens/Play/GameplayState.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 4944d5b8e2..ba08c946d2 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -12,6 +12,9 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play { + /// + /// The state of an active gameplay session, generally constructed and exposed by . + /// public class GameplayState { /// @@ -19,10 +22,23 @@ namespace osu.Game.Screens.Play /// public readonly IBeatmap Beatmap; + /// + /// The ruleset used in gameplay. + /// public readonly Ruleset Ruleset; + /// + /// The mods applied to the gameplay. + /// public IReadOnlyList Mods; + /// + /// A bindable tracking the last judgement result applied to any hit object. + /// + public IBindable LastJudgementResult => lastJudgementResult; + + private readonly Bindable lastJudgementResult = new Bindable(); + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList mods) { Beatmap = beatmap; @@ -30,10 +46,10 @@ namespace osu.Game.Screens.Play Mods = mods; } - private readonly Bindable lastJudgementResult = new Bindable(); - - public IBindable LastJudgementResult => lastJudgementResult; - + /// + /// Applies the score change of a to this . + /// + /// The to apply. public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; } } From dcd7d7a709beb3b5b837b4c88c9861c764613072 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 03:05:04 +0900 Subject: [PATCH 069/170] Add `JsonIgnore` rule for `StoryboardFile` Not sure why this is required, doesn't make much sense. --- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 4804c7032c..9f5a07ec43 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using JetBrains.Annotations; +using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Database; @@ -61,6 +62,7 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } + [JsonIgnore] public string StoryboardFile => ((IBeatmapSetInfo)this).StoryboardFile; /// From 5ea51f4a9ff1d001882f41409f2adef11df4e396 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 18:15:07 +0000 Subject: [PATCH 070/170] Bump Sentry from 3.9.0 to 3.9.4 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 3.9.0 to 3.9.4. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/3.9.0...3.9.4) --- updated-dependencies: - dependency-name: Sentry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3e0809c359..ff89fadcc3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -38,7 +38,7 @@ - + From 9517d69f21fdd51ce7539eed07b42f7ff6a9f4ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:59:31 +0000 Subject: [PATCH 071/170] Bump MessagePack from 2.3.75 to 2.3.85 Bumps [MessagePack](https://github.com/neuecc/MessagePack-CSharp) from 2.3.75 to 2.3.85. - [Release notes](https://github.com/neuecc/MessagePack-CSharp/releases) - [Changelog](https://github.com/neuecc/MessagePack-CSharp/blob/master/prepare_release.ps1) - [Commits](https://github.com/neuecc/MessagePack-CSharp/compare/v2.3.75...v2.3.85) --- updated-dependencies: - dependency-name: MessagePack dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ff89fadcc3..c110aadac1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + From f60f712bcc232177f909d98e4ab686478c6c364b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:59:32 +0000 Subject: [PATCH 072/170] Bump Microsoft.AspNetCore.SignalR.Protocols.MessagePack Bumps [Microsoft.AspNetCore.SignalR.Protocols.MessagePack](https://github.com/dotnet/aspnetcore) from 5.0.9 to 5.0.10. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.9...v5.0.10) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.MessagePack dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ff89fadcc3..868074a32f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + From cb8165ca504f426c9b0b2013d6c58a28d73bfcb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:59:32 +0000 Subject: [PATCH 073/170] Bump Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson Bumps [Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson](https://github.com/dotnet/aspnetcore) from 5.0.9 to 5.0.10. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.9...v5.0.10) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ff89fadcc3..73b95b60d5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + From 973c31132be80ccfc14cac0621fc871db987f406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 12:44:22 +0900 Subject: [PATCH 074/170] Rename `BeatmapInfo` variables which were named `beatmap` for clarity --- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../SongSelect/TestSceneAdvancedStats.cs | 24 ++++---- .../SongSelect/TestSceneBeatmapCarousel.cs | 22 +++---- .../SongSelect/TestScenePlaySongSelect.cs | 60 +++++++++---------- osu.Game.Tournament/Components/SongBar.cs | 32 +++++----- .../Screens/BeatmapInfoScreen.cs | 2 +- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 20 +++---- .../BeatmapSet/BeatmapSetHeaderContent.cs | 2 +- osu.Game/Overlays/BeatmapSet/Details.cs | 10 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++--- osu.Game/Screens/Select/BeatmapDetails.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 54 ++++++++--------- .../Select/Carousel/CarouselBeatmapSet.cs | 6 +- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Carousel/FilterableDifficultyIcon.cs | 2 +- .../FilterableGroupedDifficultyIcon.cs | 2 +- .../Select/Carousel/SetPanelContent.cs | 2 +- .../Screens/Select/Details/AdvancedStats.cs | 20 +++---- osu.Game/Screens/Select/SongSelect.cs | 2 +- 19 files changed, 140 insertions(+), 140 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 1670d86545..12a85c3f26 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background private void setupUserSettings() { AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); - AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null); + AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null); AddStep("Set default user settings", () => { SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index dcc2111ad3..4538e36c5e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNoMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("no mods selected", () => SelectedMods.Value = Array.Empty()); @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestManiaFirstBarText() { - AddStep("set beatmap", () => advancedStats.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo { Ruleset = rulesets.GetRuleset(3), BaseDifficulty = new BeatmapDifficulty @@ -84,11 +84,11 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestEasyMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select EZ mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); SelectedMods.Value = new[] { ruleset.CreateMod() }; }); @@ -101,11 +101,11 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestHardRockMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select HR mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); SelectedMods.Value = new[] { ruleset.CreateMod() }; }); @@ -118,13 +118,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestUnchangedDifficultyAdjustMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select unchanged Difficulty Adjust mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); var difficultyAdjustMod = ruleset.CreateMod(); - difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty); + difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.BaseDifficulty); SelectedMods.Value = new[] { difficultyAdjustMod }; }); @@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangedDifficultyAdjustMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select changed Difficulty Adjust mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); var difficultyAdjustMod = ruleset.CreateMod(); - var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; + var originalDifficulty = advancedStats.BeatmapInfo.BaseDifficulty; difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 78ddfa9ed2..66f15670f5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.SongSelect private readonly Stack selectedSets = new Stack(); private readonly HashSet eagerSelectedIDs = new HashSet(); - private BeatmapInfo currentSelection => carousel.SelectedBeatmap; + private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; private const int set_count = 5; @@ -75,11 +75,11 @@ namespace osu.Game.Tests.Visual.SongSelect { for (int i = 0; i < 3; i++) { - AddStep("store selection", () => selection = carousel.SelectedBeatmap); + AddStep("store selection", () => selection = carousel.SelectedBeatmapInfo); if (isIterating) - AddUntilStep("selection changed", () => carousel.SelectedBeatmap != selection); + AddUntilStep("selection changed", () => carousel.SelectedBeatmapInfo != selection); else - AddUntilStep("selection not changed", () => carousel.SelectedBeatmap == selection); + AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo == selection); } } } @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Set non-empty mode filter", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false)); - AddAssert("Something is selected", () => carousel.SelectedBeatmap != null); + AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null); } /// @@ -562,7 +562,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap.RulesetID == 0); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo.RulesetID == 0); AddStep("remove mixed set", () => { @@ -653,7 +653,7 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); }); - AddAssert("selection lost", () => carousel.SelectedBeatmap == null); + AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null); AddStep("Restore different ruleset filter", () => { @@ -661,7 +661,7 @@ namespace osu.Game.Tests.Visual.SongSelect eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); }); - AddAssert("selection changed", () => carousel.SelectedBeatmap != manySets.First().Beatmaps.First()); + AddAssert("selection changed", () => carousel.SelectedBeatmapInfo != manySets.First().Beatmaps.First()); } AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2); @@ -763,9 +763,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => { if (diff != null) - return carousel.SelectedBeatmap == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First(); + return carousel.SelectedBeatmapInfo == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First(); - return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmap); + return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmapInfo); }); private void setSelected(int set, int diff) => @@ -800,7 +800,7 @@ namespace osu.Game.Tests.Visual.SongSelect { carousel.RandomAlgorithm.Value = RandomSelectAlgorithm.RandomPermutation; - if (!selectedSets.Any() && carousel.SelectedBeatmap != null) + if (!selectedSets.Any() && carousel.SelectedBeatmapInfo != null) selectedSets.Push(carousel.SelectedBeatmapSet); carousel.SelectNextRandom(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 102e5ee425..f9e81d3da6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() - .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); + .First(b => ((CarouselBeatmap)b.Item).BeatmapInfo != songSelect.Carousel.SelectedBeatmapInfo)); InputManager.Click(MouseButton.Left); @@ -172,7 +172,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() - .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); + .First(b => ((CarouselBeatmap)b.Item).BeatmapInfo != songSelect.Carousel.SelectedBeatmapInfo)); InputManager.PressButton(MouseButton.Left); @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addRulesetImportStep(2); - AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -322,13 +322,13 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); addRulesetImportStep(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); changeRuleset(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 1); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 1); changeRuleset(0); - AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -338,7 +338,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); // this is an important check, to make sure updateComponentFromBeatmap() was actually run AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target)); @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.SongSelect Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); @@ -444,7 +444,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addManyTestMaps(); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); bool startRequested = false; @@ -473,13 +473,13 @@ namespace osu.Game.Tests.Visual.SongSelect // used for filter check below AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); BeatmapInfo target = null; @@ -494,7 +494,7 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddAssert("selected only shows expected ruleset (plus converts)", () => { @@ -502,16 +502,16 @@ namespace osu.Game.Tests.Visual.SongSelect // special case for converts checked here. return selectedPanel.ChildrenOfType().All(i => - i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0); + i.IsFiltered || i.Item.BeatmapInfo.Ruleset.ID == targetRuleset || i.Item.BeatmapInfo.Ruleset.ID == 0); }); - AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddStep("reset filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = string.Empty); AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); - AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmap.OnlineBeatmapID == target.OnlineBeatmapID); + AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); } [Test] @@ -522,13 +522,13 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(0); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); BeatmapInfo target = null; @@ -540,15 +540,15 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); - AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nononoo"); AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); - AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -581,9 +581,9 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); addRulesetImportStep(0); AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last())); - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); + AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmapInfo.ID); AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First())); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmapInfo.ID == previousID); } [Test] @@ -641,7 +641,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmapInfo == filteredBeatmap); } [Test] @@ -717,7 +717,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); + .First(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -735,7 +735,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); - AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmap.BeatmapSet.ID == previousSetID); + AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet.ID == previousSetID); AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3); } @@ -767,7 +767,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find group icon for different ruleset", () => { groupIcon = set.ChildrenOfType() - .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); + .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -781,7 +781,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); - AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().Beatmap)); + AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().BeatmapInfo)); } [Test] @@ -856,7 +856,7 @@ namespace osu.Game.Tests.Visual.SongSelect private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); - private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); + private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo); private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) { diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..357c82df61 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -21,22 +21,22 @@ namespace osu.Game.Tournament.Components { public class SongBar : CompositeDrawable { - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; public const float HEIGHT = 145 / 2f; [Resolved] private IBindable ruleset { get; set; } - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (beatmap == value) + if (beatmapInfo == value) return; - beatmap = value; + beatmapInfo = value; update(); } } @@ -95,18 +95,18 @@ namespace osu.Game.Tournament.Components private void update() { - if (beatmap == null) + if (beatmapInfo == null) { flow.Clear(); return; } - var bpm = beatmap.BeatmapSet.OnlineInfo.BPM; - var length = beatmap.Length; + var bpm = beatmapInfo.BeatmapSet.OnlineInfo.BPM; + var length = beatmapInfo.Length; string hardRockExtra = ""; string srExtra = ""; - var ar = beatmap.BaseDifficulty.ApproachRate; + var ar = beatmapInfo.BaseDifficulty.ApproachRate; if ((mods & LegacyMods.HardRock) > 0) { @@ -132,9 +132,9 @@ namespace osu.Game.Tournament.Components default: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), + ("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}{hardRockExtra}"), - ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), }; break; @@ -142,15 +142,15 @@ namespace osu.Game.Tournament.Components case 3: stats = new (string heading, string content)[] { - ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), - ("HP", $"{beatmap.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") + ("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("HP", $"{beatmapInfo.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") }; break; case 2: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), + ("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}"), }; break; @@ -186,7 +186,7 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DiffPiece(stats), - new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) + new DiffPiece(("Star Rating", $"{beatmapInfo.StarDifficulty:0.#}{srExtra}")) } }, new FillFlowContainer @@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components } } }, - new TournamentBeatmapPanel(beatmap) + new TournamentBeatmapPanel(beatmapInfo) { RelativeSizeAxes = Axes.X, Width = 0.5f, diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 50498304ca..b94b164116 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tournament.Screens private void beatmapChanged(ValueChangedEvent beatmap) { SongBar.FadeInFromZero(300, Easing.OutQuint); - SongBar.Beatmap = beatmap.NewValue; + SongBar.BeatmapInfo = beatmap.NewValue; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 5a6cde8229..683f4f0c49 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -38,16 +38,16 @@ namespace osu.Game.Overlays.BeatmapSet } } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateDisplay(); } @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.BeatmapSet { bpm.Value = BeatmapSet?.OnlineInfo?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-"; - if (beatmap == null) + if (beatmapInfo == null) { length.Value = string.Empty; circleCount.Value = string.Empty; @@ -65,11 +65,11 @@ namespace osu.Game.Overlays.BeatmapSet } else { - length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmap.Length).ToFormattedDuration()); - length.Value = TimeSpan.FromMilliseconds(beatmap.Length).ToFormattedDuration(); + length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration()); + length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); - circleCount.Value = beatmap.OnlineInfo.CircleCount.ToLocalisableString(@"N0"); - sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToLocalisableString(@"N0"); + circleCount.Value = beatmapInfo.OnlineInfo.CircleCount.ToLocalisableString(@"N0"); + sliderCount.Value = beatmapInfo.OnlineInfo.SliderCount.ToLocalisableString(@"N0"); } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index c3b6444a24..dcf06ac7fb 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -211,7 +211,7 @@ namespace osu.Game.Overlays.BeatmapSet Picker.Beatmap.ValueChanged += b => { - Details.Beatmap = b.NewValue; + Details.BeatmapInfo = b.NewValue; externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; }; } diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index 680487ffbb..92361ae4f8 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -37,16 +37,16 @@ namespace osu.Game.Overlays.BeatmapSet } } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - basic.Beatmap = advanced.Beatmap = beatmap = value; + basic.BeatmapInfo = advanced.BeatmapInfo = beatmapInfo = value; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5eceae3c6e..f424587e22 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select /// /// The currently selected beatmap. /// - public BeatmapInfo SelectedBeatmap => selectedBeatmap?.Beatmap; + public BeatmapInfo SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo; private CarouselBeatmap selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected); @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet selectedBeatmapSet; /// - /// Raised when the is changed. + /// Raised when the is changed. /// public Action SelectionChanged; @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Select // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required if (existingSet?.State?.Value == CarouselItemState.Selected) - previouslySelectedID = selectedBeatmap?.Beatmap.ID; + previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; var newSet = createCarouselSet(beatmapSet); @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Select // check if we can/need to maintain our current selection. if (previouslySelectedID != null) - select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); + select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); itemsCache.Invalidate(); Schedule(() => BeatmapSetsChanged?.Invoke()); @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Select if (!bypassFilters && set.Filtered.Value) continue; - var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmap)); if (item == null) // The beatmap that needs to be selected doesn't exist in this set @@ -472,7 +472,7 @@ namespace osu.Game.Screens.Select private float? scrollTarget; /// - /// Scroll to the current . + /// Scroll to the current . /// /// /// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels. @@ -720,7 +720,7 @@ namespace osu.Game.Screens.Select if (state.NewValue == CarouselItemState.Selected) { selectedBeatmapSet = set; - SelectionChanged?.Invoke(c.Beatmap); + SelectionChanged?.Invoke(c.BeatmapInfo); itemsCache.Invalidate(); ScrollToSelected(); diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 973f54c038..d59d76300a 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Select private void updateStatistics() { - advanced.Beatmap = Beatmap; + advanced.BeatmapInfo = Beatmap; description.Text = Beatmap?.Version; source.Text = Beatmap?.Metadata?.Source; tags.Text = Beatmap?.Metadata?.Tags; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index f95ddfee41..3f729d9477 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -12,11 +12,11 @@ namespace osu.Game.Screens.Select.Carousel { public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT; - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; - public CarouselBeatmap(BeatmapInfo beatmap) + public CarouselBeatmap(BeatmapInfo beatmapInfo) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; State.Value = CarouselItemState.Collapsed; } @@ -28,36 +28,36 @@ namespace osu.Game.Screens.Select.Carousel bool match = criteria.Ruleset == null || - Beatmap.RulesetID == criteria.Ruleset.ID || - (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + BeatmapInfo.RulesetID == criteria.Ruleset.ID || + (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); - if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // only check ruleset equality or convertability for selected beatmap Filtered.Value = !match; return; } - match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty); - match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); - match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); - match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize); - match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty); - match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length); - match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM); + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); + match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.BaseDifficulty.ApproachRate); + match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate); + match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.BaseDifficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.BaseDifficulty.OverallDifficulty); + match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); + match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM); - match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor); - match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(Beatmap.Status); + match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); + match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status); - match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(Beatmap.Metadata.AuthorString); - match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(Beatmap.Metadata.Artist) || - criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode); + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.AuthorString); + match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || + criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); - match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(Beatmap.StarDifficulty); + match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); if (match) { - var terms = Beatmap.SearchableTerms; + var terms = BeatmapInfo.SearchableTerms; foreach (var criteriaTerm in criteria.SearchTerms) match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); @@ -66,16 +66,16 @@ namespace osu.Game.Screens.Select.Carousel // this should be done after text matching so we can prioritise matching numbers in metadata. if (!match && criteria.SearchNumber.HasValue) { - match = (Beatmap.OnlineBeatmapID == criteria.SearchNumber.Value) || - (Beatmap.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); + match = (BeatmapInfo.OnlineBeatmapID == criteria.SearchNumber.Value) || + (BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); } } if (match) - match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; + match &= criteria.Collection?.Beatmaps.Contains(BeatmapInfo) ?? true; if (match && criteria.RulesetCriteria != null) - match &= criteria.RulesetCriteria.Matches(Beatmap); + match &= criteria.RulesetCriteria.Matches(BeatmapInfo); Filtered.Value = !match; } @@ -89,13 +89,13 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - var ruleset = Beatmap.RulesetID.CompareTo(otherBeatmap.Beatmap.RulesetID); + var ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID); if (ruleset != 0) return ruleset; - return Beatmap.StarDifficulty.CompareTo(otherBeatmap.Beatmap.StarDifficulty); + return BeatmapInfo.StarDifficulty.CompareTo(otherBeatmap.BeatmapInfo.StarDifficulty); } } - public override string ToString() => Beatmap.ToString(); + public override string ToString() => BeatmapInfo.ToString(); } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 00c2c2cb4a..0d7882bf17 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -47,8 +47,8 @@ namespace osu.Game.Screens.Select.Carousel { if (LastSelected == null || LastSelected.Filtered.Value) { - if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended) - return Children.OfType().First(b => b.Beatmap == recommended); + if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended) + return Children.OfType().First(b => b.BeatmapInfo == recommended); } return base.GetNextToSelect(); @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select.Carousel /// /// All beatmaps which are not filtered and valid for display. /// - protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.Beatmap); + protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.BeatmapInfo); private int compareUsingAggregateMax(CarouselBeatmapSet other, Func func) { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 633ef9297e..2fe7ff4562 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel public DrawableCarouselBeatmap(CarouselBeatmap panel) { - beatmap = panel.Beatmap; + beatmap = panel.BeatmapInfo; Item = panel; } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index 51fe7796c7..ce0cec837b 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) - : base(item.Beatmap, performBackgroundDifficultyLookup: false) + : base(item.BeatmapInfo, performBackgroundDifficultyLookup: false) { filtered.BindTo(item.Filtered); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs index d2f9ed3a6a..acffdd9f64 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly List Items; public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) - : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) + : base(items.Select(i => i.BeatmapInfo).ToList(), ruleset, Color4.White) { Items = items; diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 23a02547b2..9fb640ba1a 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 53e30fd9ca..8c978e25ae 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -38,16 +38,16 @@ namespace osu.Game.Screens.Select.Details protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateStatistics(); } @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + BeatmapDifficulty baseDifficulty = BeatmapInfo?.BaseDifficulty; BeatmapDifficulty adjustedDifficulty = null; if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Select.Details mod.ApplyToDifficulty(adjustedDifficulty); } - switch (Beatmap?.Ruleset?.ID ?? 0) + switch (BeatmapInfo?.Ruleset?.ID ?? 0) { case 3: // Account for mania differences locally for now @@ -145,13 +145,13 @@ namespace osu.Game.Screens.Select.Details { starDifficultyCancellationSource?.Cancel(); - if (Beatmap == null) + if (BeatmapInfo == null) return; starDifficultyCancellationSource = new CancellationTokenSource(); - var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); - var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + var normalStarDifficulty = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token); + var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() => { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9801098952..e4ab360765 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -377,7 +377,7 @@ namespace osu.Game.Screens.Select // avoid attempting to continue before a selection has been obtained. // this could happen via a user interaction while the carousel is still in a loading state. - if (Carousel.SelectedBeatmap == null) return; + if (Carousel.SelectedBeatmapInfo == null) return; if (beatmap != null) Carousel.SelectBeatmap(beatmap); From d55836c0b2bd529735efef5fd281e4da4e023baa Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 2 Oct 2021 15:10:30 +0200 Subject: [PATCH 075/170] Make `ResetButton` no longer part of search filtering The button will now appear if and only if all the bindings in its section are visible (not filtered out by the search) --- .../Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 0e8e10c086..806390c0ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -75,5 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Content.CornerRadius = 5; } + + public override IEnumerable FilterTerms => Enumerable.Empty(); } } From 6ec2223b5c231fc747b31e3d9f87ba810d2e376b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 23:01:44 +0900 Subject: [PATCH 076/170] Catch potential file access exceptions also in async flow --- osu.Game.Tests/Database/RealmTest.cs | 38 +++++++++++++--------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index b7658d6408..219690db30 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -39,25 +39,9 @@ namespace osu.Game.Tests.Database realmFactory.Dispose(); - try - { - Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); - } - catch - { - // windows runs may error due to file still being open. - } - + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); realmFactory.Compact(); - - try - { - Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); - } - catch - { - // windows runs may error due to file still being open. - } + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); } }); } @@ -71,16 +55,28 @@ namespace osu.Game.Tests.Database using (var realmFactory = new RealmContextFactory(testStorage, caller)) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - await testAction(realmFactory, testStorage); realmFactory.Dispose(); - Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); realmFactory.Compact(); - Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); } }); } + + private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) + { + try + { + return testStorage.GetStream(realmFactory.Filename)?.Length ?? 0; + } + catch + { + // windows runs may error due to file still being open. + return 0; + } + } } } From ec61c3c5eeb2a72e74930902eccb78bc06abf1ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Oct 2021 00:55:29 +0900 Subject: [PATCH 077/170] Rename all remaining cases --- osu.Desktop/DiscordRichPresence.cs | 4 +- .../Beatmaps/ManiaBeatmapConverter.cs | 4 +- .../ManiaFilterCriteria.cs | 4 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 6 +- .../NonVisual/Filtering/FilterMatchingTest.cs | 2 +- .../Filtering/FilterQueryParserTest.cs | 2 +- .../Online/TestSceneBeatmapSetOverlay.cs | 2 +- .../TestSceneBeatmapSetOverlaySuccessRate.cs | 8 +-- .../SongSelect/TestSceneBeatmapDetails.cs | 14 ++-- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 26 ++++---- .../TestSceneDeleteLocalScore.cs | 13 ++-- .../TestSceneTournamentBeatmapPanel.cs | 2 +- .../TestSceneTournamentModDisplay.cs | 6 +- .../Components/TournamentBeatmapPanel.cs | 20 +++--- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- .../Screens/Editors/RoundEditorScreen.cs | 2 +- .../Screens/Editors/SeedingEditorScreen.cs | 2 +- .../Screens/MapPool/MapPoolScreen.cs | 6 +- osu.Game.Tournament/TournamentGameBase.cs | 4 +- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 24 +++---- osu.Game/Beatmaps/BeatmapManager.cs | 16 ++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 18 ++--- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 66 +++++++++---------- osu.Game/Beatmaps/BeatmapStore.cs | 24 +++---- osu.Game/Beatmaps/DifficultyRecommender.cs | 6 +- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 36 +++++----- .../Drawables/DifficultyIconTooltip.cs | 8 +-- .../Online/API/Requests/GetBeatmapRequest.cs | 8 +-- .../Online/API/Requests/GetScoresRequest.cs | 14 ++-- .../API/Requests/Responses/APIBeatmap.cs | 2 +- .../API/Requests/Responses/APIBeatmapSet.cs | 2 +- .../Requests/Responses/APILegacyScoreInfo.cs | 8 +-- .../Responses/APIUserMostPlayedBeatmap.cs | 8 +-- osu.Game/Online/Chat/NowPlayingCommand.cs | 10 +-- osu.Game/Online/Rooms/APIPlaylistBeatmap.cs | 4 +- osu.Game/Online/Rooms/PlaylistItem.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 20 +++--- osu.Game/Overlays/BeatmapSet/Info.cs | 6 +- osu.Game/Overlays/BeatmapSet/SuccessRate.cs | 16 ++--- osu.Game/Overlays/BeatmapSetOverlay.cs | 2 +- .../Sections/BeatmapMetadataContainer.cs | 18 ++--- .../Historical/DrawableMostPlayedBeatmap.cs | 24 +++---- .../Sections/Ranks/DrawableProfileScore.cs | 12 ++-- .../Rulesets/Filter/IRulesetFilterCriteria.cs | 6 +- .../Components/Menus/DifficultyMenuItem.cs | 4 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +-- .../Select/BeatmapClearScoresDialog.cs | 6 +- osu.Game/Screens/Select/BeatmapDetailArea.cs | 2 +- osu.Game/Screens/Select/BeatmapDetails.cs | 38 +++++------ .../Carousel/DrawableCarouselBeatmap.cs | 32 ++++----- .../Screens/Select/Carousel/TopLocalRank.cs | 12 ++-- .../Select/Leaderboards/BeatmapLeaderboard.cs | 20 +++--- .../Screens/Select/LocalScoreDeleteDialog.cs | 4 +- .../Screens/Select/PlayBeatmapDetailArea.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 32 ++++----- osu.Game/Skinning/LegacyBeatmapSkin.cs | 8 +-- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 4 +- osu.Game/Users/UserActivity.cs | 22 +++---- 58 files changed, 342 insertions(+), 341 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index dcb88efeb6..e2b40e9dc6 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -140,10 +140,10 @@ namespace osu.Desktop switch (activity) { case UserActivity.InGame game: - return game.Beatmap.ToString(); + return game.BeatmapInfo.ToString(); case UserActivity.Editing edit: - return edit.Beatmap.ToString(); + return edit.BeatmapInfo.ToString(); case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 26393c8edb..0321a5325b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } - public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo) { - var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); + var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize); return (int)Math.Max(1, roundedCircleSize); } diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index d9a278ef29..0290230490 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Mania { private FilterCriteria.OptionalRange keys; - public bool Matches(BeatmapInfo beatmap) + public bool Matches(BeatmapInfo beatmapInfo) { - return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); + return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cba7f34ede..b536fc61b7 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -945,13 +945,13 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) + private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo) { return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2, - Beatmap = beatmap, - BeatmapInfoID = beatmap.ID + Beatmap = beatmapInfo, + BeatmapInfoID = beatmapInfo.ID }, new ImportScoreTest.TestArchiveReader()); } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 8ff2743b6a..ed86daf8b6 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.NonVisual.Filtering match = shouldMatch; } - public bool Matches(BeatmapInfo beatmap) => match; + public bool Matches(BeatmapInfo beatmapInfo) => match; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index a55bdd2df8..df42c70c87 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -256,7 +256,7 @@ namespace osu.Game.Tests.NonVisual.Filtering { public string CustomValue { get; set; } - public bool Matches(BeatmapInfo beatmap) => true; + public bool Matches(BeatmapInfo beatmapInfo) => true; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index f420ad976b..453e26ef96 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -233,7 +233,7 @@ namespace osu.Game.Tests.Visual.Online }); }); - AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); + AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.BeatmapInfo.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index fd5c188b94..fe8e33f783 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -58,10 +58,10 @@ namespace osu.Game.Tests.Visual.Online var firstBeatmap = createBeatmap(); var secondBeatmap = createBeatmap(); - AddStep("set first set", () => successRate.Beatmap = firstBeatmap); + AddStep("set first set", () => successRate.BeatmapInfo = firstBeatmap); AddAssert("ratings set", () => successRate.Graph.Metrics == firstBeatmap.Metrics); - AddStep("set second set", () => successRate.Beatmap = secondBeatmap); + AddStep("set second set", () => successRate.BeatmapInfo = secondBeatmap); AddAssert("ratings set", () => successRate.Graph.Metrics == secondBeatmap.Metrics); static BeatmapInfo createBeatmap() => new BeatmapInfo @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnlyFailMetrics() { - AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo { Metrics = new BeatmapMetrics { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEmptyMetrics() { - AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo { Metrics = new BeatmapMetrics() }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs index b4544fbc85..d5b4fb9a80 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllMetrics() { - AddStep("all metrics", () => details.Beatmap = new BeatmapInfo + AddStep("all metrics", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllMetricsExceptSource() { - AddStep("all except source", () => details.Beatmap = new BeatmapInfo + AddStep("all except source", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOnlyRatings() { - AddStep("ratings", () => details.Beatmap = new BeatmapInfo + AddStep("ratings", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOnlyFailsAndRetries() { - AddStep("fails retries", () => details.Beatmap = new BeatmapInfo + AddStep("fails retries", () => details.BeatmapInfo = new BeatmapInfo { Version = "Only Retries and Fails", Metadata = new BeatmapMetadata @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNoMetrics() { - AddStep("no metrics", () => details.Beatmap = new BeatmapInfo + AddStep("no metrics", () => details.BeatmapInfo = new BeatmapInfo { Version = "No Metrics", Metadata = new BeatmapMetadata @@ -166,13 +166,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNullBeatmap() { - AddStep("null beatmap", () => details.Beatmap = null); + AddStep("null beatmap", () => details.BeatmapInfo = null); } [Test] public void TestOnlineMetrics() { - AddStep("online ratings/retries/fails", () => details.Beatmap = new BeatmapInfo + AddStep("online ratings/retries/fails", () => details.BeatmapInfo = new BeatmapInfo { OnlineBeatmapID = 162, }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 29815ce9ff..95cf6a9903 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - leaderboard.Beatmap = beatmapInfo; + leaderboard.BeatmapInfo = beatmapInfo; }); clearScores(); @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void checkCount(int expected) => AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType().Count() == expected); - private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmap) + private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) { return new[] { @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 6602580, @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 4608074, @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 1014222, @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 1541390, @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 2243452, @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 2705430, @@ -311,7 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 7151382, @@ -330,7 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 2051389, @@ -349,7 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 6169483, @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 6702666, @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void showBeatmapWithStatus(BeatmapSetOnlineStatus status) { - leaderboard.Beatmap = new BeatmapInfo + leaderboard.BeatmapInfo = new BeatmapInfo { OnlineBeatmapID = 1113057, Status = status, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 2e30ed9827..f58dbef145 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -37,7 +37,8 @@ namespace osu.Game.Tests.Visual.UserInterface private ScoreManager scoreManager; private readonly List importedScores = new List(); - private BeatmapInfo beatmap; + + private BeatmapInfo beatmapInfo; [Cached] private readonly DialogOverlay dialogOverlay; @@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Size = new Vector2(550f, 450f), Scope = BeatmapLeaderboardScope.Local, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { ID = 1, Metadata = new BeatmapMetadata @@ -84,15 +85,15 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; + beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; for (int i = 0; i < 50; i++) { var score = new ScoreInfo { OnlineScoreID = i, - Beatmap = beatmap, - BeatmapInfoID = beatmap.ID, + Beatmap = beatmapInfo, + BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), TotalScore = RNG.Next(1, 1000000), MaxCombo = RNG.Next(1, 1000), @@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface leaderboard.Scores = null; leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables - leaderboard.Beatmap = beatmap; + leaderboard.BeatmapInfo = beatmapInfo; leaderboard.RefreshScores(); // Required in the case that the beatmap hasn't changed }); diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index bc32a12ab7..f9c553cb3f 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { - var beatmap = apiBeatmap.ToBeatmap(rulesets); + var beatmap = apiBeatmap.ToBeatmapInfo(rulesets); Add(new TournamentBeatmapPanel(beatmap) { Anchor = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index 47e7ed9b61..27eb55a9fb 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests.Components private FillFlowContainer fillFlow; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; [BackgroundDependencyLoader] private void load() @@ -44,12 +44,12 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { - beatmap = apiBeatmap.ToBeatmap(rulesets); + beatmapInfo = apiBeatmap.ToBeatmapInfo(rulesets); var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods; foreach (var mod in mods) { - fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym) + fillFlow.Add(new TournamentBeatmapPanel(beatmapInfo, mod.Acronym) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e6d73c6e83..0e5a66e7fe 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components { public class TournamentBeatmapPanel : CompositeDrawable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; private readonly string mod; private const float horizontal_padding = 10; @@ -32,11 +32,11 @@ namespace osu.Game.Tournament.Components private readonly Bindable currentMatch = new Bindable(); private Box flash; - public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null) + public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null) { - if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo)); - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; this.mod = mod; Width = 400; Height = HEIGHT; @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - BeatmapSet = Beatmap.BeatmapSet, + BeatmapSet = BeatmapInfo.BeatmapSet, }, new FillFlowContainer { @@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components new TournamentSpriteText { Text = new RomanisableString( - $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), + $"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}", + $"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer @@ -93,7 +93,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Metadata.AuthorString, + Text = BeatmapInfo.Metadata.AuthorString, Padding = new MarginPadding { Right = 20 }, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, @@ -105,7 +105,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Version, + Text = BeatmapInfo.Version, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, } @@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Components private void updateState() { - var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineBeatmapID); + var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID); bool doFlash = found != choice; choice = found; diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index f538d4a7d9..7010a30eb7 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tournament.IPC else { beatmapLookupRequest = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); - beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); + beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmapInfo(Rulesets); API.Queue(beatmapLookupRequest); } } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 27ad6650d1..6e4fc8fe1a 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -238,7 +238,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.BeatmapInfo = res.ToBeatmap(rulesets); + Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 6418bf97da..b64a3993e6 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -246,7 +246,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.BeatmapInfo = res.ToBeatmap(rulesets); + Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index d4292c5492..1e3c550323 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool if (map != null) { - if (e.Button == MouseButton.Left && map.Beatmap.OnlineBeatmapID != null) - addForBeatmap(map.Beatmap.OnlineBeatmapID.Value); + if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null) + addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value); else { - var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID); + var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID); if (existing != null) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 531da00faf..2e4ed9d5b1 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); API.Perform(req); - b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); addedInfo = true; } @@ -203,7 +203,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); req.Perform(API); - b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); addedInfo = true; } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 0aa6a6dd0b..c46ab93ece 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -242,7 +242,7 @@ namespace osu.Game.Beatmaps { // GetDifficultyAsync will fall back to existing data from BeatmapInfo if not locally available // (contrary to GetAsync) - GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken) + GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken) .ContinueWith(t => { // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. @@ -262,7 +262,7 @@ namespace osu.Game.Beatmaps private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - var beatmapInfo = key.Beatmap; + var beatmapInfo = key.BeatmapInfo; var rulesetInfo = key.Ruleset; try @@ -270,7 +270,7 @@ namespace osu.Game.Beatmaps var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.Beatmap)); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); var attributes = calculator.Calculate(key.OrderedMods); return new StarDifficulty(attributes); @@ -300,21 +300,21 @@ namespace osu.Game.Beatmaps public readonly struct DifficultyCacheLookup : IEquatable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly RulesetInfo Ruleset; public readonly Mod[] OrderedMods; - public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) + public DifficultyCacheLookup([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - Ruleset = ruleset ?? Beatmap.Ruleset; + Ruleset = ruleset ?? BeatmapInfo.Ruleset; OrderedMods = mods?.OrderBy(m => m.Acronym).Select(mod => mod.DeepClone()).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => Beatmap.ID == other.Beatmap.ID + => BeatmapInfo.ID == other.BeatmapInfo.ID && Ruleset.ID == other.Ruleset.ID && OrderedMods.SequenceEqual(other.OrderedMods); @@ -322,7 +322,7 @@ namespace osu.Game.Beatmaps { var hashCode = new HashCode(); - hashCode.Add(Beatmap.ID); + hashCode.Add(BeatmapInfo.ID); hashCode.Add(Ruleset.ID); foreach (var mod in OrderedMods) @@ -334,12 +334,12 @@ namespace osu.Game.Beatmaps private class BindableStarDifficulty : Bindable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly CancellationToken CancellationToken; - public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken) + public BindableStarDifficulty(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; CancellationToken = cancellationToken; } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2f80633279..a3081cc462 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps private readonly BeatmapModelDownloader beatmapModelDownloader; private readonly WorkingBeatmapCache workingBeatmapCache; - private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue; + private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) @@ -48,8 +48,8 @@ namespace osu.Game.Beatmaps if (performOnlineLookups) { - onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue; + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.OnlineLookupQueue = onlineBeatmapLookupQueue; } } @@ -182,14 +182,14 @@ namespace osu.Game.Beatmaps /// /// Delete a beatmap difficulty. /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmapModelManager.Hide(beatmap); + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) => beatmapModelManager.Hide(beatmapInfo); /// /// Restore a beatmap difficulty. /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmapModelManager.Restore(beatmap); + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) => beatmapModelManager.Restore(beatmapInfo); #endregion @@ -329,7 +329,7 @@ namespace osu.Game.Beatmaps public void Dispose() { - onlineBetamapLookupQueue?.Dispose(); + onlineBeatmapLookupQueue?.Dispose(); } #endregion diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 0beddc1e9b..aa14f95863 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -173,24 +173,24 @@ namespace osu.Game.Beatmaps /// /// Delete a beatmap difficulty. /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) => beatmaps.Hide(beatmapInfo); /// /// Restore a beatmap difficulty. /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) => beatmaps.Restore(beatmapInfo); /// /// Saves an file against a given . /// - /// The to save the content against. The file referenced by will be replaced. + /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) + public virtual void Save(BeatmapInfo baetmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) { - var setInfo = info.BeatmapSet; + var setInfo = baetmapInfo.BeatmapSet; using (var stream = new MemoryStream()) { @@ -201,7 +201,7 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == baetmapInfo.ID); var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; // grab the original file (or create a new one if not found). @@ -219,7 +219,7 @@ namespace osu.Game.Beatmaps } } - WorkingBeatmapCache?.Invalidate(info); + WorkingBeatmapCache?.Invalidate(baetmapInfo); } /// diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 55164e2442..e1faf6005b 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -58,18 +58,18 @@ namespace osu.Game.Beatmaps } // todo: expose this when we need to do individual difficulty lookups. - protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmapInfo, CancellationToken cancellationToken) + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmapInfo), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); - private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) + private void lookup(BeatmapSetInfo set, BeatmapInfo beatmapInfo) { - if (checkLocalCache(set, beatmap)) + if (checkLocalCache(set, beatmapInfo)) return; if (api?.State.Value != APIState.Online) return; - var req = new GetBeatmapRequest(beatmap); + var req = new GetBeatmapRequest(beatmapInfo); req.Failure += fail; @@ -82,18 +82,18 @@ namespace osu.Game.Beatmaps if (res != null) { - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + beatmapInfo.Status = res.Status; + beatmapInfo.BeatmapSet.Status = res.BeatmapSet.Status; + beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmapInfo.OnlineBeatmapID = res.OnlineBeatmapID; - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = res.AuthorID; + if (beatmapInfo.Metadata != null) + beatmapInfo.Metadata.AuthorID = res.AuthorID; - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; + if (beatmapInfo.BeatmapSet.Metadata != null) + beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID; - logForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); } } catch (Exception e) @@ -103,8 +103,8 @@ namespace osu.Game.Beatmaps void fail(Exception e) { - beatmap.OnlineBeatmapID = null; - logForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); + beatmapInfo.OnlineBeatmapID = null; + logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})"); } } @@ -149,7 +149,7 @@ namespace osu.Game.Beatmaps cacheDownloadRequest.PerformAsync(); } - private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) + private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) { // download is in progress (or was, and failed). if (cacheDownloadRequest != null) @@ -159,9 +159,9 @@ namespace osu.Game.Beatmaps if (!storage.Exists(cache_database_name)) return false; - if (string.IsNullOrEmpty(beatmap.MD5Hash) - && string.IsNullOrEmpty(beatmap.Path) - && beatmap.OnlineBeatmapID == null) + if (string.IsNullOrEmpty(beatmapInfo.MD5Hash) + && string.IsNullOrEmpty(beatmapInfo.Path) + && beatmapInfo.OnlineBeatmapID == null) return false; try @@ -174,9 +174,9 @@ namespace osu.Game.Beatmaps { cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; - cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); - cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); + cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmapInfo.OnlineBeatmapID ?? (object)DBNull.Value)); + cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path)); using (var reader = cmd.ExecuteReader()) { @@ -184,18 +184,18 @@ namespace osu.Game.Beatmaps { var status = (BeatmapSetOnlineStatus)reader.GetByte(2); - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); - beatmap.OnlineBeatmapID = reader.GetInt32(1); + beatmapInfo.Status = status; + beatmapInfo.BeatmapSet.Status = status; + beatmapInfo.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); + beatmapInfo.OnlineBeatmapID = reader.GetInt32(1); - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = reader.GetInt32(3); + if (beatmapInfo.Metadata != null) + beatmapInfo.Metadata.AuthorID = reader.GetInt32(3); - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); + if (beatmapInfo.BeatmapSet.Metadata != null) + beatmapInfo.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); - logForModel(set, $"Cached local retrieval for {beatmap}."); + logForModel(set, $"Cached local retrieval for {beatmapInfo}."); return true; } } @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps } catch (Exception ex) { - logForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); + logForModel(set, $"Cached local retrieval for {beatmapInfo} failed with {ex}."); } return false; diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index e3214b7c03..197581db88 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -25,40 +25,40 @@ namespace osu.Game.Beatmaps /// /// Hide a in the database. /// - /// The beatmap to hide. + /// The beatmap to hide. /// Whether the beatmap's was changed. - public bool Hide(BeatmapInfo beatmap) + public bool Hide(BeatmapInfo beatmapInfo) { using (ContextFactory.GetForWrite()) { - Refresh(ref beatmap, Beatmaps); + Refresh(ref beatmapInfo, Beatmaps); - if (beatmap.Hidden) return false; + if (beatmapInfo.Hidden) return false; - beatmap.Hidden = true; + beatmapInfo.Hidden = true; } - BeatmapHidden?.Invoke(beatmap); + BeatmapHidden?.Invoke(beatmapInfo); return true; } /// /// Restore a previously hidden . /// - /// The beatmap to restore. + /// The beatmap to restore. /// Whether the beatmap's was changed. - public bool Restore(BeatmapInfo beatmap) + public bool Restore(BeatmapInfo beatmapInfo) { using (ContextFactory.GetForWrite()) { - Refresh(ref beatmap, Beatmaps); + Refresh(ref beatmapInfo, Beatmaps); - if (!beatmap.Hidden) return false; + if (!beatmapInfo.Hidden) return false; - beatmap.Hidden = false; + beatmapInfo.Hidden = false; } - BeatmapRestored?.Invoke(beatmap); + BeatmapRestored?.Invoke(beatmapInfo); return true; } diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index ca910e70b8..b1b1e58ab7 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -62,14 +62,14 @@ namespace osu.Game.Beatmaps if (!recommendedDifficultyMapping.TryGetValue(r, out var recommendation)) continue; - BeatmapInfo beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => + BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { var difference = b.StarDifficulty - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); - if (beatmap != null) - return beatmap; + if (beatmapInfo != null) + return beatmapInfo; } return null; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 0751a777d8..880d70aec2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -37,7 +37,7 @@ namespace osu.Game.Beatmaps.Drawables } [NotNull] - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; [CanBeNull] private readonly RulesetInfo ruleset; @@ -56,26 +56,26 @@ namespace osu.Game.Beatmaps.Drawables /// /// Creates a new with a given and combination. /// - /// The beatmap to show the difficulty of. + /// The beatmap to show the difficulty of. /// The ruleset to show the difficulty with. /// The mods to show the difficulty with. /// Whether to display a tooltip when hovered. - public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) - : this(beatmap, shouldShowTooltip) + public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) + : this(beatmapInfo, shouldShowTooltip) { - this.ruleset = ruleset ?? beatmap.Ruleset; + this.ruleset = ruleset ?? beatmapInfo.Ruleset; this.mods = mods ?? Array.Empty(); } /// /// Creates a new that follows the currently-selected ruleset and mods. /// - /// The beatmap to show the difficulty of. + /// The beatmap to show the difficulty of. /// Whether to display a tooltip when hovered. /// Whether to perform difficulty lookup (including calculation if necessary). - public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) + public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) { - this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); + this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo)); this.shouldShowTooltip = shouldShowTooltip; this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup; @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps.Drawables Child = background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(beatmap.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes + Colour = colours.ForStarDifficulty(beatmapInfo.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer @@ -114,27 +114,27 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + Icon = (ruleset ?? beatmapInfo.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } }, }; if (performBackgroundDifficultyLookup) - iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); + iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); else - difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0); + difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarDifficulty, 0); difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars)); } ITooltip IHasCustomTooltip.GetCustomTooltip() => new DifficultyIconTooltip(); - DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; + DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null; private class DifficultyRetriever : Component { public readonly Bindable StarDifficulty = new Bindable(); - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly RulesetInfo ruleset; private readonly IReadOnlyList mods; @@ -143,9 +143,9 @@ namespace osu.Game.Beatmaps.Drawables [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) + public DifficultyRetriever(BeatmapInfo beatmapInfo, RulesetInfo ruleset, IReadOnlyList mods) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.ruleset = ruleset; this.mods = mods; } @@ -157,8 +157,8 @@ namespace osu.Game.Beatmaps.Drawables { difficultyCancellation = new CancellationTokenSource(); localStarDifficulty = ruleset != null - ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) - : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token); + ? difficultyCache.GetBindableDifficulty(beatmapInfo, ruleset, mods, difficultyCancellation.Token) + : difficultyCache.GetBindableDifficulty(beatmapInfo, difficultyCancellation.Token); localStarDifficulty.BindValueChanged(d => { if (d.NewValue is StarDifficulty diff) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 0329e935bc..d4c9f83a0a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps.Drawables public void SetContent(DifficultyIconTooltipContent content) { - difficultyName.Text = content.Beatmap.Version; + difficultyName.Text = content.BeatmapInfo.Version; starDifficulty.UnbindAll(); starDifficulty.BindTo(content.Difficulty); @@ -109,12 +109,12 @@ namespace osu.Game.Beatmaps.Drawables internal class DifficultyIconTooltipContent { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly IBindable Difficulty; - public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable difficulty) + public DifficultyIconTooltipContent(BeatmapInfo beatmapInfo, IBindable difficulty) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; Difficulty = difficulty; } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs index 87925b94c6..901f7365b8 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapRequest : APIRequest { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; - public GetBeatmapRequest(BeatmapInfo beatmap) + public GetBeatmapRequest(BeatmapInfo beatmapInfo) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; } - protected override string Target => $@"beatmaps/lookup?id={beatmap.OnlineBeatmapID}&checksum={beatmap.MD5Hash}&filename={System.Uri.EscapeUriString(beatmap.Path ?? string.Empty)}"; + protected override string Target => $@"beatmaps/lookup?id={beatmapInfo.OnlineBeatmapID}&checksum={beatmapInfo.MD5Hash}&filename={System.Uri.EscapeUriString(beatmapInfo.Path ?? string.Empty)}"; } } diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index b4e0e44b2c..f3bf690ed5 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Online.API.Requests { public class GetScoresRequest : APIRequest { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly BeatmapLeaderboardScope scope; private readonly RulesetInfo ruleset; private readonly IEnumerable mods; - public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) + public GetScoresRequest(BeatmapInfo beatmapInfo, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) { - if (!beatmap.OnlineBeatmapID.HasValue) + if (!beatmapInfo.OnlineBeatmapID.HasValue) throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); if (scope == BeatmapLeaderboardScope.Local) throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard"); - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.scope = scope; this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); this.mods = mods ?? Array.Empty(); @@ -42,7 +42,7 @@ namespace osu.Game.Online.API.Requests foreach (APILegacyScoreInfo score in r.Scores) { - score.Beatmap = beatmap; + score.BeatmapInfo = beatmapInfo; score.OnlineRulesetID = ruleset.ID.Value; } @@ -50,12 +50,12 @@ namespace osu.Game.Online.API.Requests if (userScore != null) { - userScore.Score.Beatmap = beatmap; + userScore.Score.BeatmapInfo = beatmapInfo; userScore.Score.OnlineRulesetID = ruleset.ID.Value; } } - protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores{createQueryParameters()}"; + protected override string Target => $@"beatmaps/{beatmapInfo.OnlineBeatmapID}/scores{createQueryParameters()}"; private string createQueryParameters() { diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 7343870dbc..c2a68c8ca1 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -64,7 +64,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"max_combo")] private int? maxCombo { get; set; } - public virtual BeatmapInfo ToBeatmap(RulesetStore rulesets) + public virtual BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) { var set = BeatmapSet?.ToBeatmapSet(rulesets); diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index f653a654ca..35963792d0 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -116,7 +116,7 @@ namespace osu.Game.Online.API.Requests.Responses beatmapSet.Beatmaps = beatmaps?.Select(b => { - var beatmap = b.ToBeatmap(rulesets); + var beatmap = b.ToBeatmapInfo(rulesets); beatmap.BeatmapSet = beatmapSet; beatmap.Metadata = beatmapSet.Metadata; return beatmap; diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 567df524b1..18a0db3928 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -37,7 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses OnlineScoreID = OnlineScoreID, Date = Date, PP = PP, - Beatmap = Beatmap, + Beatmap = BeatmapInfo, RulesetID = OnlineRulesetID, Hash = Replay ? "online" : string.Empty, // todo: temporary? Rank = Rank, @@ -100,7 +100,7 @@ namespace osu.Game.Online.API.Requests.Responses public DateTimeOffset Date { get; set; } [JsonProperty(@"beatmap")] - public BeatmapInfo Beatmap { get; set; } + public BeatmapInfo BeatmapInfo { get; set; } [JsonProperty("accuracy")] public double Accuracy { get; set; } @@ -114,10 +114,10 @@ namespace osu.Game.Online.API.Requests.Responses set { // extract the set ID to its correct place. - Beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID }; + BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID }; value.ID = 0; - Beatmap.Metadata = value; + BeatmapInfo.Metadata = value; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 4614fe29b7..15f67eda47 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online.API.Requests.Responses public int PlayCount { get; set; } [JsonProperty] - private BeatmapInfo beatmap { get; set; } + private BeatmapInfo beatmapInfo { get; set; } [JsonProperty] private APIBeatmapSet beatmapSet { get; set; } @@ -24,9 +24,9 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets) { BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets); - beatmap.BeatmapSet = setInfo; - beatmap.Metadata = setInfo.Metadata; - return beatmap; + beatmapInfo.BeatmapSet = setInfo; + beatmapInfo.Metadata = setInfo.Metadata; + return beatmapInfo; } } } diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 97a2fbdd5c..89eb00a45a 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -37,27 +37,27 @@ namespace osu.Game.Online.Chat base.LoadComplete(); string verb; - BeatmapInfo beatmap; + BeatmapInfo beatmapInfo; switch (api.Activity.Value) { case UserActivity.InGame game: verb = "playing"; - beatmap = game.Beatmap; + beatmapInfo = game.BeatmapInfo; break; case UserActivity.Editing edit: verb = "editing"; - beatmap = edit.Beatmap; + beatmapInfo = edit.BeatmapInfo; break; default: verb = "listening to"; - beatmap = currentBeatmap.Value.BeatmapInfo; + beatmapInfo = currentBeatmap.Value.BeatmapInfo; break; } - var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + var beatmapString = beatmapInfo.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineBeatmapID} {beatmapInfo}]" : beatmapInfo.ToString(); channelManager.PostMessage($"is {verb} {beatmapString}", true, target); Expire(); diff --git a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs index 973dccd528..00623282d3 100644 --- a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs @@ -13,9 +13,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("checksum")] public string Checksum { get; set; } - public override BeatmapInfo ToBeatmap(RulesetStore rulesets) + public override BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) { - var b = base.ToBeatmap(rulesets); + var b = base.ToBeatmapInfo(rulesets); b.MD5Hash = Checksum; return b; } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 1d409d4b56..48f1347fa1 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { - Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets); + Beatmap.Value ??= apiBeatmap.ToBeatmapInfo(rulesets); Ruleset.Value ??= rulesets.GetRuleset(RulesetID); Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 60e341d2ac..3df275c6d3 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -178,21 +178,21 @@ namespace osu.Game.Overlays.BeatmapSet } starRatingContainer.FadeOut(100); - Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap; + Beatmap.Value = Difficulties.FirstOrDefault()?.BeatmapInfo; plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0; favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0; updateDifficultyButtons(); } - private void showBeatmap(BeatmapInfo beatmap) + private void showBeatmap(BeatmapInfo beatmapInfo) { - version.Text = beatmap?.Version; + version.Text = beatmapInfo?.Version; } private void updateDifficultyButtons() { - Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); + Difficulties.Children.ToList().ForEach(diff => diff.State = diff.BeatmapInfo == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); } public class DifficultiesContainer : FillFlowContainer @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box backgroundBox; private readonly DifficultyIcon icon; - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public Action OnHovered; public Action OnClicked; @@ -241,9 +241,9 @@ namespace osu.Game.Overlays.BeatmapSet } } - public DifficultySelectorButton(BeatmapInfo beatmap) + public DifficultySelectorButton(BeatmapInfo beatmapInfo) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; Size = new Vector2(size); Margin = new MarginPadding { Horizontal = tile_spacing / 2 }; @@ -260,7 +260,7 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0.5f } }, - icon = new DifficultyIcon(beatmap, shouldShowTooltip: false) + icon = new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -273,7 +273,7 @@ namespace osu.Game.Overlays.BeatmapSet protected override bool OnHover(HoverEvent e) { fadeIn(); - OnHovered?.Invoke(Beatmap); + OnHovered?.Invoke(BeatmapInfo); return base.OnHover(e); } @@ -286,7 +286,7 @@ namespace osu.Game.Overlays.BeatmapSet protected override bool OnClick(ClickEvent e) { - OnClicked?.Invoke(Beatmap); + OnClicked?.Invoke(BeatmapInfo); return base.OnClick(e); } diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index f9b8de9dba..61c660cbaa 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -24,10 +24,10 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Bindable BeatmapSet = new Bindable(); - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => successRate.Beatmap; - set => successRate.Beatmap = value; + get => successRate.BeatmapInfo; + set => successRate.BeatmapInfo = value; } public Info() diff --git a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs index cde4589c98..4a9b8244a5 100644 --- a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs +++ b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs @@ -23,16 +23,16 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Bar successRate; private readonly Container percentContainer; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateDisplay(); } @@ -40,15 +40,15 @@ namespace osu.Game.Overlays.BeatmapSet private void updateDisplay() { - int passCount = beatmap?.OnlineInfo?.PassCount ?? 0; - int playCount = beatmap?.OnlineInfo?.PlayCount ?? 0; + int passCount = beatmapInfo?.OnlineInfo?.PassCount ?? 0; + int playCount = beatmapInfo?.OnlineInfo?.PlayCount ?? 0; var rate = playCount != 0 ? (float)passCount / playCount : 0; successPercent.Text = rate.ToLocalisableString(@"0.#%"); successRate.Length = rate; percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); - Graph.Metrics = beatmap?.Metrics; + Graph.Metrics = beatmapInfo?.Metrics; } public SuccessRate() diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index bdb3715e73..f987b57d6e 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays Header.HeaderContent.Picker.Beatmap.ValueChanged += b => { - info.Beatmap = b.NewValue; + info.BeatmapInfo = b.NewValue; ScrollFlow.ScrollToStart(); }; } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs index a8a4cfc365..7812a81f30 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs @@ -15,12 +15,12 @@ namespace osu.Game.Overlays.Profile.Sections /// public abstract class BeatmapMetadataContainer : OsuHoverContainer { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; - protected BeatmapMetadataContainer(BeatmapInfo beatmap) + protected BeatmapMetadataContainer(BeatmapInfo beatmapInfo) : base(HoverSampleSet.Submit) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; AutoSizeAxes = Axes.Both; } @@ -30,19 +30,19 @@ namespace osu.Game.Overlays.Profile.Sections { Action = () => { - if (beatmap.OnlineBeatmapID != null) - beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value); - else if (beatmap.BeatmapSet?.OnlineBeatmapSetID != null) - beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet.OnlineBeatmapSetID.Value); + if (beatmapInfo.OnlineBeatmapID != null) + beatmapSetOverlay?.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value); + else if (beatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) + beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapInfo.BeatmapSet.OnlineBeatmapSetID.Value); }; Child = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Children = CreateText(beatmap), + Children = CreateText(beatmapInfo), }; } - protected abstract Drawable[] CreateText(BeatmapInfo beatmap); + protected abstract Drawable[] CreateText(BeatmapInfo beatmapInfo); } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index a419bef233..2c6fa76ca4 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -22,12 +22,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private const int cover_width = 100; private const int corner_radius = 6; - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly int playCount; - public DrawableMostPlayedBeatmap(BeatmapInfo beatmap, int playCount) + public DrawableMostPlayedBeatmap(BeatmapInfo beatmapInfo, int playCount) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.playCount = playCount; RelativeSizeAxes = Axes.X; @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { RelativeSizeAxes = Axes.Y, Width = cover_width, - BeatmapSet = beatmap.BeatmapSet, + BeatmapSet = beatmapInfo.BeatmapSet, }, new Container { @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Direction = FillDirection.Vertical, Children = new Drawable[] { - new MostPlayedBeatmapMetadataContainer(beatmap), + new MostPlayedBeatmapMetadataContainer(beatmapInfo), new LinkFlowContainer(t => { t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }.With(d => { d.AddText("mapped by "); - d.AddUserLink(beatmap.Metadata.Author); + d.AddUserLink(beatmapInfo.Metadata.Author); }), } }, @@ -120,23 +120,23 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer { - public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmap) - : base(beatmap) + public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmapInfo) + : base(beatmapInfo) { } - protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[] { new OsuSpriteText { Text = new RomanisableString( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] "), + $"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} [{beatmapInfo.Version}] ", + $"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} [{beatmapInfo.Version}] "), Font = OsuFont.GetFont(weight: FontWeight.Bold) }, new OsuSpriteText { - Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Regular) }, }; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 713303285a..c221f070df 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -245,27 +245,27 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks private class ScoreBeatmapMetadataContainer : BeatmapMetadataContainer { - public ScoreBeatmapMetadataContainer(BeatmapInfo beatmap) - : base(beatmap) + public ScoreBeatmapMetadataContainer(BeatmapInfo beatmapInfo) + : base(beatmapInfo) { } - protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[] { new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Text = new RomanisableString( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} "), + $"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} ", + $"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} "), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) }, new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), Font = OsuFont.GetFont(size: 12, italics: true) }, }; diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index 13cc41f8e0..dd2ad2cbfa 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Filter public interface IRulesetFilterCriteria { /// - /// Checks whether the supplied satisfies ruleset-specific custom criteria, + /// Checks whether the supplied satisfies ruleset-specific custom criteria, /// in addition to the ones mandated by song select. /// - /// The beatmap to test the criteria against. + /// The beatmap to test the criteria against. /// /// true if the beatmap matches the ruleset-specific custom filtering criteria, /// false otherwise. /// - bool Matches(BeatmapInfo beatmap); + bool Matches(BeatmapInfo beatmapInfo); /// /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box. diff --git a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs index 5f9b72447b..c458b65607 100644 --- a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs +++ b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs @@ -10,12 +10,12 @@ namespace osu.Game.Screens.Edit.Components.Menus { public class DifficultyMenuItem : StatefulMenuItem { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action difficultyChangeFunc) : base(beatmapInfo.Version ?? "(unnamed)", null) { - Beatmap = beatmapInfo; + BeatmapInfo = beatmapInfo; State.Value = selected; if (!selected) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f424587e22..e5e28d2fde 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -242,15 +242,15 @@ namespace osu.Game.Screens.Select /// /// Selects a given beatmap on the carousel. /// - /// The beatmap to select. + /// The beatmap to select. /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). /// True if a selection was made, False if it wasn't. - public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true) + public bool SelectBeatmap(BeatmapInfo beatmapInfo, bool bypassFilters = true) { // ensure that any pending events from BeatmapManager have been run before attempting a selection. Scheduler.Update(); - if (beatmap?.Hidden != false) + if (beatmapInfo?.Hidden != false) return false; foreach (CarouselBeatmapSet set in beatmapSets) @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Select if (!bypassFilters && set.Filtered.Value) continue; - var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmap)); + var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmapInfo)); if (item == null) // The beatmap that needs to be selected doesn't exist in this set diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index b32416b361..8c33b1ea0b 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -17,9 +17,9 @@ namespace osu.Game.Screens.Select [Resolved] private ScoreManager scoreManager { get; set; } - public BeatmapClearScoresDialog(BeatmapInfo beatmap, Action onCompletion) + public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion) { - BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}"; + BodyText = $@"{beatmapInfo.Metadata?.Artist} - {beatmapInfo.Metadata?.Title}"; Icon = FontAwesome.Solid.Eraser; HeaderText = @"Clearing all local scores. Are you sure?"; Buttons = new PopupDialogButton[] @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmap.ID).ToList())) + Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmapInfo.ID).ToList())) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index 89ae92ec91..72c2ba708b 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Select { beatmap = value; - Details.Beatmap = value?.BeatmapInfo; + Details.BeatmapInfo = value?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index d59d76300a..6ace92370c 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -41,16 +41,16 @@ namespace osu.Game.Screens.Select [Resolved] private RulesetStore rulesets { get; set; } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; Scheduler.AddOnce(updateStatistics); } @@ -170,26 +170,26 @@ namespace osu.Game.Screens.Select private void updateStatistics() { - advanced.BeatmapInfo = Beatmap; - description.Text = Beatmap?.Version; - source.Text = Beatmap?.Metadata?.Source; - tags.Text = Beatmap?.Metadata?.Tags; + advanced.BeatmapInfo = BeatmapInfo; + description.Text = BeatmapInfo?.Version; + source.Text = BeatmapInfo?.Metadata?.Source; + tags.Text = BeatmapInfo?.Metadata?.Tags; // metrics may have been previously fetched - if (Beatmap?.BeatmapSet?.Metrics != null && Beatmap?.Metrics != null) + if (BeatmapInfo?.BeatmapSet?.Metrics != null && BeatmapInfo?.Metrics != null) { updateMetrics(); return; } // for now, let's early abort if an OnlineBeatmapID is not present (should have been populated at import time). - if (Beatmap?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) + if (BeatmapInfo?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) { updateMetrics(); return; } - var requestedBeatmap = Beatmap; + var requestedBeatmap = BeatmapInfo; var lookup = new GetBeatmapRequest(requestedBeatmap); @@ -197,11 +197,11 @@ namespace osu.Game.Screens.Select { Schedule(() => { - if (beatmap != requestedBeatmap) + if (beatmapInfo != requestedBeatmap) // the beatmap has been changed since we started the lookup. return; - var b = res.ToBeatmap(rulesets); + var b = res.ToBeatmapInfo(rulesets); if (requestedBeatmap.BeatmapSet == null) requestedBeatmap.BeatmapSet = b.BeatmapSet; @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Select { Schedule(() => { - if (beatmap != requestedBeatmap) + if (beatmapInfo != requestedBeatmap) // the beatmap has been changed since we started the lookup. return; @@ -232,12 +232,12 @@ namespace osu.Game.Screens.Select private void updateMetrics() { - var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; - var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false); + var hasRatings = beatmapInfo?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; + var hasRetriesFails = (beatmapInfo?.Metrics?.Retries?.Any() ?? false) || (beatmapInfo?.Metrics?.Fails?.Any() ?? false); if (hasRatings) { - ratings.Metrics = beatmap.BeatmapSet.Metrics; + ratings.Metrics = beatmapInfo.BeatmapSet.Metrics; ratings.FadeIn(transition_duration); } else @@ -249,7 +249,7 @@ namespace osu.Game.Screens.Select if (hasRetriesFails) { - failRetryGraph.Metrics = beatmap.Metrics; + failRetryGraph.Metrics = beatmapInfo.Metrics; failRetryContainer.FadeIn(transition_duration); } else diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 2fe7ff4562..5940911d4a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Select.Carousel private const float height = MAX_HEIGHT * 0.6f; - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private Sprite background; @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel public DrawableCarouselBeatmap(CarouselBeatmap panel) { - beatmap = panel.BeatmapInfo; + beatmapInfo = panel.BeatmapInfo; Item = panel; } @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.CentreLeft, Children = new Drawable[] { - new DifficultyIcon(beatmap, shouldShowTooltip: false) + new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) { Scale = new Vector2(1.8f), }, @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = beatmap.Version, + Text = beatmapInfo.Version, Font = OsuFont.GetFont(size: 20), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Select.Carousel }, new OsuSpriteText { - Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author.Username}", + Text = $"{(beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata).Author.Username}", Font = OsuFont.GetFont(italics: true), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Select.Carousel AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new TopLocalRank(beatmap) + new TopLocalRank(beatmapInfo) { Scale = new Vector2(0.8f), Size = new Vector2(40, 20) @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnClick(ClickEvent e) { if (Item.State.Value == CarouselItemState.Selected) - startRequested?.Invoke(beatmap); + startRequested?.Invoke(beatmapInfo); return base.OnClick(e); } @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmapInfo, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => { starCounter.Current = (float)(d.NewValue?.Stars ?? 0); @@ -233,13 +233,13 @@ namespace osu.Game.Screens.Select.Carousel List items = new List(); if (startRequested != null) - items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmap))); + items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmapInfo))); if (editRequested != null) - items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap))); + items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmapInfo))); - if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) - items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); + if (beatmapInfo.OnlineBeatmapID.HasValue && beatmapOverlay != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value))); if (collectionManager != null) { @@ -251,7 +251,7 @@ namespace osu.Game.Screens.Select.Carousel } if (hideRequested != null) - items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap))); + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmapInfo))); return items.ToArray(); } @@ -262,12 +262,12 @@ namespace osu.Game.Screens.Select.Carousel return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s => { if (s) - collection.Beatmaps.Add(beatmap); + collection.Beatmaps.Add(beatmapInfo); else - collection.Beatmaps.Remove(beatmap); + collection.Beatmaps.Remove(beatmapInfo); }) { - State = { Value = collection.Beatmaps.Contains(beatmap) } + State = { Value = collection.Beatmaps.Contains(beatmapInfo) } }; } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 3ad57c1cb0..f2485587d8 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel { public class TopLocalRank : UpdateableRank { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; [Resolved] private ScoreManager scores { get; set; } @@ -31,10 +31,10 @@ namespace osu.Game.Screens.Select.Carousel private IBindable> itemUpdated; private IBindable> itemRemoved; - public TopLocalRank(BeatmapInfo beatmap) + public TopLocalRank(BeatmapInfo beatmapInfo) : base(null) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; } [BackgroundDependencyLoader] @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select.Carousel { if (weakScore.NewValue.TryGetTarget(out var score)) { - if (score.BeatmapInfoID == beatmap.ID) + if (score.BeatmapInfoID == beatmapInfo.ID) fetchAndLoadTopScore(); } } @@ -79,10 +79,10 @@ namespace osu.Game.Screens.Select.Carousel private ScoreInfo fetchTopScore() { - if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null) + if (scores == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null) return null; - return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) + return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) .OrderByDescending(s => s.TotalScore) .FirstOrDefault(); } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 7820264505..2fdb41a1a1 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -25,17 +25,17 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private RulesetStore rulesets { get; set; } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (beatmap == value) + if (beatmapInfo == value) return; - beatmap = value; + beatmapInfo = value; Scores = null; UpdateScores(); @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (score.NewValue.TryGetTarget(out var scoreInfo)) { - if (Beatmap?.ID != scoreInfo.BeatmapInfoID) + if (BeatmapInfo?.ID != scoreInfo.BeatmapInfoID) return; } @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Select.Leaderboards loadCancellationSource?.Cancel(); loadCancellationSource = new CancellationTokenSource(); - if (Beatmap == null) + if (BeatmapInfo == null) { PlaceholderState = PlaceholderState.NoneSelected; return null; @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.Beatmap.ID == Beatmap.ID && s.Ruleset.ID == ruleset.Value.ID); + .QueryScores(s => !s.DeletePending && s.Beatmap.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { @@ -168,7 +168,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (Beatmap.OnlineBeatmapID == null || Beatmap?.Status <= BeatmapSetOnlineStatus.Pending) + if (BeatmapInfo.OnlineBeatmapID == null || BeatmapInfo?.Status <= BeatmapSetOnlineStatus.Pending) { PlaceholderState = PlaceholderState.Unavailable; return null; @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods); + var req = new GetScoresRequest(BeatmapInfo, ruleset.Value ?? BeatmapInfo.Ruleset, Scope, requestMods); req.Success += r => { diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 085ea372c0..1ae244281b 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -29,8 +29,8 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load() { - BeatmapInfo beatmap = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - Debug.Assert(beatmap != null); + BeatmapInfo beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); + Debug.Assert(beatmapInfo != null); BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index c87a4bbc54..b8b8e3e4bc 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Select { base.Beatmap = value; - Leaderboard.Beatmap = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; + Leaderboard.BeatmapInfo = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e4ab360765..6cafcb9d16 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -345,22 +345,22 @@ namespace osu.Game.Screens.Select /// protected abstract BeatmapDetailArea CreateBeatmapDetailArea(); - public void Edit(BeatmapInfo beatmap = null) + public void Edit(BeatmapInfo beatmapInfo = null) { if (!AllowEditing) throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce); this.Push(new EditorLoader()); } /// /// Call to make a selection and perform the default action for this SongSelect. /// - /// An optional beatmap to override the current carousel selection. + /// An optional beatmap to override the current carousel selection. /// An optional ruleset to override the current carousel selection. /// An optional custom action to perform instead of . - public void FinaliseSelection(BeatmapInfo beatmap = null, RulesetInfo ruleset = null, Action customStartAction = null) + public void FinaliseSelection(BeatmapInfo beatmapInfo = null, RulesetInfo ruleset = null, Action customStartAction = null) { // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. if (!Carousel.BeatmapSetsLoaded) @@ -379,8 +379,8 @@ namespace osu.Game.Screens.Select // this could happen via a user interaction while the carousel is still in a loading state. if (Carousel.SelectedBeatmapInfo == null) return; - if (beatmap != null) - Carousel.SelectBeatmap(beatmap); + if (beatmapInfo != null) + Carousel.SelectBeatmap(beatmapInfo); if (selectionChangedDebounce?.Completed == false) { @@ -435,18 +435,18 @@ namespace osu.Game.Screens.Select } // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. - private BeatmapInfo beatmapNoDebounce; + private BeatmapInfo beatmapInfoNoDebounce; private RulesetInfo rulesetNoDebounce; - private void updateSelectedBeatmap(BeatmapInfo beatmap) + private void updateSelectedBeatmap(BeatmapInfo beatmapInfo) { - if (beatmap == null && beatmapNoDebounce == null) + if (beatmapInfo == null && beatmapInfoNoDebounce == null) return; - if (beatmap?.Equals(beatmapNoDebounce) == true) + if (beatmapInfo?.Equals(beatmapInfoNoDebounce) == true) return; - beatmapNoDebounce = beatmap; + beatmapInfoNoDebounce = beatmapInfo; performUpdateSelected(); } @@ -467,12 +467,12 @@ namespace osu.Game.Screens.Select /// private void performUpdateSelected() { - var beatmap = beatmapNoDebounce; + var beatmap = beatmapInfoNoDebounce; var ruleset = rulesetNoDebounce; selectionChangedDebounce?.Cancel(); - if (beatmapNoDebounce == null) + if (beatmapInfoNoDebounce == null) run(); else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); @@ -803,11 +803,11 @@ namespace osu.Game.Screens.Select dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } - private void clearScores(BeatmapInfo beatmap) + private void clearScores(BeatmapInfo beatmapInfo) { - if (beatmap == null || beatmap.ID <= 0) return; + if (beatmapInfo == null || beatmapInfo.ID <= 0) return; - dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap, () => + dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () => // schedule done here rather than inside the dialog as the dialog may fade out and never callback. Schedule(() => BeatmapDetails.Refresh()))); } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index e6ddeba316..2093182dcc 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -20,8 +20,8 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources) + : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; @@ -76,7 +76,7 @@ namespace osu.Game.Skinning return base.GetSample(sampleInfo); } - private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => - new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata?.AuthorString }; + private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => + new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.AuthorString }; } } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 27162b1d66..5c522058d9 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -111,8 +111,8 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; - public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, new ResourceStore(), null) + public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) + : base(beatmapInfo, new ResourceStore(), null) { if (hasColours) { diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 75aa4866ff..91bcb37fcc 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -27,13 +27,13 @@ namespace osu.Game.Users public abstract class InGame : UserActivity { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public RulesetInfo Ruleset { get; } - protected InGame(BeatmapInfo info, RulesetInfo ruleset) + protected InGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) { - Beatmap = info; + BeatmapInfo = beatmapInfo; Ruleset = ruleset; } @@ -42,8 +42,8 @@ namespace osu.Game.Users public class InMultiplayerGame : InGame { - public InMultiplayerGame(BeatmapInfo beatmap, RulesetInfo ruleset) - : base(beatmap, ruleset) + public InMultiplayerGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } @@ -52,27 +52,27 @@ namespace osu.Game.Users public class InPlaylistGame : InGame { - public InPlaylistGame(BeatmapInfo beatmap, RulesetInfo ruleset) - : base(beatmap, ruleset) + public InPlaylistGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } } public class InSoloGame : InGame { - public InSoloGame(BeatmapInfo info, RulesetInfo ruleset) - : base(info, ruleset) + public InSoloGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } } public class Editing : UserActivity { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public Editing(BeatmapInfo info) { - Beatmap = info; + BeatmapInfo = info; } public override string Status => @"Editing a beatmap"; From 281a3a0cea270b1531cc13ee1a9ae6cb559788c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Oct 2021 18:40:41 +0200 Subject: [PATCH 078/170] Add test case for legacy loop count behaviour --- .../Formats/LegacyStoryboardDecoderTest.cs | 27 +++++++++++++++++++ osu.Game.Tests/Resources/loop-count.osb | 15 +++++++++++ 2 files changed, 42 insertions(+) create mode 100644 osu.Game.Tests/Resources/loop-count.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index bcde899789..560e2ef894 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType); } } + + [Test] + public void TestDecodeLoopCount() + { + // all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms). + const double loop_duration = 2000; + + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("loop-count.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + + // stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative. + StoryboardSprite zeroTimes = background.Elements.OfType().Single(s => s.Path == "zero-times.png"); + Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration)); + + StoryboardSprite oneTime = background.Elements.OfType().Single(s => s.Path == "one-time.png"); + Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration)); + + StoryboardSprite manyTimes = background.Elements.OfType().Single(s => s.Path == "many-times.png"); + Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration)); + } + } } } diff --git a/osu.Game.Tests/Resources/loop-count.osb b/osu.Game.Tests/Resources/loop-count.osb new file mode 100644 index 0000000000..ec75e85ef1 --- /dev/null +++ b/osu.Game.Tests/Resources/loop-count.osb @@ -0,0 +1,15 @@ +osu file format v14 + +[Events] +Sprite,Background,TopCentre,"zero-times.png",320,240 + L,1000,0 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"one-time.png",320,240 + L,4000,1 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"many-times.png",320,240 + L,9000,40 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 From 3e403cfe031604792798898218927691d3c2fe21 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 2 Oct 2021 19:16:46 +0200 Subject: [PATCH 079/170] Add comment explaining the purpose of the empty `FilterTerms` --- .../Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 806390c0ec..2cc2857e9b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -76,6 +76,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Content.CornerRadius = 5; } + // Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible. public override IEnumerable FilterTerms => Enumerable.Empty(); } } From f05cb6bb5b677255517212369ed4292d1d4c48e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 13:53:26 +0200 Subject: [PATCH 080/170] Add test case covering reset section button hiding --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 168d9fafcf..1effe52608 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Input; @@ -230,6 +231,22 @@ namespace osu.Game.Tests.Visual.Settings AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding); } + [Test] + public void TestFilteringHidesResetSectionButtons() + { + SearchTextBox searchTextBox = null; + + AddStep("add any search term", () => + { + searchTextBox = panel.ChildrenOfType().Single(); + searchTextBox.Current.Value = "chat"; + }); + AddUntilStep("all reset section bindings buttons hidden", () => panel.ChildrenOfType().All(button => button.Alpha == 0)); + + AddStep("clear search term", () => searchTextBox.Current.Value = string.Empty); + AddUntilStep("all reset section bindings buttons shown", () => panel.ChildrenOfType().All(button => button.Alpha == 1)); + } + private void checkBinding(string name, string keyName) { AddAssert($"Check {name} is bound to {keyName}", () => From 4f00a9e165af5d7b8a321e2bf72fe81144331482 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Oct 2021 22:32:46 +0900 Subject: [PATCH 081/170] Adjust max runtime for diffcalc runs --- .github/workflows/diffcalc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index bc2626d3d6..9e11ab6663 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -53,6 +53,7 @@ jobs: diffcalc: name: Run runs-on: self-hosted + timeout-minutes: 1440 if: needs.metadata.outputs.continue == 'yes' needs: metadata strategy: From 07c11953cddbf66b5b84c996449f6af82821b1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 15:39:54 +0200 Subject: [PATCH 082/170] Modify special test skin to visually cover regression --- .../special-skin/hitcircleoverlay@2x.png | Bin 247101 -> 26595 bytes .../Resources/special-skin/skin.ini | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png index a9b2d95d882b99a53aeba501919b5e5f1600bf26..8e50cd033596fb3daacd173247d6779168cc8f99 100755 GIT binary patch literal 26595 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

yftJ!Y0GsdcN%;MEM+5u>2KjHnOqx_AeVsl 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 literal 247101 zcmb^4Ys|j+b{F(1^-wC*dWfKvbR3L8;MsGX4`VlQ9ZMT_*^~k;V&FcuWA~mJW@fj$ z4W><05G4>WM0`Pz7>yO>of6WRLIfmYh@ymGOfbIjg$a!!ZzLW*>wcd9Zee~G+|Tn| zo3i&D{?~P1ztdXZ^mw|@S2{F?c@-+Jro{N>lZ^$q`! z|M{&S|HoH<)4o4VUmVUK9xrcy_qUI4|I)ngyI=O!Tfg)V{Gs3fQ{VYtees|C-9Pm$ z%b)vWzv&zP*Z<|CU-Ij}_@%${XMXQ@{7e7V+u!@gr$6|geaG~Nj{o=vf8xjf?{EHn zAN-=<`al1xKj)Wx`=1&9kDvQHzy8nt#J@KE>p$-w{I~!5|Mf@ydjHSq#KL4%1xBt^W@8jS0 zb-(r({mVc2eLwv@fAK%LeBWRG8~?)a&;R~^_}}||f2sUa-}<|M&v*V4 zf91db^}paR{PfrV_WWDF)c&GB^_%|l|Mb87TR-;ye*Pc(tAF$I!5{thZ}`=};`6`Y z`-ZRnvCsY3Z~TS-;=l91AOGu*{^@V}>BEnH&-o|YkN)J>{Ud*J`7eIUzwv+mQ{VPY zKlaw2{`0@?|M{E0;aB|yGx>8r`ZeG8$y;xI)1Qt1|8;-pmw(f@y!G`z`a9nLp6~ti zd%y5&-#dNy<w8W=`h%zb{LZiXm2ZFBSKsr3FJC@Ay#3WL zf9Zoy-uvq3-?_c_J%1hl_}zEjzWtF;fAI6~JTB<>e&LI6Pal4KdAt5x^KiL>yd7^m96$WZr=Nevmp%^R%O8F1wI6))G)*S#-LD=#dN==E7DKxI!Sm^Bf9|8N zeEg-`kDpKPUcPks^5uh1Kk;Ytul?DNe&EATKm6neKHPZq*Z%e2@dKvz3t#x!ZzR9v z%U}N5Pki#}@lbXk7LXa3?hqxt01@4o!ZeBSZ6 zKmN6cKYjnY@ofB?zhh9BufD(WN8{go?T>!+-t_V1@acyif6s>>e(BBL`Qn>de0!SS z{-wY9{SVF`{_rQiEJpD3$1n5Y)63`IiEZ?dwGT!1J$d$CR=-y@zdS47%d*Em`h^eA z?_Yo9Yk#zlKU(^u{d-yeUXlHB|NXT;Y7d_epC10cfB1ZQ@A~1#Up`oY_rHAj{>!@` zeenJ7y#2LxeQ)~kOCNshhkaf7v;X;buyh}Pczyp%{>9&I@or!Hg)hAK9Upx1>EVOZ zNClp)by-roLRSP5C%l&(9y8z7kt9 zo_{=@=MPWb@iSl7wN=@k^8BI8y5qy8sM?3)p{X9KLwRbi*{RKo^P8{vbsxXaKpejG zv)>wDb&jt(cX?Mfmxoi|W)Ih_>K?kbygW2{-(IhIpI@@mJ8#FHc+YEm(jiXa=ikYn z{^9q=-u%*69{*wAWd8GR9{>IH@4xTw^Y4G=U;oAr-nSIRJ8#FXdYt{=z`&X1rmoBS z>7Re`{jXlW^u6=@X6KXJhZJ@5v;Woi{aycx4f>f$douxprTN58{p86)jfd}l`l~O! z|0}bs$cwx$%dTyQVra{Cn3t@n#zdD1wk-YFP4zIf zc|OjmCzhe9iaH;1;~B;xYqPOf%YGc@uI!4$6GdKT<&-%b5>%!~WM zA)ia%xh8&N=*qgvhM3W&>5IDVy`WyEX&Kvm>Jv|lPNSDwjB2fxX~~wd&7F{bs;hcf zQcq0zu=L*CEagqMBHS!a?fud93oR8S~Wa`l6~UvbmTG=Y3lH*^bJqzRcQkDN?(A zUB)n0^*rU(z}iiHQPwQw)D%tGB}UE%EOW_b)&8_EtIMpZ=PGa4`0*uiKJv_N&zi=$ z7}h467`t|==ej7nd2y~jyGwFbdY;yzs@lG8hmu7s%VntAa%l6cO?_29jmuK-a&y%$ zE&EuH*}6KA{z;!F>{VVg?NZnN^i)rC*M?KDSH{*4GQX*D6x}klQ&x@B%7}Jeo7Z#J zteuh7bz(j8g6%2FWv$1$XAg^cwxzn!?{u>RlNfnf5AD#-w(exmc6zp~%`jJtqT?~9 zo*2WŜIHp{xcoVtR=YO7*x6KkF~ZPqMVHLqPPW;c}C(9{dd()LBuC7+m`g{B{d z+U}eBZXVjcE2nYrQR|wpKDn89y328%ymk}^*neZ)7VEgo!&)WA(JonU1hr$Hb=lH3 zZ8x-IGfq`jjB~;=05Ya&Eb@xeTg#=H=dzl+FqHnunp*Q=sH(;u3M=Z<7?Z-OD*I*Z zr=d!%xnWrD!7B9D(9y2uJe+$w4CU0MMm{wQFPSmz&14ODwyt9!Mw_Yx1rj6YWO|On zUXR7-YI~y|pbp+@b$k;~%#25wEp^*0&abmqEig*Oz}0qS!ouYXCz~x<-VBXZtg;H` z$(Xl3Zydw~F3FcZYv!(AvvsTrC#xF9m4~Q+o7x6Td==MPkA}W3EFEm+<$P{e)J3~m z_|HBO!2y?4)g6bM6~mC1RW^o4%kqkyNIlUGeN#>ISkL)1tjwdS@S*m9S7wQE6xn+7 z1y$@l{=(mM&J(~e0x~K8Q8+w9KUDR~3@}4eHI^kSId)@THFaXmnal=Yt8ApE9Eye^ z;{8Ua5!T4Z)K`^jy_VTpini-^1+n}{1_~H{Ue>9(syOE+FA8S2TNYr8lPh|-r!P3| z#BMJbggB&g)77I9d88?(ys-7FYE2=4qFKwC37In65Y(vaoFnPme5g7gC^hodp|A>$ z1-l9J)VWzKd!w0?1|+G#9`qFHZYg1KAR>J;m-B?HaGx@}7uq$uba5AyE#;MM#GWS;&B;h}bsj@Amu`!x; zEu43lV+7*@W;I3XH>SD;k1MY%I#XKKEN>aEWSm-4kvLIPJNSr#yPwR0)9n{)_}h2R zQ_2q(bDgsdeYuWPUspC&-`g@UT*JwyuxK$iTtPjQVAb66eVJ{T4SBz|D>Itf?aQ!E zYnAsSZ?Vo6yB2q_b}VHxr{a%d1?U;JVqp%ZnL!zP+oM}el&~m)$e=tn#QyHvVshxY z!dVbv(&j$aGci}IEj#8WOH{CeB;cPzSOF!I|4W?@BSO z_O3W!nJN~l%Q7Apo&zu99P$rC$|jasme-=1B_Fx+9wN`@zH9-jry>4{t#qhQ!`?SZqZ&c1)Zo0SAJh zvhbio$9LqbC)!d}i9<+^qi!=6huP@*W`SXPf6my7vMRaA1TGP06uCes!Yx6^P?n76 zGz+?OuWA#vqzt1oc_I*BZ);TZn2!MB!dmj&iCxl(Az+O)Hsp6Pt9&WM4-mm04{@F z-qa292K;Y8)GoCXxz*BQG|W!LLuH~Bn|7E*MXew-F><(;Ijq+???qE=L@jQ~goX@i zk&1-NvA_U0B0}bM7IqfBaH17S7obL-+Ph9qF8)~svK>3ObXAUMT47B*MV`P6O!(T2 zg#bkOm6e%I!=aU+5U@0+VgvMr{Wn%EqXq^zF4Z_Nc{LIcPnjC| zugH?62CIkU6CCR>iYOpO&|#i^jZ-clk}67BI1nSR$1wuhhSgcmY$U_NGb}0V z<9%~(TjLZ^6SFg%+w~y1xXPOndw1feo<<;XHq4&ayQyQ(p(&dD%RP}$NqOil?Q&)t~g8~EE4b64?wunr_L48bFdZN*d8 z-^mYu_}LXi1~PxX1tl2l0_;=HVAe4DMgvJaQPrLz;IQ|n+JHqvNSlBxh;hkW`RrW#Z!rK zwAi$WOkt^N%yT%X8*Rgu7s`3X#1poiP{@XgPzd(6J=or*1G*ru1imse1xBICZPgYF zFz`U^qP>{qaDU>QqoHb`ldUPCYsVJ4uZO&aEaAT?Vd1JV*06?*E8LyKoeKgR`?*|k zqiyP|CcZY{4=)q>4>_GkRR|DT3z?_(m7unR+MH8!1;no`L%!q{+hw9ty99JzbHi*F zTN3wGHWO?N5cQU|OwF#n!3>&QzmOH^!D%vGPE={_yVROHde$o(A2MyiRYRlWRQUTw z44K-AnEYB4Z1B0o1ILk3KP17`1W0kaVL{g`gQ2bSZ&~ z@#S^fge?ILTI3DmRAXgqrWUK6g$ofz;%+~?+iPf5sM6G9Ava?~wrtxV+2G6$$&rhh zE!9*F98!+DzX^F2H&4}@Ie-lPqfy3`Z)G+2ZI7B$Ob z01vEZM0zYq%Aeb|5F{&t29X_G)}oZ8KoIgzMW3>Xb+tyW0QW3?+f?8H4sR76R})e$ zOL(;!Ik<2$nD%liaSMzUCxCE{XthuHC=bjF`p{Vw6I#t_nGy3yQAALgC$LZa1Qd=g zS&JBub;M(Ue$WPloSLgzu0%csxorWPb?7F>cPP2X6`h<~bNG2#*O5zxf>=}u1x+0E@I_lM{s`~^QoB~*9k%1I9YSz3<`lxu@ShT<_(4sb4uoH`%2oA`gg4)SfjM7&k9cm?u< zQBBR2fpZfk$({#s`xTVKXbHR~S)tS};lS{NSOqa{la+$nhCdL;;Bu-l5f5V78J)pW z%XKpIc4W>&qs|c`D5Jz&p&U!}G)O71^}i#ky=tUfN|~BE;hmcd0&${*Ljzzv6%F>P zo29;P$TBewbY>@sRRZdz+@lR5{>a+}!3vtC&WEL|rPFzf%AuD zvW7#XOM%lYHM>Y@*@7AK0lnBEwi74C2^NuJNwLnJ;Ql1!ku1_>Sq_s12QIUQ38@l$ zw<(>CNEM7_+pfKR%*jd5hkih8pL)Xno^yZ_JQQt-TSh5+21PO*1(=%Mig8)wmcc*5Nu>~w? za^!iDg{g4>M#jBFXp6bAqp~ITqGX}>R0-P-B05q?OQwdMn0S<1!WB&6PK#We`bxy$EaVKx#g2qZu(YcHWPI_op&7V;p6IPd2aPDCc) zS|w_2MSn4&nuH!TIAk~S)`{P+F~mX>JM9Uu32hQ|J-S=gJ(ar)mBUK&113x&zzoEd z{u^XJI6FHf^&2L#9Yw+9C`>}Q*%dm0`uT}e9;boWB%U#p0~r-A2caxH!rbOEO;u_q z;(HLrY>hk#L5BP;rU-E^Xozk~*b zQxAR#qGKOtd7yfM01|t*Ef5Q}GaV~{!;+1Z{*jKcQ@FTDAIxj_MxFa?mqTf)^2NMM81Z?tRy zD*})TUL;+uXE41~gxG}CT$x)uhv5oLA!76*!%`rPxM!Ou^&2P^B)XM z%YCRLpNLpm)ScG{(vVE`R%E`IcDbGoj zVMEEA3JRG}`)}p_Soj{ujVPcgqFv}vN;RVh@(HGd!9vN@Z(w8k&C6g6E1V=y$l?Oh|p%{i6(>~sWq2qroW(-5IQ+W6lYXXLN8?X+{_7&BT})0 z#-LO9j)Kg>(c=Iyqg~`96XO6yBBmt;5P3DLs9y5CL(Vbz4sq&|I#)})1Wv_VCo5hz$C#1kp-rvUnNgKvN#}-m`Ob`2>w8&+_%vq ziUx&I&g_c3T~hfdrjtI`|?GvL4IlO0XW zRZuCFBnE3p&Ph~7-A&#NML;zv&lw(5curJeks)G)StO_#3$PUNp7@QM{}RsPxFB_G zWG{`(UWnC^E~%Z^4V2N5(!|G9SvE!n47?zV5Z$F1;VxKBg5MVLjcdV#LW)6%Fk4Z0 zk@_mbL?bh%1R}DvGwnR3Fn}e%Piee_a8lsmpcvdR*COuP*nk-DB*oTJ(*QnF2M8H| zl?!zU&m#P$`T~X%UnPf(dc#*{3IXJTK~Q*unn=Kav`#(&gRwSfuW(Ep4m}4E0DwbH zp0c7Ov4tdrln04WsIF}QSRCye$SJi;kR-G#1YodeKzFM)$vwzM=OXmfsWl1!K#2^^ z5hW@mL@)sTgf=QMqls}ag2v%I!47t>vLvh@iGo-P%1l{Nf(ltTu2^k~SXI)G@q`k6 zAH`PcM6vq8UpTzjbBwS+fx{F^bWCGPS)+;t*NUsMT}P-DfGK73$d=3;O-N!KaGG?9 z{15c5_COK<;xEir&WNR$RDp8XH=AjyJ36%<$`lRSC~;p$xDEd^T}#SG$u1YgyU5C_}5@4MJ=%vc@v5^d2Te&XJ=QKAABMfG-S(m z7nyqG+~fa<4ibA;J~w2KabC><*sA^kkT=&h93mw38y(S{k}kPw!RpYL;ZGuQsp2B3 z@j_pyHgj^jjB3E2lK$r%D5kO0MM|Yd8WlF3z?T9*EFyh?Fo3Uf+H;za#8(CAjxFUZ zhP)r68ZJZt*MV6nZ-D;?_ey0^5Wjf9PbgB6CJtp8UMBGyR3(w|AeF?G%g1=YNhE|J zBXlfNPawq#p$6>&X+lW$R!|l|k=2^21bdD+w0^i$8tI^ASVr5)?1lUTj!pcA)RwOi zm<2CQu(?fq#KGj+{0a z&9F-{u)(SpRA3~Z@Ht2VG#soW62TASgvARqMle}ofoHRnH(;B5jj*eZ z9Av~d%EIW-``}PgIWNVTNDI{;jsyc0QHPp=qGIKdw|QdC2L=(F7IlY>Iu_->b~*Mn z(8HAc4ZsZ`$slj|D_9i*JW+(4FLkxL$j^>kZRjlqmN~X;7+N_BC6RYsj6cg!OSaa90cnr^~U?PmY6w8A9<(?}~?1x)!PtCDznA zW;rnq!8{L3F&FiEAOKQ{*aV0YsM7b#!Rm*^dKPdp*RiUpO0 z5vr2nNJ*0U(a)-pO0N}ErWXnE+PY?}b0*L4^s+0gJoG8V> zJo^*kUzSJxSZJ2$uvW;6OH6&0^DHgFK4`^aqJbuW4pRf`E2K+2!7)kwvvDxq;Gig@ zly0Vn8=R4p!qnRl?I;Ow5~UXMMp8TJzR)_U=1KjAOfDLf%>rBmE{alupHRw$6iz8? zBvx1upio@y$U(6}NxceK2BAXAJ3DQ{Sk+)54uyC`o}=v|ixO(WA$87?@^%C~CBo+1 zO#v?+T&euIx`V_x;;>>Z;!jv_PF(ebYDzr#sDdfAuNd@^_a=7;Ih%t%P)guo6+q$a zQ|p1LoTUBnDiS|rTSJCFCvd}BE6Yh)QDt42->6CGghKdsF>tWG)`$)%fyg2P(1wC( zWSm895|UBvOeCWYM4*=#Ira>t+eX!EA=islBC8cX%VcrD$tS>(&@#q}0ANGsM>~S) zCAMLNQok`Uhd6eKgg2&m;@`PjA{aquD%&TTApoDS!NCNJqC#VcKY_Agr4(9n)G&#` z_ORp~nYzMoNi0~4*;2}q_^OFE!uwQi*-TMdE*RDzV3A`Iho1u?y6$I$V}3f&;mTd2VyD!ORh&f zTH>ql>lTDSQiRF3J6)s=yt-pRbCNny`ZplM^fZWi`Y1g|humy5g^I=e>~FC7U^w_g zsYY*z_fs{9-B3lUGBm~9S>r)%Qxq3MtjrF67t*KwbpGXWQ@;UrN%zV5@do?|Ndreh z4nQ-VWLP3RRgn`bVj+0Ls8ob1Wy7&+seBaU5tTk6S29VIy9hOAi)_$Dsg6i|6|jmK zq{tMlL~445Q^28)-EQT)6HnlG7(*On$Yp4ERpiQa(#r6rl5vSANX8qr%~8$=wn;)W zia}Q2gj*>kAiGCTM29O`He{X5^DV_j0fUJ4!KvSXh1qB9zkGyX!otxuknT*JT1t^( zSInyhc_Y%KV!j^5 z*{Q@_SzepGl~@c0QFX12EUDdO^#9zz3ZiKVzaXI#nIHBKB1VkR zvdF^tC9Ps2iNnRgHmQ*-Qi`e%%Cp7Wr?q%iB*6+Lw+|L4$nzb zL;(#f#91evU~6x?oyZi>11HanVucrAG(*2b1IOGdOpqXR#6YaXR}pZkV+h&jC{0ZRAyyL*vyrl7a{`4&>l8|O z280e1)C)l|QHw+35YUvTek0UxAS?SL;*hS7l6ygp_=Cwm8*zju%0j( z0VP0?(jnu=SVJX`im|jS2^qb(JZLE-2uXV4t7^VW6|M#{vV53yVv~>w$KM84OFSX+ zP@}-Y>6szVAgr;4RS1By+9)M_)FhWH_sf$Lz^jl}u}nJ%W8wQW3rX1BFbVLhFeS>B zi4=p%ql7Wfl$@#f9HhSGMo6cmYH(rY966`ZL~|5)^$DL`y_wz=jYNc@)hMZ2W{}h$c58NHnCdS>*w1+Wng>)qT2bK#2T!^zJ)`MH;tAj@+6R~$Tp&e? zlYv8l#@HW-CG`Zgu81ixOsGZ}9x)mS5wdC$$HWs9GW>S5&D2yUsw@QRkt-SbIBA5` z6RO6dt0RaIZEAo%QhH(tb{#B9SO&%?Lx|b1A_(Fl4N?fgQH&9Zp(GQ(5ryL_O9&MB ze`uU=iM)#RS=9}!m3jiJ?wCjM70vF79*U5a5LovmE-UebAuw?Pu|z7c4;7pFlCWulR4#HQWN~>+c#YmuM4~39V*IvmJF*NDpfu zg3>)eJXA46s&a;vG9@*;tZ3xgrPrfN6Wr|x<0{w&68cmRA^ST50THJRuZYNY4G{2HJ6HC&9Vu|8qL}S9jkvocNEF)rD{|Z|yUcsWu$#K=mC!*rPu=#T`I_x>H z$EZt>Dy2weE%l%Zfdx!&!eA?G9NaDj!S|}IPV5q#prpRYC>r&FG%{on*oxGpWRnOW zdV?T@Wd<=C<)RG9{K`t9y@YG2dd}#1L_!~JTYxK6r|f%lU%;8s>m_HG&Zm_~ahAs)Fniwas6C)IlMo!E*v4~ld!4qu~Srj%_s>_Wh zkQY>nAsC?aq87mxaZge8Yz-6Rh{h%Y6kl#Jbvg?nsy!0bpH6)OEqNssBvJ^mz>CvE z;pu68xDP}?N*l$a3Vj95(OiqDk6l!z(c?)$E!p10S5ae-xQ9pA0YJQrLr7Sgp0cU|djLx5--y|88GQ2fM3Zg$T4$O)!Vj5@KeG)R@rC?jR zhSCm!MglV+3w(j#LrfFZQRE^-uO#SdO`OZrnopuG$t&VK2143Tr!h%C;|l6Jg|DPI z{D%|~BSwy64>D_@2_Pt4owPmy(veX5Wk1qCe0Vg5qznwzMQY8NacdiCbN!HPeGV5b zhzzGnk(No!)vQ2t9OSFLuZZHygiQiz`L+-+3 z$ku6pBEXkiqxxlymNv11!T}|EQWG}2&LyxzG#`+-lkbvRO|XN87u}7cNY64@POVA# zH2_=MR#chzDj}@8AfDB_2dm@-dM+S#I4d<@sVBfx7L7pKR*fd+K`PPPt-5XS=!qwS zD26|o4-^xhLHZz)4sk!ewo1)aG!qMgBsQuHx9D6%5RsrtLTEen8xFd3bLik`oUx1g zKXAS%!dt`e#1pjCECJz)7MVhO$per9a;H4=yQwvI#-m!vIg!o@Y%FLoPY4rdJ;gH! zRVW~&qP$#v4GS+@ji@I7K&z#qBBoGH)UBDf{p*PMpivJt3Uo4s6n_=|8+9D{4#y4! zC07V!a-q5jC1W*C0;&xm7>XTdP!UDz%n@a7nx>?BY}pu0r=$szIXX`2FQHlyeaD6w zd1?ir1=J%ePOydIL(l^lQ6v{Vgi=aCwVr4}7%Q#~pd=^F6ljC5SnKEzm{`H+|4RuC zNQbPFYZG#U{j3QMXsT-+;ziUPVjYcMlr3Ygkn{UphqaU0pQVEA?#!s;F z2)%H3ss299CZd3@LWgb%zGzVtz+HbBy-BEf662r^C4!3fRH0S2F46nMNsK@W@Jl@r zD<*slAq`tiU_yqYnUa)IWU3NRgcl?A#`#eMi_avDP=)p+4nT!p>Iq(qhBxZlJN&b`Q6`@Urqr%9wb4JkMpGuS^IE0HRo$r9I|t& zBxqy4kPvx$ZXOO*c6u6PRvjKr*Y0${qB)%M%f&jLuJK)$2UzE1(z;7_ zX}s`s|9lmvraWKshpav}@m)g@SoHVLS93b?ht~&6;KSn>583tf zZ~|Mp3m;x~_kUM&K3yef9}azfdzaA`hr`3g?z&v^b9Fi0zwbN!G+QU9J{hh|`^O&E!tGpbV1NMY-y!&0JOLs0$#lxY#WRL53 zI6Fg^?s7V0)s?wO0LqPhf6lM6CXQm$o#S0Ci%}mR`itf4vSV>5?w|WhbIf9|UeDQW zuhtKTEI&RVKu)J?(OZH9eY5dhHhI&sG|uB~?(MV}pT~;{(?aawgu{NEum0SWKJ#!e zSY+wxa_`)$E6Pr1X7F^5JMY4-9*;U87^LwPtGSz-If zI`SPyuGa@0nsayU&*%HcSR4`QVFvpfX7aA?3UZ2Uk;9$YOi{EO^HrS6=5WIk%?;vxo0E*H3uWZpa^wN3f(k7ia(WUb`B| zB=q4AQS$frU5Df0V)N(K>3F?#hg5@-jky<7*UkA5^L2|mUg6UU@aB33c3bW{_B|+Z zh40UGb?&+|k9R)YJx1-oPjGq6{S)?maST&5$M$e(?RND*PwV(n8!dy|Kuf#eucA9% zPgkIF2cK4e_vbU{rJU&1XQ)42hyn|DwjRUYht0{)wp)Lyi-VqXmsCl`#{N1erjm7g zs4mCAtFa2QOk#!8wQ9~6lCryTbeIW#t$Jv#^$iz_VcG-iD-`MjPBDsi?=O+4AWxKC zyooy?{`R5Ij=t-fUv%xc|6Rw-yms(!fP>t_;!qG23LymS;A5OdMugm!)$HO7MKJF;4RdtB-*q*yv+>yi4P_VEa0T`_W zs{3uuwQr7H3!A(ilM_gF^--{%)hx4hd<9U1&r+lHhG<0^{!KMtj&^=$OrW= zwf)sJE;zmlBd_h@P~43tz^?N#H`xch+GC8bi20PQwWk)(w8J{elb)|-iI8}G{78V- zwU>(NdVQ=w4ts5mC6xMln^Rh^YhQw@)H#p)>&3Bx#zB|rdSOUU*EjKFRrpLdOP|HN z-Su>L%oh!AFeIGC9`8Cer-qH*tUyLJgBcNQRGL2=9A6A@gQ~1U2(3@^5%E*Cgq{du2TDZ z_@VM3t~&|x_LzGy$3g0NmvUpruc>*?5*r(MRVpWs>krrd*%CQP_g$9xz6C}VQ2B?$MJ zFA9C;O9E|&xhJ@cTU1XER|KKNVdZ4j_|+b(a}X)XvfUkTV_q&tSbCrDQp86%y9XvG z_&HF4Nzh7rJlYAXDtwn=L1?m_^t$2uZ14}ErlF>Q<| zTS%vHhJ89rd3J5@&WlzOQ~5D+(yu?;@veh5Lwn>EszTbI=ppvRjbD95UKGhvo)nVq zGR9K+;9!3p%Kj;LB4ZT!4x8@MAXS>X^%J7(LPX{O?lAXW#76I9a}WpJ885d3pQF`_ zsJ7*|Rn9){)fejls`emR9Qf^#u>GgN;_lccxvs z`Nuarz)Lx+TFyN;Ibl~{{Vqv_LoQvo%e$Jh^1kpLZ*+Vw_8r7Qr)wCj9c(n&@6e)Vvcqqu31UX%NE9SIfZ@y2z$dL3(WDuUcy<_qUu9E3tU_}bdL6h7Q| zrdN*iorRr*MCKYldx|epVlGUkH=d)^y)E2J0B{M zUG^7t0w~|G{?KiaE+ z`7L%oR+JR5kAF%Nrc&}=exil~PZA{ieAiiqz1s%^9E1G|{pt?*i=fKIFZ=BK6FqxR zpR>nzo#NI%@#8Ma8x~dgX^$-HmWRvf-2B|))FS&@V4pFT0x?Di5M#Zb5_JN7;u2;={e^QUL&tApe z6UWm}=z9?Jb&k=N4^*wHl#dZA-S5-Rr=T7U@AX_7NuYPpukNw$)q}v&v_8^(G52(7 z*D(L;nyN!5C4ai#7G6TtAvLticS*co`qKmL>5bzP&8W1(uVSC{@}rAQlpb#E)yq-# zmqfJ87@iw8f%hHH;vjW#JszvW-8nGD_D8KNng`Orm@og;MM!hVlzo)f>gxOM^&Q@& zGJ#Z154+oWq!YasAXIT|$fXJj*1LJkCOe+05-x=0-sIB+ln>o0!-5o0+4*ihy2;M1 z9KQ4TdS1>Qe{NMd-thfv>*h2fP#{mZx5s?BpVPvtPkX&yPTLg)u?mKcr}jvul^@qa@g1+%uey`m&Xxn)LH`vUq|S;8C2?*xIHArf=b$N_D-|J)KmKamo683cy z%+q9@9@g&eJt3E-AY>POkCxNvDZXomF`|Z!p#U)oPtElnXB+icU-rqJD4KKQpZc^tzANgHL%IrMK4GR8_dvi-&;!V# z?ebk3{RSns2VT9jCqh1TC$(gsJ@E=HMteE*QA@bb7^AfeY0eHYa(mG!A)fy_?e8)3>U7$x@-+QzcA5KIww7kgUVZ&{9pkbLMd`1v-D+uv z^{#M+Ua$XByIdkwcN!xapz3zXi>e{N>Nt1vX|J7oiOibnbQeBazd@X{daE7wM3+T7 z8s$Da*upM5_wlw}zAG;;MSa{w1MczwNN%6Jy9~fZ3#45zIm)-B_`T@?UcJBM_^?*{ z>Y18o_0`7ZhiT}b!&uKoan-RA4{W9((- z2sw7>H>57KI~M0%YRWXOJkk2x?ANQuc4 z?Nj6FrYd)aL{ZcZXQ)5veJg(4V}F6sw+<=0d{=ZcWxrsP8|VHC-bBTbYTeKl?0_*9 zE8wFeMn1mj$M@3O=I)^To;ti;*71O|y6SJT%NVt6ZD`5%SjWSm)x1@_{rVay1+?w; z7`)sL>qso8R5kiED?q(*wJ+||+@WB!q`hIxSKEoBYuq$;-evBOCywZ`2g0kri}Eh! ze-|uw%8Sdv8QLT6I2DJR&V7%)dLqED^z7Or?l?hAeH4H1V53j^i1y}d2d-9~h<4=s z&<4{HKJ#^^YL;kcC*IT$zqB`er!157ai8zPepTAG z?lMM7pHr`Yp)>Sa4W+(giklyMS-e@tm&R7euXEhKz6UORde<&JQ(bWBx;!>|2fNy9 zspQ@()xIvkc*FoN&Of?#qRF3XDPKSLmrK_~AKlmc4&_5`+fWm`%ex2>NYi$CSCQ#J zb&!lJZZ#QEQ$}^K8Ku(o9GT@x>3cOb`k9_+j`o=^>i<(tiW%)DUdh#xGGE+!z6+M? z3##qV7w)l+Wk&gb5nH{UmlGnF?(5xWjB!OuPfva--{?eju@~lQ!Mgl6O} zxe2|=>$R}GUc6Un!##MpKAN0Tz3yUH+hZ1$Cl80>YklVBB;D0;?e*nVF?;`AAo>Bj z?k02!4RY?LQ+RQXqE`^Bp6~WuZ+eov_`CEO()8x=&|GMAwXbKb74gxg>kLK|TlS~B zb+!62w|VGu;EC7v#pn;mi=GO_Lw(UWm1gUr;t*4DRm^xOD=Llr?p{y*F+aGnh+Tbs zjNH?jP+CQ<>D8Qj?^=yhuC1~@;BV?qUWyAiRVAx|}l_BXu>=#NjE~MBc7l02{Q6>kr#~AJT z#>HQ|jFEWd+%htIeAh8WlM=YcHyj!IrfWh+~>P`X+Bqrs&#wni=h_vWwmbZFo-K>ckx5VLLHbE(z}dN z#OD<4v%gB6Z?!?*<-0_WE-cz-uew{bJ}3k@VwJ)_%6w2kM6+hMxtyA~)a71$e=Mb8 zZV~9~x!Pl;3sp_`xW^gNt1fFFYjt01S6zRji!L5ukMD9n{8d+_UEbBW9#C=SKJRjw zc{Fy|V~h=%!d1D?4tIh^(<-5mtA{QO_3dsk>K_!Tnp3s@jRqWd*OVQ#^{LCam~4l= zsub2I<$Le|gs^c}=|1~QyCIE_V(t~QKJq3n?Tdk3RN1VV%nn?~u|Mk8f=SrrU8mDo zVMg>3IXqxvRL~u?{8xh&6$2_Zv%BwrJ|5lbqFwMFm`N+KxU)MnBK1?P+lx75;iJ|n zBvvryjsLQ1Uy3o^t$8|UY>t%Et$$&tw?gLO9yFIVW%5vW-x24wJGU3RBwJL=10y$LV(cNg*zB zhsO1Oe)WXob05Vnd1O8B4wYsP@YmPo{-yW}0%-E++bPd?<3wMfO+RVxp%W#X^ZFdV z8rGbmsdi^0^5m!|S=`ZEKh&*WLv43*Rq5S*6;EaEMo_VPN5B1K3neYwQzqMUgmg4# zyBFu2O6>uZ#O%RGpIjWQUc$YUmEtD&3jzUM_nVhf0tBy*K6*bKL=1I zFo3^A8@Jf%#r)S<4!?`z<071={G%L?p5jNBa=S9Sea1af%olM1K(8B>OKMUV1hJPq`YK+jMTDnEx6ioU8DC-n6V&Xtt`jVdho$EMBx| z%#Y+Dddu8$bFZk8(#Nc9qVCs4moZ-vi@r3ry12jA1-3XC+J~5XTi)$|51OSiV|*z7(>v9nJAfyAY-u(6}Vm|sA86DiC+tWo@Qa?NKxlnQUmOI?A|?J zl*66!z&)@phjY(L?`7wgHd*%hxBcQ(G>Z)>R}!%EhP{|X&n zDAZzE>1hRnI>qGo_xHUPrOu@40^G6mWWQd_S@~t{I`D<1?0bBde%mksL(Qx7XSYin z?vCBBTCBNSWS1HPMLJcfOZG$Ey{SD_yL)-i!cIGNO|$rdd-p2Lb~Rme?o|ZcH@kW$ zXfd*j%f{+bYyP`Am}^ZuQRt|C4;gzA-xVyR7OIz~KkL91_k`R(#`@Z8 z3nxx16`=>c`sLq(b8-DmsS6e2Y`_Z|GU6uz#*8g!WF+Zb)Fo}^N#840PI z+Uojl-WPQb7_k!wrzdXcnDgL5Kp`y=P=C39uXgRB4+y;j*C9sJfR$!`2mE#Iw9nDy zHvHogo_H~Tf7KQhw=IUL-ptpF@dQD2jGLEB`%X<7SRb1;;C`zLeXP4&v*BOxuR&<9M=za60OdVNK&_~ z2es3#9X(!@e%^nEE@j;AaAm(-jwEWht-|raQ6L~KUGP=i&B0tR2m3r~br~$*1lz9HxV=Dc3}s)}FhhOAku zcGl*4^@{nk=kW2deC;ZCl9t(A+#VZUBwY++-4Elm6q{o!ilOaCRU7rZxN~wG`(~(y zH6O--5GSFUdi=GbSf@7Y>$x&QA6d5RFfXI1cPf1P)<+hd&sygxo9bAAp&#;j^qt+L z6>F976;Ho57eloyZL>5>;n%XR9;$v7VpsK&YWx29SlKjV*Uoi5XYrAaMlpF$+ppa? z&7131PWhk%{#;JkZMoa79LK5atEri)X4x8BIhFl9&+JkbW9ydOdXGzGyA|2?a+m99 zal6G3>d8t^4dv#+q3!9aweFFDF(repz@v64sKKRNht1`db6eMZo~Ak8 zpZDu3d#5&JD2j5JwtsC2^U)4jo89(g+2oUWepsx^FpOKfq^(!~X>yieo-Dwy+Eisb z&QsSc`Ly|1v-I^cteqc-k1Wf{Tb4G72HjwQLz53f>VBig&DG8AQZ&wLo8LaN zWPI8sj(e%e$fm(V+s*aN5ExpRAO+2%MH@ytg~iCV-hPcSp1XFeS6i!D zH<#OfFNZ8!hGvQ>aW~|!bPUWim1DbXZsno$V+&{2JdSN?9!;Hp3XyC-)@0eZ6w_GW zjCpMQmDdu$)Y|TCj%}DTek$V?eIn!ED$%w-x_nwHHw|q~$@sE^GiAltFZEiieH$LXkY`x;(bh9H{GO^1{c(}Q_Y`=D?igC=x*vfseH1^h7k8V5}m(3Hf z)Wb4B>zr}y$)WChfA5OZZYZX9b8O4YmV22P(tIjeqT4Yg?iuHCs+U@|+O`Se*fh3r zRJnX}(oNO4b_)&Sgw zHmdxsJNA_8=o&Z%ylI9-iTKuX=de~gm<3Dqj0MQEwao`2y z7Zhnk>koQo*7BTUl7rjP+_b6>v;3Rl{X}~C=05VmOb@V+3dT5dV=+143uwtO&(jtX z%8RL5VOTx3%Q)dfr+T@u#Xzs}?=ikITeDjKEQl}`VVJvWXx(_U@F>Hwb$Ig9zneAb ztLmHk7~!FDVALynu1`tJ9>?a>Z0K$-9hmsFa5;5N8=J37vpt*Dn#}<$tkrYI20@Y@ zKr%(QRAzFHvsZS7d8ESK++e^1qTaApj+zOB6y@2W}cQl^^4)Kw( z+RIpG)9n;+-}5|~{ISl=%!VIuodhJAXEFeCl%X^ z0gI;X#)5&^Jl)#p^_rf{16Cj`^D1iRh03`Aq1R1kCGst^rJPY7@z}UV@KQnfnz7jl zTOTgF2nqgwLmWws>3fVm0Uq_}M^VaFmCE=o+pSZ=Nx zLSf;j@^%*K1r)6^rUlDV+a|*nILzmloq$;4Y--!JzL6gzWF#!j{iM^diMw)vNjKNa zGoz|pX{~MowF@qFC8>IC@DFD-z(sM%H9yrHwAtF1MOLm3`MTQlH^b0kveWno82ODcjyPll{*RZ_{LisI19YwQJViPQb9#;trvIlY!csr@QTEaUv4P$qUyw z!A?m)XkmG80 z5It)q0Nl39L1T1dS43tdj%hi>@pF3ct5v_bZmo;rTe9I>cN!Hav_7pXdqq{M@Om5X zw7D-kmPhzduHsauXF%p6H=z^2VQMxPp|kl%2=sYDOW1?fFVy9;QVK=i2k_}JI~IIK z>$Ogfh`gI76SPGXu!>7oJD@zsZONsbrolFWWFzR^I&(u0g)p%dqZa@%Lsbe>fqUP- zNrXrK;Uzl-#NUVna9vPjOsR6af7qNRoCc zHcjl9OQs+*h1?ckvZa>Z8Ew0z9wC6OnH;N;g{^JlM>x77lZs|-GS^&Y;}&E?RH7~P z$Aj%}AmhI5STSkLD2~>%2`+TPk&Plj)QJ0l*3CR)A-z%;)u62BmYx(yFN4gG6Q@Dn zW_Ap6Kvo=cF&P5-?J7%`Ku z!gE5T0_li&Uob0=35*$YJFZOZ(oTO5XCK8&rC&12~RX0g^ZFTMskHH!yzh{QM9x5k&(_~O{IBnp)L5sp9|$=5jB|E92+FF3O02tdnEbM zqRYTNS0m!A-&_D#P;Ph%0}|(rN98CxlnZFXRBwG`QHg{+*IK&BdrW>sUHaVR7}9zB zF$b~{HV={!W^x>{1nY=+#~at(7F-kExZ{2?kqVBjTFqFsn__3e0*W7seAIP_E#VwvJM&-C3TE^;jF z`MJn%E??4jrZbV`glOu`ne0U0oE2kyf$#<0qE@+LxF*$K?%3KN* zk`bk?T-DYt>4*%#tX@=m+gy5x%wiGUSDPtsw{VHHYd>P?nb?~~NaPp(4pys%vR$?x zYx4%PBVe%V;X1MfPuhe)CgU5nK60=E7&36=Ic^b;iZieL(6ca?b3;|cd*t#g!9Ypa zI!gS)GFAB|z;Hyk3aMb(m7B+rFc%gGq(KNsU~6nM98~G@$SK~C`F*mfQgkSQdM>Yk z*VL53a6&y`aMJrsrIpT`p|3XiWdUVzJ9AT+KiNs|L5Q=;T;CRTTQ0hqM+DbD@dv1>O)ePKs<|YbI@ut}JYm;ie)GNUYd?atxk|dU5M9 zFk}X~Dmt25)KTE|4AZOF>uC!sBLX@AFxQFcmO~X}T1a3P_baa50)_%G(LHAc3*8og zyP477;lDako{f*JD8|sc*m80cd$O~~OAaZ8!U*`C0rk>e80w0 zsBM1a3^qzcxTNM86j|i^x<#zW1>VMn<%B3jtgJ51XY<$a`n_7fwY}+RMm|v%yyYeb zilZ&xt&#(EK?7Soi%6}I8z`&xncmc6!ox}u?^)=TWXDuuPb2cPxq1>d_I${tE{b(C z?uJ5%J25flKi2I|nIe)rU~32rZ`sO?r$WXFXQN z!%mLabcA2!O>7)G-&8-9o2bHLawN0S<&TOac1OrVMnktXZb||0H`A1@jlK0mMTm*p znj(hVpRz6AU5R_wnzHbDNttSj_*xQ`!tEA7_hM8tg8Tw%H}Z zw}N#z?>V4X$P0CvZz_*)9rCAgPPmD}rX6WrPcje)2qErD6N9?n`ZZf5@A{#pySSMk`u~XE<@6~{yRFSd@zUGIB#~4wF*eS7 zB645B*--s%{F+Uhp^yC1{1}L>QwXPJe`o_N;pSs&=A=mb6M9Cl61sgzzQIIg^|*y6 zYq~YkRiWV%F5!95I@kn!v#;O!wH!6jk`6krH-qO0Nh5_OJ}TKarzC%o!6Wn#Cy=ws zuii`$URs2Sy$B|6Q-+TU#r3UA@Uu5Od7tDN>ldht>i$^)v^*ah}hJm zYz8Y-DZ$RlF)lZ^C>#^Hfaw7psF=1k7wUkrE#lheaIfYfLR#IV@aoSv0krILvvp9( zv6V%!D6Do2d6x9+=1102HhlLpDO+-C{EhAWJe`U8deJVC(Rj%&;mwMu0}0M5ESK-? zp(3`D9HH7Y3hnK{5Z%|0qCsbI1J`xQim8dwxq6a~&)<^(BjzJktdALz_=M=ruix$BE@+R%*_xKmu1)i*_G3{` z;$$N7V!08MPWC7(8rZboGzx<_jTg*0nRqOit20{9) zeCV(1GLm?mB?pT(i)@G>Xc&Q>Cn`cZMb5lek`=;?Zi|cy?L?2Cd8RR^-op6QDluia z;^5%^_(-17ireBFwuBIjjGp?kdrZoYbyP~)&XLtbwQW8|=qJW!_#S1mF&{G)n<*Kj z3^yNBOpp;Yp!otk-6^VTG|DzGQ4N+X+#u}HHO zQSs7-MkJ!hDQ40lsu{3$dZj4U*nA9_nVWL7#9bIX`UEqHvu%}0sn^+Bx5j>?qclQW z>+Y&Tfm%EkLXGD=ws4Fn9Sav@yR(|4=@eWo41s|5X3P}|66Y$AHUHE}TVv}mXdyvu zh$;M>3f4>&o#I47W=hJ<$689ZF#ZTG|AP;(^+{kTykNu4ne3#(leakI4?0 z0=8PO7?-kgn|Y#fR)xuWg#|xCD$zj|x6k_O#wq}yPmIQ{j{aIpv`j_xB8cs9r^8QfIVunNw_tYvsAv=oKn#p0@i8)R%b3e^I$4oyBLj+Eq5y@%FG6-D ze%qSKC>29JV{OGY@mNNeLR^T9E2U^QAEWUGmQ@(g{>FKW;$P%D@6{u!xA;l%v;6;K zNlus`)QkigaYacuvMo)d`W5$qTamd4j}y*nkOD*j7Tc0i8W^Rjm{Y{?|IgXIBsr2K zNrOHSe!}}PJ3Q~s zjQ}h}!?;;_Z=%+e+Vr4-W|ZGL!8KoUVuGRC=z4FuF~Cu5g;$~isT>N1=+$D09-8y!`r(K&&K!Ui>KU;Larw3{IZdYI-E{2|4@1V~ z0vuc_aCA|)8Mn9=Alu`=O^948A3TV50`zDJi1ml{Tu9`{9JqYz0*It>jk(Fsd$4MEp{QSo9Vynh0u&qCpM(!!9Mc(tZ|t|_288PhCAx%Si6$-SbyB07pf-95V`=Uq*_{Uvw<#AMO$OnL8qn`ZBV zs|RXDbYkezc;>ht*j5hiO|NW}b_tF4(i{}0t_3{sL-a@Dv_J1S8cR?MIq+ab%~J3- zS_zvPOsT<|jjDl3h`d=L;%**$-FH$PKWt{Pa(G}2tMhrR{J~yvm;S566mtYTRhgM8 zC&zT^l`kY>Gms>3eX(q62fFVIL?_u4uF+K*RfnVNL)t<;?9B83mVfY z533zydz7Z25^S`=M-fBBnsCMTDycbT-%(Q|H>eta&U>SaUuz(swG%|=N$M5478MUM zNV<;YTuw7QRK+D#XpH@E2KQnvb+~$L?BMV}d2F`)Hnw-Bak+6&K$d(p*zLnq-1d!q#H1^%!~`6Md1uK0PPk=toiRuI=~1Oh(NB(xihXJOy=*?MWRA z|;5XK-yqrh5(7KlYd({HaCPluw;wMG?@s~n|xy7MTzru zmLQY=%_{rE4joJv?+!b2cfK|~{kKaM)Oh0Z2>LjH?8U^bUOf~Fw}m27)7D)aj2 zIdA7s0_92baAnxc#~3uJ8;d<@5JyGF;5yb1psZfX)b?)S21ipW;WoN-E_Yzh zDC;lA2zMW--8w5yBBQ&@*!irrGwsQxVDOMlWj-@w&#@3hM|oIF%w#j|5}xLd8i+*$ zPGUZ*q}3B7{oA|pXLT}0+5zRs}^re2a%;*RUxX6#uCT_ z`Y3vo{;t>URz+tQlQZ6x1buhUROodvzEt_WV6<*@)c7(j1O@xdI0^ZSGYb^-KXfm}NQ?+VT7hWF6fl=p6eLfulo$Gm$iFnoc zU5<-7NMZ;MnAjJxMgYo;^vP<)kQcbf=P;<0K*r9%#}C*hgy|yxdgI4@gGq2b^^y zqJwrd${$9p3J`F80)G<~V@~(P4)h6FC3$SuTOJ8an`=W7sWmf!--ft1fd*R8|yCk|MeE@=oK_{#mKwXJHiF-h3IkpTh zF?R)lX%l-*G);!8kWvW(9)DiIL{yE&;p8i6@>!kthE{Q;JQO6tKO4Pq9t?@85XJOn z?FFqxQ(4n7P~`w1$9|j48fVhpb>p-UMCwZ6%>sf7esq^;25jMefL_pPVoB_Db%+U8 z*ib*lo}@fPM~wG5HGP&%%13oHc554#%oRL}P*kly-)CZP_#* zY2BiD;c|1FMVG~zV-lr5hZ3AZ4A&ERN@}9HyXT2C@&e9oqi{l`LUS;@IJ2-?!c&@6 zQ6bCrgje6NXDgr8HvlVKpkZ``5QLD#Qlaq_Mu{R)0_Le;Ud|(JPlACcdDI|sM4n`0 zPg2*~U5hI)E4&7|=y{UVGZ;@0fmsZz+_d0vetWaD>nO*F>C~L_Y8)k~s35~S%%T9; zw%{9jBtdURv4T;}d`WsvH`m@}V6;kjjPToE?DfToBPl8R{nuY;D zKOsofX?Pf^JUa~6*VrzR?TD`7LC&Gxt9v!BiHl=2(4fCbJcZy8#9ktv64jTK>f*10 z`*kDk`q^V&(zjCusMvULCsWud15Jg8kQrz&3hJDt2Exr%Z?Uxz^n;o8VlB|Akt&aM z*DdSX$^cAOXE6hqwM)#-V1^ympEEs~^~aikZ5#+Ma>={^{h-Gl=^XizUBc+*;`T{4 zs?MLq4Fz=Q2R7!@@ZK;}WzYGG7Uf~|Nh0}4e$0-6&?gHL$IyxV9RQ2TIrh5alcFCS z8b%Jz1;}|(0n0j~yeO7AUTkks_EQI_sN_>Q3^7hyj`bE{<2|v2UnDe)vXO;%wz6Qp zE30A9Y0g5MX{pj(^9-h&Jj0UZn1gDy^dALUOfqzH^Di8Tik6qKnl?EgAsiYHC{ zv9KvxlC&u*Mv9eJrqk_#!)h$?xrH>9XFs+VtB#ur9RT))o>a6v^5u6QRp7hrY8wqhiiKup3Bk1 zn9*hEbB15Z`CI$p5(d0}EmAyT*{J;?3NDlI%XKDy@?tb!%5|SGv=OE?(kzqy2ymua zv^$^`)3gN-9Y()*?6YG2;JuD@`+~9bY%a5E zP9_P!=o_eWiv0|CP8`CF@EUz@$eCKQu5aHlAqXeT|LOGJnc0t;bzE3=+>vF`f#$ef zGHQ6x4&r$a5Yh!#AF234&8+r3j6M=$ls7~sGnIrqh`XMoK6zev#?{V=B}Y~%02X%g zgX{4$eDb2P#fV|9WFkb(t4_LiG=~C!HE}@01zOE2*V*vU?wj@j6K+*L@XG%IQ;KkG z)amR(-oDbpLQ$O44~PLFlwQ_5t2V_aaaDj5!0Qd?niv#JYXEAZ7|&|knQ$YY!5!jF z+F}W80Wz&Y!{!;qYAlvql--9d*MXtihnA&&4{+`e+ik}(1-PI%i88$7^OUGO%>^J@ zdO#YERm%*L-i0{w-zG9ZkJY3SPw7QIZ6Xu_6u?NcyC^zaLa)_<1BG3L(t-Fk_DF6J zBs(k0^_I>|vvlE`qnYeVJ&Ps2B>+$cAh`tE#s_rl({wf*Q?48Dq!$JDL9>Be!Ek?6 zfOsJ{gmvM(%)(x|C}2ra_{~#Vc-#61JL%fsP$xoj)$|n6mpsX_k~Vmuxm&mev#fYn znEHcNLquLJG1T!mJ_t4Q@`#5=o+SE@nj=&D{A=lCdKnit9x^zhpt7#XEX-!&O zS%xYr!&vT!3Gp0HWA6}Y1ro{^`1 zt2F2Mb19{GWZ{R-8^m;#gz5$;QD=bz@!-cDqw!`mof8nhEjy<hW5 ztfdpE@E=`9{*i#HRo60e^bq|YMX)(Zx{{;I7@$UAcOL3)adV1~A@rzgbAbv;r^`vUpF$=Cgepeciy7`%y;*siGZIlNp!K_coaL{aDO9p*QiOr?G*MvBVbNhzW!n4g z?w&T+LZg}Da4&;J^p`91P$fhWmxwL+UyjB}EL-eOlJZ$i3*9i5Dp)D1-WlQbhTeS8 z1w$Hc;DCbwnltV+!d*^$Bd^=C81V!DW+jGXZ1fUzyYDGjV)zsl2`*jFdlNn=lCh=W zp?M@Z+){j2a#9GO;Mi}AdzWArL-DqZT_68M7e4n~E%UKuf*j?RnP54gOOxrJ`(8Qk?Svy68&+vyIwb<0^l#RbQ@km_G@?pbtUU3GMUl$rdX3oQlN|J z$zCxx8L@dZdM;F@W3L;Ycz;M;HuHkQ8Pe+xFo=3^USgV(sqe`G?6a{OhcA)(5XoMO z)siQ(Idv8cQ#Klb(tPww(jP!1bu600I2&`NRQ$8#i{m-hjn?QYFi0tyPcQZ|stKVc z6bm9a8kOxJ0F6khotE=vScL>Y4R$audpVZQH0zv9qzd6OmGHE|mlB0I+_RoxgKjG` zq>WbCZVsbwH~J+J1wn&cKxbjEH)O_yfaF;Dk`t3BWfyL4#>D%QmQ6&lEepYcg;hf} znHrf-#G1rU<0^0&LwfCnG-9~&8cUSST$?M>04X2Da8x0~n#(MBS*Dz3xFQeG zx5I8615sJ=-Wa~ZFW{1mt2Sv)xpIx|2$MuZe%n@_W*%T!U~b=WmNf``yXU>(KA5eI z=t?-=GjAF$&hhrZPaIFAwURZ_C^`%h$N%3^28{%~ z%#(ut&23s_22LC|+ATHRn?;vyd)2cxiDYQo$p9v}r2tsrXc_H082?}+npPV3VS)+b zjnUsmF%_H_KPS6nB4S9q5Up48o_5~OCJutxbOf&Y<)f~NB$(WbF*Zr;bGm~w6OuD~ zAly;^qx7x*du`m??It6IZv@W7rhxUGZji7Cpy2hwftg1ee_9Y z=5WTOiCrzleqf~TqA=Gs+Kf7|k_XEtnGMmLw$>K28mbjOsdT`4AY;FcP+|j{dA!?p zF+u`ZQthRV8y$`6D6`UjsM)p3^V`goS4ZIRt*K^4g={XRsW@QT4dvbH8a>flM za;3fmU>kiZu~BM`{WjQN&z=;odLD^pK~GfHu7K)NR|hQ@6f)#xz1N{3CAyB*I5;So zh=*g4i3Le#Xqu8@+`M*Vz_CXnun=}X@4b=m-c}XN861d4GePQIO-07|KW6I`tn$31uCUd;g6+!Zd;kC9?GvUV;qGA zGWOe6CXcNdMZ86^6AeJwZ%2V?;5ZV67eG0pkWfuE0^6urKC#rmX*5P4kAx%_7I>t2 z%?OFpSdHUFXGpWFaaOOxuqzn(-s7KD8lkLYIjQC6NP-Fta)UL zFvpD4(HNZNi5h-WrY@KV(~u{$sVPJ1G~YZ%4vjLC_Sp+G1Pbnx%tUhMqhxw z*_xrYF2oyV!NrG+V#TDjHTpcNn!|R|9);omRaLlG8yvaSVOUR3XXw zFPHhhm8}1K;pKrF4ZctSQhYW8bqN!fzZGYrD+xFZAp|r91wGC9OcWs61|3iCm^$6R zyq2-g1kfVOs7LX#VvSZk+yDHX8WlXX^i!~;z?0>(g=x9UA1hL(0B`h37u(59v%?j1 zRDC9Cr}h=z-SBKP*a<^g5utl}`z4zG`<-4R^HfHp>$VeIab)jD4AhvY|8i9Rtu)jW zYyJ_dOW#&k1*|6P$E{Uul%L5}Tty0KZQWV)Uj`N!;LLp1I1$zFyvJ%(x4`w1+L>6D zak~J)n8EnIVkI^ReU?0ActWQ&{sm}_Zmcvln4bS$W&Ee53{5abWwG_A{PdHU;h&H{(a{EUWuHp!^XA)3RC^g|Gg52LvLbz;0!b8+nQ7n zE-Paue!uHqBKzpKb+Wk)nnWa=@aLKFZLQ3ZFCGhl)_7S{^ zh%MDTpZJeGW;~&+ys=Ip><~DAE2qZ3ZD?dtJO;xtu9@>zvXW-f%xTiS`p*6lP0VLA z(LznU*Qg;gDW6d9y|S{y1$o_|zp)P_Ekl5ci2T)O((*GiqPuXQ-DQA5iFdMZ{w}{+ zxBq*#CY9263-7DkNRdcq+Au%;WMfrKFz>F7b;jpznXvpw?34Dn;-pE%w_WR!bE;xc z+z?=jprfWSJ&ke*kW;Q?T!dN{f>LrY+=R3}bszQhejC>I_jgKrh>H9!ppPd+zfV;O ztCFM|=~C_ZHOmzcz6je8tyJKE z7@5EAe}ltDSGonMTOJIz#OvSYu2w5~7qkCj#=QP*B6+ovZ?XJMip#{RdY?a@kZIvIP~M&Cf-I68P-Hz%Z$&Q9;ttRrszfKBT$5wU)D%JR>~47QtWv%eMRju z4*lm_(YZI8RQNO8B(@Ue?{tSa8yVM}XtIH@ayt>o(gCgVgz&TaY6l7*)1A>-2f8+p z5D2aEphm!6sqU5a$8q(!`dsWjciYA5bNgC6kI&8Gd-;uj|NZ^-eXOcieM@A-0^HxrboIk&hVDOAN{7qu^H*)@5DK1cp`5aJO3@Ad*w2VUiGgI4~YGQ(vv57^TjGV(A*vAU7RvV?o8T;V2N+n?w zFQ)jmzpNy>!E@sQ@~heVzi<1$KXYR2`}vu{PNrp3zpR{?P5oR60g-I3fDt@IN>udc zNM_~gvr%r*3a3T_9$P2T1e)cA(9RKt zq52m)en&r;89pG@7EOYq@+RC9Rgta!Ld*U8YVg4BiP<`f( zJ`8{xN@M%BI)+lZELG}NF+LbN1R>tQEo;}fIK=SnA= zb2aZ}*_7U4^=*HA=JeS2^D|{NcsUG)75O@|Gd}Z%gQJ&)dQIH2sWADUpBdaZ#d4;5 z1JY)E=Jb5%$7jw!xqq%S`!{3=S5gznO#il&hf~qQpP#7?FrMh4GJ`JtCZ_y%%KmdF zAWjfEsMLWyNVetq{IPPAaA|VH0ma&gh3UU@M)msRce<)ociLb_QffhP`e*V#CT)fo zg@nX>tXc^ZVybYIC$t{4JOxJNHyYWre3bjPyRU--v=XUgU4s)V&lPl2#Thb)%&z*j zA?-H^b%=d%coxg@Hfi7XR(@NB30RPDzEKAyCFBLnLn6=Rpi+Hi>P-A8*66L18A|4H z&&t7QvD$bL*i0 zX72XyGdazZqFxTM!PohEpaU8gz-xf}bJ=?^e&s*%KA))%>YSzd_1id`f1mlkSGuan zcIZZVQ5Dbj=V!(~3gwIZBwkC^Tlu*%of5?>H!32_vd;doaz>@&{h^8xy(Q^SSCKee z%E6t20(V#4yb@n&LB*zkA|w%H?ZeEfGs9PZW@So4iD6*> zayG(W)>M&6g_?_4Lo2gKqbYl!lwWI1;w##7qsbw4pt+;mW;%G;RYXzg#H1wqon2ym z+0KfNc$I2HirYv{0^cg5hjj*MI5B#Q_es0bnK7zT|09~HVJG5XKUXFWO+&MOSxL+( z7P4GbtyjPE$G7#Fh@hN)2rO8|%~+Z24cm`g$R4^>8OG0VJ3V{*v9e`vPJ3o0n|4>- zE5EFaZF_Jql)5V3{#-ddd;7UkDIv3f98*Lw4W9mNd1g(`!R%`&k8a9vf2MN9T;Y6- zkqx=4J~Iv-z~(hv$E4~e8+z)fweL=T%%^V5HE3~yVn*01RDdaWrcqH{I=5K zqd=q)#s9P9^ZZFaV`WZRu4_ICl(fH5ddJ?1H2H2{W$8=ZNFcO)8X@YcuC|x zw^iM1aCT<~r%B(LP2u5s3GsX$^6Jd0$&3%q>xpN@JJrPb&(rPYKGaOGDL!e{JN>zG zI!Ej0N(jUmj7N2f?p>lwKUc~XbLq#k>JA87G~YpKpBG=t}BKqiF>&d zx^o2~r?Z!&Qt7<$X>!dY*+YDrPKsA8pqoybr=NgWMb6-@I1ozE<~5=h&I(qDSK(6( zRxtsb!w;Gk19%_((sIoS1V2mPpZcWy%e~{raeg5I8L+Sn{&Dr%N;GHXjgQ%MWe&UJC6F8cjJiS7n_dYM ziB*-^xC%NkKM4t5GDdha_=|ETz#m+t!$o23)S07x3Cfv^U7@r0a7 zd_~{GNww*wW zsrdz@)Yg%67fOBsPi(I+ zi;3Wx>+6Mf+jf^BXilHYuHvn`W6RSs5<(m3FPe|B5z!*}3X?}p}U3ugCmY@kd9+GwJd zTv5!g&S#6Uk%x0W3_gw+pGW1!oxUhk`|5Yz*%#q5c)H*=`H;ahB*F3__{*y#IGadou7diZ*nQ$U@xX>NVN)Ek;4l*mqn55sv0mjvhGkO5 zlBd4Ur|gY)K?WT80hg+;!+)%E4-E!W6gsof!uUeo&`uo2cJqRjGGK&n2zRlv(t(=s zHI;_VRxdnQRd8m#VwJVqL9gwxrs(rzxe%3aMX=R1;@mg)eA;uJH*c5$o0Gbjm26_4 znpfh<*p*$Vm{RPa&(Gy{^?EM8K0fvf^9eo|&((Uj;O6m-)W80EJ*(BP({s0fy%t}a z@6}?v|03}H*gY5PkAu!Wk?!{SDn9%5`dS`epNkI#%R*kVUmSPe>&0trSAY0c$d2Qy zL2sS$VP)n(Uwl}xdg{A{wLU||zVU-;H{x_%6nSRniA!_MI_hN0J|a1+a$D$ zzfJV{6MN-Axb;X8fX3W0C&U%nRbwa&mQXYmZ$*PmGl4)>o6%<~E1*?7-ED{u3z%vp zntgN28HOY|e42N}MhbR)-y$D#f4VqP&|-8De%OKePz*mA);qjK>?K_7%saKnXs^Vj z7{Sl#eOP%B*fDn{3+i^fjEsG}OH^_L6um55e49Qk+jrES%<{sE*_eE57$%uxY8PKR zo@()NF(tTsLfxm#J;aRDh|1yH@*AL?W{au$;S#7$HYf17(lM}VwKMG*Ei=tGG?U__U8FAzmI_$;vWfx zbh&y;+=+N{E;CkrR@SO7(;IVvcd8scR866rG%YFqQ^Sx_&R4~^-FBZ*xCEi2+uyh& zpDCq(5HwRH+!f+wED{d5Txq6@(0M|}OAuo5%nKRdxB1zB_pWHymnnNXFypPPadw>N z1g~Jhb01E(iMB?KOP00YXuga5h(m>P#{tdxmhVo-7gpQP*J}H5SbV<@-;3?)bGKkd zxnAfy?~fmAjN}SmwPkH_?eHaDQ9jq>C70wQYC^-c;%u#U!Y<`Au(9FDEbmEwiNKc4 zxwNPcS2>M+kxZT`=y6wdu#(XQnH6cmz|zVsGW5(TCsU-fcxM_eZSz`yj()E~xY+f` zPdWiA5NbZB8~an&PY)5AXi71#liDeV|a8g;)|L_dZ)345A5hx8HZSN)gI?*qzy#1B$EP*?qeACXpY=%z0F_ zJv2REh%;T4fsKF|L%pB#nE@&WTnQ*uO&Vvl&f8bMXlNjtA@f3lJ5p#K2j3odL>c>x#L)VZ zy$^M;!O;tJZ$Z)KvZav=sf1NHnai7;NEM#(0*1MRAAg%1 z{P*5Q?C8wkpwBdBSA(jWjTc)OnJa=*^l$vLi)g|Fvhx-+Go!PD#gnjy4CNfWiLlVg z7}lGl&1orvb-aAMEM@G9P(WJEiu7c0-1w*!;o&OJn+ z)EN^nu{&cQGmg$vM%Bq+GZT^q+|i5$h9GO14x5B?b;=z6&V6g;l~RLSX>)!j4ghZf zkoWOmAgcmG(alF{Ao97j~J@8413mb0m3WqiWBG`Wd{Olx9?b8jGE#hdr9t_&F9)uL(^ z({+kt^2)y)eoc&D#Doi?iF>5cRotJLBykbZ9V3vDhgK^Q^7U4HV(X!C5tE+Y;Xjg4 zmrS)1b5|ygdD`*>3`H!wHg&~_p)w|*)W}ik5cmCCNPEF^Nqbr;DjpcAqjZsQQ{I)qD35>$cV{zsT*WL`Dru z?aZk)KKxX4Q6Rrx+Klcesj9T4d*3I<30|roGV}$eccrw7F-#5mG>@@;Tg*|~UEl;y z8W%_LRsd+eE)&dQOwbJUE<^AfPSnPk9r+cLO$-N{_03m>-a*DNu-HCnzg6G1``POF z#RFZZ1yflZLcOOfFnioAA9jt^XG%iOf*eJ9J7r!lG6FjSn0{I1Ok)~RY#nR zB>wjnA>=#NN`bvG(f`$D)6^-L+`YF0(KWw96sz}V0wEswPm*FG{I{{te=}nYo4t1m zaG9y;TRji%h3~J$$7%hsIDT(+q_)gsbq~J@&=KX1AH`F8p-w$_hsEpoge&c~Ukjw? zZ?NoZzyExG{66>S@3;PbeQnm?r^W8;W4+jaZ+X@pj$aFy>~m|B`4ZW9{Il0f$Op{^ zdUm@J!!1Yk7pSla%APDzESEEvJOq1QWLYoG^~FXzaj8#k1Uc7?$n0+i$h7S0+p=iK zgjD7ae&2Yx%!P3~h$TvP+j@END^oAOcZGlAM`C0|kn>v+S-4qiEnPFqYTz{K(cU9p zGvG^0ullyG>b~8NCexWSgkaP8RsxD0U#o?AoB9H~?;S$=cC}a@p3mjS@nf}neim;A z2?XbqsqXN6qZfc6FUeIRZYW}5m_;{uEYNw=@+0ka9U|}_lZZ{_o~MPxCklUna@!rw z7KBs)-VJg$gr(vz_>v6AXok#AXxX{V!$yqH5V-YF>0DfCAB+SYay0$L_VoQ+JeObl z#Y;A^kr*&v`!1f63BDN=4Edg!ufSt_pD*u1MVA6!jmZz9+sSn^ zVCJ#kK4*L(9wgko`klQuqnSM>o#7A40uxo_J<1EFwGS*pW0%X%jH#AQ^iU;QIk_tE znGc{t3561vuDL1RgTO>NAnt%;|Fd(#2+_;+Crk*oRo_xA882qV{@Y78wUG z6Cj$#nZ+ADE(>@&1jJTF6&^*MQ6(F+t3EXM+Jns~mAM_8tkjHW8TyV3dx^PR?#Kzn z)cK;PZ)NXWff4lOyev0+ZwZ|UlZ@4~x7KN7q2hwc5IuuHfStu2;#Jj`(Pg(}BnT68 z-5zz*mNE3hfbum_EPYP#nFzVcPulJGZ74^g+uU>B(;%z-uGV3B^RO`)!U5w6XQnEv zBJ(?#x9F!-pXnG!Um}LIQ+5bf+nkCs2fS^iMKb~&4xtp>y-zmfxVc8r+$9w#dP}gs z-uKC^>=4&t`AvJazN<1=vtHYpCt~$GyLSQ!2Nk%9Lvwu;V<_Gk6R?Og;?}Dh_D~QC z>Ju{dEnamMZ_NYij0xOFu+>m=p-t(KE25--G`2T+)vc)7)hbFg{VVe>vQilmhb@m9cCxM^5H2qc1+@1AC?bs29WPb9uO~2Mcst$ zpE(2j2gO0d#+ExnHazep;X2B8^=%yq)*OE0%#Ol(_o*?xJucXI%1@sA<>Iu{yne-y zc)R)d{H$F0>6Uuq0^Ss+1>W-ehQMXegX7>RBubo%-)jFE_=%eKjzxNuPWA-1dMT07 z#p>UynTfga4X@DbG-1uu13jziGkG&77Hw&?th6;Uw~KC7$)ih9t;Ao^;QPZdpYA0~ z3~ej%x~1r7_4j1OiqgJ!H|PnT@H;g1nJoC7cda-fb7v}C#<~~3lOvYa6__7MteqDV zLcubwHv zGZa2@iO|?xNn5LY;RN9~qc>XjL^6dlYBWVt+5;}dVjp47p&)Ps#t?^Bp-gga6pO`i zGSDi|l{jXwk+Q5!%1mobk%(2rj)28a^_i!Q$xzS*7eo3Wll!qq^H?r%x{bl}%#~nk z3|29)Wj8yI85awn$tJOyPs2^eEXln~Wtc9kK+H&$;1#zA_q6JFcE^R&6W3W4`|*kS zyj{P(ahi@O(c9PV@TE-nI6aHcJ$Ii|06}LwI_rj+WCzt67#{IKj;$G{FqopcU(UR) zOdaMo6dBVBVS>yI1LlAxdEQt1=-j<`;jEpxkuaN#T29nt&)9V(DN= zo(>usgIDj)q$`({GE=jtj70)7wrXZrw%HI;j^pESGmUkrgeh}fsUuXMlg{kwPK%wU zdA(<;-;kzoJb4Y8KAN6J(dDENWI*f!7Ny(1HS170lCkIt=Mr1EE@jvJlZ|0&w?0J7 zI572E3~(!*&HCDXvKe=99|m3+8ti>KSf2ED1HjV*uekfhh-`tYD$@7K4(D)^8l2pi zd5ZLed1I?1CUe@;gLikih*TZPi>5yzLsBJ(OMUK{N(10P?^(gIQJI^i{pR`zU39es zSMGE6LPeyO;tC*K=4NMcCc)wX1L!dy-1t&I;w>*&Tfn7M)rF2q=UH)CG7HMEfx$a6 zEV;15;qJV3s+&;3dRn6)U|r>`Om|}hY`SYz?6)`nncb2ZwJ<6k!Tzw~+43W2g6zh( zf9;`@{@cyh+;~;2(5mY0j?IzV{triOvU7sc7u8=!Df)gH+;nHeHK>HBK;DLeD$Z(2 z$&EMyAcAu7tKyJB)Z}^k@kVRUJ1h9Wr0U>=LE$=`#m5S`H-dSYoiRZ&m3wyMskGlY zS)(7886pCz?GVAZk+oJ<^n-{A)%iq8>%2eZw4L=CmEMx)3NqDzS+{u0JIxTu%dYY~ zW&cEUen0t7dqSt9?M|JT?T_+WdGVcP!ISeTOOZeklAC^@7%4teH0V1kc_{YZS*x&dCiIiy3q)Ja zVu{;B^_ji*Cm@ZOhcBv{wLM#PD?deE2(rQbZ$o7N^jbD6FbG`AJnMl?tY0X1R7`-J z6g$s7c?t zZX_!ayp<}DftJTV%yl-(8EKNnr{XkisLa>oflC@<-CMU)h%-kToy@JRZhH}0msXOy z=S*QOjr0v5pm}48yNOxF`$S%b_#r&Z?JknkM<0TVwL)WiweM{ zv)xf4;AFI&07jlTLEB3F0=xR|Y_e?g5RB(DR~oCQZWisCI*9!{O`rWP7@&ru_`)j0mv!+J9}g->2{J z&WbmKhv(gp7Qi{({R%AeRnfJzFF98gJ)LMzuYNJdp}$s z1XroKgX+aQ5<#s|#N&Ru^zeYz^P zi_h0`zn~?54mxL8)3@{g0c9`vfA)+xdT_*Gh$svisiraV{Fgt2SBl!uO1TB*!=Fj; zldIKbpVT>Y7S!RYsN$Wr`B+_*kF(O5*IU_{&EXl+ z9}z^@f$8`mNb=jqy1@8-R1Ch2c(TW1&&g^z*)85W4zGJAGY+dKBnqJVJlrwuGu6+c z%7aq>aNAanDyU+;xe9GnX9jtCZa%)h4~zW=OQ7xHEJwF%A)hFWX`U84e?FJ%a z)HYThdX%XOL~#=bbvJ22*~%1836Z7qgj3ab{#N}1lw7sb?COUWtusf`+l*ApILDbB z7?wZ1Pe@W7i!Pef0lxv|x-)2|6$=ul;;me|qvz%5KDclb9~`pJ$VEn)g*##LWYy-9>P}0fy$fCH=++@JT9ePB5vE6sxoQfjhtr!a^QdQ%elj1A%+>b&OSHPKs zPDFfq+V1%ZYLnJDm2awOAR$%_i1Ekx<51rlHW)H|l{tDeZnOg`uw=a98fLwPAD&iK zd9v~HN#LAjCp#;Xre9G9fxFC>s{HznPm;bJy;D1%1j~FKsZ{-e=0z0mQvtGvaS78c z&$VH(BcmNK4dk?|ZhT4}HV+ST=ii+Zb&#wNU;oBdsKRd)pCJQrglPI|FBhc~1gK3f zBv!HSVHOIlzz!dG=I(pMt@^0HX%8iWO=b;l9-ckosG%J*l`5N$<=6L$Ss@mj zut(FPX>&#J;)d`CcV;#&Z9IUT$HaRz7|zkXZ7?<-p5Dg3{GA+wczc<%J47*$>xy}* ztT<)jQt>-is{Fmd+ELq!DAW1Rki4bS!SNNFG;c*X46zq{$TqX$_*jYP`1!fmeLof7 z^!khE?gJ>pPipDKJU?I64#d>%%jt;&B?cHUZ(pcPn#kY}18BHFSGsvb8L_?D8jJM-ix|(W6bZ&$-P@#puQMLKzuOEpjnk3=_emVeh@0*{Kp(!VOhT!$ zQc_Oc9hMRzGW&?)yinC`cCY2)D|*E3?rXDne&Mt)zwG0$Z-~dUz*-G)M745*r7sc3 z!fms^#w9!(#8n{^S&1{a%V^l9?vk{EdxAKTuxFjujqat|~*ROY!mA*2kf zSAG%kT0w*6NLcw5%dLG^(hc=t`<-qFV=7k5?8=fCY$fs)?&dmKYATTUYS60e;c*b0 z;;k^A$I$@t%*~Rw5Dd=GFleqFxzn^9)ssA>R>qQ7ECeevfY(N+>B+9wMq>@2B<8Yl zufJ`YXGEKxM~n57*@_O6tPw@b%*ouNTvbTlZ3dGjz(Ca#!l1(vde9OLsE>*VzgGI; zN5JTcXS*Yv1pYNOc&dTPoNGnCY>cI+WH;~A&Y{*vhu(n^SqE01siCpg%!NS+2cr)gC znjp>AY=}kVHF_?>uwoohs@j39{yX=PIt%5;EjX7cv}GLBm)9Guw3BAU%coUaNJ&`N zC)#Me@WY`%nyh5=Zjd8PqjlpZ&jZI^?~O$g1=A+@VpGp0dpxW$T2NWPlJ#q+&k^++?u6UnD;eqE3$V3ZB zMvkx#k!2qUlE4Y5RvwjQkj>1yx6;q%2MF&$os2HATA70#hZ4-0U`0#5TbVI&*$Zox zD{ThcWR}70cx_LJnJ)G+A88FET!8AWz{)$LCL&wsDbrse`~p)C6S(5flmRQTNQ3xS zn9U6jfcN$vrLqEeDn6y6>bwP6nk#Vj3j3CkPwxrXF(ljMeO_wY31KhCnva#GY@@nA zZ`~P2Mv_o^U`Y=+kx2$zFBujcVk1=D4p!b>*%Im@@0&ck3-l33bd}>!dC>N4_yO7V z2QC`-f+u0KQmNe$UFPD}+q?2r{fCp*a8;bn%Sk6HoHJ%KO9#7UzqSfD6ns?LnH^xKYU>n)SG@v5fkpvLI+eQLQ7OWG`S9%ZgKNqsyQiw>jt5WRbd z@(bhr@<1kOF@Cbz30h4*lvz1Y!(^${3VJ?NsaRFFbro;p<7W$}VxIVB^)G zO3ClkzhmeGll8U$D@7K^`tGoQZdgtly~c9}NPpUX+PmB5(vVX=a z&htGRhbr^X`%sgT`4_t$WZf5&sG}H3`0F3%*4!JUtp)|b-z#T3x2PTmgKA$b@qW5{ ze$IAZZbT2?8&Nih9Xq3A*f_WMIk}tQ4mENvEpl*kDiF=kHuAyyN$P4Y;Dp{CV@IGL8$sn@WLvTi<5CG*6bK`_+b^~b!<0kBaYNaF@<7V|| zwu&y8^8)GjC*fmPE6K5Nq4a~KpiWtcMK~MrAUv)r_aSE@j1r^}e|J{=NCX%@z8ozy zCaPEjEZXz9!I>B)7CT(HzGrsAXI2x{$u>}o^O`nwT?+|c4B#!pj-Na$QH{05 zpChUuYxFyPQ{#i<2C9Nqvsa(leV?i2Q7TN~n>(!phR_?S)w>auF}sR8tcY(eB~XIr zL#T>Xmf@a`uO|v;RW4x$CB+lcMz_~U1?N@+Op<6-6h5;R(mG&RB&Kb_ zTv|++JPF21G+8?EcOE8Z#`rcqJ8lb_W7Y3esLSXH8Mrnlr@x!6tXIjN2P>vv&2WCs zCXi31(QD=8J|U);x5o0#Y=TzxOzvZuM5<@vDLKpSGbh)Dh=H279;gH*D_sV`Bru z;H$bF_TS6TukViq_dx6*%vUV|c zH9@FTRXh+E8_sEjz$A+KUj`1IK)B!c>OOPG(9t378ilEz4NlaOhdL;$4`o(P7>hK5 z(LMC?-82&!%z`r%{!G`B8R7A1k33UZ*gSwS( zJ0r*S3qxl#3#Oqy({VAclEV68i0{8V%#cSDOZc-8XI9^qE$mHV@y9iWl0h#^bTOnB z{$8#8*gg$YHeNgR8I8^2^!(l%0QtPURV+H4T}|ShyP{RG8*NF0X0Ke54HvB9f`Y#Di~Dh|4&4-Hj z)s_S}>m116iv+#31*K5v;o{Md!M@fn9~2x4tn5vSAcVK!!);0;e)T zH;Yq`zJ9S2&fPaK<`l|2DMcoq`b?JTTtYV?>rgY7pLtt5m?jRtnL6qQ`ITNfFB*j8 z>f5^S>D*wmEk|LyDv2ZJw6}JTN~6lU(W^VMDVHaw65zH*&FIT+=}qocL?LvG6&?EC z+-ef-xbu`@Bd&lKp_Vn4;^fH|jC6X>r}x*H#W}#8^%=aR^JMFZE@^h?c3F(reMkK> z7R~`G6pB@qh4wkci0p;@wAg)C(m=46nSHmkk%3*v3lKGSIA~kV z=VrRA-7cM5US{BcwujuFqL46QLie3ktoWU7*v`t1yEr#w-LRM`r-pFcOnb;Eb$#8c zhT!1BE->A@fg>yXLw=AhKd2O{UZEE8QU49yP$){SusrNf?g!YpdF%Ho4)Oil1_;e0 zjKE#9&mryB%U{sHc zd#W&NqNC39X<%UI&70Y~+z`#-88jGn4oo=1hlvu8721O}>S!Y=yOjV`ZY1nR7`tk3 ztut5bcs?5|%`1~ST1-$9Ok|y+pQl28VCf8dXfo^6N?mE=4w!7VYG<-GYj-ymU4Dy! z(p+INd6oQ|J)?#QPOE&#*6WO#_wLyDFwOxSA;Kav&58-$nG|&;XB?`!(0gf20Dq9( z${$$B0b=A8&dzV~d9Y1GM0xApRI`XW&H<+Hwn8Z#vCgydPy`?WkxdM@y-Jb_jx~39 zb!F$`uG$^^Z0pOd>@vt{m0kW=YIVxcKv6{*Ka=2U&|x?&}vVE zaab-^r{`+*%s7{_EA{zdP{Zen!SrgmFv2n(Fq@;1WwD!jIZmgq@z37#r&;M^WWsT) z*clT%0KzTuLa(cpz2^qY8*P*RF;4le3S~bYC)lbvTHcBZWbaNnyMT?AXvoA-j>WK8 zfZOT}ugY(0@K0jIJDLT^aT3rvi55x@seOnbXIaTAiIh4G&Sd7Q%Dam> z^ewWz;$9=SU^}>hU=I^f;pG$OcDblKEcBfEYQ3xdWM8J7MJhQ=gcljZ*yE!Dq@8rvXm%csXZ6`KJ zIV5sBvc`Z7)s!gQ5ni;65DZjnpNYFh(#q)nD%GfR-k9@)$9Xcp>!z@ZH`mN!QxI4F z5_^dPYQ$!p*(xe%#(_sv9gIFI3w)94ce+gQ{)`%C;TD`;+RZE&AA4m;UV){!@%W*c z?Eu@Dx%=!ejo-bIL`}C8c#cmuCV?^GAj#s}?oeM)rrp0}J<0!s!=wNazOb??S8ka< zYJvW zsoZyVGMgT4E|b9O&%7=-CU>Dk4O3=y5aDIA9Xb+p=IYya?_I4A(=5)kRzv6SOzss$ z#ebb{wr8r$ozbRM7N%!NPYnY=V12}rql#h_vzS_n*}3{RpUHW~fo8wbcHd;&Xn#D> zBQk|J%;iz3Y3 zU&qDq^TeF?1AF@U{5Sy_KK7qX{ETw=EcOv8^mgmp=}@)2C{0Fapi)!cX;;O1+W*e; z!fjm(?Cxj`E(Vibh>Sh$jo7l=&gYvBr(65H<=K(@MeEs1-6{TAB2>PsSpVS|sk&9* zEiamimhOk~ba;+)eYcoU^k(I}6=|Xs1WMZQ4(YR5*SMP2zINrWW6B$eX%I)YN?GYT zH71CksQed1TM-UFBDpVUwzhG*T6*nD!#2mn5#Tq-E3sy(j7Uar=5{AHbu!44T@;4 zF@kWc*D|t9(pq4{wSZ1&FzDK=U0(b$^^|fuM7t+f z01}vL=@3=lma{f1ZcY$$V?uPB+{(#5hJ17KEFwn?%9-kFE*lh73G0TSU~sm5yJPUZ z<%H((0)wkKq^sIlpUGrW6oIswBgNV1nw$BcdxOBPxEjT?y}<^Uni(A8KJ->5cj5N8 z?l=@rLgLA#dNiNOI|H~+r>fRZ9aDWs$xA8oRx4%}}xP6X8HjMUidCu6eBifFqJR0GqHw!+fL^Q)UzJUDMR z7<58yyxG~*AvD1;cF4vhBJm(M+o=ZaxS25lLTEgODr@jKJhNGh*ROA43|z&sH5!iu z@^N*t2X5u`lN~?SzB}6|b>6%WAu}(5Ph0L4MFV&f#Y`n^z*YM*XninA}{^c zFkft*LE67xhsD!CCL-3&>$R;u`?dX$;L@tDK7#!0Ui5wj2jEV887hnw`z%+xRSbSu zeDjvGde4ax&wODX@qYVSexYMmxY0NA)>C`n_4)MF$YaS2iEjO%&+qTeCm6H%-3K}e z&R;xo-`S3W+!>*m;DMvsuP2 zn!SD;J!fn=a#Kv5a~0z*jY`|mJcHaZlPd!;<)8!1qRcbxQNa6me7 z_al;k{RbSWSg?qBhnqS^I@#*}6MtKtka7UrEEPlXRygr;6xW7xD+Y~D8;-aKf7TSb z%4e)i8o07VLDgtaXmUqGXS}nHF-Z&b?YLqHycQH{VN)#vN;w0;!~46D8V zJq7VxZP6i9*SoThh{WiC389ohb|6ffWfVE7`6W{60_{p8mq7uo-&|Z-D;Q$}HiF04 z-f0smx^bhvc7YVT!S+FmJ^>nrS_TzE^#%WyN5dehmDCQ0wOI1Gc`jZ?(JXA=VGzhfuNMA5iHx?S$N)k=a1zN`OJm3ii{WrryrE6<$+QBU73 zDI%!TxrR9?Gh>6e&>0&b`Y44wj|cD9>65Zd%I@7qNN#<21EUK??G<`CH|sG}mA1$< zgV}Rstkl=#NW>T*c*Ary4Mke->E3*OZ&$DF!l;+m!iFOF+8kI!Z+0INkk2o(Uy8j8 z+t{{A6Law#F1{;52Z+N`=!Yuu!G!gAt&A$b5{`>n7Kqm6O72IB zaioS6<8%R>v=hY)Np)VUmA#ebVGGyLIMG-bT>QhX$#~Bt!48bF`b@J=Fh1h@)Ow}U zZ(%uof(}X4z&_K3Hm?9{ok9AsjDe^Doe>4z1puJNpkncEPZ=4YxhEMX-KnO*Vi@&C z6lb&ZelaPK)nc-mc1y&q&k%Jv1LMa>rDxBKsLtQH>#gFMaif6|hyiR{UA)nw2#iQb zyFFCSn+Ig0&RZ#buf8ZopvGGf3t@N9DRaf>2AR8KR<6KS-N15NUZ~{u@4GvVL~Kj6 z$4uM!FDpBD!odhvY;I0-$(3bE>|r#}w`Q2(9~YQ?(qjR~5Zc*U^5GNFR<$PRY`FWF z1o2#*z82q44*t>rB~0~Ob#gm%BaDmAXzj&y=GaHGa^md+C{0{Y8w6$s;0nAjn1?Yds7&!oU4dtI{S&SwI$ z$txdt2__Z8EXtMOR)a_~)+Pw?$;@Yeb+uq;S6Tu?Cwilp3>?pg~N zKeYI^aYHo;iw)qkheYc!|?(*E-)+L=AA|u#AE($)Btnw!0Flx3@9_KJ=Jfo{{ zcaqP`lL~eKK&3Z|#Z#s4bQ^PW00iL%H?A;Mxqdc5zYYi5TD9Xtl+btiGf@*i84W{k zhEKe)-u21{xhVd`+__qIOL(`;MuxMZMw~dC<6z`cXUt^?Y8#*>47xb_JF{#=To??} zd6jQU_K@V_)jiaO2Ypj~Cn79IzUuw>f2;9tQ2O`p6bsI*tktLhxDq9hHZg?cuJ?{6 z&NG|7o6a8LSB2Y0aSYPF5c6sN+3^S5eC-3qhU7(+7sG+lfKT_5Fuhfico;Ald_YYG z&E;yxUtz7DM2x0d&k~H7*|1D=dG?igwG>fnBFXUJD>T`Clcf&pR2vsFv&DEN7fDPe zatWkW-}?~Pe-z1+K^)0!rgw0HBzK-q{9L3e;~IzEH(l{h;=@eUS^3j>M0M8{bdeEI zL#vB;%Pl*l#(h1^ap3>PIqG)u$ANmUOLqT+= z^R6rBC2WP8;@4I3J}ZK#ADV$d0!*cI zq&+qRP%b>1n`dl%XXHGtNHcf|>RYtA#~u;lk{XXIu^JHQN6Df03s?`J`RO0(Tw*((Ckf-2AKD}MmD?78e z%qi{MyL7gF*-gANiHEsT{I;dkypQW;LyC^fo7v$zFF7M!Ap8+LH-c{?y>$rmjF?HDAAhW$-u!4f~Q12l8pb_kvkgA3zc;s-$iQ%(BOs?*}9+d^?`H*i+taZ9$h zI2lFU< zQN>%ybiTV(KDu%@gu*=&?jg@+uds$FR~nB3stbuCOblW00@sC%X^vLDx|Wbqu%VG7 zw>^Y{pAmmJcc75UA>2xA#T$9$J1-)cqBle6!G^q5oO<~oU92(Lo4L+bRvJ$$-;+GH zJp`ejHhChhBUsSX`z)ShOX&E~~T~~fv z1PdS>Nkn6roZ@|myu)=~rhBjA?e3c4e&YH(z-`f>=G<`{l9ZJ7^4G+X?(pVxDbYuPdUAeTbv(6}@BBwp7h0j+^Y74AcTS zR1@9Udhhoy1elNS&+qiIJ{FR`&&8T6`=`z>a!J7|@fr5s72SF`FT4&!LY*n0c(3>> zek(9{iNs=u~PWLT78Iub885d zIaRqGWCBQTUYP>2Ul?$4n=e1p93ihW>cX5fbAAky0JmQy0vbluw`J>hxe~M?uqLz7 zL1k&B@FKKSZIvr~W6^=T=*Q~0K=fKKzF!(3Pr5M#E{Xc;WBI-LuHr7UW3pD8X+T3R zD!Lf9Lv%9Au)2W-+Rl3voH1C=x=S%&B=-E6__q58D&6Yaa?j*tnZ#=5Y&NEpy?0tJ z%f5;dT-3Q6Q1Cl%CdW)~oLSRb?FEjdvJBzW5bb5P>*;DlhAoc9z)HfzH&B};9rzP{ zxY)a7kX*a|;YHq~8-{+GkO@Luth{Wl^wt`Nv-3X%VP}qV0YXUTvGQA7kXn&1Xr7T7 zxxUM9D^_trJE8$L?K}(_84pIwR8!ls`kfInNl$e_?RZ29Vv0IFqv|8C6=dULA zNQWbdODxiMo2WYF(b)S|t|3HwHh=qs6m64ySda7|UWvSo5+sq6N5oYZj-@?I#8Z{6 z>d~!D;Nh3ovN%TJq&8+KvoV&XRWbLfsl@6GqomcL)0v>+dhhLPz%i#98X+ul!Rak;Y@v{kdvgAD|fz^5C!U`tkMOe;h~O~4Yt z(?BN|uv>8(l4U4DB~~mBI60^lt8WXqV1T}gAHMggCA-r<(}PKJRk0Ki3{jpWio{o~ zRuZ)?Knfj&xbapIOn2B+E)~w;pKbv{vlk^p#{{h+*nM-OwUe#R)jX?E5PWX3jJf!{ z@EE5O9r@W2Oo#Z+fy$7b)*06^h9_KZx=W0ID*XU!2SRl6mA_bqgl~5Zx=IqYE&7B( zb{O0`-GmE6UQJnU$fKzcs^$vq(c?SZU75lD5)&pX(YXWk?x7ehh9Zvp(JI)@fra8D z_A#!cOSOl9lt{EO_-nP9QkIreWuW(lI#K;j#ez%7@fhl)bjCCLEHe%a`l#w8bYt+6 z-8Va$e16Jozqub7cxM9Gkv|{Yj>F#R^Yvnavwan}uYR$2p0_iX)xigWy9MIg!yOZ? z0Mruh0yBm7KdSqT)P?)s$M<4)^q1!{geJxaT>TjFe+d(Z=jV7-iBN`28yYotW)EGy zc_|yh!FRR3rpzQLWpv>1R9GC+O#DSQw?DINCg*?N1b3fU)y(a!^C9g#;miV|A>!v- zo2%~wM^Oc5oW-qjuTi6P-Y@#Z&2d(}+w_K)$xfqC8>&Wye&(I!PD6vraZ#O7fjx?N zbXwCr>0zM7%~X7y^s>c+^HHUo5|!(-UrE z-1Juq=484Ant^0l#je+e#qxG{2V(M+q*IC#18I2*igDFSL!R=l1FYLE9P3)~kFpt7 zQ2thPwKC^3T%>mMJ3O?$5+2jtO1(8P6~Q$XypL|NH(wflemXPh*=>qj*xeMbt;on5 z?kKr-!65(fMRTJU+2cuqj5?w#IcT3fVNwO69=_X|O;_j@q52nY!1t>{r8p-Z~rPj8$Qu_ zV{F%<3t_Vl+D@ue{2z8mnq)jNi~5^^+GUr#G$s6aYZ2H)l;(nmL^2Y)!Xej zSxI+7nCaZdTt7N*DN@5sh;&m743iY3IDHtC#>_s7t|PvpCM+%M>CJ_fZaebvDK?g+ zaxzQ}WI*##b7E$f%F5omX8XVlXV4hppPis3mig!~Pn`)Pfr|GT_P^?jY4S|Z6soR} zLfG%jTn6vi$$V0N%q!UW2?M9qGP=ic186h>)oD>3@Y@zz z!{VE(inQs}%xfR~ezwyyy7;5@-HHrMI-`lnDLiI9q2VSbo6#DL?`~YHPDQu;ge#^g zd%-nolOMH8srZ?pqpXi2!WuAoco?geV#Mso%qVREnp*G06Kdui_fXmf4p)oM3K+kuy= zh7ZT&xoEUXnWh4}iMViP{ot-$HE!L%T;aH3IkZLK-B-Ozk97-7IU|F#Y)X?K*_(?Q zy05=gnJVe#nl46#-#A>;@m=hN%~B>Tg_@~fKhcNM00DRhHjB<>R6U*}1KFmmr4$IOAssW~LHW)`4>J#$2s@g#$b&p}-%)XKQ>4rryP$FCr!$1n! zXl`;-24%?Ay~1_Cz_OKWqRnK=G>X_O+aQ`xm8*)XQAgG+W0+^6Chnn;W(o=$h|O+w zJ6w5a8cLv7IP}=<4DgsZc!mX5>Q=vteoi(Ku4@oWZboswqr(%@DpoO91{Ho+yoG1_ zPza#p3zm$=fYHekMVc+Wu6Dw$I}Y7e8k+G281U8OQPW*VcF$zhd#NgbTlXHyaLY_d z&0v=xmiJ7h$uK^yYcyBIj@LH=|}$=2G7Pdh7uO<7y>TXH6TnkzpjY(GXT` zVaf&YsixIOsn;3%T+WUo{_${^ohGaMsnI|kB-naBx$PPBI_CTZ0PU*i@(Dr5ag1U| z)%p<_sKUfOb8d_huU6i}eunpjRh%buGYxdfu(ZiZN)!!@fkS^W)wNXs;RQ_N(Vv7! zo_CJcfMaNd=v3~vY=pejRdqW^^UO= zZdjta%LcZEzv1ZOVdXniEFqIQI^U;GO(%xIAlV=f6gMSjQ>%a{O|ihG+fuMxicUGI zDzYjUO2qDV%Hr*-3BAToj<0L+Y!M>>g}T%xd4?q(a!Qbp%(^dGnV=leH426I`;;5; zfiZ~T8VIMT0NkCABNf*gb)`jWG!*d+DVX^tV7L0V5-qF>^2H(Vo6J@;%<=+qrjeWd zZTYV%lUh2IXFHu2?E357L#RM~nG~Wn7ixyTde$wPOlFmMX{!4xop2C&J7KbGEbb^# zvxFwR-qhLZD$`-~ksgN|l4$Wh@{HlF;NGuIXJa{n{bf6rD|>ez=V0gSz{qL9 z7|}5u9p?Hwe`3|yl-*sNPZLpHeV)*Cq>yWsr;#C=6ee;or833A?&b!5&|6ne$mBZR ze-M@@V)r>B6ZyFeuXK0?JIHZ+pC;b|z@YoGFb)yLvo)0iqa-B2O^XReryxk_o*3&F*uO5 zhaL+B8&-=+d&x8_SCbfz)&kB+O!7|BAPE?@ln>3K7&0P7tu2VAKKR z%~ec39MhTNckThieBE3U12%53IFHPD>w~qbsjO$6ODPCav7?rJXhrHz>19x5JZ`ET zZ)gb#q|Up;mD+*sjU0!R`$|AxO%?*$aYQ=uS{?d2OswOJ0o0X#I^)IfJlL`{S=5Vz zmJwT3Y~jNaD6Mi=;?l=)fDe@K+s-HIzWZNq`lh%$RJT2`UFV&B@D|kBI9mhP21AL# zlS@jzX^KPjJ9WYtI)y~l`19%&7HoG9iXf*#VIyl#C!CA^kiAW=0NmHT&o-}3c|xBz z6Sd0_my|g6+%WTVve%52`Ue^@fBf1|w0;Q zCynWMq^6jQ8K1llVtK4${0SQj&&yRgL@G50? zEUNo;HNTL#$eJ%8zAG7w-x(y;lw{bF7QVMt{LV8}nIDZ8GZViB)|04^XKo1;#32!s z?l;1U7{^o`Z!2Tp3B#B>h08cKMn|p|KeA;tlV^sVGtn8Mg?KkGN2cHQa(JZ!{l;83 z3MC`%y2tXT>cFUA^2@+2!_v;x?n8BwkhChNWvMq6&z2wuQ_y~>R%)AqU!%hP8^M6h zX>oJ&zgJm}aR?BFl*+AMQAB8Lx{X-1ntW58Pr=-1yA6ioz^$3=5pvdb=-fm4dYJ)Z zzYQ@S`UOD~v?1#RE5(ji#-m*NbtV|)w&(XNK43l*kwMnVmA&uN#6zUHC8Er(IdsJe zEd`e+PJB{+Ca~sa)~Dm#>g*s9JNN}bcA{VLZB32tj0tc}12eaB3ZE1t>kO3@Nj2JN zPOIH_>pt0t+;SqXI zF*g!T{=~^Q62sm+fwlk#*KH&R%Z;>MV=hyY?Tl4lS5B#fG`(j9bpueRN=duTd__Kt z{B^WSj9)@-E1nSdDX=D}`7oe%zO0!BI1;L)ONpR4kN!2@5V!&Po#L0gPxmQ${#DVL zL39S%q0rzhCu1~)WNNN?*FWSpr3P(G^a~MO%5xSlT2l4z>^`6O481b07i=6XB18J* zD^60gAdM9i|Jzfwu&DAm4ulRiK?dh zOrOX~&XwO3dh#Kj1(CUnGQi_Flh_{zz?)i%fuk`%cU|dZ$+j%Z90hyr1O;7+?{6ySeIN)uK&Kv z6i2N0*k+MJkP43PTT`ly5rCTfxdmyRBpq@2Ei8G{)zyTfX zeYrJ$v6ujtmzO})C|k(Qi_cug+(nd!#{v7bnos%AJj~iU;N7x_$u+dz_NB3 z7T*>)$(_{{z`1kL(35)i(1vosE8M)lt-jSxIT-nyw*R%L=rBEqk)`5z;tg>LBB3^P zEh;|`OLI%_T0Idnd=Alq4z53$*ASoJ%PuKZb+R)x?utfce$#OCPMuw4x8htM9jfUj-n}VLQ25_#NbZPM?Q+aR{Y;3_51-Te@658NMml zPTnVPX?FvlX$^H`z*g)8J5TRht~u->Jsp1xwDQ6IiHxgUd52WnZF6x=T`I!1)C^FZJ8(qx>GJY8@M+xBrfR?yJHd*+sslsG zC=ea22;_qlnzVo(t2;-OL=jVcW<2sPh^-(dkA{Q zMQH9jXU14)26!1rf69pB+hPfCwT%c_2d>>@{5IZ1q9ynA$B?Bq)MbhfryJ zyW(OO`6Ir8%KUpG@&L&;IXfEf|GG{ z+X5uhl~XgLmQn^I9*1jKekNKC-Hb4I%D0Wn+dbYd&0T#{#UYCOo$QuAY^c!jl9?r} zkDv*?wrck=duPm|Iim~hGg)lt7fa%4r%x`|Vh>r$I%0(Ce>&WenKQ_x9o01RDqy{znw@wy}k6?X*ahShk)6P;5?h`=5P?nF*i z?ri5A|4w6F9>{06cq`}b?9Hq1d@D+wT?S8SMz;HN@{BC8+;jrbI>g!RV6G6kX3#$o zuSyI9)&cA=Q<;t+C;Ndo0P!P5B^|5ynsigW=aX|AaF~TG@GFg>4NC<}UICzD@`!W!G0mCBW@Z}{cB@p!XUCpzY3^obZPhXEHRADY|^QmE{x zqYv|^YY{esEs|oQ5_+H0^(z-XB^p938)<`r4^5t4VP$V7W%^0y`4mNVMr-BWwWz73 zu008WhP}zMiuZXn#f|~HH$Ti~fWdPfBWfqI5IL)rE-qfe`jJh00Mj_$s0~5C@$#x- z0!fz^l&*>xpJzMSKP<*rcLVzDpP3y+pGY}w;5f2Mo)F$J%)~#WVPW)R@jDgcj6U7Z ztR`BXfFEjbl@S_ON%(DF{&jj#nsOomZn9icr|X?-jnU@ch+TE!=0xrEv~Lprp7Tb zg$9o}taTJK0+B{c{6$QykV3H?EgBt1anjd2(50ZK0-eO4>AcH;p=~zrS)!58L~q5^6iA2$CF7JflKC~>xPhE* zeFA(2qDBc6j5((NVdQ(n?=Fc*EpO1zj&1Ix$Tf+WI?qZ>8P)bs#<_hY>8Y}nNmv+n z?R^@tKpBaVW|nQ`pI}IVhXYo}S8tdUuH3X48}tL*-br6(S65ca2yN?IOfx)d6Ny3wcvm9}3< z-nritd7l;=^A4famS~*c$)4F5`-JcJ80rmkzJakGnO-xK18%g%w>5~`tWdsC+NVJU z4jTC}SRr@4mh(b=CW<^fQrpt)K$z(bBf_pD3o!zv`pn)eRf80tlq3!-F{r?F2be2% zQ)aDi4a{lv3hYiu(lf!ZU;|%#TUb{qS9m{-0XOpM{~mkI}LkC zWarGRo6jo@%3Z~Hfu!?%xhH{`PItqeXF~7(k?KrWN^Ql+9GJKv`*w0?hUvdk4cz8Vnw3Ui;>i&dn0$xRTYV;HF+&pLJ=q-iF{}{FPQ;v) zJH9~iY~$k%S2xb8Jt3A9%x=x)%}6>F_;RJ8%CTQ#1T4ne1`qZG`tDRXyE510?baC+ zjQ#9{}UXa9_$ZagbJ3?BjjcncouHu=h7EJ+sH`KIShf zF1cK0kev&3QmF#t-u~4)WG0}Ui^#p)Bdb^M6WS4moT_7BaIm#=KFxwn0a2+N@$u|i z9m}|o=B@Y+MjkVcUwXG?eoGdao`WL0IfA*CkAM$J<4RhrI=abp$~&7JB!&<$%2qoZDG>-IwY@;<$oj=8 z)1)(_9b_XU15FizUWS_IOA7(ry>R{wSH8GMY11xUQwTUV$WNKEjFNCg~{Z(=xU_k*iXCSFB4n zHv1I;lQtqx=xq1|%XForVMgXy6mR9K=gRdNxnRgmL^3vPlJm@kC~+_!D$~RBxDBcW z=gr4TI-kXYqv0+hJ?T zDfm5uE4QHGs5XSUwNg}qoluhHe!A#w-`=FG+BF0wC^;3o6p7y+2f#r_vlC`uQYN%I z8HAB>{I(JYB&v4ddWYs;)%NkaKgC8-s^NUPh}|dr5(Ty;KcMOfaqsi9{&XyYxIoaf zUTt7iPqC6(xO3m4xF9&KcGzSypAb)JE)BY_b5)#IgC#mE*E@ibmQf3lHb9)Hd5|1ecXpZuUS7KNZTHcSwARuQC+Awtnvm^ZP4e(yXV*xs zF&10S)9jiTq%5C+kPnV{bY6@jOo686jqbkKL!34OFbmZ)862u3547tp(>E|+UdK~! zIniV?k5L<3iVMq!NHl%|kEGK96^J(-u(QfdGOM^NtHFoZs-z4KR%Rt*M0Yt-55_m- zs!kk#wl3WaBBu!%^7!d4B~~IRqc{CwJB1S~1HiKp8!FC{IjfyJp4Pr|-d5Cx)n$Syk9zsxx04CS{#~+^6;(=KMqTDm|X2T$|yN?mClmBK=nJZ zZ_T`M6}Gz!=a+4qRrCQTEWBbRy|3Js8elF^Pi@AYg1~-*t8}Mr8Sekl;5|=hNJmzPpS}IK6O0skdh!)`^+md@~=$K9uK^ z3tcASaM_)C^Wp>^&_>Qs?xxNNDfXuDX12=i;a=?o_+DSU_2lK^B!!B)A*W`&fqJWs zH2M8eK3h)4EF)?426dz9EwLwyi|LKLvelG)gH^%D$xcYcb{oA+G6nHmBf`5V;U32g3q|lwu7B=2_%4Ghb zf6sR$phQ9h<8}KdCQQ~S$}M_NHrk>QZ}`Hq&*u)Vr$43&;H4(3VbkwI(zSQZC=ZL> zM==*6Ne1ZVludVJS044=*$nMi{6f_ms7PNnwTo#Kd+%A{*G#;ZfYlLU+6qXd3yxsmI%bpKt>`gaEj-imH^=P9E>GiQcFJwzg$h;Yb}@3utu$ofT>gjwp5ut?wWeLkVE z_vQc3+?xl~xUcWSt9jO7s6->muxhm$RvDVhkRh=NQEN(Q&P=T$DN`AXY>AM)QK7+9 zi$cZ>QJN(}8k7dDTI;=@?0w#IobP!~dVlZtea{~bwolLVe1`jT-`9QJ*KLLj22wld zYezOi&P3xr#5q4kqRk)ZToAXwha>zZn;{1z+WM6(f3*)OXyYKm!)XE|`^Vg(9XxC{ zAx{SwmMpf%YYy`MM9usl&nKEHIHRx&ZOeWUK?92cC1@z>1>vk8%^GmE4Ztq&3LwD= z@AB6WG!QybHiJu(DB2d%})<{HHVNXY=Y2J4B;#)E_al@7R4kRBp1 zDSOD*AQXY*rX|#ZyXrHhLhdNKA_ z!wtNf9b`qIo(4ibetv)60eJ}2%p%%02P>5f;Raoi@Fai@{5aVFe86Lb4MjaNkxnJq z37DTjo-zok5Au|O^)pD@2MQX&pG3XtkIX4yLDBjLW)t$aWSj_GCP);3J&A@k#J&ZU z4RyfK`XaiRl2+iM2L!m2P!)qZF%b)*j2T=3)WqS0LoJpJDTMMA+MI)yhYk^hH%?T8 zA|;9@SfHUW$ZWhbO7$R44LI{h-7*UDfJniDLI4seD3bMr4jSwU8Z1Dx79bpYT_F!o zT*jaZg17ZwM*<)E^^wG}gv_7Ak5h)U3T{xM+Y%^Hpd*ny6E)z2+T9{ zMAN;$Ze9cvV3na{8w6go9Qn^P&{80%EA`ZTr=$irSPnJ2wNW|ml4)d25 zZGVJLbipO+I^j!^Jrgx;&_;oBE~p~d@o>oiHJQUjfrtWGuLWn?U~K|sbC8}CI7X-~ zM8|K4!~R+n31v5;fdo|lez5t$tOiRHn$6g>bQ+m53d{g@9xTbv6& z7LvIISqU&I!t7toegy{fPW0q&1*p2HWk3R2Z2b z4?!+SjzU!Z@5q(l_c@_P*3<E#u61{Syu|P%yX!8$d11kPu@gYS6rXcz)k&VPf zLiA08NMirn60?M6t4Oxtb|CXjP`F26y~vyXIN1Yd5E44@Nc*$zpl<-5$IDs} zs$nFUF^DQ?DS# zutVqw1=I~%v6e7Qpm35s)1HpnKn4=ozgYW@{4luq49JUsa!#-jLFz%bdW6bQfCjmq zZZBlQko1(|{$Tv$}ygJ+C#N|16bVZ_OviLOk5yzF7y`|lMXl#sUh?(6J@(Ex7n(in8#!K`@529fW-PpNWpf$mPRZAS%=t zB>pXUW`ngR5Mp$MR|EU^BSY*A(~TS`1c(pe$3A}#~5j&A+qz3XOKt!^e7qte1++|l(185`k=`F#KtvggGpwD#~12{3{{&`|LOfB`*KV1La~-9a`I*a18lI2!$X2&V)h+(0&PMS(X@5`Q9X zggJ!-0T6`#Tac)mK;8lBC4`&|+3o{#%^F;608>BIl0YtnKLjTS(IN0ylD7rd2~2HN#BDDjt;r<{Lq-BzwK$M+8L>O&0 zezAWUq+AEP<57r#s#CalzuF%Ojl&?C3|UVLIQ0EKHSirk0@{$e%=3NwTCYKYQ6 z8o2f7OGNBjf_n=lA`Uwu9{6=6yAM_f&Etr7{!vX1JA~*3$Q0r;7qSXjfM0{Xvk@Rb z>lcCiU$;FFQJfvD6>w4D$z*Rk=sg6!KWfUL4~V)=VunzqOI%KH#ZW#*mgfbZ9S6+` zfX9!wCD^5qxIj(-h#Z|1i9n!(rZ`A2BZp2{6GT1eC1wHk0NNyx?jeFsGdMehE{ImB z?n4Oii=CRmY#Qv%OB68;&YEDgf$Bg$<1b5%y@=2Zm^`jxlq!+z1VS_%;R$~IQJ)Q< z)5*cy6qHi}OC}!)#}pZ9BwPFQf&)LSMf7{A@Q(pVfocr?Fi|B!_Dqx> zpnMm391tk5Z(#%-Y5j875WObjjiXl->Q2xi5S;IR9BjavDlQNtA=<`&_sBRNMREl zhv3qKs|C3(7%)c?dy86!LGC`-8gzg%g_toGgm_z2%OkB$;1Pg3$@CK-+BVqThiBjl zfxH-|3UM}0CD0OtazqDaL{B6$gmf7c-*6-SYPU5Ul0n`ME_7tB2!ZLpS(nHo*(2!2 zNk_7ltkMwvI+8hX;vrAjzi)}l5M3Lf*+g&)N#bmb9Hdu+4D=xH6(ZA#gfOaC;B^wa z4|<}gen%G8ltzSFB&&i^7KCdHE7`w?aIUe3php1ohen2ox&YKc&KscLj~oCjA39OM z973xM86mP60$2=+2@-i|Faz6%(2#+*1TZ2*A>D zB2~7Ag@zg}Y%?nQ`>}BZ>|hxnGXwAjJ>$uqIq0)uKIk1!w2x)b;9A1C0yjYs608Xz z6ZjYw@U+R^7StpFia3gZCkgZeqOiahpcvo;+9*kEPh9rkOF9C(`jMjpjl9255O)&t zu4E@09yRg@z~n(pATo;Jw>qJ*FC>G3f{^uF#xsFMARR<#ixC+`iUI!iL(L? zdf?M&`-8d^Y7C=$RDLO~8#A(>7-(Nq_F976Ho2OkVzEHuBt zv?pY?$>L8Js3qYdWVj&W?6=fb$VR~qLI46ea%Zv~k1pb; z!3q0S4J)EIH_QQQ=l@z2bm_CgZI8lzl$q1X_N~RBcRY}6=n>*8&HJ-nxT;b7j0g#A zLDc1u-S(g&4)Ux(b^*%(Q8Gd!9eap|zzkOlY!DQb%*Z@O5Z=IS0LJq}vjH&#x|=|T z0h{@6qI*y&IWLDqsu zgOHq0lmQd9mxQh*@tr8{!L4NJgxm_Dn?-^OK-v_!U9jW-%G(dTlN~+>D(a{Y`hi41 z!WasIgino@2_Wi`?RcWm?BKj{H=p;h{;2sexz$YW69 z0r>F4zeSymDdHC}1qo<_D=0eT-!CG6`zAugac;_#u38g31;x&3J*h(v^f8d@)yq1>J%clB@2#LY`c z4WKp`kPDfD+P{x9N1_EJT=?VGPzn2mwfhdWz+D>tZ6qqY(Fp}jdZD4}NX8ZTw~+&nbbk6yYg2H4;eFv(u=mKa?>|2i9{d0i z?tdFe6a-)wBjy4aLDq%r=aF#e!2)K$&2ffK9GN=$zm0^(1qcR^DS)dA8MuM(A>a_e zYEZ8MhAMhek=@0Vf%5H6(BBAwMzjw?!8Y7ZgB4D-oC) z9AC1LNS&fs1(H|4Se*g*-wat+fIYuj{|PD-<`HlNnnCsZLui0O#Lt#6+UT=M`kg>v z;hqxBRQhwS4Ad1N;edb;yU>E{wueFyP8BMipqxrD$)IiluNcyFAOq1!y&6zE;rSue z3sn;W2SX(5f;2vZZh^{BG8K5Vsu-+?fvEmpEHwm>h^8ut?+{**buV!Fc_hRX5VE7N zjVMHQCff-=jYK6A!v#AKjslz&OEPlsAHRMxb`+a%oA>fRQQzT0pY*XRhxgCui`+~;6@6A-Zm84Chp8DvBU z*B_n~s7=4h{U8{|zBS6fFIZ&E#o{xAQOGVp~;3c z8p%$u{%ObK=7p{XEDVqd6x98~$N{TbKvwl%PZ`R-kkfHM0T;@mNMnbeMnXgnsE4_! z6I2!Oc(Qxt=aDGk1488tI1KDmQq1_Lkw94ndY2*j2k8am0H8@mV8#RPm;f>;L{*UG z{VOjB78s=M;KA90r^9e0^BBR<1r-6<-Vfy>6vmSzn#+D{~+dDd$d@#rpK<*HfBBI)i z{LV&;@IhjXAdw9cDFg==MG9DU=u~2gU|gWQgu{v7p_+@#zlD_RAa5j);UPH%gyx5@ zIq<&H5avQNC7c&$2us1k7~oGXSbaBg@SE^h`kAW&ncGnFkuGs5T^fCe$e$apB{C z1hNG35+)TbWB@8gw8KoG@(7YBl0HD zNg;V#I241#-cZo`FZPze69&Zj1sUpJM-Du;|7j%nX9MJG{`|~eu+%_lkw*j-;fI>DojD7hTsdhutWw5 zZBLOTfj$-BF{JKEo(WibkQNL$#UL#hiW*SjfU;^31aVdf_AlaKI)rG@fW?6|1oCLA4gR2*28$3(W^nYmb$O!bAT~KqET@c@V5Kx3(}N z>r4n3X|Ns!^yQIvLY^4(3#^Yd(ibSoAQTR)A?ZPeL=d{AgUe#N4ElWbSBL!*HjX_6 zwkHZ@aX66?BUzQ7M-DuS{AuJbV5u%BSw?an6;u714OXxxVDkeSLf8XwC$hIi(`20C z{#HIjR(inPW~3eA%Q^%0`%9@D-{}G}Eb8^mXjo9PXA)30fFQFUctXFDh6Nq^(qKGb zp~*%LIu=A)170EurcwO|tBHMxzV!q<8&r9e`;$Er?ITe0XZBYe`oR4mra7I71iuxP z!==-xs3PT1L5r}Ya;%vaW~M%-K0a2Q|NLC0r4Nl|>O-gcSo?6P<~}SomBnDvsFrLd zhfU*h0OqhsMgw+1Ck!T)LF1TF@ws>b8q=C;&7!lR;%3IOVv>wzp-hOu<}#>sABHK_ zoatjp#h1{i=4=+t2W=pHd}t)=%c7fc%-AeqAkBixq?!9rSsc0@zqq5kROscsRhe-u)%AqpZ)>dpY3nrvFNM?;?MdNaPtgWaPrffQu!8W&~ zLS~ACUo6b%_zx_O1&id_$b|ctaxAC}A1;?l)N4|iEK46M&4PpKd^j=4`;d%gnKLbc zEKq?Ev#DH~Ih$(jV{J*Ln=@HVgsw~rHpzFhE$K`vCd-FvPBUjw%~^P9D>Dv_%Ce^W zAn(crE=sauY%5bHlgnUJiMhvUYjY~g0_)9XnX`$s9Fs{WIah3Jwz-cL9&2XJGN&R* z%cU~8OdqO`l_l5IoXa)^Rqj83Hya;kMPpe|k+Q~$u`DrbTo#k+gWiuI1e!CU%}+L( z#f1_Ei)!k_U{K8=gn;ih=Mr1S)Pl=KyI-=Hg9A-Y45U+8bQV^OW^P5brgK)j*Qn=RdzWhhR=XxPxlSrdboG z1mBIbNi}1#X&kzbCDWQia!+u~IhHK=!mtSzcsADpZ^7cSs6JfufTS@kSY}Kv$+Imu zOdn+CaJ*n5%(-+^>@^E(*cKljwiV7OgH4jj=UAF@iIyQ$I@cPu#TqYd&1S(mSaA_d zVY_k7N&F5D&~_BtGEae?C@bY$_U0a;O|E0oI#tMWtIZ%{eIFWLR=YW{ofna9Ux= zSWJ93oA_>1E7%q@bB+&wXT`Q4nKiDD1>4G;jr9hJ1{Rraj+1V|pu(nG(l{ua=aR8W zxjvQ*j2+?|&8==(BbMGlXlu5^q0E-<}i5;_ABn{aw0_j=DyR zR|RkaH-wJYH~EjR9vHBm8}d&tAH)n{`g7NELqezN`-BAg>#p)=uH>2o1+3Iv8>q_+ z3i4aUW)h!mvOa)g%p@La{Ex}}&(8^Et|y-$xK5U)*ez5u7EUaPU_P)L*pH?xxD|9J zozC|8#{~cL1X&XkM5j-t6LU293Hod4*uKnwm0Zp=eUtwOp91+^>_-}|XyU>|Cu-c4 z=D3B}947wOl1;ZT_xXDffBF=&g{J0{P0c2oG5%LRh5cVw|Gz9au_ykqLH_fUakwF? z{%QUHd5`_$6?FS|+y9y8{ny; zK6U?ChyQmEDve9Gwq}}|QW*>u?r9=;fNzN_+Q$?{Bam6(np^#csk2;Y%9u zU)kONbr&Fe3_b}y#hP$XiQ0E~Sr+DSo6O)aqD}}=*uQ;>HDQ!#W(#RnlTGQ9&1nBC zpYo4W^7rff^AY+#cyI>3+y0jhDhG|xd?15CwZwgn+ua;4odq0AI-6TANg9ce6Y(6@NZE3;v@j*|p|M^qQ-~w1rrdi{X zn}}b3#RY&DVXbR4hsj&A1i- z0f-MGep+D2s+Fq(n0|k|bALNuoPj!Dgp1Ta4NlxpcF3wA!pHghuiTwnh~I9UjJbew z#Ka$LHwH1;Yq)E5xdCt&LiBBI!p3L+L7~hvndxM|aO3f&-!~+U&N=#?SM_96(vno8 z*kOmy*U~y4fB6#reR=VhFTQ@R+1t3g)SOLsXC8Ij(4up0g@n4go!Gj-mB$;-pS!d7 zw;iF?5$b72-`h@lPSY@xx*sAiJA8jujzGv(I-;l zmfo2+L4K^@Oqw2>`X=Fqh}+oe@mcg(QZQzp#juldcGl4z>v>z2Hs+Pi>U5nyfpR9Q zWm)kv6LDczL*Yp|d$D7b`uXgpH8X9$WmQh@)H$aqen{RfpsH)Bk$4y@UO&8DUjCV+ z?!K9I-Fjn8-pmcN>D>HjQ>xpV@BTLqb&svcEKZ2AZe7l?J=fTdUR^l?;=Lm-PN0k} zEWY;rOw^^}5lc-o`j)hG``vEnafleDnxJ1XY^(8lzcfnf)NStR_~$z>j-yx<&bwA| zCTgE*uzO14-btbS_1@Dns$Ww>tLTjv3u@%$@;+Wvk6g@u6wr67B0K9A!}F~DmqV?G zqKC|p-JJ28 zuUWnEMc4X{t7w?!=(RPAbsC(~k=#d_e&fB0zQD%iqG&GX{mP;F!>5#%Wtm2O-I_Zj zHYe()#88QLacw=-T$#$|BU7p}cUHBf3MwbwE08tVI-@O7HtK|ZvTl96{OFLB!svTG z!y;bk3gi8kO`6NNqa!&);(o-?s-77ew(2~+So0ultg_?IRVSUsY@M`0Wb#dWgz7b_ z=fR?XiF?2qf-blTRcp(?ad z+rEb15g?emq1W{ECP^>FBQq4X%@fYtp{qW^Z=`D3ByqRZ9j-II9NH3hFNV`}S|OE62#^+;|tKp*(zRV1c@r z!MT>+uNIqEs23ZiUy<3T6uEZ-B`Y(0)z;)I&2NRHo|fylIN2)P+*{c@v`C@M z(IAYv)pX;w3!*v0w0k28YbUpjcswztV@%0LxofSu!z6XI%C*X3#NF!J5|;KI-!ZRg znP0xn5&IcrEgSpX`I09~J_{Wa4KFzuBHgXBL%L>NW8)tA+xnNz;d8u(9nl!R-M!1|H%3OY zv$uh)-M(InMfrudy50$_@5PGrF87>s+nTSiyuhbvvHF!<`*~a3$~wliYPU(}aXn=( z^jf{PaY*NP4ViUAX+bsr`iRI5W6Ht!{Nh*hsG&yTjJjkA8@s)Y>*}{Pv)0m{Y_{`i zn~*)k^x%^FN3!lI22?s~X!nlp){8$ zTM5**h85;q5Xw3Aw#W3IoByu-N%>IQ*S~WQuNTsmYvrvm4w~fJ%y`i{`8#`f_FHMq zrq=8Rja$uY4hs@L2~R8SId)&}c1=Y3ew9kj)zXv2p|dwhTzls2_+IMuY09B{g0V;P z4|P`rU8>i_1JahY$Qs}CYP#3Nurm0n;3899HhI*h2IZGZ&bQSiH1bE-ykQsY>r1(R zLu2$w&hWi!=D+fF$VeBM?2K36ARWsu<&P~n*ebb!+T+Cc?qVIe9PeG#AoV(lGW*ec z<(z`I8B8CZ_T6|L@nqdc!Qu(8ye>-n?R`+xTv}{bC3oCNCsk}8>vUXibjDKR&tb&mT_vYk04|VxA zB`vuky2Qqb<$W-)dqiHUt=0=A+VItPFJ-mpl}V1-vfrr8%}48vRoqbNiF?XFgf+}! zo)yF$F|gXx+_H?HkXBkd`FgeGV*3e&itXa@L*y)HukgMl%O6v368t?(tW~Q(v-V|x z_0Exv#*6Zp#}4y$Obl9Lci2Dwq{}9W{o!I3&!)$;`n+kHa%aqMdp+p78yb1Cxsf+Sk=8)M~BO(WZY^D!eJ~cwxiX;o28mRXx^-8R_vRRHp}bU7RPPeJ&Oa zZ(TENsJ6>%=9$XAj6<(i8DUyz08ohHrw=d`F&$QQ*(F!Ia;>3xJLPwvF-U|H`E(lBwvS2 z;-?aG-S@p?uKpU)vRZa(ufRjI^yP(!yWf01nHX$nZwL_CT#BwT4EtlAt;;u0p6w1n zw8k5@Ku^`Z;=G_?u}8C`Ha~cb+9=WS`6jQG&*f!@_iay`9~NCC(mtIjG-oP(mI~SKU;A)uWzeGCjmf5S zdpzIig-*&DBJz@BIqlwW>v?VxH`hb?e(3QH-V$5))kQBzzO9sTx$uj*R7Kl}&MKK8 zxyL5Y!?y5!t0FFjerz1;!8pNRD$Bq6K*u2OTJLn#x4fN_wTjmtyxiBZx4SsVaj{8? zbeNoag{{w8hus>V1fD|oM_#Kt78Grg7<+s)FJbt*oc zn7DrIhe@+M^P;Ejb4+X6THO~{v1g~GaI(_`(~s=sG5pmEcGT~oM}-<+FK&70DQxH4 z8YGA~8>Sx54^Y$inyk+djr_wtKf*v5=DFT_L}^0qo->7Bu3{PMk~@^)K^0yykyd`z zaU<%F?@vz#YXqLYd0X%7vZL19%b!j)o@o`Qc&{`!UUyx!UhkFz6AiM9(_>{XoTbcm z2(RcIy46&yY+pcGlF{?7EejuRIrOQsQ7}TJ9nxfSGJ3PzCd!8oT3;RSbS)8j-bpWM z(wQJ`-6jy65m#vzbZvN0!FaUFp;dg-oDD%MZfmZcSHtDr^Ldq^J;vcy#?&Uxn#8|$EQBU!k97G>)^zOsVPc6PXPeHbT}2L;Y?lIMa` zZkr$2v4{~Q&iMYw{*Z33c5L2zDR&R~?9wx`7osV%5B7$A70U4QPa5Yjf|m$adCMhK z-q>~FIU~*9({OI@%%bGW-G4fV7nnQoYv%5dx^jEHpZ|^p3N!q8PU5|)0arg<{lkhn z#_I9M&{69RZs**L5QXzbvzG7TFO83UeT@tVdt*Ddl~dSVZYSoJtM2S z_=}6gm+RR`%-Pp_#?SN4;G$ zJ8;zfQToD)+>nN8zO!%7sXuy%6(f7WXwgzi$X>S5cmE2P*54gcMT`>4T-s;_Z%){* z65({G-axnQH@0idKk`}O#M$>Y~sPfxO>W3fGOLUUGkWg!WsbrJkLj_OvRO>{?i?SEeG%lo43XNTAo|udktxqjX zKb_adb7Tl*uiGE|Lr8yGCDPQ>eZxYjz&64OgkeAbWcVK%M zEzodB<+>9S7R{o#@6~%RS29N6SGSWf|5R}iFXq!2!HE5*Y8--v2{S0+<3=|$#Mq`L z#npOjoa*#P@a)siHS}DoWr80KKTvIZWN1X|4R;4`eQ#xrYdfQAC$tTzab(t|R`JbZ zzD?iHFLV1OayiwWD^NbudMm!qvwnt}__OK4LybD}_PcroD&Ki=MIy0%y$AWM zKB@XTk<@3dOnHuAVtp`=({lWyfaIlI}ickc4OB+A`Z zaw^M-XV7K_cuOaYnp*sEs!YQE4P`P@)NO~!#v3uWbNQBh-9JT!hw8r{eeV8j)c2hp;rbQA$(6=$;+VdtW8aHK>$}vahaUIF&cu;#AO$&%MvM(Gb3 zPp{~Df8-D)%392Jl5D$#<@tyAjZzq&yfgfZC}FjI&59GGwc1tB3k=8%S-Nh z9}`!(5pzvzsl!&+Zq4r^&F*BYws~sBM<4NYu6^e@EUD6oZ}c@Q>Rcjpqd2HWn15v- z$%|^8*i@dN+o=<6{xtDXlDx8yrh0^0Apg#``yZ>TU8?z0(jt5h#PgkM!(56*(>We@ z1hY9hZLzmo`xXq1TqS2`+^jQe>%^w4sN}o7FLgy~`rRhJ9~&BsKICspQn_>CerkYi zY#8tNil?KO1Dg6V(k?bpQ(Q5&FHA2ZTv!_XMz|`zM!Ln*IatU`$?Hqn)7e;hz;+zP zsZ(x+LfrFEztbn@DfZZCg^C>KdyM;CQ8D3KKp%xMufQQ^>#p*nO2#9%xk*l1+k14r zYDT+4%dO&A4`V*B=DNI?dvN~fiC-xn^dA|UZ);)f8P_{*tjIC-G0*9UNPL$7`K9o^%V@LVZ(_vbE=&ShbO=I@jjhGAZ> zZ4D{3>ashcgB3LsisT~mbp)Ol!rrW0_}a3(49 zdv(azJX0~<%T4J8O=ZGGcir?dN?8vawp?_g9GS0OJKbqf(&oOYtG76Ami2KKwWRkJ|t$TWu)DJ|K&ZAr6zVr2cM zk1a}mduLCA(Xc0a-$HuZ?d-`_nYhbk1IHihgXGjK^PIg{= z{=G?N&))8r)Ay=;+a%F>qU2Uv+%#vlOmAnaWLj?mI7gfAEuFCySMk{^gZ2S-y>ffxBJ{RDeAbr zf7@as6N@pXA4_E?otzq>w0!RGjv9ATr$*jRvD-_jpKHbK2-S|b+qZK5nPl$roiQ)y zmzy`Z-ARw@y_Rdc^1I$Vp-b=KyKaHHi#&&hul&+p9Hj5<(r8wnIpovP^62vJO_iss zyq>kJitd#cZHZx-CYsBrOz_*=7${-r1Y2^p3IZ@%n<#dheC$9mGS=CFr+2xLqt)w$Qub`HVZ|LF=Rw zhV@eAm-z3xc3if5nuJQjE}qh^Lhq$)vDM2(eQEV=Ile2pBoucG65L83I@j7p+74}L z&~XrcXbV(~sZQy^`(yzZjMTbm^&Xyt1tHD*KU4+11X@Q4jJ` zy)SJzJu*x@{)$i-KJ#|mHDTasn^!r{r7ryTlsn9%ZyQ~he0s^&Nk`Ro3$(uH3}IE< zJso*uquA@GP4m5#pO=Y=R@tY|ubF1G+sgZRZlvT{lVPo*8@%vnmdVL%tqk+;>hjx@ zJ4A|%gDvs7Jk{~p`-lFQ%!J#t6t!8@u{Bi)`*Ak zP5ar*6%QL0ew0g9+jgs{?~P~n^L+`|MHO!MzJ)DZHtp)8KsoMg6a7abrCBEvD~5#% z7KHVMeJ(G`&)lyTSt$suwkvz;buzn4Y1YTlcDE$5X4)%A&D9U^yghAeN%hsmVzNS~>g`>(YyQc~p z_m$Ur#YbLI$;o!DaEQDTV^FH0B;_DBL(V>?Iy*@%VrgUY?!Jq!MF!?ic`-?n#?2!6 zb$31WM1FZb-?dK8mFztsUNAeO$t2xP>*lud`uStT4d>rTkDR05rZ>x1v`s<`q0)2 zh13es#F7y_C;xWYwvB5-JkscZ<*)^%^ZZDLqN_r}0GP?Fyz z?xQ9M+vdGsYgp8Qk&Z4sw;Y^Ry*4~DDVdg@zCT}Y=Tg<8{Wa=hTk4NzRaB%=OTDFo zKbY4&8(;ZyPiRze@Cf};-|%%gH95j|qfTA|b2x8NYrDi}cAq9=>1wsQ`!Ur;`Rn$t zRz4_uTYvSRwx_cN6009w(?6|o!J&0|;h9Zm8>D*QZJZ#PSkja1)VzEA;)@-;$j_(p z9hyrdJYVyCD_aacNSXW1`IfbVm_y5sE~%2}z>}8Cw`(3Z>=UaiF~1q1`gn7oe%sj> zk9it`{JmSgg#

>Yvp|~X6yDxUT?flxj)t$a%;QQ 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�R>aQjB9oI!EpJo00!>LKYVJ-AR(0vZg zg(M;+D_&!VKQ61dJ8`r%>#-JP%tglYrkj=v#g(@o}~;adv+E4 zg)sKLsQNd>h3)K6oM)_4gawM6N87$mj529pb`4~XgfNnj8H@FkH^s{wx`kZ6iK$?O zs>qL(-!S Date: Sun, 3 Oct 2021 15:40:32 +0200 Subject: [PATCH 083/170] Proxy slider head circle number along with overlay --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 22 ++++++++++++------- .../Legacy/LegacySliderHeadHitCircle.cs | 8 +++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 3afd814174..8b45513a2e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -35,8 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable hitCircleSprite; - protected Drawable HitCircleOverlay { get; private set; } + protected Container OverlayLayer; + private Drawable hitCircleOverlay; private SkinnableSpriteText hitCircleText; private readonly Bindable accentColour = new Bindable(); @@ -78,17 +79,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - HitCircleOverlay = new KiaiFlashingSprite + OverlayLayer = new Container { - Texture = overlayTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, + Child = hitCircleOverlay = new KiaiFlashingSprite + { + Texture = overlayTexture, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }; if (hasNumber) { - AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText + OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, @@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; if (overlayAboveNumber) - ChangeInternalChildDepth(HitCircleOverlay, float.MinValue); + OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); @@ -147,8 +153,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out); hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); - HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); if (hasNumber) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs index 13ba42ba50..7de2b8c7fa 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [Resolved(canBeNull: true)] private DrawableHitObject drawableHitObject { get; set; } - private Drawable proxiedHitCircleOverlay; + private Drawable proxiedOverlayLayer; public LegacySliderHeadHitCircle() : base("sliderstartcircle") @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void LoadComplete() { base.LoadComplete(); - proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy(); + proxiedOverlayLayer = OverlayLayer.CreateProxy(); if (drawableHitObject != null) { @@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void onHitObjectApplied(DrawableHitObject drawableObject) { - Debug.Assert(proxiedHitCircleOverlay.Parent == null); + Debug.Assert(proxiedOverlayLayer.Parent == null); // see logic in LegacyReverseArrow. (drawableObject as DrawableSliderHead)?.DrawableSlider - .OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue)); + .OverlayElementContainer.Add(proxiedOverlayLayer.With(d => d.Depth = float.MinValue)); } protected override void Dispose(bool isDisposing) From 5e5cdaab5ef1a931541fe76941a4770624b0eed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 19:14:01 +0200 Subject: [PATCH 084/170] Privatise setter Co-authored-by: Dean Herbert --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 8b45513a2e..d1c9b1bf92 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable hitCircleSprite; - protected Container OverlayLayer; + protected Container OverlayLayer { get; private set; } private Drawable hitCircleOverlay; private SkinnableSpriteText hitCircleText; From 86240cc8ecf570e9c9eba1760db1d651d67c2c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 23:36:39 +0200 Subject: [PATCH 085/170] Add alternate Torus font --- osu.Game/Graphics/OsuFont.cs | 6 ++++++ osu.Game/OsuGameBase.cs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index b6090d0e1a..edb484021c 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -21,6 +21,8 @@ namespace osu.Game.Graphics public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular); + public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); ///

@@ -57,6 +59,9 @@ namespace osu.Game.Graphics case Typeface.Torus: return "Torus"; + case Typeface.TorusAlternate: + return "Torus-Alternate"; + case Typeface.Inter: return "Inter"; } @@ -113,6 +118,7 @@ namespace osu.Game.Graphics { Venera, Torus, + TorusAlternate, Inter, } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index adb819bf20..02de92e805 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -347,6 +347,11 @@ namespace osu.Game AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus/Torus-Bold"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Regular"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Light"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Bold"); + AddFont(Resources, @"Fonts/Inter/Inter-Regular"); AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic"); AddFont(Resources, @"Fonts/Inter/Inter-Light"); From 67d08a3eeee036fc94fca611e4986efa3002c372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Oct 2021 00:20:16 +0200 Subject: [PATCH 086/170] Add test scene for previewing Torus alternates --- .../Visual/UserInterface/TestSceneOsuFont.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs new file mode 100644 index 0000000000..eedafce271 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs @@ -0,0 +1,77 @@ +// 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; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuFont : OsuTestScene + { + private OsuSpriteText spriteText; + + private readonly BindableBool useAlternates = new BindableBool(); + private readonly Bindable weight = new Bindable(FontWeight.Regular); + + [BackgroundDependencyLoader] + private void load() + { + Child = spriteText = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AllowMultiline = true, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + useAlternates.BindValueChanged(_ => updateFont()); + weight.BindValueChanged(_ => updateFont(), true); + } + + private void updateFont() + { + FontUsage usage = useAlternates.Value ? OsuFont.TorusAlternate : OsuFont.Torus; + spriteText.Font = usage.With(size: 40, weight: weight.Value); + } + + [Test] + public void TestTorusAlternates() + { + AddStep("set all ASCII letters", () => spriteText.Text = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz"); + AddStep("set all alternates", () => spriteText.Text = @"A Á Ă Â Ä À Ā Ą Å Ã +Æ B D Ð Ď Đ E É Ě Ê +Ë Ė È Ē Ę F G Ğ Ģ Ġ +H I Í Î Ï İ Ì Ī Į K +Ķ O Œ P Þ Q R Ŕ Ř Ŗ +T Ŧ Ť Ţ Ț V W Ẃ Ŵ Ẅ +Ẁ X Y Ý Ŷ Ÿ Ỳ a á ă +â ä à ā ą å ã æ b d +ď đ e é ě ê ë ė è ē +ę f g ğ ģ ġ k ķ m n +ń ň ņ ŋ ñ o œ p þ q +t ŧ ť ţ ț u ú û ü ù +ű ū ų ů w ẃ ŵ ẅ ẁ x +y ý ŷ ÿ ỳ"); + + AddToggleStep("toggle alternates", alternates => useAlternates.Value = alternates); + + addSetWeightStep(FontWeight.Light); + addSetWeightStep(FontWeight.Regular); + addSetWeightStep(FontWeight.SemiBold); + addSetWeightStep(FontWeight.Bold); + + void addSetWeightStep(FontWeight newWeight) => AddStep($"set weight {newWeight}", () => weight.Value = newWeight); + } + } +} From 017756cbcae754236a4e6cdb3b37f0301121b6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Oct 2021 00:21:36 +0200 Subject: [PATCH 087/170] Use Torus alternates on online play screens as per design --- osu.Game/Screens/OnlinePlay/Header.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index b0db9256f5..2d4b5cc527 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -72,21 +72,21 @@ namespace osu.Game.Screens.OnlinePlay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = mainTitle }, dot = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = "·" }, pageTitle = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = "Lounge" } } From 11e9c16b92eec768d23c389329600ca89cf0e2aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 11:13:46 +0900 Subject: [PATCH 088/170] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index b84f1730ac..eeca40e73d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c162025f1f..33d4e5a6c8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 8597a06c03..e30722c334 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 537b29654e60e738128fa123345a01ab39074b22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 14:30:22 +0900 Subject: [PATCH 089/170] Fix stream being held open causing windows CI failures --- osu.Game.Tests/Database/RealmTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 219690db30..576f901c1a 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -70,7 +70,8 @@ namespace osu.Game.Tests.Database { try { - return testStorage.GetStream(realmFactory.Filename)?.Length ?? 0; + using (var stream = testStorage.GetStream(realmFactory.Filename)) + return stream?.Length ?? 0; } catch { From c6aba3e78b3a9f8cf89e60c5f4b069c2873532ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 14:44:16 +0900 Subject: [PATCH 090/170] Ensure a `DrawableChannel` is not attempted to be added after disposal --- osu.Game/Overlays/ChatOverlay.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 25c5154d4a..a61b80cc8e 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -284,6 +284,10 @@ namespace osu.Game.Overlays if (currentChannel.Value != e.NewValue) return; + // check once more to ensure the channel hasn't since been removed from the loaded channels like (may have been left by some automated means). + if (loadedChannels.Contains(loaded)) + return; + loading.Hide(); currentChannelContainer.Clear(false); @@ -426,7 +430,7 @@ namespace osu.Game.Overlays base.PopOut(); } - private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) => Schedule(() => { switch (args.Action) { @@ -444,10 +448,9 @@ namespace osu.Game.Overlays if (loaded != null) { - loadedChannels.Remove(loaded); - // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared // to ensure that the previous channel doesn't get updated after it's disposed + loadedChannels.Remove(loaded); currentChannelContainer.Remove(loaded); loaded.Dispose(); } @@ -455,7 +458,7 @@ namespace osu.Game.Overlays break; } - } + }); private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { From c19c2335eccdd752a521db4b869abf7d96428c19 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 14:58:54 +0900 Subject: [PATCH 091/170] Remove added schedule due to changing flow --- osu.Game/Overlays/ChatOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index a61b80cc8e..7be9258248 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -430,7 +430,7 @@ namespace osu.Game.Overlays base.PopOut(); } - private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) => Schedule(() => + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) { @@ -458,7 +458,7 @@ namespace osu.Game.Overlays break; } - }); + } private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { From bc984dff4f2b48d1a4975dfdb031eb378e4d2045 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 15:35:28 +0900 Subject: [PATCH 092/170] Fix typo --- osu.Game/Overlays/ChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 7be9258248..20d637d957 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -284,7 +284,7 @@ namespace osu.Game.Overlays if (currentChannel.Value != e.NewValue) return; - // check once more to ensure the channel hasn't since been removed from the loaded channels like (may have been left by some automated means). + // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means). if (loadedChannels.Contains(loaded)) return; From 5aaafce597e370f9c6c900d3c9f9e992499e0361 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 15:40:00 +0900 Subject: [PATCH 093/170] Make `AuthenticateWithLogin` throw instead of return a `bool` success status --- .../ErrorTextFlowContainer.cs | 2 +- osu.Game/Online/API/OAuth.cs | 37 ++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) rename osu.Game/{Overlays/AccountCreation => Graphics}/ErrorTextFlowContainer.cs (95%) diff --git a/osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs b/osu.Game/Graphics/ErrorTextFlowContainer.cs similarity index 95% rename from osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs rename to osu.Game/Graphics/ErrorTextFlowContainer.cs index 87ff4dd398..f17a2a2c3d 100644 --- a/osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs +++ b/osu.Game/Graphics/ErrorTextFlowContainer.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osuTK.Graphics; -namespace osu.Game.Overlays.AccountCreation +namespace osu.Game.Graphics { public class ErrorTextFlowContainer : OsuTextFlowContainer { diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index bdc47aab8d..693e7c5336 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -1,8 +1,10 @@ // 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.Diagnostics; using System.Net.Http; +using Newtonsoft.Json; using osu.Framework.Bindables; namespace osu.Game.Online.API @@ -32,10 +34,10 @@ namespace osu.Game.Online.API this.endpoint = endpoint; } - internal bool AuthenticateWithLogin(string username, string password) + internal void AuthenticateWithLogin(string username, string password) { - if (string.IsNullOrEmpty(username)) return false; - if (string.IsNullOrEmpty(password)) return false; + if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); + if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); using (var req = new AccessTokenRequestPassword(username, password) { @@ -49,13 +51,27 @@ namespace osu.Game.Online.API { req.Perform(); } - catch + catch (Exception ex) { - return false; + Token.Value = null; + + var throwableException = ex; + + try + { + // attempt to decode a displayable error string. + var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty); + if (error != null) + throwableException = new APIException(error.Message, ex); + } + catch + { + } + + throw throwableException; } Token.Value = req.ResponseObject; - return true; } } @@ -182,5 +198,14 @@ namespace osu.Game.Online.API base.PrePerform(); } } + + private class OAuthError + { + [JsonProperty("error")] + public string ErrorType { get; set; } + + [JsonProperty("hint")] + public string Message { get; set; } + } } } From 266b4c7124fe4b578087afaaba46e18ec4ee0423 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 15:40:24 +0900 Subject: [PATCH 094/170] Expose login errors from `IAPIProvider` and show on the login form --- .../Visual/Menus/TestSceneLoginPanel.cs | 16 ++++++++++- osu.Game/Online/API/APIAccess.cs | 28 ++++++++++++------- osu.Game/Online/API/DummyAPIAccess.cs | 18 ++++++++++++ osu.Game/Online/API/IAPIProvider.cs | 6 ++++ osu.Game/Overlays/Login/LoginForm.cs | 14 +++++++++- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs index 5fdadfc2fb..4754a73f83 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Login; namespace osu.Game.Tests.Visual.Menus @@ -30,12 +31,25 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestBasicLogin() + public void TestLoginSuccess() { AddStep("logout", () => API.Logout()); AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); } + + [Test] + public void TestLoginFailure() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).FailNextLogin(); + }); + + AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index af14cdc7b3..94508e3a81 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -35,9 +35,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } - /// - /// The username/email provided by the user when initiating a login. - /// + public Exception LastLoginError { get; private set; } + public string ProvidedUsername { get; private set; } private string password; @@ -136,14 +135,23 @@ namespace osu.Game.Online.API // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); - if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password)) + if (!authentication.HasValidAccessToken) { - //todo: this fails even on network-related issues. we should probably handle those differently. - //NotificationOverlay.ShowMessage("Login failed!"); - log.Add(@"Login failed!"); - password = null; - authentication.Clear(); - continue; + LastLoginError = null; + + try + { + authentication.AuthenticateWithLogin(ProvidedUsername, password); + } + catch (Exception e) + { + //todo: this fails even on network-related issues. we should probably handle those differently. + LastLoginError = e; + log.Add(@"Login failed!"); + password = null; + authentication.Clear(); + continue; + } } var userReq = new GetUserRequest(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 1ba31db9fa..8f91a4d198 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -32,6 +32,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl => "http://localhost"; + public Exception LastLoginError { get; private set; } + /// /// Provide handling logic for an arbitrary API request. /// Should return true is a request was handled. If null or false return, the request will be failed with a . @@ -40,6 +42,8 @@ namespace osu.Game.Online.API private readonly Bindable state = new Bindable(APIState.Online); + private bool shouldFailNextLogin; + /// /// The current connectivity state of the API. /// @@ -74,6 +78,18 @@ namespace osu.Game.Online.API public void Login(string username, string password) { + state.Value = APIState.Connecting; + + if (shouldFailNextLogin) + { + LastLoginError = new APIException("Not powerful enough to login.", new ArgumentException(nameof(shouldFailNextLogin))); + + state.Value = APIState.Offline; + shouldFailNextLogin = false; + return; + } + + LastLoginError = null; LocalUser.Value = new User { Username = username, @@ -102,5 +118,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + + public void FailNextLogin() => shouldFailNextLogin = true; } } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 5ad5367924..72ca37bcf4 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.Users; @@ -55,6 +56,11 @@ namespace osu.Game.Online.API /// string WebsiteRootUrl { get; } + /// + /// The last login error that occurred, if any. + /// + Exception? LastLoginError { get; } + /// /// The current connection state of the API. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index e43b84d52a..f7842dcd30 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -42,6 +43,9 @@ namespace osu.Game.Overlays.Login Spacing = new Vector2(0, 5); AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; + + ErrorTextFlowContainer errorText; + Children = new Drawable[] { username = new OsuTextBox @@ -57,6 +61,11 @@ namespace osu.Game.Overlays.Login RelativeSizeAxes = Axes.X, TabbableContentContainer = this, }, + errorText = new ErrorTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, new SettingsCheckbox { LabelText = "Remember username", @@ -97,6 +106,9 @@ namespace osu.Game.Overlays.Login }; password.OnCommit += (sender, newText) => performLogin(); + + if (api?.LastLoginError?.Message is string error) + errorText.AddErrors(new[] { error }); } public override bool AcceptsFocus => true; @@ -108,4 +120,4 @@ namespace osu.Game.Overlays.Login Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); } } -} \ No newline at end of file +} From 4e1322effac966e1b3fec6dab2231d131648defc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 16:02:45 +0900 Subject: [PATCH 095/170] Fix typo --- osu.Game/Beatmaps/BeatmapModelManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index aa14f95863..250d6653d5 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -185,12 +185,12 @@ namespace osu.Game.Beatmaps /// /// Saves an file against a given . /// - /// The to save the content against. The file referenced by will be replaced. + /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo baetmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) + public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) { - var setInfo = baetmapInfo.BeatmapSet; + var setInfo = beatmapInfo.BeatmapSet; using (var stream = new MemoryStream()) { @@ -201,7 +201,7 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == baetmapInfo.ID); + beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID); var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; // grab the original file (or create a new one if not found). @@ -219,7 +219,7 @@ namespace osu.Game.Beatmaps } } - WorkingBeatmapCache?.Invalidate(baetmapInfo); + WorkingBeatmapCache?.Invalidate(beatmapInfo); } /// From 3a0b7ba8fffe61e85b8adceebf48a7eeffb10fd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:18:55 +0900 Subject: [PATCH 096/170] Add fallback to use `Message` when `Hint` is not available --- osu.Game/Online/API/OAuth.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 693e7c5336..d79fc58d1c 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -62,7 +62,7 @@ namespace osu.Game.Online.API // attempt to decode a displayable error string. var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty); if (error != null) - throwableException = new APIException(error.Message, ex); + throwableException = new APIException(error.UserDisplayableError, ex); } catch { @@ -201,10 +201,15 @@ namespace osu.Game.Online.API private class OAuthError { + public string UserDisplayableError => !string.IsNullOrEmpty(Hint) ? Hint : ErrorIdentifier; + [JsonProperty("error")] - public string ErrorType { get; set; } + public string ErrorIdentifier { get; set; } [JsonProperty("hint")] + public string Hint { get; set; } + + [JsonProperty("message")] public string Message { get; set; } } } From 3c15ef720f96e0cfd52cd4f7b90929a834ff2f6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:26:28 +0900 Subject: [PATCH 097/170] Remove setter from `IHasGuidPrimaryKey` interface --- osu.Game/Database/IHasGuidPrimaryKey.cs | 2 +- osu.Game/Database/ILive.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index c9cd9b257a..f52dc5c8ef 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -11,6 +11,6 @@ namespace osu.Game.Database { [JsonIgnore] [PrimaryKey] - Guid ID { get; set; } + Guid ID { get; } } } diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs index 29e5756dba..9359b09eaf 100644 --- a/osu.Game/Database/ILive.cs +++ b/osu.Game/Database/ILive.cs @@ -9,7 +9,7 @@ namespace osu.Game.Database /// A wrapper to provide access to database backed classes in a thread-safe manner. /// /// The databased type. - public interface ILive where T : class + public interface ILive where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more. { Guid ID { get; } From 857000b756765a99337d4b22c5085d4a27fdabfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:29:46 +0900 Subject: [PATCH 098/170] Mark `IPresentImports` as covariant --- osu.Game/Database/IPresentImports.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPresentImports.cs index 6aa29a5083..fb3aad7ee1 100644 --- a/osu.Game/Database/IPresentImports.cs +++ b/osu.Game/Database/IPresentImports.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace osu.Game.Database { - public interface IPresentImports + public interface IPresentImports where TModel : class { /// From e631653f4b1b471ec3794e7845b34492da88e801 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:30:12 +0900 Subject: [PATCH 099/170] Remove incorrectly committed `FodyWeavers` file --- osu.Game/FodyWeavers.xml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 osu.Game/FodyWeavers.xml diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml deleted file mode 100644 index cc07b89533..0000000000 --- a/osu.Game/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file From 63f0b0c93215cf1b0266b4891e6e82a160641951 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:35:55 +0900 Subject: [PATCH 100/170] Rename out of place interface name --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 8 ++++---- osu.Game/Database/{IPresentImports.cs => IPostImports.cs} | 4 ++-- osu.Game/OsuGame.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Database/{IPresentImports.cs => IPostImports.cs} (76%) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b302df1516..f8181cd010 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps /// /// Fired when the user requests to view the resulting import. /// - public Action>> PresentImport { set => beatmapModelManager.PresentImport = value; } + public Action>> PresentImport { set => beatmapModelManager.PostImport = value; } /// /// Delete a beatmap difficulty. diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 403bfdf621..9ad2dec12e 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPresentImports + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -200,12 +200,12 @@ namespace osu.Game.Database ? $"Imported {imported.First()}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; - if (imported.Count > 0 && PresentImport != null) + if (imported.Count > 0 && PostImport != null) { notification.CompletionText += " Click to view."; notification.CompletionClickAction = () => { - PresentImport?.Invoke(imported); + PostImport?.Invoke(imported); return true; }; } @@ -249,7 +249,7 @@ namespace osu.Game.Database return import; } - public Action>> PresentImport { protected get; set; } + public Action>> PostImport { protected get; set; } /// /// Silently import an item from an . diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPostImports.cs similarity index 76% rename from osu.Game/Database/IPresentImports.cs rename to osu.Game/Database/IPostImports.cs index fb3aad7ee1..f09285089a 100644 --- a/osu.Game/Database/IPresentImports.cs +++ b/osu.Game/Database/IPostImports.cs @@ -6,12 +6,12 @@ using System.Collections.Generic; namespace osu.Game.Database { - public interface IPresentImports + public interface IPostImports where TModel : class { /// /// Fired when the user requests to view the resulting import. /// - public Action>> PresentImport { set; } + public Action>> PostImport { set; } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 35ec213755..64c77c370e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -627,7 +627,7 @@ namespace osu.Game BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); - ScoreManager.PresentImport = items => PresentScore(items.First().Value); + ScoreManager.PostImport = items => PresentScore(items.First().Value); // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index aa0ee4bbbb..922b4f0a38 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { - public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPresentImports + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports { private readonly Scheduler scheduler; private readonly Func difficulties; @@ -365,9 +365,9 @@ namespace osu.Game.Scoring #region Implementation of IPresentImports - public Action>> PresentImport + public Action>> PostImport { - set => scoreModelManager.PresentImport = value; + set => scoreModelManager.PostImport = value; } #endregion From 8bfdfe3672997fcacfed8a571629c17403348d9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:54:00 +0900 Subject: [PATCH 101/170] Add literal string marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/IBeatmapSetInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index 548a48367c..d5aded7b53 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -53,6 +53,6 @@ namespace osu.Game.Beatmaps /// /// The filename for the storyboard. /// - string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; + string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(@".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; } } From 4df5f931522f63553e4d2049b3147f5c7fbb650b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:50:29 +0900 Subject: [PATCH 102/170] Inline single usage of `StoryboardFile` to avoid interface default method woes --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 6 ++++-- osu.Game/Beatmaps/BeatmapSetInfo.cs | 4 ---- osu.Game/Beatmaps/IBeatmapSetInfo.cs | 6 ------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 45112ae74c..f9889474ac 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -107,12 +107,14 @@ namespace osu.Game.Beatmaps { var decoder = Decoder.GetDecoder(stream); + var storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; + // todo: support loading from both set-wide storyboard *and* beatmap specific. - if (BeatmapSetInfo?.StoryboardFile == null) + if (string.IsNullOrEmpty(storyboardFilename)) storyboard = decoder.Decode(stream); else { - using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) + using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename)))) storyboard = decoder.Decode(stream, secondaryStream); } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 9f5a07ec43..8b01831b3c 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using JetBrains.Annotations; -using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Database; @@ -62,9 +61,6 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } - [JsonIgnore] - public string StoryboardFile => ((IBeatmapSetInfo)this).StoryboardFile; - /// /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. /// The path returned is relative to the user file storage. diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index d5aded7b53..0cfb0c4242 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Game.Database; #nullable enable @@ -49,10 +48,5 @@ namespace osu.Game.Beatmaps /// The maximum BPM of all beatmaps in this set. /// double MaxBPM { get; } - - /// - /// The filename for the storyboard. - /// - string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(@".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; } } From fd6b10656c638753a15efcb004f8848e109852bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:55:16 +0900 Subject: [PATCH 103/170] Add TODO reminder about ruleset reference transfer quirk --- osu.Game/Rulesets/IRulesetInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index d4dec0de64..ded3ac4b58 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets var ruleset = Activator.CreateInstance(type) as Ruleset; // overwrite the pre-populated RulesetInfo with a potentially database attached copy. + // TODO: figure if we still want/need this after switching to realm. // ruleset.RulesetInfo = this; return ruleset; From 51b7dce16f2bfb031a135183cdabecbbf2a2359b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:55:51 +0900 Subject: [PATCH 104/170] Remove reference to `osu-web-10` --- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 6153a0af08..d552a3f1e3 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps string Hash { get; } /// - /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). + /// MD5 is kept for legacy support (matching against replays etc.). /// string MD5Hash { get; } From f293e008d953d334ebb291481502679519d2c88c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:00:22 +0900 Subject: [PATCH 105/170] Move `BeatmapInfo`'s `SearchableTerms` implementation to interface --- osu.Game/Beatmaps/BeatmapInfo.cs | 5 +---- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 1 + osu.Game/Beatmaps/IBeatmapInfo.cs | 13 +++++++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 09f237a5de..cd5f8fb9a1 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -152,10 +152,7 @@ namespace osu.Game.Beatmaps [JsonIgnore] public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); - public string[] SearchableTerms => new[] - { - Version - }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + public IEnumerable SearchableTerms => ((IBeatmapInfo)this).SearchableTerms; public override string ToString() => ((IBeatmapInfo)this).DisplayTitle; diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index f9889474ac..5fec8f7a89 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index d552a3f1e3..fb30b0279c 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.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 System.Linq; using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Rulesets; @@ -22,7 +23,7 @@ namespace osu.Game.Beatmaps /// /// The metadata representing this beatmap. May be shared between multiple beatmaps. /// - IBeatmapMetadataInfo Metadata { get; } + IBeatmapMetadataInfo? Metadata { get; } /// /// The difficulty settings for this beatmap. @@ -76,12 +77,20 @@ namespace osu.Game.Beatmaps { get { - var metadata = Metadata.DisplayTitleRomanisable; + var metadata = closestMetadata.DisplayTitleRomanisable; return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); } } + string[] SearchableTerms => new[] + { + DifficultyName + }.Concat(closestMetadata.SearchableTerms).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + private string versionString => string.IsNullOrEmpty(DifficultyName) ? string.Empty : $"[{DifficultyName}]"; + + // temporary helper methods until we figure which metadata should be where. + private IBeatmapMetadataInfo closestMetadata => (Metadata ?? BeatmapSet.Metadata)!; } } From 95f1cc85d4b57a5eb710da9ace0cc00d87952197 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:12:40 +0900 Subject: [PATCH 106/170] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8fad10d247..fb3e4b3bbd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..e53b83100e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 37931d0c38..4e7053d816 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 6ac0601d2cdba14f61f3bc589ab8198f5ecb63da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:18:08 +0900 Subject: [PATCH 107/170] Fix incorrect csproj merge --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 36c5fd89bf..4877ddf725 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 853cf6feaa165e833ecb7ca18c6cdffe8ca6e005 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:35:53 +0900 Subject: [PATCH 108/170] Rename last remaining `BeatmapInfo Beatmap` usage --- .../TestSceneAccuracyHeatmap.cs | 2 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 2 +- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 4 ++-- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Gameplay/TestSceneReplayRecording.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 2 +- .../TestSceneMultiplayerResults.cs | 2 +- .../TestSceneMultiplayerTeamResults.cs | 2 +- .../Navigation/TestScenePresentScore.cs | 2 +- .../Online/TestSceneUserProfileScores.cs | 8 ++++---- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 2 +- .../TestSceneExpandedPanelMiddleContent.cs | 8 ++++---- .../Visual/Ranking/TestSceneResultsScreen.cs | 4 ++-- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 20 +++++++++---------- .../SongSelect/TestScenePlaySongSelect.cs | 4 ++-- .../TestSceneDeleteLocalScore.cs | 2 +- .../Requests/Responses/APILegacyScoreInfo.cs | 2 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../BeatmapSet/Scores/ScoresContainer.cs | 2 +- .../Scores/TopScoreStatisticsSection.cs | 2 +- .../Sections/Ranks/DrawableProfileScore.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 5 +++-- osu.Game/Scoring/ScoreManager.cs | 12 +++++------ osu.Game/Scoring/ScorePerformanceCache.cs | 2 +- osu.Game/Scoring/ScoreStore.cs | 6 +++--- .../Multiplayer/Spectate/PlayerArea.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- .../Expanded/ExpandedPanelMiddleContent.cs | 2 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++-- .../Ranking/Statistics/StatisticsPanel.cs | 2 +- .../Select/BeatmapClearScoresDialog.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- osu.Game/Tests/TestScoreInfo.cs | 2 +- 43 files changed, 74 insertions(+), 73 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index 10d9d7ffde..79150a1941 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(100, 300), }, - accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) + accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index b536fc61b7..dce01448f4 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -950,7 +950,7 @@ namespace osu.Game.Tests.Beatmaps.IO return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, BeatmapInfoID = beatmapInfo.ID }, new ImportScoreTest.TestArchiveReader()); } diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index cd7d744f53..2cd02329b7 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Scores.IO var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); - beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); + beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Scores.IO { var beatmapManager = osu.Dependencies.Get(); - score.Beatmap ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); score.Ruleset ??= new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 12a85c3f26..693c66ccb0 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" }, - Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, + BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo, Ruleset = Ruleset.Value, }))); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index d89fd322d1..c8040f42f0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 07514ad51a..3545fc96e8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 07ff35f77b..b4de060578 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -356,7 +356,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { public TestReplayRecorder() - : base(new Score { ScoreInfo = { Beatmap = new BeatmapInfo() } }) + : base(new Score { ScoreInfo = { BeatmapInfo = new BeatmapInfo() } }) { } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index ff06d4d9c7..5032cdaec7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Accuracy = 0.8, MaxCombo = 500, Combo = 250, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineScoreID = 12345, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index 0a8bda7ec0..99d5fd46e9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Accuracy = 0.8, MaxCombo = 500, Combo = 250, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineScoreID = 12345, diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 52b577b402..ee84d775d2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Navigation { Hash = Guid.NewGuid().ToString(), OnlineScoreID = i, - Beatmap = beatmap.Beatmaps.First(), + BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }).Result; }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs index 5dca218531..513631a221 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 1047.21, Rank = ScoreRank.SH, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 134.32, Rank = ScoreRank.A, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 96.83, Rank = ScoreRank.S, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Online var noPPScore = new ScoreInfo { Rank = ScoreRank.B, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index a5e2f02f31..df8500fab2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.Ranking Id = 2, Username = "peppy", }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, TotalScore = 2845370, Accuracy = accuracy, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 5180854aba..899f351a2a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) { - Beatmap = createTestBeatmap(author) + BeatmapInfo = createTestBeatmap(author) })); } @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true) { - Beatmap = createTestBeatmap(author) + BeatmapInfo = createTestBeatmap(author) })); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name")); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) { - Beatmap = createTestBeatmap(null) + BeatmapInfo = createTestBeatmap(null) })); AddAssert("mapped by text not present", () => @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(new TestScoreInfo(ruleset.RulesetInfo) { Mods = mods, - Beatmap = beatmap, + BeatmapInfo = beatmap, Date = default, }); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 631455b727..8d5d0ba8c7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -337,8 +337,8 @@ namespace osu.Game.Tests.Visual.Ranking public UnrankedSoloResultsScreen(ScoreInfo score) : base(score, true) { - Score.Beatmap.OnlineBeatmapID = 0; - Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; + Score.BeatmapInfo.OnlineBeatmapID = 0; + Score.BeatmapInfo.Status = BeatmapSetOnlineStatus.Pending; } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 95cf6a9903..13b769c80a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 6602580, @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 4608074, @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 1014222, @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 1541390, @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 2243452, @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 2705430, @@ -311,7 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 7151382, @@ -330,7 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 2051389, @@ -349,7 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 6169483, @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 6702666, diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index f9e81d3da6..78040b3d6a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -805,7 +805,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = getPresentBeatmap(), + BeatmapInfo = getPresentBeatmap(), Ruleset = getPresentBeatmap().Ruleset }); }); @@ -837,7 +837,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = getPresentBeatmap(), + BeatmapInfo = getPresentBeatmap(), Ruleset = getPresentBeatmap().Ruleset }); }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index f58dbef145..c237fcaebf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.UserInterface var score = new ScoreInfo { OnlineScoreID = i, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), TotalScore = RNG.Next(1, 1000000), diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 18a0db3928..aaf2dccc82 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -37,7 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses OnlineScoreID = OnlineScoreID, Date = Date, PP = PP, - Beatmap = BeatmapInfo, + BeatmapInfo = BeatmapInfo, RulesetID = OnlineRulesetID, Hash = Replay ? "online" : string.Empty, // todo: temporary? Rank = Rank, diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 30c1d2f826..7ec34e70d5 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms OnlineScoreID = ID, TotalScore = TotalScore, MaxCombo = MaxCombo, - Beatmap = playlistItem.Beatmap.Value, + BeatmapInfo = playlistItem.Beatmap.Value, BeatmapInfoID = playlistItem.BeatmapID, Ruleset = playlistItem.Ruleset.Value, RulesetID = playlistItem.RulesetID, diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index d55ad45ff5..b597b2f214 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -144,7 +144,7 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.Beatmap.OnlineBeatmapID; + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineBeatmapID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 99925bb1fb..9dd879fd7e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -482,7 +482,7 @@ namespace osu.Game return; } - var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.Beatmap.ID); + var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.BeatmapInfo.ID); if (databasedBeatmap == null) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 02de92e805..aec06e18f6 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -250,7 +250,7 @@ namespace osu.Game List getBeatmapScores(BeatmapSetInfo set) { var beatmapIds = BeatmapManager.QueryBeatmaps(b => b.BeatmapSetInfoID == set.ID).Select(b => b.ID).ToList(); - return ScoreManager.QueryScores(s => beatmapIds.Contains(s.Beatmap.ID)).ToList(); + return ScoreManager.QueryScores(s => beatmapIds.Contains(s.BeatmapInfo.ID)).ToList(); } BeatmapManager.ItemRemoved.BindValueChanged(i => diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 8fe1d35b62..018faf2011 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -172,7 +172,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Text = score.MaxCombo.ToLocalisableString(@"0\x"), Font = OsuFont.GetFont(size: text_size), - Colour = score.MaxCombo == score.Beatmap?.MaxCombo ? highAccuracyColour : Color4.White + Colour = score.MaxCombo == score.BeatmapInfo?.MaxCombo ? highAccuracyColour : Color4.White } }; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index fb1769fbe1..82657afc86 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var topScore = ordered.Result.First(); - scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true); + scoreTable.DisplayScores(ordered.Result, topScore.BeatmapInfo?.Status.GrantsPerformancePoints() == true); scoreTable.Show(); var userScore = value.UserScore; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 883e83ce6e..630aa8fe53 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo?.Status.GrantsPerformancePoints() == true ? 1 : 0; ppColumn.Text = value.PP?.ToLocalisableString(@"N0"); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index c221f070df..3561e9700e 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Spacing = new Vector2(0, 2), Children = new Drawable[] { - new ScoreBeatmapMetadataContainer(Score.Beatmap), + new ScoreBeatmapMetadataContainer(Score.BeatmapInfo), new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { new OsuSpriteText { - Text = $"{Score.Beatmap.Version}", + Text = $"{Score.BeatmapInfo.Version}", Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Colour = colours.Yellow }, diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 2e1a29372d..a1658b4cf3 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -70,7 +70,7 @@ namespace osu.Game.Scoring.Legacy scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod()).ToArray(); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); - scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; + scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; /* score.HpGraphString = */ sr.ReadString(); @@ -119,7 +119,7 @@ namespace osu.Game.Scoring.Legacy // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. - score.ScoreInfo.Beatmap = workingBeatmap.BeatmapInfo; + score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; return score; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 288552879c..58e4192f77 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring.Legacy this.score = score; this.beatmap = beatmap; - if (score.ScoreInfo.Beatmap.RulesetID < 0 || score.ScoreInfo.Beatmap.RulesetID > 3) + if (score.ScoreInfo.BeatmapInfo.RulesetID < 0 || score.ScoreInfo.BeatmapInfo.RulesetID > 3) throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } @@ -44,7 +44,7 @@ namespace osu.Game.Scoring.Legacy { sw.Write((byte)(score.ScoreInfo.Ruleset.ID ?? 0)); sw.Write(LATEST_VERSION); - sw.Write(score.ScoreInfo.Beatmap.MD5Hash); + sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); sw.Write(score.ScoreInfo.UserString); sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 890ead40e3..5cf22f7945 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -150,7 +150,8 @@ namespace osu.Game.Scoring public int BeatmapInfoID { get; set; } [JsonIgnore] - public virtual BeatmapInfo Beatmap { get; set; } + [Column("Beatmap")] + public virtual BeatmapInfo BeatmapInfo { get; set; } [JsonIgnore] public long? OnlineScoreID { get; set; } @@ -252,7 +253,7 @@ namespace osu.Game.Scoring return clone; } - public override string ToString() => $"{User} playing {Beatmap}"; + public override string ToString() => $"{User} playing {BeatmapInfo}"; public bool Equals(ScoreInfo other) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d83b4e3f1d..27d087dc30 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -67,7 +67,7 @@ namespace osu.Game.Scoring // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. foreach (var s in scores) { - await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); + await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } @@ -126,7 +126,7 @@ namespace osu.Game.Scoring /// The total score. public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { - if (score.Beatmap == null) + if (score.BeatmapInfo == null) return score.TotalScore; int beatmapMaxCombo; @@ -147,18 +147,18 @@ namespace osu.Game.Scoring // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. - if (score.Beatmap.MaxCombo != null) - beatmapMaxCombo = score.Beatmap.MaxCombo.Value; + if (score.BeatmapInfo.MaxCombo != null) + beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value; else { - if (score.Beatmap.ID == 0 || difficulties == null) + if (score.BeatmapInfo.ID == 0 || difficulties == null) { // We don't have enough information (max combo) to compute the score, so use the provided score. return score.TotalScore; } // We can compute the max combo locally after the async beatmap difficulty computation. - var difficulty = await difficulties().GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); + var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); beatmapMaxCombo = difficulty.MaxCombo; } } diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index bb15983de3..82685e9a04 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false); + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) diff --git a/osu.Game/Scoring/ScoreStore.cs b/osu.Game/Scoring/ScoreStore.cs index f5c5cd5dad..fd1f5ae3ec 100644 --- a/osu.Game/Scoring/ScoreStore.cs +++ b/osu.Game/Scoring/ScoreStore.cs @@ -17,9 +17,9 @@ namespace osu.Game.Scoring protected override IQueryable AddIncludesForConsumption(IQueryable query) => base.AddIncludesForConsumption(query) - .Include(s => s.Beatmap) - .Include(s => s.Beatmap).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmap).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(s => s.BeatmapInfo) + .Include(s => s.BeatmapInfo).ThenInclude(b => b.Metadata) + .Include(s => s.BeatmapInfo).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) .Include(s => s.Ruleset); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 95ccc08608..c3190cd845 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a05a8f5056..69a1c6c8ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -164,7 +164,7 @@ namespace osu.Game.Screens.Play Score = CreateScore(); // ensure the score is in a consistent state with the current player. - Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; + Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = Mods.Value.ToArray(); diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index d90e8e0168..675cb71311 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { - var beatmap = score.ScoreInfo.Beatmap; + var beatmap = score.ScoreInfo.BeatmapInfo; Debug.Assert(beatmap.OnlineBeatmapID != null); diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index bcb5e7999f..262d1e8293 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking.Expanded [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { - var beatmap = score.Beatmap; + var beatmap = score.BeatmapInfo; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; var creator = metadata.Author?.Username; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9bc696948f..5e582a8dcb 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -27,10 +27,10 @@ namespace osu.Game.Screens.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { - if (Score.Beatmap.OnlineBeatmapID == null || Score.Beatmap.Status <= BeatmapSetOnlineStatus.Pending) + if (Score.BeatmapInfo.OnlineBeatmapID == null || Score.BeatmapInfo.Status <= BeatmapSetOnlineStatus.Pending) return null; - getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); return getScoreRequest; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index f1ae1f9d73..bc62bcf2b2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Ranking.Statistics // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. Task.Run(() => { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { var rows = new FillFlowContainer diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index 8c33b1ea0b..4970db8955 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmapInfo.ID).ToList())) + Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID).ToList())) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2fdb41a1a1..07300635aa 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.Beatmap.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); + .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 418cf23ce7..94aa165785 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); + FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 71bcc336f3..7861d4cb72 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Spectate { ScoreInfo = new ScoreInfo { - Beatmap = resolvedBeatmap, + BeatmapInfo = resolvedBeatmap, User = user, Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), Ruleset = resolvedRuleset.RulesetInfo, diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 5ce6aae647..719d31b092 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }; - Beatmap = new TestBeatmap(ruleset).BeatmapInfo; + BeatmapInfo = new TestBeatmap(ruleset).BeatmapInfo; Ruleset = ruleset; RulesetID = ruleset.ID ?? 0; From 2e3450b3f58e4290289194c0fd58ecf28fe3931c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:20:24 +0900 Subject: [PATCH 109/170] Make Mods readonly --- osu.Game/Screens/Play/GameplayState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index ba08c946d2..d4da17ce37 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play /// /// The mods applied to the gameplay. /// - public IReadOnlyList Mods; + public readonly IReadOnlyList Mods; /// /// A bindable tracking the last judgement result applied to any hit object. From 5aae673240e2263cb15d2b9e7c1ad055d17ecaf9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:33:54 +0900 Subject: [PATCH 110/170] Use GameplayState --- osu.Game/Screens/Play/GameplayState.cs | 6 +++++ .../HUD/DefaultPerformancePointsCounter.cs | 22 +++++++++---------- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index d4da17ce37..ef4967b34d 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -7,6 +7,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; #nullable enable @@ -32,6 +33,11 @@ namespace osu.Game.Screens.Play /// public readonly IReadOnlyList Mods; + /// + /// The gameplay score. + /// + public Score? Score { get; set; } = null; + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index d93d626c72..3c31848c32 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } + [Resolved] [CanBeNull] - [Resolved(CanBeNull = true)] - private Player player { get; set; } + private GameplayState gameplayState { get; set; } private TimedDifficultyAttributes[] timedAttributes; private Ruleset gameplayRuleset; @@ -53,10 +53,10 @@ namespace osu.Game.Screens.Play.HUD { Colour = colours.BlueLighter; - if (player != null) + if (gameplayState != null) { - gameplayRuleset = player.GameplayRuleset; - timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + gameplayRuleset = gameplayState.Ruleset; + timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(gameplayState.Beatmap)).CalculateTimed(gameplayState.Mods.ToArray()).ToArray(); } } @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (player == null || timedAttributes.Length == 0) + if (gameplayState?.Score == null || timedAttributes.Length == 0) return; var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play.HUD attribIndex = ~attribIndex - 1; attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); - var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, player.Score.ScoreInfo); + var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); Current.Value = (int)(ppProcessor?.Calculate() ?? 0); } @@ -134,18 +134,18 @@ namespace osu.Game.Screens.Play.HUD private class GameplayWorkingBeatmap : WorkingBeatmap { - private readonly GameplayBeatmap gameplayBeatmap; + private readonly IBeatmap gameplayBeatmap; - public GameplayWorkingBeatmap(GameplayBeatmap gameplayBeatmap) + public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap) : base(gameplayBeatmap.BeatmapInfo, null) { this.gameplayBeatmap = gameplayBeatmap; } public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) - => gameplayBeatmap.PlayableBeatmap; + => gameplayBeatmap; - protected override IBeatmap GetBeatmap() => gameplayBeatmap.PlayableBeatmap; + protected override IBeatmap GetBeatmap() => gameplayBeatmap; protected override Texture GetBackground() => throw new NotImplementedException(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 15cf8388f0..00907584e1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Play [Cached] [Cached(Type = typeof(IBindable>))] - public new readonly Bindable> Mods = new Bindable>(Array.Empty()); + protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); /// /// Whether failing should be allowed. From 221cc1747c1ec62e05cf6288cda88f876f31f58d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:34:08 +0900 Subject: [PATCH 111/170] Drop "default" prefix --- ...erformancePointsCounter.cs => PerformancePointsCounter.cs} | 4 ++-- osu.Game/Skinning/DefaultSkin.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Screens/Play/HUD/{DefaultPerformancePointsCounter.cs => PerformancePointsCounter.cs} (97%) diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs similarity index 97% rename from osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs rename to osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 3c31848c32..ad0a928d1c 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -28,7 +28,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { - public class DefaultPerformancePointsCounter : RollingCounter, ISkinnableDrawable + public class PerformancePointsCounter : RollingCounter, ISkinnableDrawable { public bool UsesFixedAnchor { get; set; } @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Play.HUD private TimedDifficultyAttributes[] timedAttributes; private Ruleset gameplayRuleset; - public DefaultPerformancePointsCounter() + public PerformancePointsCounter() { Current.Value = DisplayedCount = 0; } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 41b7875cd1..8c1e5313d5 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -68,7 +68,7 @@ namespace osu.Game.Skinning var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); - var ppCounter = container.OfType().FirstOrDefault(); + var ppCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -131,7 +131,7 @@ namespace osu.Game.Skinning new SongProgress(), new BarHitErrorMeter(), new BarHitErrorMeter(), - new DefaultPerformancePointsCounter() + new PerformancePointsCounter() } }; From fb63e5ed87fab819899e8a137d0bf7b67ce720c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:35:26 +0900 Subject: [PATCH 112/170] Add todo --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index ad0a928d1c..740f286246 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -132,6 +132,7 @@ namespace osu.Game.Screens.Play.HUD } } + // Todo: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. private class GameplayWorkingBeatmap : WorkingBeatmap { private readonly IBeatmap gameplayBeatmap; From 1837e1bf3ccb4e471979e1034c3ee92362d49739 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:35:53 +0900 Subject: [PATCH 113/170] Share rounding with PerformanceStatistic --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 740f286246..265c1a9d01 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.Play.HUD attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); - Current.Value = (int)(ppProcessor?.Calculate() ?? 0); + Current.Value = (int)Math.Round(ppProcessor?.Calculate() ?? 0, MidpointRounding.AwayFromZero); } protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); From 0b0316e27ee68696d090cb4410f13e9aa0117045 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:59:31 +0900 Subject: [PATCH 114/170] Fix missing CanBeNull --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 265c1a9d01..154b6accb8 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] [CanBeNull] private GameplayState gameplayState { get; set; } From d1e7191f94fa1de0d3e7911951a5cb9179125a17 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:59:51 +0900 Subject: [PATCH 115/170] Pass score into GameplayState --- osu.Game/Screens/Play/Player.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 00907584e1..69093db883 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -162,6 +162,7 @@ namespace osu.Game.Screens.Play return; Score = CreateScore(); + GameplayState.Score = Score; // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; From d120678e306e9dc85cfb0e8d2fd82efd96c3a7cb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 21:13:14 +0900 Subject: [PATCH 116/170] Fix redundant default value --- osu.Game/Screens/Play/GameplayState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index ef4967b34d..9c83eddb45 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play /// /// The gameplay score. /// - public Score? Score { get; set; } = null; + public Score? Score { get; set; } /// /// A bindable tracking the last judgement result applied to any hit object. From 15ec315ec62a9b32e9d6a81fcd9a2ddffbcabf9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 01:14:58 +0900 Subject: [PATCH 117/170] Fix test runs hanging due to missing `ConfigureAwait` --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index d1c23f1442..4cc71717ff 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -909,7 +909,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(new ImportTask(temp)); + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); ensureLoaded(osu); @@ -924,7 +924,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(new ImportTask(temp)); + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); ensureLoaded(osu); From 86df4919f7ab8207e62d9b49cdcd06bbb0c6e6c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 11:06:24 +0900 Subject: [PATCH 118/170] Fix skin fallbacks test --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 7d4673c901..7398527f57 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEmptyLegacyBeatmapSkinFallsBack() { CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } From 593da79bbc96bd18fe6bf0eb35c5758fb1c16153 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 11:26:13 +0900 Subject: [PATCH 119/170] Further asyncify load process --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 9 +++++++++ .../Screens/Play/HUD/PerformancePointsCounter.cs | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index c46ab93ece..e6c287112f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -17,6 +17,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -147,6 +148,14 @@ namespace osu.Game.Beatmaps }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } + public Task GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) + { + return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods).ToArray(), + token, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); + } + /// /// Retrieves the that describes a star rating. /// diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 154b6accb8..b02ab440a0 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -40,7 +43,10 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private GameplayState gameplayState { get; set; } + [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; + + [CanBeNull] private Ruleset gameplayRuleset; public PerformancePointsCounter() @@ -49,14 +55,15 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache, CancellationToken cancellationToken) { Colour = colours.BlueLighter; if (gameplayState != null) { gameplayRuleset = gameplayState.Ruleset; - timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(gameplayState.Beatmap)).CalculateTimed(gameplayState.Mods.ToArray()).ToArray(); + difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), cancellationToken) + .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -70,9 +77,11 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (gameplayState?.Score == null || timedAttributes.Length == 0) + if (gameplayState?.Score == null || timedAttributes == null || timedAttributes.Length == 0) return; + Debug.Assert(gameplayRuleset != null); + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; From 5624dd9af67376b73038e4f75570927f161ff602 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 12:07:41 +0900 Subject: [PATCH 120/170] Fix incorrect CancellationToken usage Apparently I wrote the BDL system and don't know how this works. I believe you need `CancellationToken?` or CanBeNull=true, however that doesn't actually play well when actually using the token in code... --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index b02ab440a0..13b94e6cd6 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -49,20 +49,22 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private Ruleset gameplayRuleset; + private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); + public PerformancePointsCounter() { Current.Value = DisplayedCount = 0; } [BackgroundDependencyLoader] - private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache, CancellationToken cancellationToken) + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) { Colour = colours.BlueLighter; if (gameplayState != null) { gameplayRuleset = gameplayState.Ruleset; - difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), cancellationToken) + difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -101,6 +103,8 @@ namespace osu.Game.Screens.Play.HUD if (scoreProcessor != null) scoreProcessor.NewJudgement -= onNewJudgement; + + loadCancellationSource?.Cancel(); } private class TextComponent : CompositeDrawable, IHasText From b41fa41c8539d6370fb4d3efd8539ec664176ff2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:28:56 +0900 Subject: [PATCH 121/170] Rename `APIRequest.Result` to `Response` --- .../Online/TestDummyAPIRequestHandling.cs | 2 +- .../TestSceneUpdateableBeatmapBackgroundSprite.cs | 4 ++-- osu.Game.Tournament/TournamentGameBase.cs | 4 ++-- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- osu.Game/Database/UserLookupCache.cs | 2 +- osu.Game/Online/API/APIRequest.cs | 15 ++++++++++----- osu.Game/Overlays/RankingsOverlay.cs | 6 +++--- .../Screens/OnlinePlay/Components/RoomManager.cs | 2 +- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index aa29d76843..91c6b6c008 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Online AddAssert("response event fired", () => response != null); - AddAssert("request has response", () => request.Result == response); + AddAssert("request has response", () => request.Response == response); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index 198cc70e01..74cd675a05 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface var req = new GetBeatmapSetRequest(1); api.Queue(req); - AddUntilStep("wait for api response", () => req.Result != null); + AddUntilStep("wait for api response", () => req.Response != null); TestUpdateableBeatmapBackgroundSprite background = null; @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.UserInterface Child = background = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both, - Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Result?.ToBeatmapSet(rulesets) } } + Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Response?.ToBeatmapSet(rulesets) } } }; }); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 2e4ed9d5b1..bdf7269c83 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); API.Perform(req); - b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); + b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); addedInfo = true; } @@ -203,7 +203,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); req.Perform(API); - b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); + b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); addedInfo = true; } diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index e1faf6005b..1fe120557d 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -78,7 +78,7 @@ namespace osu.Game.Beatmaps // intentionally blocking to limit web request concurrency api.Perform(req); - var res = req.Result; + var res = req.Response; if (res != null) { diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 13c37ddfe9..ff81637efb 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -115,7 +115,7 @@ namespace osu.Game.Database createNewTask(); } - List foundUsers = request.Result?.Users; + List foundUsers = request.Response?.Users; if (foundUsers != null) { diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index cf17ed4b5d..d60c9cfe65 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; @@ -17,7 +18,11 @@ namespace osu.Game.Online.API { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result { get; private set; } + /// + /// The deserialised response object. May be null if the request or deserialisation failed. + /// + [CanBeNull] + public T Response { get; private set; } /// /// Invoked on successful completion of an API request. @@ -27,21 +32,21 @@ namespace osu.Game.Online.API protected APIRequest() { - base.Success += () => Success?.Invoke(Result); + base.Success += () => Success?.Invoke(Response); } protected override void PostProcess() { base.PostProcess(); - Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + Response = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; } internal void TriggerSuccess(T result) { - if (Result != null) + if (Response != null) throw new InvalidOperationException("Attempted to trigger success more than once"); - Result = result; + Response = result; TriggerSuccess(); } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index b8bdef925e..2263d54d7b 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -146,16 +146,16 @@ namespace osu.Game.Overlays switch (userRequest.Type) { case UserRankingsType.Performance: - return new PerformanceTable(1, userRequest.Result.Users); + return new PerformanceTable(1, userRequest.Response.Users); case UserRankingsType.Score: - return new ScoresTable(1, userRequest.Result.Users); + return new ScoresTable(1, userRequest.Response.Users); } return null; case GetCountryRankingsRequest countryRequest: - return new CountriesTable(1, countryRequest.Result.Countries); + return new CountriesTable(1, countryRequest.Response.Countries); } return null; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index a64d89b699..381849189d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += exception => { - onError?.Invoke(req.Result?.Error ?? exception.Message); + onError?.Invoke(req.Response?.Error ?? exception.Message); }; api.Queue(req); From d3b9660148d3ebc79660915c1db35b779b001402 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:41:14 +0900 Subject: [PATCH 122/170] Move common interface implementations to extension methods --- .../BeatmapMetadataRomanisationTest.cs | 4 +- osu.Game/Beatmaps/BeatmapInfo.cs | 7 +-- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 37 +++++++++++++++ osu.Game/Beatmaps/BeatmapMetadata.cs | 7 +-- .../Beatmaps/BeatmapMetadataInfoExtensions.cs | 46 +++++++++++++++++++ osu.Game/Beatmaps/IBeatmapInfo.cs | 30 ------------ osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 43 ----------------- osu.Game/Overlays/Music/PlaylistItem.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 2 +- 11 files changed, 91 insertions(+), 91 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapInfoExtensions.cs create mode 100644 osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs index dab4825919..9926acf772 100644 --- a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs +++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Localisation Title = "Romanised title", TitleUnicode = "Unicode Title" }; - var romanisableString = metadata.ToRomanisableString(); + var romanisableString = metadata.GetDisplayTitleRomanisable(); Assert.AreEqual(metadata.ToString(), romanisableString.Romanised); Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Localisation Artist = "Romanised Artist", Title = "Romanised title" }; - var romanisableString = metadata.ToRomanisableString(); + var romanisableString = metadata.GetDisplayTitleRomanisable(); Assert.AreEqual(romanisableString.Romanised, romanisableString.Original); } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index cd5f8fb9a1..ac5b5d7a8a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -7,7 +7,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Rulesets; @@ -152,11 +151,7 @@ namespace osu.Game.Beatmaps [JsonIgnore] public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); - public IEnumerable SearchableTerms => ((IBeatmapInfo)this).SearchableTerms; - - public override string ToString() => ((IBeatmapInfo)this).DisplayTitle; - - public RomanisableString ToRomanisableString() => ((IBeatmapInfo)this).DisplayTitleRomanisable; + public override string ToString() => this.GetDisplayTitle(); public bool Equals(BeatmapInfo other) { diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs new file mode 100644 index 0000000000..deab8b915a --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Localisation; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapInfoExtensions + { + /// + /// A user-presentable display title representing this beatmap. + /// + public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{getClosestMetadata(beatmapInfo)} {getVersionString(beatmapInfo)}".Trim(); + + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo) + { + var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(); + var versionString = getVersionString(beatmapInfo); + + return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + } + + public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] + { + beatmapInfo.DifficultyName + }.Concat(getClosestMetadata(beatmapInfo).GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; + + // temporary helper methods until we figure which metadata should be where. + private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => (beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata)!; + } +} diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 3da80580cb..711533e118 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Users; @@ -87,11 +86,7 @@ namespace osu.Game.Beatmaps public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); - public override string ToString() => ((IBeatmapMetadataInfo)this).DisplayTitle; - - public RomanisableString ToRomanisableString() => ((IBeatmapMetadataInfo)this).DisplayTitleRomanisable; - - public IEnumerable SearchableTerms => ((IBeatmapMetadataInfo)this).SearchableTerms; + public override string ToString() => this.GetDisplayTitle(); string IBeatmapMetadataInfo.Author => AuthorString; } diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs new file mode 100644 index 0000000000..ee946eeeec --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Localisation; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapMetadataInfoExtensions + { + /// + /// An array of all searchable terms provided in contained metadata. + /// + public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[] + { + metadataInfo.Author, + metadataInfo.Artist, + metadataInfo.ArtistUnicode, + metadataInfo.Title, + metadataInfo.TitleUnicode, + metadataInfo.Source, + metadataInfo.Tags + }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + /// + /// A user-presentable display title representing this metadata. + /// + public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo) + { + string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; + return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim(); + } + + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo) + { + string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; + var artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; + var titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; + + return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim()); + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index fb30b0279c..6a3f1b43d8 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; -using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Rulesets; @@ -64,33 +62,5 @@ namespace osu.Game.Beatmaps /// The basic star rating for this beatmap (with no mods applied). /// double StarRating { get; } - - /// - /// A user-presentable display title representing this metadata. - /// - string DisplayTitle => $"{Metadata} {versionString}".Trim(); - - /// - /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. - /// - RomanisableString DisplayTitleRomanisable - { - get - { - var metadata = closestMetadata.DisplayTitleRomanisable; - - return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); - } - } - - string[] SearchableTerms => new[] - { - DifficultyName - }.Concat(closestMetadata.SearchableTerms).Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - private string versionString => string.IsNullOrEmpty(DifficultyName) ? string.Empty : $"[{DifficultyName}]"; - - // temporary helper methods until we figure which metadata should be where. - private IBeatmapMetadataInfo closestMetadata => (Metadata ?? BeatmapSet.Metadata)!; } } diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs index d0dae296a0..55aee7d7bc 100644 --- a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Localisation; #nullable enable @@ -65,47 +63,6 @@ namespace osu.Game.Beatmaps /// string BackgroundFile { get; } - /// - /// A user-presentable display title representing this metadata. - /// - string DisplayTitle - { - get - { - string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; - return $"{Artist} - {Title} {author}".Trim(); - } - } - - /// - /// A user-presentable display title representing this metadata, with localisation handling for potentially romanisable fields. - /// - RomanisableString DisplayTitleRomanisable - { - get - { - string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; - var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; - var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; - - return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); - } - } - - /// - /// An array of all searchable terms provided in contained metadata. - /// - string[] SearchableTerms => new[] - { - Author, - Artist, - ArtistUnicode, - Title, - TitleUnicode, - Source, - Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); - bool IEquatable.Equals(IBeatmapMetadataInfo? other) { if (other == null) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 571b14428e..ef25de77c6 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Music { Padding = new MarginPadding { Left = 5 }; - FilterTerms = item.Metadata.SearchableTerms; + FilterTerms = item.Metadata.GetSearchableTerms(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 69eb857661..585b024623 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) }; beatmapText.Clear(); - beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => + beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => { text.Truncate = true; text.RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 03d13c353a..acd87ed864 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -379,7 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (item.NewValue?.Beatmap.Value != null) { statusText.Text = "Currently playing "; - beatmapText.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), + beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString(), creationParameters: s => diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 3f729d9477..d8c5aa760e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel if (match) { - var terms = BeatmapInfo.SearchableTerms; + var terms = BeatmapInfo.GetSearchableTerms(); foreach (var criteriaTerm in criteria.SearchTerms) match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); From a5aa32811aaf215f7d96d601a82d44362d140ad3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:49:59 +0900 Subject: [PATCH 123/170] Remove null check suppression and add non-null fallback --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index deab8b915a..eba19ac1a1 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -32,6 +32,7 @@ namespace osu.Game.Beatmaps private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; // temporary helper methods until we figure which metadata should be where. - private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => (beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata)!; + private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => + beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet?.Metadata ?? new BeatmapMetadata(); } } From e19be8ebe4eb49f456b61cd8f26c1143dad606ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:48:10 +0900 Subject: [PATCH 124/170] Make `GameplayState.Score` immutable --- osu.Game/Screens/Play/GameplayState.cs | 8 +++++--- osu.Game/Screens/Play/Player.cs | 20 ++++++++++---------- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 3 ++- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 9c83eddb45..44f72022f7 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.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 System; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Play /// /// The gameplay score. /// - public Score? Score { get; set; } + public readonly Score Score; /// /// A bindable tracking the last judgement result applied to any hit object. @@ -45,11 +46,12 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); - public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList mods) + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null) { Beatmap = beatmap; Ruleset = ruleset; - Mods = mods; + Score = score ?? new Score(); + Mods = mods ?? ArraySegment.Empty; } /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 69093db883..a688330f54 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -161,14 +161,6 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; - Score = CreateScore(); - GameplayState.Score = Score; - - // ensure the score is in a consistent state with the current player. - Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; - Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - Score.ScoreInfo.Mods = Mods.Value.ToArray(); - PrepareReplay(); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); @@ -226,7 +218,14 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value)); + Score = CreateScore(playableBeatmap); + + // ensure the score is in a consistent state with the current player. + Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; + Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; + Score.ScoreInfo.Mods = Mods.Value.ToArray(); + + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -989,8 +988,9 @@ namespace osu.Game.Screens.Play /// /// Creates the player's . /// + /// /// The . - protected virtual Score CreateScore() => new Score + protected virtual Score CreateScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index eefea737cf..93054b7bb5 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value); + protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fbb4fb5699..f6a89e7fa9 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Play NonFrameStableSeek(score.Replay.Frames[0].Time); } - protected override Score CreateScore() => score; + protected override Score CreateScore(IBeatmap beatmap) => score; protected override ResultsScreen CreateResults(ScoreInfo score) => new SpectatorResultsScreen(score); From 7176dc95e5d9a100cd7486122031b8b7dcd2caea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:51:53 +0900 Subject: [PATCH 125/170] Revert `Player.Score` to `protected` --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a688330f54..ecc65c6bb0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - internal Score Score { get; private set; } + protected Score Score { get; private set; } /// /// Create a new player instance. From b6af93d43445b96db2adae8ebee954891b4b35d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 15:10:56 +0900 Subject: [PATCH 126/170] Apply some code quality refactoring --- .../Difficulty/DifficultyCalculator.cs | 19 +++++++++---------- .../Play/HUD/PerformancePointsCounter.cs | 19 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 3d90cc59f4..49a4b2c265 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -234,33 +234,32 @@ namespace osu.Game.Rulesets.Difficulty this.baseBeatmap = baseBeatmap; } + public readonly List HitObjects = new List(); + + IReadOnlyList IBeatmap.HitObjects => HitObjects; + + #region Delegated IBeatmap implementation + public BeatmapInfo BeatmapInfo { get => baseBeatmap.BeatmapInfo; set => baseBeatmap.BeatmapInfo = value; } - public BeatmapMetadata Metadata => baseBeatmap.Metadata; - public ControlPointInfo ControlPointInfo { get => baseBeatmap.ControlPointInfo; set => baseBeatmap.ControlPointInfo = value; } + public BeatmapMetadata Metadata => baseBeatmap.Metadata; public List Breaks => baseBeatmap.Breaks; - public double TotalBreakTime => baseBeatmap.TotalBreakTime; - - public readonly List HitObjects = new List(); - - IReadOnlyList IBeatmap.HitObjects => HitObjects; - public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); - public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); - public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); + + #endregion } } } diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 13b94e6cd6..7babc90427 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -46,9 +45,6 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; - [CanBeNull] - private Ruleset gameplayRuleset; - private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); public PerformancePointsCounter() @@ -63,8 +59,8 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState != null) { - gameplayRuleset = gameplayState.Ruleset; - difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) + var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -82,15 +78,14 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState?.Score == null || timedAttributes == null || timedAttributes.Length == 0) return; - Debug.Assert(gameplayRuleset != null); - - var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); - var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); - Current.Value = (int)Math.Round(ppProcessor?.Calculate() ?? 0, MidpointRounding.AwayFromZero); + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); + + Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); } protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); @@ -145,7 +140,7 @@ namespace osu.Game.Screens.Play.HUD } } - // Todo: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. + // TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. private class GameplayWorkingBeatmap : WorkingBeatmap { private readonly IBeatmap gameplayBeatmap; From 81a13566bc36574c943d9c4aa37fc7b47dcdd711 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 15:23:34 +0900 Subject: [PATCH 127/170] Adjust default location slightly, fix alignment of "pp" subtext --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 3 ++- osu.Game/Skinning/DefaultSkin.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 7babc90427..2fc3f23190 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -133,7 +133,8 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Text = @"pp", - Font = OsuFont.Numeric.With(size: 8) + Font = OsuFont.Numeric.With(size: 8), + Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better } } }; diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 8c1e5313d5..8e03bddb4d 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -84,7 +84,7 @@ namespace osu.Game.Skinning if (ppCounter != null) { - ppCounter.Y = score.Position.Y + score.ScreenSpaceDrawQuad.Size.Y; + ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; ppCounter.Origin = Anchor.TopCentre; ppCounter.Anchor = Anchor.TopCentre; } From 676df55a0e40df9a48e3661cf4f6ed89a90828d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 15:39:29 +0900 Subject: [PATCH 128/170] Fade display out during rewind (as the value displayed is no longer valid) --- .../Graphics/UserInterface/RollingCounter.cs | 12 +++++---- .../Rulesets/Scoring/JudgementProcessor.cs | 7 +++++ .../Play/HUD/PerformancePointsCounter.cs | 26 +++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 67b0b6a06b..16555075d1 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -25,7 +25,9 @@ namespace osu.Game.Graphics.UserInterface set => current.Current = value; } - private IHasText displayedCountSpriteText; + private IHasText displayedCountText; + + public Drawable DrawableCount { get; private set; } /// /// If true, the roll-up duration will be proportional to change in value. @@ -72,16 +74,16 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load() { - displayedCountSpriteText = CreateText(); + displayedCountText = CreateText(); UpdateDisplay(); - Child = (Drawable)displayedCountSpriteText; + Child = DrawableCount = (Drawable)displayedCountText; } protected void UpdateDisplay() { - if (displayedCountSpriteText != null) - displayedCountSpriteText.Text = FormatCount(DisplayedCount); + if (displayedCountText != null) + displayedCountText.Text = FormatCount(DisplayedCount); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 201a05e569..ed4a16f0e8 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -18,6 +18,11 @@ namespace osu.Game.Rulesets.Scoring /// public event Action NewJudgement; + /// + /// Invoked when a judgement is reverted, usually due to rewinding gameplay. + /// + public event Action JudgementReverted; + /// /// The maximum number of hits that can be judged. /// @@ -71,6 +76,8 @@ namespace osu.Game.Rulesets.Scoring JudgedHits--; RevertResultInternal(result); + + JudgementReverted?.Invoke(result); } /// diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 2fc3f23190..b82a85691a 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -42,6 +42,9 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private GameplayState gameplayState { get; set; } + [Resolved] + private GameplayClock gameplayClock { get; set; } + [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; @@ -70,7 +73,24 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); if (scoreProcessor != null) + { scoreProcessor.NewJudgement += onNewJudgement; + scoreProcessor.JudgementReverted += onJudgementReverted; + } + } + + private bool isValid; + + protected bool IsValid + { + set + { + if (value == isValid) + return; + + isValid = value; + DrawableCount.FadeTo(isValid ? 1 : 0.3f, 1000, Easing.OutQuint); + } } private void onNewJudgement(JudgementResult judgement) @@ -86,6 +106,12 @@ namespace osu.Game.Screens.Play.HUD var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); + IsValid = true; + } + + private void onJudgementReverted(JudgementResult obj) + { + IsValid = false; } protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); From 45b63cbad9dbb31a35cf2da3d3a88b2de8a5460a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 16:03:25 +0900 Subject: [PATCH 129/170] Remove unnecessary dependency --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index b82a85691a..c554a038ed 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -42,9 +42,6 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private GameplayState gameplayState { get; set; } - [Resolved] - private GameplayClock gameplayClock { get; set; } - [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; From 565e888f587e6bed57318d17eff71167e6820325 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:40:07 +0900 Subject: [PATCH 130/170] Tidy up attribute retrieval code --- .../Play/HUD/PerformancePointsCounter.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index c554a038ed..fe34dd1d22 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -92,25 +92,32 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (gameplayState?.Score == null || timedAttributes == null || timedAttributes.Length == 0) + var attrib = getAttributeAtTime(judgement); + + if (gameplayState == null || attrib == null) return; - int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); - if (attribIndex < 0) - attribIndex = ~attribIndex - 1; - attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); - - var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo); Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); IsValid = true; } - private void onJudgementReverted(JudgementResult obj) + [CanBeNull] + private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) { - IsValid = false; + if (timedAttributes == null || timedAttributes.Length == 0) + return null; + + int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + if (attribIndex < 0) + attribIndex = ~attribIndex - 1; + + return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Length - 1)].Attributes; } + private void onJudgementReverted(JudgementResult obj) => IsValid = false; + protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); protected override IHasText CreateText() => new TextComponent(); From eeb5f3d5191f87f20e9582caabab5792ab69bf2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:48:54 +0900 Subject: [PATCH 131/170] Add basic test scene --- .../TestScenePerformancePointsCounter.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs new file mode 100644 index 0000000000..350d08f63d --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePerformancePointsCounter : OsuTestScene + { + [Cached] + private GameplayState gameplayState; + + [Cached] + private ScoreProcessor scoreProcessor; + + private int iteration; + + public TestScenePerformancePointsCounter() + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo) + .GetPlayableBeatmap(ruleset.RulesetInfo); + + gameplayState = new GameplayState(beatmap, ruleset); + scoreProcessor = new ScoreProcessor(); + } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create counter", () => + { + Child = new PerformancePointsCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + }; + }); + + AddRepeatStep("Add judgement", () => + { + var scoreInfo = gameplayState.Score.ScoreInfo; + + scoreInfo.MaxCombo = iteration * 1000; + scoreInfo.Accuracy = 1; + scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + + scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000, + }, new OsuJudgement()) + { + Type = HitResult.Perfect, + }); + + iteration++; + }, 10); + + AddStep("Revert judgement", () => + { + scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); + }); + } + } +} From fa7f11d906f8214dc15b01b04b405f8f957909ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:51:49 +0900 Subject: [PATCH 132/170] Add easing to rolling counter value --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index fe34dd1d22..ab10399903 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -34,6 +34,10 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } + protected override bool IsRollingProportional => true; + + protected override double RollingDuration => 1000; + [CanBeNull] [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } From 599d82e383f167632719758554b024fb4ab58bf4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:59:54 +0900 Subject: [PATCH 133/170] Avoid returning a live `IEnumerable` --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 4 ++-- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 10 +++++++--- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index e6c287112f..3777365088 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -148,9 +148,9 @@ namespace osu.Game.Beatmaps }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } - public Task GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) + public Task> GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) { - return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods).ToArray(), + return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods), token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 49a4b2c265..a7c4790366 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -58,12 +58,14 @@ namespace osu.Game.Rulesets.Difficulty return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); } - public IEnumerable CalculateTimed(params Mod[] mods) + public List CalculateTimed(params Mod[] mods) { preProcess(mods); + var attribs = new List(); + if (!Beatmap.HitObjects.Any()) - yield break; + return attribs; var skills = CreateSkills(Beatmap, playableMods, clockRate); var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); @@ -75,8 +77,10 @@ namespace osu.Game.Rulesets.Difficulty foreach (var skill in skills) skill.ProcessInternal(hitObject); - yield return new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)); + attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); } + + return attribs; } /// diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index ab10399903..514369735e 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD private GameplayState gameplayState { get; set; } [CanBeNull] - private TimedDifficultyAttributes[] timedAttributes; + private List timedAttributes; private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); @@ -110,14 +110,14 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) { - if (timedAttributes == null || timedAttributes.Length == 0) + if (timedAttributes == null || timedAttributes.Count == 0) return null; - int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; - return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Length - 1)].Attributes; + return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; } private void onJudgementReverted(JudgementResult obj) => IsValid = false; From 04538a69e403fcbd8bbcf3e8e2baea5afc00191e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:10:24 +0900 Subject: [PATCH 134/170] Add assert tests --- .../TestScenePerformancePointsCounter.cs | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index 350d08f63d..26f4bda171 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay private ScoreProcessor scoreProcessor; private int iteration; + private PerformancePointsCounter counter; public TestScenePerformancePointsCounter() { @@ -47,37 +49,55 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Create counter", () => { - Child = new PerformancePointsCounter + iteration = 0; + + Child = counter = new PerformancePointsCounter { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(5), }; }); + } - AddRepeatStep("Add judgement", () => - { - var scoreInfo = gameplayState.Score.ScoreInfo; + [Test] + public void TestBasicCounting() + { + AddAssert("counter displaying zero", () => counter.Current.Value == 0); - scoreInfo.MaxCombo = iteration * 1000; - scoreInfo.Accuracy = 1; - scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + AddRepeatStep("Add judgement", applyOneJudgement, 10); - scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject - { - StartTime = iteration * 10000, - }, new OsuJudgement()) - { - Type = HitResult.Perfect, - }); - - iteration++; - }, 10); + AddUntilStep("counter non-zero", () => counter.Current.Value > 0); AddStep("Revert judgement", () => { scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); }); + + AddUntilStep("counter faded", () => counter.Child.Alpha < 1); + + AddStep("Add judgement", applyOneJudgement); + + AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); + } + + private void applyOneJudgement() + { + var scoreInfo = gameplayState.Score.ScoreInfo; + + scoreInfo.MaxCombo = iteration * 1000; + scoreInfo.Accuracy = 1; + scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + + scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000, + }, new OsuJudgement()) + { + Type = HitResult.Perfect, + }); + + iteration++; } } } From f64226ded6da72516c59a65047b19b1eaf4427b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:10:32 +0900 Subject: [PATCH 135/170] Fix display not displaying correctly after initial load --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 514369735e..da821d76ad 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -51,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); + private JudgementResult lastJudgement; + public PerformancePointsCounter() { Current.Value = DisplayedCount = 0; @@ -65,7 +67,12 @@ namespace osu.Game.Screens.Play.HUD { var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) - .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); + .ContinueWith(r => Schedule(() => + { + timedAttributes = r.Result; + if (lastJudgement != null) + onNewJudgement(lastJudgement); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -96,6 +103,8 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { + lastJudgement = judgement; + var attrib = getAttributeAtTime(judgement); if (gameplayState == null || attrib == null) From 1e4da8112088b5a6a315fa4b962e3e7f5245c39a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:14:09 +0900 Subject: [PATCH 136/170] Fix import notifications not showing correct text --- osu.Game/Database/ArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9ad2dec12e..ee1a7e2900 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -197,7 +197,7 @@ namespace osu.Game.Database else { notification.CompletionText = imported.Count == 1 - ? $"Imported {imported.First()}!" + ? $"Imported {imported.First().Value}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; if (imported.Count > 0 && PostImport != null) From 0859c336de52dcb5d69492f02ef5de704d782653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:24:36 +0900 Subject: [PATCH 137/170] Also dim counter during initial calculation phase --- .../Gameplay/TestScenePerformancePointsCounter.cs | 1 + osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index 26f4bda171..c7d2204de2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -68,6 +68,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("Add judgement", applyOneJudgement, 10); AddUntilStep("counter non-zero", () => counter.Current.Value > 0); + AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); AddStep("Revert judgement", () => { diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index da821d76ad..9eac62fe73 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -38,6 +38,8 @@ namespace osu.Game.Screens.Play.HUD protected override double RollingDuration => 1000; + private const float alpha_when_invalid = 0.3f; + [CanBeNull] [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } @@ -70,6 +72,7 @@ namespace osu.Game.Screens.Play.HUD .ContinueWith(r => Schedule(() => { timedAttributes = r.Result; + IsValid = true; if (lastJudgement != null) onNewJudgement(lastJudgement); }), TaskContinuationOptions.OnlyOnRanToCompletion); @@ -97,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD return; isValid = value; - DrawableCount.FadeTo(isValid ? 1 : 0.3f, 1000, Easing.OutQuint); + DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint); } } @@ -133,7 +136,10 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); - protected override IHasText CreateText() => new TextComponent(); + protected override IHasText CreateText() => new TextComponent + { + Alpha = alpha_when_invalid + }; protected override void Dispose(bool isDisposing) { From 2be44188efbd6befbd4a39242a9f823509d7e35c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:59:38 +0900 Subject: [PATCH 138/170] Add missing null checks --- osu.Game/Overlays/RankingsOverlay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 2263d54d7b..80ce2e038d 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -143,6 +143,9 @@ namespace osu.Game.Overlays switch (request) { case GetUserRankingsRequest userRequest: + if (userRequest.Response == null) + return null; + switch (userRequest.Type) { case UserRankingsType.Performance: @@ -155,7 +158,12 @@ namespace osu.Game.Overlays return null; case GetCountryRankingsRequest countryRequest: + { + if (countryRequest.Response == null) + return null; + return new CountriesTable(1, countryRequest.Response.Countries); + } } return null; From 94153e8bba3293d7141ab4091d0ab3203c20280f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 18:06:24 +0900 Subject: [PATCH 139/170] Fix `TestDifficultyIconSelectingForDifferentRuleset` potentially failing due to async load --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 70224ae9f2..067f1cabb4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -714,10 +714,11 @@ namespace osu.Game.Tests.Visual.SongSelect }); FilterableDifficultyIcon difficultyIcon = null; - AddStep("Find an icon for different ruleset", () => + AddUntilStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); + .FirstOrDefault(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); + return difficultyIcon != null; }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); From 5d708b612d3c4326c56a308b5e42723d00d91ed0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 18:17:20 +0900 Subject: [PATCH 140/170] Fix delete local score test not waiting for "fetch" to complete Even though this is a completely local operation in this case, there's still a level of asynchronous operation which was recent introduced with the score ordering: https://github.com/ppy/osu/blob/853cf6feaa165e833ecb7ca18c6cdffe8ca6e005/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs#L159 This means there is a brief period where the `Scores` property is null, after `Reset()` is called in the re-fetch procedure. --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index d0a76bac27..189b143a35 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -162,6 +162,8 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for fetch", () => leaderboard.Scores != null); + AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); } From e6efdae7c95967543caedd282f91cf03b2ea953d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 18:53:20 +0900 Subject: [PATCH 141/170] Add various logging output in an atttempt to figure multiplayer test failure --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 3 +++ osu.Game/Tests/Visual/ScreenTestScene.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index a8fda19c60..c040ab27e4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -68,6 +69,8 @@ namespace osu.Game.Tests.Visual.Multiplayer LoadScreen(dependenciesScreen = new DependenciesScreen(client)); }); + AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen); + AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.Ready); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index b30be05ac4..966c513269 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -4,6 +4,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; @@ -32,6 +34,9 @@ namespace osu.Game.Tests.Visual content = new Container { RelativeSizeAxes = Axes.Both }, DialogOverlay = new DialogOverlay() }); + + Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + Stack.ScreenExited += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); From 98fef6ece2d39efb59ea89384a7799b5f7c0efba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 19:08:30 +0900 Subject: [PATCH 142/170] Handle judgement reverts with actual display updates --- .../Gameplay/TestScenePerformancePointsCounter.cs | 8 ++++++-- .../Screens/Play/HUD/PerformancePointsCounter.cs | 15 ++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index c7d2204de2..4c48d52acd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -63,6 +63,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBasicCounting() { + int previousValue = 0; + AddAssert("counter displaying zero", () => counter.Current.Value == 0); AddRepeatStep("Add judgement", applyOneJudgement, 10); @@ -72,14 +74,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Revert judgement", () => { + previousValue = counter.Current.Value; + scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); }); - AddUntilStep("counter faded", () => counter.Child.Alpha < 1); + AddUntilStep("counter decreased", () => counter.Current.Value < previousValue); AddStep("Add judgement", applyOneJudgement); - AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); + AddUntilStep("counter non-zero", () => counter.Current.Value > 0); } private void applyOneJudgement() diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 9eac62fe73..2ae7b5660a 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.Play.HUD timedAttributes = r.Result; IsValid = true; if (lastJudgement != null) - onNewJudgement(lastJudgement); + onJudgementChanged(lastJudgement); }), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -85,8 +85,8 @@ namespace osu.Game.Screens.Play.HUD if (scoreProcessor != null) { - scoreProcessor.NewJudgement += onNewJudgement; - scoreProcessor.JudgementReverted += onJudgementReverted; + scoreProcessor.NewJudgement += onJudgementChanged; + scoreProcessor.JudgementReverted += onJudgementChanged; } } @@ -104,14 +104,17 @@ namespace osu.Game.Screens.Play.HUD } } - private void onNewJudgement(JudgementResult judgement) + private void onJudgementChanged(JudgementResult judgement) { lastJudgement = judgement; var attrib = getAttributeAtTime(judgement); if (gameplayState == null || attrib == null) + { + IsValid = false; return; + } var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo); @@ -132,8 +135,6 @@ namespace osu.Game.Screens.Play.HUD return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; } - private void onJudgementReverted(JudgementResult obj) => IsValid = false; - protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); protected override IHasText CreateText() => new TextComponent @@ -146,7 +147,7 @@ namespace osu.Game.Screens.Play.HUD base.Dispose(isDisposing); if (scoreProcessor != null) - scoreProcessor.NewJudgement -= onNewJudgement; + scoreProcessor.NewJudgement -= onJudgementChanged; loadCancellationSource?.Cancel(); } From 12da27cde728b0d17517e23306b0e180be9e75d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Oct 2021 20:52:40 +0200 Subject: [PATCH 143/170] Add test coverage for loading process on channel join --- .../Visual/Online/TestSceneChatOverlay.cs | 38 ++++++++++++++++++- .../Online/API/Requests/GetMessagesRequest.cs | 6 +-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 609e637914..9562b41363 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -18,6 +19,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Selection; using osu.Game.Overlays.Chat.Tabs; using osu.Game.Users; @@ -41,6 +43,9 @@ namespace osu.Game.Tests.Visual.Online private Channel channel2 => channels[1]; private Channel channel3 => channels[2]; + [CanBeNull] + private Func> onGetMessages; + [Resolved] private GameHost host { get; set; } @@ -79,6 +84,8 @@ namespace osu.Game.Tests.Visual.Online { AddStep("register request handling", () => { + onGetMessages = null; + ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -102,6 +109,12 @@ namespace osu.Game.Tests.Visual.Online } return true; + + case GetMessagesRequest getMessages: + var messages = onGetMessages?.Invoke(getMessages.Channel); + if (messages != null) + getMessages.TriggerSuccess(messages); + return true; } return false; @@ -122,14 +135,37 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSelectingChannelClosesSelector() + public void TestChannelSelection() { AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); + AddStep("Setup get message response", () => onGetMessages = channel => + { + if (channel == channel1) + { + return new List + { + new Message(1) + { + ChannelId = channel1.Id, + Content = "hello from channel 1!", + Sender = new User + { + Id = 2, + Username = "test_user" + } + } + }; + } + + return null; + }); AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); AddAssert("Current channel is channel 1", () => currentChannel == channel1); + AddUntilStep("Loading spinner hidden", () => chatOverlay.ChildrenOfType().All(spinner => !spinner.IsPresent)); + AddAssert("Channel message shown", () => chatOverlay.ChildrenOfType().Count() == 1); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } diff --git a/osu.Game/Online/API/Requests/GetMessagesRequest.cs b/osu.Game/Online/API/Requests/GetMessagesRequest.cs index 36e81a9348..651f8a06c5 100644 --- a/osu.Game/Online/API/Requests/GetMessagesRequest.cs +++ b/osu.Game/Online/API/Requests/GetMessagesRequest.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests { public class GetMessagesRequest : APIRequest> { - private readonly Channel channel; + public readonly Channel Channel; public GetMessagesRequest(Channel channel) { - this.channel = channel; + Channel = channel; } - protected override string Target => $@"chat/channels/{channel.Id}/messages"; + protected override string Target => $@"chat/channels/{Channel.Id}/messages"; } } From a5b07ce4feaf88cf2f2f5eee6e43b166a8207737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Oct 2021 20:53:06 +0200 Subject: [PATCH 144/170] Fix backwards containment check in chat channel load callback --- osu.Game/Overlays/ChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 20d637d957..4b27335c7c 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -285,7 +285,7 @@ namespace osu.Game.Overlays return; // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means). - if (loadedChannels.Contains(loaded)) + if (!loadedChannels.Contains(loaded)) return; loading.Hide(); From baa8baaa1efd762b650c4e2b9e409f700e601fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Oct 2021 21:12:35 +0200 Subject: [PATCH 145/170] Fix "most played beatmap" request breakage after property rename --- .../Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 15f67eda47..10f7ca6fe2 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("count")] public int PlayCount { get; set; } - [JsonProperty] + [JsonProperty("beatmap")] private BeatmapInfo beatmapInfo { get; set; } [JsonProperty] From 777763a55095147c307b32e502aa5fd9220f5066 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:27:36 +0900 Subject: [PATCH 146/170] Add more comprehensive (and failing) test coverage of replay download button --- .../Gameplay/TestSceneReplayDownloadButton.cs | 84 +++++++++++++++++-- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 1809332bce..5dd4ecf30b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -8,34 +8,97 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osu.Game.Users; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Rulesets; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneReplayDownloadButton : OsuTestScene + public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene { [Resolved] private RulesetStore rulesets { get; set; } private TestReplayDownloadButton downloadButton; - public TestSceneReplayDownloadButton() + [Test] + public void TestDisplayStates() { - createButton(true); + AddStep(@"create button with replay", () => + { + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddStep("click button", () => downloadButton.TriggerClick()); + AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading)); AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); - createButton(false); - createButtonNoScore(); } - private void createButton(bool withReplay) + [Test] + public void TestButtonWithReplayStartsDownload() { - AddStep(withReplay ? @"create button with replay" : "create button without replay", () => + bool downloadStarted = false; + bool downloadFinished = false; + + AddStep(@"create button with replay", () => { - Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(withReplay)) + downloadStarted = false; + downloadFinished = false; + + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + downloadButton.State.BindValueChanged(state => + { + switch (state.NewValue) + { + case DownloadState.Downloading: + downloadStarted = true; + break; + } + + switch (state.OldValue) + { + case DownloadState.Downloading: + downloadFinished = true; + break; + } + }); + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(downloadButton); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("state entered downloading", () => downloadStarted); + AddUntilStep("state left downloading", () => downloadFinished); + } + + [Test] + public void TestButtonWithoutReplay() + { + AddStep("create button without replay", () => + { + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(false)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +108,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for load", () => downloadButton.IsLoaded); } - private void createButtonNoScore() + [Test] + public void CreateButtonWithNoScore() { AddStep("create button with null score", () => { @@ -78,6 +142,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public void SetDownloadState(DownloadState state) => State.Value = state; + public new Bindable State => base.State; + public TestReplayDownloadButton(ScoreInfo score) : base(score) { From 5a4474e1b2560896c3f09dc2d4fc94c437b70d88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:28:11 +0900 Subject: [PATCH 147/170] Fix incorrect DI retrieval in `ReplayDownloadButton` --- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index d96b6989b4..e644eb671a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreModelDownloader scores) + private void load(OsuGame game, ScoreManager scores) { InternalChild = shakeContainer = new ShakeContainer { @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking break; case DownloadState.NotDownloaded: - scores.Download(Model.Value); + scores.Download(Model.Value, false); break; case DownloadState.Importing: From 1f6a31355c514e809ca61a64f3a82e3a608e1777 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:30:49 +0900 Subject: [PATCH 148/170] Remove unused using statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Tests/Visual/ScreenTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 966c513269..aa46b516bf 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; -using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; From 1a784b788dba00e79be4011b2af652d9250d3be4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:31:07 +0900 Subject: [PATCH 149/170] Fix incorrect load state check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index c040ab27e4..80217a7726 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen); - AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.Ready); + AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.NotLoaded); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); From 31c0c7a8881e56a02b44794a3d2406548e79b69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:49:04 +0900 Subject: [PATCH 150/170] Remove pointless (and incorrect) click step --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 5dd4ecf30b..8197e09594 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -37,8 +37,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for load", () => downloadButton.IsLoaded); - AddStep("click button", () => downloadButton.TriggerClick()); - AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading)); AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); From d6f25e07ccc453bc06de28fef5eefaa2540e1e4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:49:18 +0900 Subject: [PATCH 151/170] Add assert coverage of non-downloadable states --- .../Visual/Gameplay/TestSceneReplayDownloadButton.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8197e09594..5e2374cbcb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.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 System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online; @@ -9,6 +10,8 @@ using osu.Game.Scoring; using osu.Game.Users; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Screens.Ranking; using osuTK.Input; @@ -104,6 +107,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } [Test] @@ -119,6 +125,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } private ScoreInfo getScoreInfo(bool replayAvailable) From 4d5696959b0435bf4bcb0fad96b7c233eaf2bdb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:52:28 +0900 Subject: [PATCH 152/170] Remove unnecessary access modifier in interface Co-authored-by: Dan Balasescu --- osu.Game/Rulesets/IRulesetInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index ded3ac4b58..779433dc81 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets /// string InstantiationInfo { get; } - public Ruleset? CreateInstance() + Ruleset? CreateInstance() { var type = Type.GetType(InstantiationInfo); From 4f59fc15a50910eb4716f3968779e64b28856acc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:54:37 +0900 Subject: [PATCH 153/170] Mark `BeatmapSet` as nullable for the time being --- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 6a3f1b43d8..3d51c5d4b6 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps /// /// The beatmap set this beatmap is part of. /// - IBeatmapSetInfo BeatmapSet { get; } + IBeatmapSetInfo? BeatmapSet { get; } /// /// The playable length in milliseconds of this beatmap. From ffbb7a9b1a45692305c984a1a7fc42d37db2993d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:22:32 +0900 Subject: [PATCH 154/170] Remove incorrect csproj change Co-authored-by: Dan Balasescu --- osu.Game/osu.Game.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 05c587bcc0..4877ddf725 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -43,7 +43,4 @@ - - - From 6e797ddcac59f939f7fed68a9f0a91c1bf9a5143 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:41:17 +0900 Subject: [PATCH 155/170] Add test coverage of creating, saving and loading a new beatmap --- .../Visual/Editing/TestSceneEditorSaving.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs new file mode 100644 index 0000000000..2258a209e2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.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.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorSaving : OsuGameTestScene + { + private Editor editor => Game.ChildrenOfType().FirstOrDefault(); + + private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap)); + + /// + /// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select. + /// + [Test] + public void TestNewBeatmapSaveThenLoad() + { + AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + + PushAndConfirm(() => new EditorLoader()); + + AddUntilStep("wait for editor load", () => editor != null); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); + AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + + AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); + AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); + + AddStep("Save and exit", () => + { + InputManager.Keys(PlatformAction.Save); + InputManager.Key(Key.Escape); + }); + + AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + + PushAndConfirm(() => new PlaySongSelect()); + + AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault); + AddStep("Open options", () => InputManager.Key(Key.F3)); + AddStep("Enter editor", () => InputManager.Key(Key.Number5)); + + AddUntilStep("Wait for editor load", () => editor != null); + AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + } + } +} From 007b33cd88d465253613f6d74d05c93c8a6a0882 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:05:11 +0900 Subject: [PATCH 156/170] Add missing methods to interfaces --- osu.Game/Beatmaps/BeatmapModelManager.cs | 4 ++-- osu.Game/Beatmaps/IBeatmapModelManager.cs | 20 ++++++++++++++++++++ osu.Game/Beatmaps/IWorkingBeatmapCache.cs | 12 ++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Beatmaps/IBeatmapModelManager.cs diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 250d6653d5..787559899a 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : ArchiveModelManager + public class BeatmapModelManager : ArchiveModelManager, IBeatmapModelManager { /// /// Fired when a single difficulty has been hidden. @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps /// /// The game working beatmap cache, used to invalidate entries on changes. /// - public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } + public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; } private readonly Bindable> beatmapRestored = new Bindable>(); diff --git a/osu.Game/Beatmaps/IBeatmapModelManager.cs b/osu.Game/Beatmaps/IBeatmapModelManager.cs new file mode 100644 index 0000000000..8c243c2b77 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapModelManager.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Database; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapModelManager : IModelManager + { + /// + /// Provide an online lookup queue component to handle populating online beatmap metadata. + /// + BeatmapOnlineLookupQueue OnlineLookupQueue { set; } + + /// + /// Provide a working beatmap cache, used to invalidate entries on changes. + /// + IWorkingBeatmapCache WorkingBeatmapCache { set; } + } +} diff --git a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs index 881e734292..3eb33f10d6 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs @@ -11,5 +11,17 @@ namespace osu.Game.Beatmaps /// The beatmap to lookup. /// A instance correlating to the provided . WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo); + + /// + /// Invalidate a cache entry if it exists. + /// + /// The beatmap set info to invalidate any cached entries for. + void Invalidate(BeatmapSetInfo beatmapSetInfo); + + /// + /// Invalidate a cache entry if it exists. + /// + /// The beatmap info to invalidate any cached entries for. + void Invalidate(BeatmapInfo beatmapInfo); } } From 8ffaa491e7303289b35de86eb1fb85e3a9097e81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:05:30 +0900 Subject: [PATCH 157/170] Fix `BeatmapModelManager` not receiving `WorkingBeatmapCache` --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 91d5b16204..240db22c00 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -45,6 +45,7 @@ namespace osu.Game.Beatmaps workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; + beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; if (performOnlineLookups) { @@ -305,6 +306,9 @@ namespace osu.Game.Beatmaps public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); + void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo); + #endregion #region Implementation of IModelFileManager From 90fdaf18c027ac08b3cd15e636105709dd6903e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:40:25 +0900 Subject: [PATCH 158/170] Fix `PushAndConfirm` potentially failing if new screen quickly pushes a child screen --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 881c4bab02..c025cf85c7 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -83,8 +83,17 @@ namespace osu.Game.Tests.Visual protected void PushAndConfirm(Func newScreen) { Screen screen = null; - AddStep("Push new screen", () => Game.ScreenStack.Push(screen = newScreen())); - AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen == screen && screen.IsLoaded); + IScreen previousScreen = null; + + AddStep("Push new screen", () => + { + previousScreen = Game.ScreenStack.CurrentScreen; + Game.ScreenStack.Push(screen = newScreen()); + }); + + AddUntilStep("Wait for new screen", () => screen.IsLoaded + && Game.ScreenStack.CurrentScreen != previousScreen + && previousScreen.GetChildScreen() == screen); } protected void ConfirmAtMainMenu() => AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); From d9849bcf4995b69bf493d9dcaab88950c40f3a4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 13:14:39 +0900 Subject: [PATCH 159/170] Fix dragging on an editor file selection text box causing repeated popover display Local fix and no tests as this is a pretty weird usage of `TextBox`. We'll probably want to change it to not use a textbox eventually. Closes #14969. --- osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index fd43349793..f833bc49f7 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -89,6 +89,13 @@ namespace osu.Game.Screens.Edit.Setup { public Action OnFocused; + protected override bool OnDragStart(DragStartEvent e) + { + // This text box is intended to be "read only" without actually specifying that. + // As such we don't want to allow the user to select its content with a drag. + return false; + } + protected override void OnFocus(FocusEvent e) { OnFocused?.Invoke(); From 3803f2f4624293b7c948a4ee9b6eb3211572a853 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 16:07:27 +0900 Subject: [PATCH 160/170] Fix leaderboard potentially displaying the wrong scores Closes #14762. This class is ugly. I think the whole process should be clened up once we have correctly-scheduled `SynchronizationContext`s. There's not much saving it as long as all these interdispersed `Schedule`s around required. --- osu.Game/Online/Leaderboards/Leaderboard.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 4f8b27602b..e3ac9f603d 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -255,6 +255,7 @@ namespace osu.Game.Online.Leaderboards } private APIRequest getScoresRequest; + private ScheduledDelegate getScoresRequestCallback; protected abstract bool IsOnlineScope { get; } @@ -282,13 +283,16 @@ namespace osu.Game.Online.Leaderboards getScoresRequest?.Cancel(); getScoresRequest = null; + getScoresRequestCallback?.Cancel(); + getScoresRequestCallback = null; + pendingUpdateScores?.Cancel(); pendingUpdateScores = Schedule(() => { PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = FetchScores(scores => Schedule(() => + getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => { Scores = scores.ToArray(); PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; @@ -297,7 +301,7 @@ namespace osu.Game.Online.Leaderboards if (getScoresRequest == null) return; - getScoresRequest.Failure += e => Schedule(() => + getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => { if (e is OperationCanceledException) return; From 456cfd62bf50fb58da56594555fa338be0380f14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 6 Oct 2021 16:46:24 +0900 Subject: [PATCH 161/170] Fix intermittent score panel test failure --- osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 6f3b3028be..b7b7407428 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -221,6 +221,8 @@ namespace osu.Game.Tests.Visual.Ranking list.SelectedScore.Value = middleScore; }); + AddUntilStep("wait for all scores to be visible", () => list.ChildrenOfType().All(t => t.IsPresent)); + assertScoreState(highestScore, false); assertScoreState(middleScore, true); assertScoreState(lowestScore, false); From 433e7cd4030ff1e2bd428b3112c23934369afccf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 21:26:30 +0900 Subject: [PATCH 162/170] Fix rate mods not working if pp counter is displayed --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 5 +++-- .../Screens/Play/HUD/PerformancePointsCounter.cs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index a7c4790366..1143549f7f 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -110,10 +110,11 @@ namespace osu.Game.Rulesets.Difficulty private void preProcess(Mod[] mods) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + + Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, playableMods); var track = new TrackVirtual(10000); - mods.OfType().ForEach(m => m.ApplyToTrack(track)); + playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); clockRate = track.Rate; } diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 2ae7b5660a..15d9f9517b 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.Play.HUD Current.Value = DisplayedCount = 0; } + private Mod[] clonedMods; + [BackgroundDependencyLoader] private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) { @@ -67,8 +69,10 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState != null) { + clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); + var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); - difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.Select(m => m.DeepClone()).ToArray(), loadCancellationSource.Token) .ContinueWith(r => Schedule(() => { timedAttributes = r.Result; @@ -116,7 +120,11 @@ namespace osu.Game.Screens.Play.HUD return; } - var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo); + // awkward but we need to make sure the true mods are not passed to PerformanceCalculator as it makes a mess of track applications. + var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone(); + scoreInfo.Mods = clonedMods; + + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, scoreInfo); Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); IsValid = true; From 9705c7b546ff0ebe61e7bae5bd5ea8e3fdbcadd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 21:30:30 +0900 Subject: [PATCH 163/170] Use cloned mods in one more place --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 15d9f9517b..ef289c2a20 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); - difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.Select(m => m.DeepClone()).ToArray(), loadCancellationSource.Token) + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token) .ContinueWith(r => Schedule(() => { timedAttributes = r.Result; From 5f129ae33c515adc30874c1df6cd475769e5d1a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 14:53:36 +0900 Subject: [PATCH 164/170] Remove local overridden storage of `Mods` in `Player` Not required and only causing headaches. Accessing mods should now be done via `GameplayState`. Closes #14912. --- osu.Game/Screens/Play/Player.cs | 39 ++++++++++++++------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ecc65c6bb0..e2ed71e28a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -125,15 +124,11 @@ namespace osu.Game.Screens.Play public DimmableStoryboard DimmableStoryboard { get; private set; } - [Cached] - [Cached(Type = typeof(IBindable>))] - protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); - /// /// Whether failing should be allowed. /// By default, this checks whether all selected mods allow failing. /// - protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); + protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType().All(m => m.PerformFail()); public readonly PlayerConfiguration Configuration; @@ -179,12 +174,12 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { - Mods.Value = base.Mods.Value.Select(m => m.DeepClone()).ToArray(); + var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) return; - IBeatmap playableBeatmap = loadPlayableBeatmap(); + IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods); if (playableBeatmap == null) return; @@ -199,12 +194,12 @@ namespace osu.Game.Screens.Play if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods); dependencies.CacheAs(DrawableRuleset); ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); - ScoreProcessor.Mods.BindTo(Mods); + ScoreProcessor.Mods.Value = gameplayMods; dependencies.CacheAs(ScoreProcessor); @@ -223,9 +218,9 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - Score.ScoreInfo.Mods = Mods.Value.ToArray(); + Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -302,13 +297,13 @@ namespace osu.Game.Screens.Play // this is required for mods that apply transforms to these processors. ScoreProcessor.OnLoadComplete += _ => { - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in gameplayMods.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); }; HealthProcessor.OnLoadComplete += _ => { - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in gameplayMods.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); }; @@ -356,7 +351,7 @@ namespace osu.Game.Screens.Play // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), - HUDOverlay = new HUDOverlay(DrawableRuleset, Mods.Value) + HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods) { HoldToQuit = { @@ -467,7 +462,7 @@ namespace osu.Game.Screens.Play } } - private IBeatmap loadPlayableBeatmap() + private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods) { IBeatmap playable; @@ -481,7 +476,7 @@ namespace osu.Game.Screens.Play try { - playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods); } catch (BeatmapInvalidForRulesetException) { @@ -489,7 +484,7 @@ namespace osu.Game.Screens.Play rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; ruleset = rulesetInfo.CreateInstance(); - playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods); } if (playable.HitObjects.Count == 0) @@ -789,7 +784,7 @@ namespace osu.Game.Screens.Play failAnimation.Start(); - if (Mods.Value.OfType().Any(m => m.RestartOnFail)) + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(); return true; @@ -919,17 +914,17 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToPlayer(this); - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToHUD(HUDOverlay); // Our mods are local copies of the global mods so they need to be re-applied to the track. // This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack. // Todo: In the future, player will receive in a track and will probably not have to worry about this... musicController.ResetTrackAdjustments(); - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToTrack(musicController.CurrentTrack); updateGameplayState(); From 697f53c4450882f3c3e70262a4c167422e9921a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:00:37 +0900 Subject: [PATCH 165/170] Fix test failure due to reference of `Player.Mods` --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 8160a62991..aee15a145c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); - AddStep("retrieve mods", () => playerMod1 = (TestMod)player.Mods.Value.Single()); + AddStep("retrieve mods", () => playerMod1 = (TestMod)player.GameplayState.Mods.Single()); AddAssert("game mods not applied", () => gameMod.Applied == false); AddAssert("player mods applied", () => playerMod1.Applied); @@ -217,7 +217,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); - AddStep("retrieve mods", () => playerMod2 = (TestMod)player.Mods.Value.Single()); + AddStep("retrieve mods", () => playerMod2 = (TestMod)player.GameplayState.Mods.Single()); AddAssert("game mods not applied", () => gameMod.Applied == false); AddAssert("player has different mods", () => playerMod1 != playerMod2); AddAssert("player mods applied", () => playerMod2.Applied); From a57b080f10050c8c9630def0fdcbf99f1718e97a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:27:57 +0900 Subject: [PATCH 166/170] Avoid showing the disclaimer in game tests No real performance gain, but this is handy to bypass when actually using one of these tests to test something. --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index c025cf85c7..dba73b0024 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -126,6 +126,8 @@ namespace osu.Game.Tests.Visual public new Bindable> SelectedMods => base.SelectedMods; + public override Version AssemblyVersion => new Version(0, 0); + // if we don't do this, when running under nUnit the version that gets populated is that of nUnit. public override string Version => "test game"; From 290c9755e261d2732b85f305feb9ba6b0a5535ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:46:50 +0900 Subject: [PATCH 167/170] Always use circles intro for `OsuGame` tests The triangles intro tracks video time, which is not adjusted based on the game's playback rate (ie. it runs in realtime even for headless tests). Maybe we want to make the triangles video adjust its rate along with tests? --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index dba73b0024..94715dfc1a 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -22,6 +22,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; +using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Tests.Visual { @@ -144,6 +145,9 @@ namespace osu.Game.Tests.Visual protected override void LoadComplete() { base.LoadComplete(); + + LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles); + API.Login("Rhythm Champion", "osu!"); Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true); From 0bd5136a293c99e31a3ead1af16f989027666b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:47:59 +0900 Subject: [PATCH 168/170] Fix `TestOverlayClosing` occasionally failing due to running too fast --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index aeb800f58a..ce437e7299 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -350,13 +350,13 @@ namespace osu.Game.Tests.Visual.Navigation // since most overlays use a scroll container that absorbs on mouse down NowPlayingOverlay nowPlayingOverlay = null; - AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("Wait for now playing load", () => (nowPlayingOverlay = Game.ChildrenOfType().FirstOrDefault()) != null); - AddStep("get and press now playing hotkey", () => - { - nowPlayingOverlay = Game.ChildrenOfType().Single(); - InputManager.Key(Key.F6); - }); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("toolbar displayed", () => Game.Toolbar.State.Value == Visibility.Visible); + + AddStep("open now playing", () => InputManager.Key(Key.F6)); + AddUntilStep("now playing is visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // drag tests @@ -417,7 +417,7 @@ namespace osu.Game.Tests.Visual.Navigation pushEscape(); // returns to osu! logo AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); - AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles); + AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); AddStep("test dispose doesn't crash", () => Game.Dispose()); From c41271ea78871e4b39af13521ff93ed853552e80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 16:26:24 +0900 Subject: [PATCH 169/170] Fix hidden test failures --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index af64be78f8..ed9da36b05 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool objectWithIncreasedVisibilityHasIndex(int index) - => Player.Mods.Value.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; + => Player.GameplayState.Mods.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; private class TestOsuModHidden : OsuModHidden { From d0001f760d82bd1953da17b39898d5ac2a4d96a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 16:50:05 +0900 Subject: [PATCH 170/170] Group applicable comment above new addition --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 94715dfc1a..77db697cb6 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -127,9 +127,8 @@ namespace osu.Game.Tests.Visual public new Bindable> SelectedMods => base.SelectedMods; + // if we don't apply these changes, when running under nUnit the version that gets populated is that of nUnit. public override Version AssemblyVersion => new Version(0, 0); - - // if we don't do this, when running under nUnit the version that gets populated is that of nUnit. public override string Version => "test game"; protected override Loader CreateLoader() => new TestLoader();