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 @@
-
+