diff --git a/Directory.Build.props b/Directory.Build.props index 5bdf12218c..709545bf1d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,14 +26,6 @@ true $(NoWarn);CS1591 - - - $(NoWarn);NU1701 - false ppy Pty Ltd @@ -42,7 +34,7 @@ https://github.com/ppy/osu Automated release. ppy Pty Ltd - Copyright (c) 2021 ppy Pty Ltd + Copyright (c) 2022 ppy Pty Ltd osu game diff --git a/LICENCE b/LICENCE index b5962ad3b2..d3e7537cef 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2021 ppy Pty Ltd . +Copyright (c) 2022 ppy Pty Ltd . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj index 4624d3d771..b8c3ad373a 100644 --- a/Templates/osu.Game.Templates.csproj +++ b/Templates/osu.Game.Templates.csproj @@ -8,7 +8,7 @@ https://github.com/ppy/osu/blob/master/Templates https://github.com/ppy/osu Automated release. - Copyright (c) 2021 ppy Pty Ltd + Copyright (c) 2022 ppy Pty Ltd Templates to use when creating a ruleset for consumption in osu!. dotnet-new;templates;osu netstandard2.1 diff --git a/osu.Android.props b/osu.Android.props index fbe13b11ee..10685d5990 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index a06484214b..3ca6411812 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -25,7 +25,6 @@ - @@ -33,7 +32,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index 1757fd7c73..dc1ec17e2c 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -11,7 +11,7 @@ false A free-to-win rhythm game. Rhythm is just a *click* away! testing - Copyright (c) 2021 ppy Pty Ltd + Copyright (c) 2022 ppy Pty Ltd en-AU diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index 7516e7500b..76c49edf78 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -69,6 +69,34 @@ namespace osu.Game.Tests.NonVisual.Skinning "Gameplay/osu/followpoint", "followpoint", 1 }, + new object[] + { + // Looking up a filename with extension specified should work. + new[] { "followpoint.png" }, + "followpoint.png", + "followpoint.png", 1 + }, + new object[] + { + // Looking up a filename with extension specified should also work with @2x sprites. + new[] { "followpoint@2x.png" }, + "followpoint.png", + "followpoint@2x.png", 2 + }, + new object[] + { + // Looking up a path with extension specified should work. + new[] { "Gameplay/osu/followpoint.png" }, + "Gameplay/osu/followpoint.png", + "Gameplay/osu/followpoint.png", 1 + }, + new object[] + { + // Looking up a path with extension specified should also work with @2x sprites. + new[] { "Gameplay/osu/followpoint@2x.png" }, + "Gameplay/osu/followpoint.png", + "Gameplay/osu/followpoint@2x.png", 2 + }, }; [TestCaseSource(nameof(fallbackTestCases))] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8b7e1c4e58..5d25287e45 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -150,10 +150,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true))); AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable); + AddAssert("button is enabled", () => downloadButton.ChildrenOfType().First().Enabled.Value); AddStep("delete score", () => scoreManager.Delete(imported.Value)); AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 381b9b58bd..714951cc42 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -8,13 +8,12 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; @@ -27,6 +26,7 @@ using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Select; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Multiplayer { @@ -35,10 +35,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager manager; private RulesetStore rulesets; - private List beatmaps; + private IList beatmaps => importedBeatmapSet?.PerformRead(s => s.Beatmaps) ?? new List(); private TestMultiplayerMatchSongSelect songSelect; + private Live importedBeatmapSet; + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -46,44 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); - beatmaps = new List(); - - var metadata = new BeatmapMetadata - { - Artist = "Some Artist", - Title = "Some Beatmap", - Author = { Username = "Some Author" }, - }; - - var beatmapSetInfo = new BeatmapSetInfo - { - OnlineID = 10, - Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - DateAdded = DateTimeOffset.UtcNow - }; - - for (int i = 0; i < 8; ++i) - { - int beatmapId = 10 * 10 + i; - - int length = RNG.Next(30000, 200000); - double bpm = RNG.NextSingle(80, 200); - - var beatmap = new BeatmapInfo - { - Ruleset = rulesets.GetRuleset(i % 4) ?? throw new InvalidOperationException(), - OnlineID = beatmapId, - Length = length, - BPM = bpm, - Metadata = metadata, - Difficulty = new BeatmapDifficulty() - }; - - beatmaps.Add(beatmap); - beatmapSetInfo.Beatmaps.Add(beatmap); - } - - manager.Import(beatmapSetInfo); + importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray())); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 13404a9810..72dfc53c01 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -1,7 +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; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -17,7 +16,6 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; -using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -60,20 +58,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Size = new Vector2(550f, 450f), Scope = BeatmapLeaderboardScope.Local, - BeatmapInfo = new BeatmapInfo - { - ID = Guid.NewGuid(), - Metadata = new BeatmapMetadata - { - Title = "TestSong", - Artist = "TestArtist", - Author = new RealmUser - { - Username = "TestAuthor" - }, - }, - DifficultyName = "Insane" - }, + BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First() } }, dialogOverlay = new DialogOverlay() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs new file mode 100644 index 0000000000..34d9ddcc4e --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -0,0 +1,140 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModSelectScreen : OsuManualInputManagerTestScene + { + [Resolved] + private RulesetStore rulesetStore { get; set; } + + private ModSelectScreen modSelectScreen; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("clear contents", Clear); + AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); + AddStep("reset mods", () => SelectedMods.SetDefault()); + } + + private void createScreen() + { + AddStep("create screen", () => Child = modSelectScreen = new ModSelectScreen + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods } + }); + waitForColumnLoad(); + } + + [Test] + public void TestStateChange() + { + createScreen(); + AddStep("toggle state", () => modSelectScreen.ToggleVisibility()); + } + + [Test] + public void TestPreexistingSelection() + { + AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() }); + createScreen(); + AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + AddAssert("mod multiplier correct", () => + { + double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); + return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value); + }); + assertCustomisationToggleState(disabled: false, active: false); + } + + [Test] + public void TestExternalSelection() + { + createScreen(); + AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() }); + AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + AddAssert("mod multiplier correct", () => + { + double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); + return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value); + }); + assertCustomisationToggleState(disabled: false, active: false); + } + + [Test] + public void TestRulesetChange() + { + createScreen(); + changeRuleset(0); + changeRuleset(1); + changeRuleset(2); + changeRuleset(3); + } + + [Test] + public void TestCustomisationToggleState() + { + createScreen(); + assertCustomisationToggleState(disabled: true, active: false); + + AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + assertCustomisationToggleState(disabled: false, active: false); + + AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("dismiss mod customisation", () => + { + InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray()); + assertCustomisationToggleState(disabled: false, active: false); + + AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() }); + assertCustomisationToggleState(disabled: true, active: false); + + AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() }); + assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action. + } + + private void waitForColumnLoad() => AddUntilStep("all column content loaded", + () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); + + private void changeRuleset(int id) + { + AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id)); + waitForColumnLoad(); + } + + private void assertCustomisationToggleState(bool disabled, bool active) + { + ShearedToggleButton getToggle() => modSelectScreen.ChildrenOfType().Single(); + + AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled); + AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active); + } + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 397d47c389..bb64ec796c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -17,7 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -152,24 +151,7 @@ namespace osu.Game.Beatmaps { const double excess_length = 1000; - var lastObject = Beatmap?.HitObjects.LastOrDefault(); - - double length; - - switch (lastObject) - { - case null: - length = emptyLength; - break; - - case IHasDuration endTime: - length = endTime.EndTime + excess_length; - break; - - default: - length = lastObject.StartTime + excess_length; - break; - } + double length = (BeatmapInfo?.Length + excess_length) ?? emptyLength; return audioManager.Tracks.GetVirtual(length); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs new file mode 100644 index 0000000000..4729765084 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using osu.Framework.Logging; + +namespace osu.Game.Online.Multiplayer +{ + public static class MultiplayerClientExtensions + { + public static void FireAndForget(this Task task, Action? onSuccess = null, Action? onError = null) => + task.ContinueWith(t => + { + if (t.IsFaulted) + { + Exception? exception = t.Exception; + + if (exception is AggregateException ae) + exception = ae.InnerException; + + Debug.Assert(exception != null); + + string message = exception is HubException + // HubExceptions arrive with additional message context added, but we want to display the human readable message: + // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once." + // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now. + ? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim() + : exception.Message; + + Logger.Log(message, level: LogLevel.Important); + onError?.Invoke(exception); + } + else + { + onSuccess?.Invoke(); + } + }); + } +} diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 4fc3a904fa..248d4f288e 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -20,6 +20,8 @@ namespace osu.Game.Overlays.Mods { public class DifficultyMultiplierDisplay : CompositeDrawable, IHasCurrentValue { + public const float HEIGHT = 42; + public Bindable Current { get => current.Current; @@ -42,13 +44,12 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } - private const float height = 42; private const float multiplier_value_area_width = 56; private const float transition_duration = 200; public DifficultyMultiplierDisplay() { - Height = height; + Height = HEIGHT; AutoSizeAxes = Axes.X; InternalChild = new Container @@ -145,8 +146,9 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); + current.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); + // required to prevent the counter initially rolling up from 0 to 1 // due to `Current.Value` having a nonstandard default value of 1. multiplierCounter.SetCountWithoutRolling(Current.Value); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 736a0205e2..f84ae4ac8a 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -31,6 +31,10 @@ namespace osu.Game.Overlays.Mods { public class ModColumn : CompositeDrawable { + public readonly Container TopLevelContent; + + public readonly ModType ModType; + private Func? filter; /// @@ -48,7 +52,8 @@ namespace osu.Game.Overlays.Mods } } - private readonly ModType modType; + public Bindable> SelectedMods = new Bindable>(Array.Empty()); + private readonly Key[]? toggleKeys; private readonly Bindable>> availableMods = new Bindable>>(); @@ -69,95 +74,103 @@ namespace osu.Game.Overlays.Mods public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) { - this.modType = modType; + ModType = modType; this.toggleKeys = toggleKeys; Width = 320; RelativeSizeAxes = Axes.Y; Shear = new Vector2(ModPanel.SHEAR_X, 0); - CornerRadius = ModPanel.CORNER_RADIUS; - Masking = true; Container controlContainer; InternalChildren = new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.X, - Height = header_height + ModPanel.CORNER_RADIUS, - Children = new Drawable[] - { - headerBackground = new Box - { - RelativeSizeAxes = Axes.X, - Height = header_height + ModPanel.CORNER_RADIUS - }, - headerText = new OsuTextFlowContainer(t => - { - t.Font = OsuFont.TorusAlternate.With(size: 17); - t.Shadow = false; - t.Colour = Colour4.Black; - }) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Shear = new Vector2(-ModPanel.SHEAR_X, 0), - Padding = new MarginPadding - { - Horizontal = 17, - Bottom = ModPanel.CORNER_RADIUS - } - } - } - }, - new Container + TopLevelContent = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = header_height }, - Child = contentContainer = new Container + CornerRadius = ModPanel.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = ModPanel.CORNER_RADIUS, - BorderThickness = 3, - Children = new Drawable[] + new Container { - contentBackground = new Box + RelativeSizeAxes = Axes.X, + Height = header_height + ModPanel.CORNER_RADIUS, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - new GridContainer + headerBackground = new Box + { + RelativeSizeAxes = Axes.X, + Height = header_height + ModPanel.CORNER_RADIUS + }, + headerText = new OsuTextFlowContainer(t => + { + t.Font = OsuFont.TorusAlternate.With(size: 17); + t.Shadow = false; + t.Colour = Colour4.Black; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = new Vector2(-ModPanel.SHEAR_X, 0), + Padding = new MarginPadding + { + Horizontal = 17, + Bottom = ModPanel.CORNER_RADIUS + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = header_height }, + Child = contentContainer = new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Masking = true, + CornerRadius = ModPanel.CORNER_RADIUS, + BorderThickness = 3, + Children = new Drawable[] { - new Dimension(GridSizeMode.AutoSize), - new Dimension() - }, - Content = new[] - { - new Drawable[] + contentBackground = new Box { - controlContainer = new Container - { - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 14 } - } + RelativeSizeAxes = Axes.Both }, - new Drawable[] + new GridContainer { - new OsuScrollContainer + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = panelFlow = new FillFlowContainer + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 7), - Padding = new MarginPadding(7) + controlContainer = new Container + { + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Horizontal = 14 } + } + }, + new Drawable[] + { + new NestedVerticalScrollContainer + { + RelativeSizeAxes = Axes.Both, + ClampExtension = 100, + ScrollbarOverlapsContent = false, + Child = panelFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 7), + Padding = new MarginPadding(7) + } + } } } } @@ -193,7 +206,7 @@ namespace osu.Game.Overlays.Mods private void createHeaderText() { - IEnumerable headerTextWords = modType.Humanize(LetterCasing.Title).Split(' '); + IEnumerable headerTextWords = ModType.Humanize(LetterCasing.Title).Split(' '); if (headerTextWords.Count() > 1) { @@ -209,7 +222,7 @@ namespace osu.Game.Overlays.Mods { availableMods.BindTo(game.AvailableMods); - headerBackground.Colour = accentColour = colours.ForModType(modType); + headerBackground.Colour = accentColour = colours.ForModType(ModType); if (toggleAllCheckbox != null) { @@ -225,6 +238,12 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); + SelectedMods.BindValueChanged(_ => + { + // if a load is in progress, don't try to update the selection - the load flow will do so. + if (latestLoadTask == null) + updateActiveState(); + }); updateMods(); } @@ -232,7 +251,7 @@ namespace osu.Game.Overlays.Mods private void updateMods() { - var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(modType) ?? Array.Empty()).ToList(); + var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()).ToList(); if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod))) return; @@ -250,11 +269,20 @@ namespace osu.Game.Overlays.Mods { panelFlow.ChildrenEnumerable = loaded; - foreach (var panel in panelFlow) - panel.Active.BindValueChanged(_ => updateToggleState()); - updateToggleState(); - + updateActiveState(); + updateToggleAllState(); updateFilter(); + + foreach (var panel in panelFlow) + { + panel.Active.BindValueChanged(_ => + { + updateToggleAllState(); + SelectedMods.Value = panel.Active.Value + ? SelectedMods.Value.Append(panel.Mod).ToArray() + : SelectedMods.Value.Except(new[] { panel.Mod }).ToArray(); + }); + } }, (cancellationTokenSource = new CancellationTokenSource()).Token); loadTask.ContinueWith(_ => { @@ -263,6 +291,12 @@ namespace osu.Game.Overlays.Mods }); } + private void updateActiveState() + { + foreach (var panel in panelFlow) + panel.Active.Value = SelectedMods.Value.Contains(panel.Mod, EqualityComparer.Default); + } + #region Bulk select / deselect private const double initial_multiple_selection_delay = 120; @@ -297,7 +331,7 @@ namespace osu.Game.Overlays.Mods } } - private void updateToggleState() + private void updateToggleAllState() { if (toggleAllCheckbox != null && !SelectionAnimationRunning) { @@ -399,7 +433,7 @@ namespace osu.Game.Overlays.Mods foreach (var modPanel in panelFlow) modPanel.ApplyFilter(Filter); - updateToggleState(); + updateToggleAllState(); } #endregion diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs new file mode 100644 index 0000000000..62080ec1b5 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -0,0 +1,392 @@ +// 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.Diagnostics; +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.Input.Events; +using osu.Framework.Layout; +using osu.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Overlays.Mods +{ + public class ModSelectScreen : OsuFocusedOverlayContainer + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Cached] + public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); + + protected override bool StartHidden => true; + + private readonly BindableBool customisationVisible = new BindableBool(); + + private DifficultyMultiplierDisplay multiplierDisplay; + private ModSettingsArea modSettingsArea; + private FillFlowContainer columnFlow; + private GridContainer grid; + private Container mainContent; + + private PopupScreenTitle header; + private Container footer; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + RelativePositionAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + mainContent = new Container + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + grid = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 75), + }, + Content = new[] + { + new Drawable[] + { + header = new PopupScreenTitle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Title = "Mod Select", + Description = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun.", + Close = Hide + } + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + RelativePositionAxes = Axes.X, + X = 0.3f, + Height = DifficultyMultiplierDisplay.HEIGHT, + Margin = new MarginPadding + { + Horizontal = 100, + Vertical = 10 + }, + Child = multiplierDisplay = new DifficultyMultiplierDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + } + }, + new Drawable[] + { + new Container + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Children = new Drawable[] + { + new OsuScrollContainer(Direction.Horizontal) + { + RelativeSizeAxes = Axes.Both, + Masking = false, + ClampExtension = 100, + ScrollbarOverlapsContent = false, + Child = columnFlow = new ModColumnContainer + { + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Right = 70 }, + Children = new[] + { + new ModColumn(ModType.DifficultyReduction, false, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }), + new ModColumn(ModType.DifficultyIncrease, false, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }), + new ModColumn(ModType.Automation, false, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }), + new ModColumn(ModType.Conversion, false), + new ModColumn(ModType.Fun, false) + } + } + } + } + } + }, + new[] { Empty() } + } + }, + footer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = 50, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = colourProvider.Background5 + }, + new ShearedToggleButton(200) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Vertical = 14, Left = 70 }, + Text = "Mod Customisation", + Active = { BindTarget = customisationVisible } + } + } + }, + new ClickToReturnContainer + { + RelativeSizeAxes = Axes.Both, + HandleMouse = { BindTarget = customisationVisible }, + OnClicked = () => customisationVisible.Value = false + } + } + }, + modSettingsArea = new ModSettingsArea + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Height = 0 + } + }; + + columnFlow.Shear = new Vector2(ModPanel.SHEAR_X, 0); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); + + SelectedMods.BindValueChanged(val => + { + updateMultiplier(); + updateCustomisation(val); + updateSelectionFromBindable(); + }, true); + + foreach (var column in columnFlow) + { + column.SelectedMods.BindValueChanged(_ => updateBindableFromSelection()); + } + + customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); + } + + private void updateMultiplier() + { + double multiplier = 1.0; + + foreach (var mod in SelectedMods.Value) + multiplier *= mod.ScoreMultiplier; + + multiplierDisplay.Current.Value = multiplier; + } + + private void updateCustomisation(ValueChangedEvent> valueChangedEvent) + { + bool anyCustomisableMod = false; + bool anyModWithRequiredCustomisationAdded = false; + + foreach (var mod in SelectedMods.Value) + { + anyCustomisableMod |= mod.GetSettingsSourceProperties().Any(); + anyModWithRequiredCustomisationAdded |= !valueChangedEvent.OldValue.Contains(mod) && mod.RequiresConfiguration; + } + + if (anyCustomisableMod) + { + customisationVisible.Disabled = false; + + if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value) + customisationVisible.Value = true; + } + else + { + if (customisationVisible.Value) + customisationVisible.Value = false; + + customisationVisible.Disabled = true; + } + } + + private void updateCustomisationVisualState() + { + const double transition_duration = 300; + + grid.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic); + + float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0; + + modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic); + mainContent.TransformTo(nameof(Margin), new MarginPadding { Bottom = modAreaHeight }, transition_duration, Easing.InOutCubic); + } + + private bool selectionBindableSyncInProgress; + + private void updateSelectionFromBindable() + { + if (selectionBindableSyncInProgress) + return; + + selectionBindableSyncInProgress = true; + + foreach (var column in columnFlow) + column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray(); + + selectionBindableSyncInProgress = false; + } + + private void updateBindableFromSelection() + { + if (selectionBindableSyncInProgress) + return; + + selectionBindableSyncInProgress = true; + + SelectedMods.Value = columnFlow.SelectMany(column => column.SelectedMods.Value).ToArray(); + + selectionBindableSyncInProgress = false; + } + + protected override void PopIn() + { + const double fade_in_duration = 400; + + base.PopIn(); + this.FadeIn(fade_in_duration, Easing.OutQuint); + + header.MoveToY(0, fade_in_duration, Easing.OutQuint); + footer.MoveToY(0, fade_in_duration, Easing.OutQuint); + + multiplierDisplay + .Delay(fade_in_duration * 0.65f) + .FadeIn(fade_in_duration / 2, Easing.OutQuint) + .ScaleTo(1, fade_in_duration, Easing.OutElastic); + + for (int i = 0; i < columnFlow.Count; i++) + { + columnFlow[i].TopLevelContent + .Delay(i * 30) + .MoveToY(0, fade_in_duration, Easing.OutQuint) + .FadeIn(fade_in_duration, Easing.OutQuint); + } + } + + protected override void PopOut() + { + const double fade_out_duration = 500; + + base.PopOut(); + this.FadeOut(fade_out_duration, Easing.OutQuint); + + multiplierDisplay + .FadeOut(fade_out_duration / 2, Easing.OutQuint) + .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint); + + header.MoveToY(-header.DrawHeight, fade_out_duration, Easing.OutQuint); + footer.MoveToY(footer.DrawHeight, fade_out_duration, Easing.OutQuint); + + for (int i = 0; i < columnFlow.Count; i++) + { + const float distance = 700; + + columnFlow[i].TopLevelContent + .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) + .FadeOut(fade_out_duration, Easing.OutQuint); + } + } + + private class ModColumnContainer : FillFlowContainer + { + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + public ModColumnContainer() + { + AddLayout(drawSizeLayout); + } + + public override void Add(ModColumn column) + { + base.Add(column); + + Debug.Assert(column != null); + column.Shear = Vector2.Zero; + } + + protected override void Update() + { + base.Update(); + + if (!drawSizeLayout.IsValid) + { + Padding = new MarginPadding + { + Left = DrawHeight * ModPanel.SHEAR_X, + Bottom = 10 + }; + + drawSizeLayout.Validate(); + } + } + } + + private class ClickToReturnContainer : Container + { + public BindableBool HandleMouse { get; } = new BindableBool(); + + public Action OnClicked { get; set; } + + protected override bool Handle(UIEvent e) + { + if (!HandleMouse.Value) + return base.Handle(e); + + switch (e) + { + case ClickEvent _: + OnClicked?.Invoke(); + return true; + + case MouseEvent _: + return true; + } + + return base.Handle(e); + } + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index e0a30f60c2..be72c1e3e3 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Mods { public Bindable> SelectedMods { get; } = new Bindable>(); + public const float HEIGHT = 250; + private readonly Box background; private readonly FillFlowContainer modSettingsFlow; @@ -32,7 +34,7 @@ namespace osu.Game.Overlays.Mods public ModSettingsArea() { RelativeSizeAxes = Axes.X; - Height = 250; + Height = HEIGHT; Anchor = Anchor.BottomRight; Origin = Anchor.BottomRight; @@ -52,6 +54,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, ScrollbarOverlapsContent = false, + ClampExtension = 100, Child = modSettingsFlow = new FillFlowContainer { AutoSizeAxes = Axes.X, @@ -155,9 +158,10 @@ namespace osu.Game.Overlays.Mods new[] { Empty() }, new Drawable[] { - new OsuScrollContainer(Direction.Vertical) + new NestedVerticalScrollContainer { RelativeSizeAxes = Axes.Both, + ClampExtension = 100, Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs new file mode 100644 index 0000000000..aba47d5423 --- /dev/null +++ b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Overlays.Mods +{ + /// + /// A scroll container that handles the case of vertically scrolling content inside a larger horizontally scrolling parent container. + /// + public class NestedVerticalScrollContainer : OsuScrollContainer + { + private OsuScrollContainer? parentScrollContainer; + + protected override void LoadComplete() + { + base.LoadComplete(); + + parentScrollContainer = this.FindClosestParent(); + } + + protected override bool OnScroll(ScrollEvent e) + { + if (parentScrollContainer == null) + return base.OnScroll(e); + + bool topRightInView = parentScrollContainer.ScreenSpaceDrawQuad.Contains(ScreenSpaceDrawQuad.TopRight); + bool bottomLeftInView = parentScrollContainer.ScreenSpaceDrawQuad.Contains(ScreenSpaceDrawQuad.BottomLeft); + + // If not completely on-screen, handle scroll but also allow parent to scroll at the same time (to hopefully bring our content into full view). + if (!topRightInView || !bottomLeftInView) + return false; + + bool scrollingPastEnd = e.ScrollDelta.Y < 0 && IsScrolledToEnd(); + bool scrollingPastStart = e.ScrollDelta.Y > 0 && Target <= 0; + + // If at either of our extents, delegate scroll to the horizontal parent container. + if (scrollingPastStart || scrollingPastEnd) + return false; + + return base.OnScroll(e); + } + } +} diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a6b54dd1f2..e569b0e517 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Menu bool loadThemedIntro() { - var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); + var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == BeatmapHash); if (setInfo == null) return false; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 1201279929..d048676872 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -114,18 +114,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating; - void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation()); + void toggleReady() => Client.ToggleReady().FireAndForget( + onSuccess: endOperation, + onError: _ => endOperation()); - void startMatch() => Client.StartMatch().ContinueWith(t => + void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () => { - // accessing Exception here silences any potential errors from the antecedent task - if (t.Exception != null) - { - // gameplay was not started due to an exception; unblock button. - endOperation(); - } - // gameplay is starting, the button will be unblocked on load requested. + }, onError: _ => + { + // gameplay was not started due to an exception; unblock button. + endOperation(); }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index d275f309cb..22a0243f8f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -30,13 +30,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private MultiplayerRoom room => multiplayerClient.Room; private Sample countdownTickSample; + private Sample countdownWarnSample; + private Sample countdownWarnFinalSample; [BackgroundDependencyLoader] private void load(AudioManager audio) { countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); - // disabled for now pending further work on sound effect - // countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final"); + countdownWarnSample = audio.Samples.Get(@"Multiplayer/countdown-warn"); + countdownWarnFinalSample = audio.Samples.Get(@"Multiplayer/countdown-warn-final"); } protected override void LoadComplete() @@ -102,8 +104,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void playTickSound(int secondsRemaining) { if (secondsRemaining < 10) countdownTickSample?.Play(); - // disabled for now pending further work on sound effect - // if (secondsRemaining <= 3) countdownTickFinalSample?.Play(); + + if (secondsRemaining <= 3) + { + if (secondsRemaining > 0) + countdownWarnSample?.Play(); + else + countdownWarnFinalSample?.Play(); + } } private void updateButtonText() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 1653d416d8..d72ce5e960 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { base.LoadComplete(); - RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID); + RequestDeletion = item => multiplayerClient.RemovePlaylistItem(item.ID).FireAndForget(); multiplayerClient.RoomUpdated += onRoomUpdated; onRoomUpdated(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 28c9bef3f0..2482d52492 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay. if (!playerLoader.GameplayPassed) { - client.AbortGameplay(); + client.AbortGameplay().FireAndForget(); return; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index d49c122bd1..848424bc76 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -1,13 +1,9 @@ // 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.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR; using osu.Framework.Allocation; -using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; @@ -76,40 +72,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); - task.ContinueWith(t => + task.FireAndForget(onSuccess: () => Schedule(() => { - Schedule(() => - { - // If an error or server side trigger occurred this screen may have already exited by external means. - if (!this.IsCurrentScreen()) - return; - - loadingLayer.Hide(); - - if (t.IsFaulted) - { - Exception exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); - - string message = exception is HubException - // HubExceptions arrive with additional message context added, but we want to display the human readable message: - // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once." - // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now. - ? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim() - : exception.Message; - - Logger.Log(message, level: LogLevel.Important); - Carousel.AllowSelection = true; - return; - } + loadingLayer.Hide(); + // If an error or server side trigger occurred this screen may have already exited by external means. + if (this.IsCurrentScreen()) this.Exit(); - }); - }); + }), onError: _ => Schedule(() => + { + loadingLayer.Hide(); + Carousel.AllowSelection = true; + })); } else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index e53153e017..7ef0bb5923 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -281,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.Room == null) return; - client.ChangeUserMods(mods.NewValue); + client.ChangeUserMods(mods.NewValue).FireAndForget(); modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); modSettingChangeTracker.SettingChanged += onModSettingsChanged; @@ -296,7 +296,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.Room == null) return; - client.ChangeUserMods(UserMods.Value); + client.ChangeUserMods(UserMods.Value).FireAndForget(); }, 500); } @@ -305,7 +305,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.Room == null) return; - client.ChangeBeatmapAvailability(availability.NewValue); + client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget(); if (availability.NewValue.State != DownloadState.LocallyAvailable) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 7ba0a63856..e091559046 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.Centre, Alpha = 0, Margin = new MarginPadding(4), - Action = () => Client.KickUser(User.UserID), + Action = () => Client.KickUser(User.UserID).FireAndForget(), }, }, } @@ -231,7 +231,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants if (!Client.IsHost) return; - Client.TransferHost(targetUser); + Client.TransferHost(targetUser).FireAndForget(); }), new OsuMenuItem("Kick", MenuItemType.Destructive, () => { @@ -239,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants if (!Client.IsHost) return; - Client.KickUser(targetUser); + Client.KickUser(targetUser).FireAndForget(); }) }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs index 73aca0acdc..aca2c6073a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Client.SendMatchRequest(new ChangeTeamRequest { TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0, - }); + }).FireAndForget(); } public int? DisplayedTeam { get; private set; } diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 6a74fdaf75..0c9c909395 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -87,31 +87,33 @@ namespace osu.Game.Screens.Ranking }); } - button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; - updateTooltip(); + updateState(); }, true); State.BindValueChanged(state => { button.State.Value = state.NewValue; - updateTooltip(); + updateState(); }, true); } - private void updateTooltip() + private void updateState() { switch (replayAvailability) { case ReplayAvailability.Local: button.TooltipText = @"watch replay"; + button.Enabled.Value = true; break; case ReplayAvailability.Online: button.TooltipText = @"download replay"; + button.Enabled.Value = true; break; default: button.TooltipText = @"replay unavailable"; + button.Enabled.Value = false; break; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 92713023f4..f7d5581621 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -443,7 +443,9 @@ namespace osu.Game.Skinning string lookupName = name.Replace(@"@2x", string.Empty); float ratio = 2; - var texture = Textures?.Get(@$"{lookupName}@2x", wrapModeS, wrapModeT); + string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}"; + + var texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); if (texture == null) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 88cb5f40a1..2ef6f28720 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -5,8 +5,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Storyboards.Drawables @@ -88,6 +90,14 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = animation.EndTime; } + protected override Vector2 GetCurrentDisplaySize() + { + Texture texture = (CurrentFrame as Sprite)?.Texture + ?? ((CurrentFrame as SkinnableSprite)?.Drawable as Sprite)?.Texture; + + return new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); + } + [BackgroundDependencyLoader] private void load(TextureStore textureStore, Storyboard storyboard) { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1bebf78d97..7fd2647a8a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,15 +29,14 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index efd5bac38e..a5efc40d51 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,8 +61,8 @@ - - + + @@ -84,7 +84,7 @@ - +