diff --git a/osu.Android.props b/osu.Android.props index 2866ec24a6..97d9dbc380 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index cca3701a60..fec253924f 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddMoveStep(end_time, 0); AddClickStep(MouseButton.Left); + AddMoveStep(start_time, 0); + AddAssert("duration is positive", () => ((BananaShower)CurrentBlueprint.HitObject).Duration > 0); + AddClickStep(MouseButton.Right); AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time)); AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time)); diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 6dea8b0712..039008f901 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.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 osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; @@ -13,11 +14,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { private readonly TimeSpanOutline outline; + private double placementStartTime; + private double placementEndTime; + public BananaShowerPlacementBlueprint() { InternalChild = outline = new TimeSpanOutline(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + BeginPlacement(); + } + protected override void Update() { base.Update(); @@ -38,13 +49,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints case PlacementState.Active: if (e.Button != MouseButton.Right) break; - // If the duration is negative, swap the start and the end time to make the duration positive. - if (HitObject.Duration < 0) - { - HitObject.StartTime = HitObject.EndTime; - HitObject.Duration = -HitObject.Duration; - } - EndPlacement(HitObject.Duration > 0); return true; } @@ -61,13 +65,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints switch (PlacementActive) { case PlacementState.Waiting: - HitObject.StartTime = time; + placementStartTime = placementEndTime = time; break; case PlacementState.Active: - HitObject.EndTime = time; + placementEndTime = time; break; } + + HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); + HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 4a5a1d8160..b5dcb62543 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints base.LoadComplete(); inputManager = GetContainingInputManager(); + + BeginPlacement(); } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 22ace52c2a..0b34ab28a3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -107,6 +107,18 @@ namespace osu.Game.Rulesets.Osu.Mods { switch (e.Type) { + case SliderEventType.Tick: + AddNested(new SliderTick + { + SpanIndex = e.SpanIndex, + SpanStartTime = e.SpanStartTime, + StartTime = e.Time, + Position = Position + Path.PositionAt(e.PathProgress), + StackHeight = StackHeight, + Scale = Scale, + }); + break; + case SliderEventType.Head: AddNested(HeadCircle = new SliderHeadCircle { diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 4c126f0a3b..6c9dddf51f 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -137,33 +137,137 @@ namespace osu.Game.Tests.Mods // incompatible pair. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, - new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) } }, // incompatible pair with derived class. new object[] { - new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, - new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } + new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } }, // system mod. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, new[] { typeof(OsuModTouchDevice) } }, // multi mod. new object[] { - new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() }, + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, new[] { typeof(MultiMod) } }, + // invalid multiplayer mod is valid for local. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + null + }, + // invalid free mod is valid for local. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + null + }, // valid pair. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, null - } + }, + }; + + private static readonly object[] invalid_multiplayer_mod_test_scenarios = + { + // incompatible pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) } + }, + // incompatible pair with derived class. + new object[] + { + new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } + }, + // system mod. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, + new[] { typeof(MultiMod) } + }, + // invalid multiplayer mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + new[] { typeof(InvalidMultiplayerMod) } + }, + // invalid free mod is valid for multiplayer global. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + null + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + null + }, + }; + + private static readonly object[] invalid_free_mod_test_scenarios = + { + // system mod. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, + new[] { typeof(MultiMod) } + }, + // invalid multiplayer mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + new[] { typeof(InvalidMultiplayerMod) } + }, + // invalid free mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + new[] { typeof(InvalidMultiplayerFreeMod) } + }, + // incompatible pair is valid for free mods. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + null, + }, + // incompatible pair with derived class is valid for free mods. + new object[] + { + new Mod[] { new OsuModDeflate(), new OsuModSpinIn() }, + null, + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + null + }, }; [TestCaseSource(nameof(invalid_mod_test_scenarios))] @@ -179,6 +283,32 @@ namespace osu.Game.Tests.Mods Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } + [TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))] + public void TestInvalidMultiplayerModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } + + [TestCaseSource(nameof(invalid_free_mod_test_scenarios))] + public void TestInvalidFreeModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } @@ -187,6 +317,27 @@ namespace osu.Game.Tests.Mods { } + public class InvalidMultiplayerMod : Mod + { + public override string Name => string.Empty; + public override string Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => 1; + public override bool HasImplementation => true; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; + } + + private class InvalidMultiplayerFreeMod : Mod + { + public override string Name => string.Empty; + public override string Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => 1; + public override bool HasImplementation => true; + public override bool ValidForMultiplayerAsFreeMod => false; + } + public interface IModCompatibilitySpecification { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index ae2bc60fc6..815cc09448 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects.Drawables; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 64afe1235b..31abcb6748 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; -using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay @@ -131,7 +130,6 @@ namespace osu.Game.Tests.Visual.Gameplay } [Cached(typeof(ISkinSource))] - [Cached(typeof(ISamplePlaybackDisabler))] private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler { [Resolved] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 2abde82e92..6173580f0b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; @@ -176,5 +177,41 @@ namespace osu.Game.Tests.Visual.Multiplayer .ChildrenOfType() .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); } + + [Test] + public void TestNextPlaylistItemSelectedAfterCompletion() + { + AddStep("add two playlist items", () => + { + SelectedRoom.Value.Playlist.AddRange(new[] + { + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + }, + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + }); + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + + AddStep("change user to loaded", () => MultiplayerClient.ChangeState(MultiplayerUserState.Loaded)); + AddUntilStep("user playing", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Playing); + AddStep("abort gameplay", () => MultiplayerClient.AbortGameplay()); + + AddUntilStep("last playlist item selected", () => + { + var lastItem = this.ChildrenOfType().Single(p => p.Item.ID == MultiplayerClient.APIRoom?.Playlist.Last().ID); + return lastItem.IsSelectedItem; + }); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index bcb36a585f..0237298fa1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -8,11 +8,23 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Resources; +using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerTeamResults : ScreenTestScene { + [Test] + public void TestScaling() + { + // scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason. + AddSliderStep("scale", 0.5f, 1.6f, 1f, v => Schedule(() => + { + Stack.Scale = new Vector2(v); + Stack.Size = new Vector2(1f / v); + })); + } + [TestCase(7483253, 1048576)] [TestCase(1048576, 7483253)] [TestCase(1048576, 1048576)] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f8eee7be56..9674ef7ae1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -503,6 +504,22 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("test dispose doesn't crash", () => Game.Dispose()); } + [Test] + public void TestRapidBackButtonExit() + { + AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); + + AddStep("press escape twice rapidly", () => + { + InputManager.Key(Key.Escape); + InputManager.Key(Key.Escape); + }); + + pushEscape(); + + AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog != null); + } + private Func playToResults() { Player player = null; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4eed2a25f5..a1d51683e4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -56,6 +56,17 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestScaling() + { + // scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason. + AddSliderStep("scale", 0.5f, 1.6f, 1f, v => Schedule(() => + { + Content.Scale = new Vector2(v); + Content.Size = new Vector2(1f / v); + })); + } + [Test] public void TestResultsWithoutPlayer() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index 42ffeba444..fa7758df59 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -451,6 +451,36 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod select hidden", () => modSelectScreen.State.Value == Visibility.Hidden); } + [Test] + public void TestColumnHiding() + { + AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods }, + IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion + }); + waitForColumnLoad(); + changeRuleset(0); + + AddAssert("two columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2); + + AddStep("unset filter", () => modSelectScreen.IsValidMod = _ => true); + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + + AddStep("filter out everything", () => modSelectScreen.IsValidMod = _ => false); + AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent)); + + AddStep("hide", () => modSelectScreen.Hide()); + AddStep("set filter for 3 columns", () => modSelectScreen.IsValidMod = mod => mod.Type == ModType.DifficultyReduction + || mod.Type == ModType.Automation + || mod.Type == ModType.Conversion); + + AddStep("show", () => modSelectScreen.Show()); + AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); diff --git a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs b/osu.Game/Audio/ISamplePlaybackDisabler.cs similarity index 72% rename from osu.Game/Screens/Play/ISamplePlaybackDisabler.cs rename to osu.Game/Audio/ISamplePlaybackDisabler.cs index 6b37021fe6..4167316780 100644 --- a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs +++ b/osu.Game/Audio/ISamplePlaybackDisabler.cs @@ -1,15 +1,18 @@ // 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.Skinning; -namespace osu.Game.Screens.Play +namespace osu.Game.Audio { /// /// Allows a component to disable sample playback dynamically as required. - /// Handled by . + /// Automatically handled by . + /// May also be manually handled locally to particular components. /// + [Cached] public interface ISamplePlaybackDisabler { /// diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 46f5b418bd..fc39887e79 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -153,7 +153,17 @@ namespace osu.Game.Beatmaps } }; - Task.Run(() => cacheDownloadRequest.PerformAsync()); + Task.Run(async () => + { + try + { + await cacheDownloadRequest.PerformAsync(); + } + catch + { + // Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway. + } + }); } private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 79d8bd3bb3..3a893a1238 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -431,9 +431,10 @@ namespace osu.Game.Beatmaps.Formats OmitFirstBarLine = omitFirstBarSignature, }; - bool isOsuRuleset = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; - // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments. - if (!isOsuRuleset) + int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; + + // osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments. + if (onlineRulesetID == 1 || onlineRulesetID == 3) effectPoint.ScrollSpeed = speedMultiplier; addControlPoint(time, effectPoint, timingChange); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7ddbc2f768..b91a74c4a1 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -183,15 +183,15 @@ namespace osu.Game.Beatmaps.Formats SampleControlPoint lastRelevantSamplePoint = null; DifficultyControlPoint lastRelevantDifficultyPoint = null; - bool isOsuRuleset = onlineRulesetID == 0; + // In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats. + // In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored. + bool scrollSpeedEncodedAsSliderVelocity = onlineRulesetID == 1 || onlineRulesetID == 3; // iterate over hitobjects and pull out all required sample and difficulty changes extractDifficultyControlPoints(beatmap.HitObjects); extractSampleControlPoints(beatmap.HitObjects); - // handle scroll speed, which is stored as "slider velocity" in legacy formats. - // this is relevant for scrolling ruleset beatmaps. - if (!isOsuRuleset) + if (scrollSpeedEncodedAsSliderVelocity) { foreach (var point in legacyControlPoints.EffectPoints) legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); @@ -242,7 +242,7 @@ namespace osu.Game.Beatmaps.Formats IEnumerable collectDifficultyControlPoints(IEnumerable hitObjects) { - if (!isOsuRuleset) + if (scrollSpeedEncodedAsSliderVelocity) yield break; foreach (var hitObject in hitObjects) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index d08b6b7beb..5959fe656c 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -100,10 +100,6 @@ namespace osu.Game.Overlays.Dialog } } - // We always want dialogs to show their appear animation, so we request they start hidden. - // Normally this would not be required, but is here due to the manual Show() call that occurs before LoadComplete(). - protected override bool StartHidden => true; - protected PopupDialog() { RelativeSizeAxes = Axes.Both; @@ -272,7 +268,7 @@ namespace osu.Game.Overlays.Dialog protected override void PopOut() { - if (!actionInvoked && content.IsPresent) + if (!actionInvoked) // In the case a user did not choose an action before a hide was triggered, press the last button. // This is presumed to always be a sane default "cancel" action. buttonsContainer.Last().TriggerClick(); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index f0741cdc40..3a2fda0bb0 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -55,8 +55,18 @@ namespace osu.Game.Overlays.Mods } } + /// + /// Determines whether this column should accept user input. + /// public Bindable Active = new BindableBool(true); + private readonly Bindable allFiltered = new BindableBool(); + + /// + /// True if all of the panels in this column have been filtered out by the current . + /// + public IBindable AllFiltered => allFiltered; + /// /// List of mods marked as selected in this column. /// @@ -339,6 +349,8 @@ namespace osu.Game.Overlays.Mods panel.ApplyFilter(Filter); } + allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value); + if (toggleAllCheckbox != null && !SelectionAnimationRunning) { toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index f2a97da3b2..4c4951307d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -1,6 +1,8 @@ // 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 osu.Framework.Allocation; using osu.Framework.Audio; @@ -12,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -21,8 +24,6 @@ using osu.Game.Rulesets.UI; using osuTK; using osuTK.Input; -#nullable enable - namespace osu.Game.Overlays.Mods { public class ModPanel : OsuClickableContainer @@ -50,6 +51,7 @@ namespace osu.Game.Overlays.Mods private Colour4 activeColour; + private readonly Bindable samplePlaybackDisabled = new BindableBool(); private Sample? sampleOff; private Sample? sampleOn; @@ -139,13 +141,16 @@ namespace osu.Game.Overlays.Mods Action = Active.Toggle; } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler) { sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); activeColour = colours.ForModType(Mod.Type); + + if (samplePlaybackDisabler != null) + ((IBindable)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); } protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); @@ -166,6 +171,9 @@ namespace osu.Game.Overlays.Mods private void playStateChangeSamples() { + if (samplePlaybackDisabled.Value) + return; + if (Active.Value) sampleOn?.Play(); else diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 0a1b4e857e..8b19e38954 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Framework.Lists; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -27,7 +28,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract class ModSelectScreen : ShearedOverlayContainer + public abstract class ModSelectScreen : ShearedOverlayContainer, ISamplePlaybackDisabler { protected const int BUTTON_WIDTH = 200; @@ -128,7 +129,6 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(SHEAR, 0), RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Spacing = new Vector2(10, 0), Margin = new MarginPadding { Horizontal = 70 }, Children = new[] { @@ -188,6 +188,8 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); + State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true); + ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); SelectedMods.BindValueChanged(val => @@ -205,6 +207,14 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); updateAvailableMods(); + + // Start scrolled slightly to the right to give the user a sense that + // there is more horizontal content available. + ScheduleAfterChildren(() => + { + columnScroll.ScrollTo(200, false); + columnScroll.ScrollToStart(); + }); } /// @@ -226,12 +236,21 @@ namespace osu.Game.Overlays.Mods } private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) - => new ColumnDimContainer(CreateModColumn(modType, toggleKeys)) + { + var column = CreateModColumn(modType, toggleKeys).With(column => + { + column.Filter = IsValidMod; + // spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden. + column.Margin = new MarginPadding { Right = 10 }; + }); + + return new ColumnDimContainer(column) { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, - RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) + RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140), }; + } private ShearedButton[] createDefaultFooterButtons() => new[] @@ -340,6 +359,8 @@ namespace osu.Game.Overlays.Mods #region Transition handling + private const float distance = 700; + protected override void PopIn() { const double fade_in_duration = 400; @@ -351,13 +372,26 @@ namespace osu.Game.Overlays.Mods .FadeIn(fade_in_duration / 2, Easing.OutQuint) .ScaleTo(1, fade_in_duration, Easing.OutElastic); + int nonFilteredColumnCount = 0; + for (int i = 0; i < columnFlow.Count; i++) { - columnFlow[i].Column - .TopLevelContent - .Delay(i * 30) - .MoveToY(0, fade_in_duration, Easing.OutQuint) - .FadeIn(fade_in_duration, Easing.OutQuint); + var column = columnFlow[i].Column; + + double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30; + double duration = column.AllFiltered.Value ? 0 : fade_in_duration; + float startingYPosition = 0; + if (!column.AllFiltered.Value) + startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; + + column.TopLevelContent + .MoveToY(startingYPosition) + .Delay(delay) + .MoveToY(0, duration, Easing.OutQuint) + .FadeIn(duration, Easing.OutQuint); + + if (!column.AllFiltered.Value) + nonFilteredColumnCount += 1; } } @@ -371,16 +405,24 @@ namespace osu.Game.Overlays.Mods .FadeOut(fade_out_duration / 2, Easing.OutQuint) .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint); + int nonFilteredColumnCount = 0; + for (int i = 0; i < columnFlow.Count; i++) { - const float distance = 700; - var column = columnFlow[i].Column; + double duration = column.AllFiltered.Value ? 0 : fade_out_duration; + float newYPosition = 0; + if (!column.AllFiltered.Value) + newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; + column.FlushPendingSelections(); column.TopLevelContent - .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) - .FadeOut(fade_out_duration, Easing.OutQuint); + .MoveToY(newYPosition, duration, Easing.OutQuint) + .FadeOut(duration, Easing.OutQuint); + + if (!column.AllFiltered.Value) + nonFilteredColumnCount += 1; } } @@ -393,35 +435,50 @@ namespace osu.Game.Overlays.Mods if (e.Repeat) return false; - // This is handled locally here because this overlay is being registered at the game level - // and therefore takes away keyboard focus from the screen stack. - if (e.Action == GlobalAction.Back) - { - if (customisationVisible.Value) - customisationVisible.Value = false; - else - backButton.TriggerClick(); - return true; - } - switch (e.Action) { + case GlobalAction.Back: + // Pressing the back binding should only go back one step at a time. + hideOverlay(false); + return true; + + // This is handled locally here because this overlay is being registered at the game level + // and therefore takes away keyboard focus from the screen stack. case GlobalAction.ToggleModSelection: case GlobalAction.Select: { - if (customisationVisible.Value) - customisationVisible.Value = false; - Hide(); + // Pressing toggle or select should completely hide the overlay in one shot. + hideOverlay(true); return true; } + } - default: - return base.OnPressed(e); + return base.OnPressed(e); + + void hideOverlay(bool immediate) + { + if (customisationVisible.Value) + { + Debug.Assert(customisationButton != null); + customisationButton.TriggerClick(); + + if (!immediate) + return; + } + + backButton.TriggerClick(); } } #endregion + #region Sample playback control + + private readonly Bindable samplePlaybackDisabled = new BindableBool(true); + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; + + #endregion + /// /// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility. /// @@ -531,17 +588,20 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - Active.BindValueChanged(_ => updateDim(), true); + Active.BindValueChanged(_ => updateState()); + Column.AllFiltered.BindValueChanged(_ => updateState(), true); FinishTransforms(); } protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning; - private void updateDim() + private void updateState() { Colour4 targetColour; - if (Active.Value) + Column.Alpha = Column.AllFiltered.Value ? 0 : 1; + + if (Column.Active.Value) targetColour = Colour4.White; else targetColour = IsHovered ? colours.GrayC : colours.Gray8; @@ -560,14 +620,14 @@ namespace osu.Game.Overlays.Mods protected override bool OnHover(HoverEvent e) { base.OnHover(e); - updateDim(); + updateState(); return Active.Value; } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - updateDim(); + updateState(); } } diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index d5d1de91de..30fa1ea8cb 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -39,6 +39,18 @@ namespace osu.Game.Rulesets.Mods /// bool UserPlayable { get; } + /// + /// Whether this mod is valid for multiplayer matches. + /// Should be false for mods that make gameplay duration dependent on user input (e.g. ). + /// + bool ValidForMultiplayer { get; } + + /// + /// Whether this mod is valid as a free mod in multiplayer matches. + /// Should be false for mods that affect the gameplay duration (e.g. and ). + /// + bool ValidForMultiplayerAsFreeMod { get; } + /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b2d4be54ce..af1550f8a9 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -94,6 +94,12 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool UserPlayable => true; + [JsonIgnore] + public virtual bool ValidForMultiplayer => true; + + [JsonIgnore] + public virtual bool ValidForMultiplayerAsFreeMod => true; + [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 public virtual bool Ranked => false; diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 1115b95e6f..93251f7b2d 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; [SettingSource("Initial rate", "The starting speed of the track")] diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 87dc627b19..0ebe11b393 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 88fb609c07..05953f903f 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { + public override bool ValidForMultiplayerAsFreeMod => false; + public abstract BindableNumber SpeedChange { get; } public virtual void ApplyToTrack(ITrack track) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b6b2decede..fe6d54332c 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } + public override bool ValidForMultiplayerAsFreeMod => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index b426386d7a..72de0ad653 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0; public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override ModType Type => ModType.System; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3fde033587..947c184009 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -19,6 +19,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -50,7 +51,6 @@ using osuTK.Input; namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] - [Cached(typeof(ISamplePlaybackDisabler))] [Cached] public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 459b861d96..2618e15d31 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -65,6 +65,8 @@ namespace osu.Game.Screens.OnlinePlay public readonly PlaylistItem Item; + public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID; + private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); @@ -128,12 +130,10 @@ namespace osu.Game.Screens.OnlinePlay SelectedItem.BindValueChanged(selected => { - bool isCurrent = selected.NewValue == Model; - if (!valid.Value) { // Don't allow selection when not valid. - if (isCurrent) + if (IsSelectedItem) { SelectedItem.Value = selected.OldValue; } @@ -142,7 +142,7 @@ namespace osu.Game.Screens.OnlinePlay return; } - maskingContainer.BorderThickness = isCurrent ? 5 : 0; + maskingContainer.BorderThickness = IsSelectedItem ? 5 : 0; }, true); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 848424bc76..929c3ee321 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; + + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ae3eb1ed8b..51c1e6b43b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -37,7 +38,6 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play { [Cached] - [Cached(typeof(ISamplePlaybackDisabler))] public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo { /// diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index c2ef5529e8..a5341242e2 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -85,7 +85,6 @@ namespace osu.Game.Screens.Ranking InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, - HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. Child = flow = new Flow { Anchor = Anchor.Centre, @@ -359,11 +358,6 @@ namespace osu.Game.Screens.Ranking /// public float? InstantScrollTarget; - /// - /// Whether this container should handle scroll trigger events. - /// - public Func HandleScroll; - protected override void UpdateAfterChildren() { if (InstantScrollTarget != null) @@ -374,10 +368,6 @@ namespace osu.Game.Screens.Ranking base.UpdateAfterChildren(); } - - public override bool HandlePositionalInput => HandleScroll(); - - public override bool HandleNonPositionalInput => HandleScroll(); } } } diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 10b8c47028..b34351d4e7 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; using osu.Game.Audio; -using osu.Game.Screens.Play; namespace osu.Game.Skinning { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 8df44216b6..ea092a8ca3 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -106,22 +106,69 @@ namespace osu.Game.Utils } /// - /// Check the provided combination of mods are valid for a local gameplay session. + /// Checks that all s in a combination are valid for a local gameplay session. /// /// The mods to check. - /// Invalid mods, if any were found. Can be null if all mods were valid. + /// Invalid mods, if any were found. Will be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. public static bool CheckValidForGameplay(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); - // exclude multi mods from compatibility checks. - // the loop below automatically marks all multi mods as not valid for gameplay anyway. - CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods); + // checking compatibility of multi mods would try to flatten them and return incompatible mods. + // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first. + if (!checkValid(mods, m => !(m is MultiMod), out invalidMods)) + return false; + + if (!CheckCompatibleSet(mods, out invalidMods)) + return false; + + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods); + } + + /// + /// Checks that all s in a combination are valid as "required mods" in a multiplayer match session. + /// + /// The mods to check. + /// Invalid mods, if any were found. Will be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidRequiredModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + { + mods = mods.ToArray(); + + // checking compatibility of multi mods would try to flatten them and return incompatible mods. + // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first. + if (!checkValid(mods, m => !(m is MultiMod), out invalidMods)) + return false; + + if (!CheckCompatibleSet(mods, out invalidMods)) + return false; + + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods); + } + + /// + /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. + /// + /// + /// Note that this does not check compatibility between mods, + /// given that the passed mods are expected to be the ones to be allowed for the multiplayer match, + /// not to be confused with the list of mods the user currently has selected for the multiplayer match. + /// + /// The mods to check. + /// Invalid mods, if any were found. Will be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidFreeModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayerAsFreeMod && !(m is MultiMod), out invalidMods); + + private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) + { + mods = mods.ToArray(); + invalidMods = null; foreach (var mod in mods) { - if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) + if (!valid(mod)) { invalidMods ??= new List(); invalidMods.Add(mod); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index dbf04283b6..d9c8199f75 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -24,7 +24,7 @@ namespace osu.Game.Utils var options = new SentryOptions { - Dsn = "https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255", + Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2", Release = game.Version }; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fa7563da55..2f32c843c0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index f987ae9bf8..b483267696 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - +