1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 13:33:52 +08:00

Merge branch 'master' into catch-editor-per-object-sv

This commit is contained in:
Dean Herbert 2022-05-09 19:18:01 +09:00 committed by GitHub
commit c7e9bd7751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 540 additions and 102 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.430.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.509.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMoveStep(end_time, 0); AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left); AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0); AddMoveStep(start_time, 0);
AddAssert("duration is positive", () => ((BananaShower)CurrentBlueprint.HitObject).Duration > 0);
AddClickStep(MouseButton.Right); AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time)); AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time)); AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -13,11 +14,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
private readonly TimeSpanOutline outline; private readonly TimeSpanOutline outline;
private double placementStartTime;
private double placementEndTime;
public BananaShowerPlacementBlueprint() public BananaShowerPlacementBlueprint()
{ {
InternalChild = outline = new TimeSpanOutline(); InternalChild = outline = new TimeSpanOutline();
} }
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -38,13 +49,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
case PlacementState.Active: case PlacementState.Active:
if (e.Button != MouseButton.Right) break; 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); EndPlacement(HitObject.Duration > 0);
return true; return true;
} }
@ -61,13 +65,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
switch (PlacementActive) switch (PlacementActive)
{ {
case PlacementState.Waiting: case PlacementState.Waiting:
HitObject.StartTime = time; placementStartTime = placementEndTime = time;
break; break;
case PlacementState.Active: case PlacementState.Active:
HitObject.EndTime = time; placementEndTime = time;
break; break;
} }
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
} }
} }
} }

View File

@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
base.LoadComplete(); base.LoadComplete();
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
BeginPlacement();
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -107,6 +107,18 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
switch (e.Type) 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: case SliderEventType.Head:
AddNested(HeadCircle = new SliderHeadCircle AddNested(HeadCircle = new SliderHeadCircle
{ {

View File

@ -137,33 +137,137 @@ namespace osu.Game.Tests.Mods
// incompatible pair. // incompatible pair.
new object[] new object[]
{ {
new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) }
}, },
// incompatible pair with derived class. // incompatible pair with derived class.
new object[] new object[]
{ {
new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
}, },
// system mod. // system mod.
new object[] new object[]
{ {
new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) } new[] { typeof(OsuModTouchDevice) }
}, },
// multi mod. // multi mod.
new object[] new object[]
{ {
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() }, new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) } 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. // valid pair.
new object[] new object[]
{ {
new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null 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))] [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)); 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 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 public interface IModCompatibilitySpecification
{ {
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -131,7 +130,6 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[Cached(typeof(ISkinSource))] [Cached(typeof(ISkinSource))]
[Cached(typeof(ISamplePlaybackDisabler))]
private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{ {
[Resolved] [Resolved]

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -176,5 +177,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); .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<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
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<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.APIRoom?.Playlist.Last().ID);
return lastItem.IsSelectedItem;
});
}
} }
} }

View File

@ -8,11 +8,23 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiplayerTeamResults : ScreenTestScene 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(7483253, 1048576)]
[TestCase(1048576, 7483253)] [TestCase(1048576, 7483253)]
[TestCase(1048576, 1048576)] [TestCase(1048576, 1048576)]

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -503,6 +504,22 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("test dispose doesn't crash", () => Game.Dispose()); 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<IDialogOverlay>().CurrentDialog != null);
}
private Func<Player> playToResults() private Func<Player> playToResults()
{ {
Player player = null; Player player = null;

View File

@ -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] [Test]
public void TestResultsWithoutPlayer() public void TestResultsWithoutPlayer()
{ {

View File

@ -451,6 +451,36 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod select hidden", () => modSelectScreen.State.Value == Visibility.Hidden); 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<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("unset filter", () => modSelectScreen.IsValidMod = _ => true);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("filter out everything", () => modSelectScreen.IsValidMod = _ => false);
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().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<ModColumn>().Count(col => col.IsPresent) == 3);
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectScreen.ChildrenOfType<ModColumn>().Any() && modSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)); () => modSelectScreen.ChildrenOfType<ModColumn>().Any() && modSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));

View File

@ -1,15 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Screens.Play namespace osu.Game.Audio
{ {
/// <summary> /// <summary>
/// Allows a component to disable sample playback dynamically as required. /// Allows a component to disable sample playback dynamically as required.
/// Handled by <see cref="PausableSkinnableSound"/>. /// Automatically handled by <see cref="PausableSkinnableSound"/>.
/// May also be manually handled locally to particular components.
/// </summary> /// </summary>
[Cached]
public interface ISamplePlaybackDisabler public interface ISamplePlaybackDisabler
{ {
/// <summary> /// <summary>

View File

@ -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) private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)

View File

@ -431,9 +431,10 @@ namespace osu.Game.Beatmaps.Formats
OmitFirstBarLine = omitFirstBarSignature, OmitFirstBarLine = omitFirstBarSignature,
}; };
bool isOsuRuleset = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
// scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments.
if (!isOsuRuleset) // osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
if (onlineRulesetID == 1 || onlineRulesetID == 3)
effectPoint.ScrollSpeed = speedMultiplier; effectPoint.ScrollSpeed = speedMultiplier;
addControlPoint(time, effectPoint, timingChange); addControlPoint(time, effectPoint, timingChange);

View File

@ -183,15 +183,15 @@ namespace osu.Game.Beatmaps.Formats
SampleControlPoint lastRelevantSamplePoint = null; SampleControlPoint lastRelevantSamplePoint = null;
DifficultyControlPoint lastRelevantDifficultyPoint = 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 // iterate over hitobjects and pull out all required sample and difficulty changes
extractDifficultyControlPoints(beatmap.HitObjects); extractDifficultyControlPoints(beatmap.HitObjects);
extractSampleControlPoints(beatmap.HitObjects); extractSampleControlPoints(beatmap.HitObjects);
// handle scroll speed, which is stored as "slider velocity" in legacy formats. if (scrollSpeedEncodedAsSliderVelocity)
// this is relevant for scrolling ruleset beatmaps.
if (!isOsuRuleset)
{ {
foreach (var point in legacyControlPoints.EffectPoints) foreach (var point in legacyControlPoints.EffectPoints)
legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
@ -242,7 +242,7 @@ namespace osu.Game.Beatmaps.Formats
IEnumerable<DifficultyControlPoint> collectDifficultyControlPoints(IEnumerable<HitObject> hitObjects) IEnumerable<DifficultyControlPoint> collectDifficultyControlPoints(IEnumerable<HitObject> hitObjects)
{ {
if (!isOsuRuleset) if (scrollSpeedEncodedAsSliderVelocity)
yield break; yield break;
foreach (var hitObject in hitObjects) foreach (var hitObject in hitObjects)

View File

@ -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() protected PopupDialog()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -272,7 +268,7 @@ namespace osu.Game.Overlays.Dialog
protected override void PopOut() 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. // 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. // This is presumed to always be a sane default "cancel" action.
buttonsContainer.Last().TriggerClick(); buttonsContainer.Last().TriggerClick();

View File

@ -55,8 +55,18 @@ namespace osu.Game.Overlays.Mods
} }
} }
/// <summary>
/// Determines whether this column should accept user input.
/// </summary>
public Bindable<bool> Active = new BindableBool(true); public Bindable<bool> Active = new BindableBool(true);
private readonly Bindable<bool> allFiltered = new BindableBool();
/// <summary>
/// True if all of the panels in this column have been filtered out by the current <see cref="Filter"/>.
/// </summary>
public IBindable<bool> AllFiltered => allFiltered;
/// <summary> /// <summary>
/// List of mods marked as selected in this column. /// List of mods marked as selected in this column.
/// </summary> /// </summary>
@ -339,6 +349,8 @@ namespace osu.Game.Overlays.Mods
panel.ApplyFilter(Filter); panel.ApplyFilter(Filter);
} }
allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value);
if (toggleAllCheckbox != null && !SelectionAnimationRunning) if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{ {
toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0;

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -12,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -21,8 +24,6 @@ using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
#nullable enable
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public class ModPanel : OsuClickableContainer public class ModPanel : OsuClickableContainer
@ -50,6 +51,7 @@ namespace osu.Game.Overlays.Mods
private Colour4 activeColour; private Colour4 activeColour;
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool();
private Sample? sampleOff; private Sample? sampleOff;
private Sample? sampleOn; private Sample? sampleOn;
@ -139,13 +141,16 @@ namespace osu.Game.Overlays.Mods
Action = Active.Toggle; Action = Active.Toggle;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours) private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler)
{ {
sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off"); sampleOff = audio.Samples.Get(@"UI/check-off");
activeColour = colours.ForModType(Mod.Type); activeColour = colours.ForModType(Mod.Type);
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
} }
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@ -166,6 +171,9 @@ namespace osu.Game.Overlays.Mods
private void playStateChangeSamples() private void playStateChangeSamples()
{ {
if (samplePlaybackDisabled.Value)
return;
if (Active.Value) if (Active.Value)
sampleOn?.Play(); sampleOn?.Play();
else else

View File

@ -15,6 +15,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -27,7 +28,7 @@ using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public abstract class ModSelectScreen : ShearedOverlayContainer public abstract class ModSelectScreen : ShearedOverlayContainer, ISamplePlaybackDisabler
{ {
protected const int BUTTON_WIDTH = 200; protected const int BUTTON_WIDTH = 200;
@ -128,7 +129,6 @@ namespace osu.Game.Overlays.Mods
Shear = new Vector2(SHEAR, 0), Shear = new Vector2(SHEAR, 0),
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Spacing = new Vector2(10, 0),
Margin = new MarginPadding { Horizontal = 70 }, Margin = new MarginPadding { Horizontal = 70 },
Children = new[] Children = new[]
{ {
@ -188,6 +188,8 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true);
((IBindable<IReadOnlyList<Mod>>)modSettingsArea.SelectedMods).BindTo(SelectedMods); ((IBindable<IReadOnlyList<Mod>>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
SelectedMods.BindValueChanged(val => SelectedMods.BindValueChanged(val =>
@ -205,6 +207,14 @@ namespace osu.Game.Overlays.Mods
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
updateAvailableMods(); 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();
});
} }
/// <summary> /// <summary>
@ -226,12 +236,21 @@ namespace osu.Game.Overlays.Mods
} }
private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) 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, AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140),
}; };
}
private ShearedButton[] createDefaultFooterButtons() private ShearedButton[] createDefaultFooterButtons()
=> new[] => new[]
@ -340,6 +359,8 @@ namespace osu.Game.Overlays.Mods
#region Transition handling #region Transition handling
private const float distance = 700;
protected override void PopIn() protected override void PopIn()
{ {
const double fade_in_duration = 400; const double fade_in_duration = 400;
@ -351,13 +372,26 @@ namespace osu.Game.Overlays.Mods
.FadeIn(fade_in_duration / 2, Easing.OutQuint) .FadeIn(fade_in_duration / 2, Easing.OutQuint)
.ScaleTo(1, fade_in_duration, Easing.OutElastic); .ScaleTo(1, fade_in_duration, Easing.OutElastic);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++) for (int i = 0; i < columnFlow.Count; i++)
{ {
columnFlow[i].Column var column = columnFlow[i].Column;
.TopLevelContent
.Delay(i * 30) double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30;
.MoveToY(0, fade_in_duration, Easing.OutQuint) double duration = column.AllFiltered.Value ? 0 : fade_in_duration;
.FadeIn(fade_in_duration, Easing.OutQuint); 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) .FadeOut(fade_out_duration / 2, Easing.OutQuint)
.ScaleTo(0.75f, fade_out_duration, Easing.OutQuint); .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++) for (int i = 0; i < columnFlow.Count; i++)
{ {
const float distance = 700;
var column = columnFlow[i].Column; 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.FlushPendingSelections();
column.TopLevelContent column.TopLevelContent
.MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) .MoveToY(newYPosition, duration, Easing.OutQuint)
.FadeOut(fade_out_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) if (e.Repeat)
return false; 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) 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.ToggleModSelection:
case GlobalAction.Select: case GlobalAction.Select:
{ {
if (customisationVisible.Value) // Pressing toggle or select should completely hide the overlay in one shot.
customisationVisible.Value = false; hideOverlay(true);
Hide();
return 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 #endregion
#region Sample playback control
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool(true);
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
#endregion
/// <summary> /// <summary>
/// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility. /// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility.
/// </summary> /// </summary>
@ -531,17 +588,20 @@ namespace osu.Game.Overlays.Mods
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Active.BindValueChanged(_ => updateDim(), true); Active.BindValueChanged(_ => updateState());
Column.AllFiltered.BindValueChanged(_ => updateState(), true);
FinishTransforms(); FinishTransforms();
} }
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning; protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning;
private void updateDim() private void updateState()
{ {
Colour4 targetColour; Colour4 targetColour;
if (Active.Value) Column.Alpha = Column.AllFiltered.Value ? 0 : 1;
if (Column.Active.Value)
targetColour = Colour4.White; targetColour = Colour4.White;
else else
targetColour = IsHovered ? colours.GrayC : colours.Gray8; targetColour = IsHovered ? colours.GrayC : colours.Gray8;
@ -560,14 +620,14 @@ namespace osu.Game.Overlays.Mods
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
base.OnHover(e); base.OnHover(e);
updateDim(); updateState();
return Active.Value; return Active.Value;
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
base.OnHoverLost(e); base.OnHoverLost(e);
updateDim(); updateState();
} }
} }

View File

@ -39,6 +39,18 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
bool UserPlayable { get; } bool UserPlayable { get; }
/// <summary>
/// Whether this mod is valid for multiplayer matches.
/// Should be <c>false</c> for mods that make gameplay duration dependent on user input (e.g. <see cref="ModAdaptiveSpeed"/>).
/// </summary>
bool ValidForMultiplayer { get; }
/// <summary>
/// Whether this mod is valid as a free mod in multiplayer matches.
/// Should be <c>false</c> for mods that affect the gameplay duration (e.g. <see cref="ModRateAdjust"/> and <see cref="ModTimeRamp"/>).
/// </summary>
bool ValidForMultiplayerAsFreeMod { get; }
/// <summary> /// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod. /// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary> /// </summary>

View File

@ -94,6 +94,12 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual bool UserPlayable => true; 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 [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; public virtual bool Ranked => false;

View File

@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) };
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mods
public bool RestartOnFail => false; public bool RestartOnFail => false;
public override bool UserPlayable => 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) }; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };

View File

@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModRateAdjust : Mod, IApplicableToRate public abstract class ModRateAdjust : Mod, IApplicableToRate
{ {
public override bool ValidForMultiplayerAsFreeMod => false;
public abstract BindableNumber<double> SpeedChange { get; } public abstract BindableNumber<double> SpeedChange { get; }
public virtual void ApplyToTrack(ITrack track) public virtual void ApplyToTrack(ITrack track)

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public abstract BindableBool AdjustPitch { get; } public abstract BindableBool AdjustPitch { get; }
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";

View File

@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 0; public override double ScoreMultiplier => 0;
public override bool UserPlayable => false; public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override ModType Type => ModType.System; public override ModType Type => ModType.System;

View File

@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
@ -50,7 +51,6 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
[Cached(typeof(ISamplePlaybackDisabler))]
[Cached] [Cached]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider, ISamplePlaybackDisabler public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider, ISamplePlaybackDisabler
{ {

View File

@ -65,6 +65,8 @@ namespace osu.Game.Screens.OnlinePlay
public readonly PlaylistItem Item; public readonly PlaylistItem Item;
public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID;
private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
private readonly IBindable<bool> valid = new Bindable<bool>(); private readonly IBindable<bool> valid = new Bindable<bool>();
@ -128,12 +130,10 @@ namespace osu.Game.Screens.OnlinePlay
SelectedItem.BindValueChanged(selected => SelectedItem.BindValueChanged(selected =>
{ {
bool isCurrent = selected.NewValue == Model;
if (!valid.Value) if (!valid.Value)
{ {
// Don't allow selection when not valid. // Don't allow selection when not valid.
if (isCurrent) if (IsSelectedItem)
{ {
SelectedItem.Value = selected.OldValue; SelectedItem.Value = selected.OldValue;
} }
@ -142,7 +142,7 @@ namespace osu.Game.Screens.OnlinePlay
return; return;
} }
maskingContainer.BorderThickness = isCurrent ? 5 : 0; maskingContainer.BorderThickness = IsSelectedItem ? 5 : 0;
}, true); }, true);
valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh));

View File

@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); 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;
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -37,7 +38,6 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
[Cached] [Cached]
[Cached(typeof(ISamplePlaybackDisabler))]
public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo
{ {
/// <summary> /// <summary>

View File

@ -85,7 +85,6 @@ namespace osu.Game.Screens.Ranking
InternalChild = scroll = new Scroll InternalChild = scroll = new Scroll
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel.
Child = flow = new Flow Child = flow = new Flow
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -359,11 +358,6 @@ namespace osu.Game.Screens.Ranking
/// </summary> /// </summary>
public float? InstantScrollTarget; public float? InstantScrollTarget;
/// <summary>
/// Whether this container should handle scroll trigger events.
/// </summary>
public Func<bool> HandleScroll;
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
if (InstantScrollTarget != null) if (InstantScrollTarget != null)
@ -374,10 +368,6 @@ namespace osu.Game.Screens.Ranking
base.UpdateAfterChildren(); base.UpdateAfterChildren();
} }
public override bool HandlePositionalInput => HandleScroll();
public override bool HandleNonPositionalInput => HandleScroll();
} }
} }
} }

View File

@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {

View File

@ -106,22 +106,69 @@ namespace osu.Game.Utils
} }
/// <summary> /// <summary>
/// Check the provided combination of mods are valid for a local gameplay session. /// Checks that all <see cref="Mod"/>s in a combination are valid for a local gameplay session.
/// </summary> /// </summary>
/// <param name="mods">The mods to check.</param> /// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Can be null if all mods were valid.</param> /// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns> /// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidForGameplay(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods) public static bool CheckValidForGameplay(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
{ {
mods = mods.ToArray(); mods = mods.ToArray();
// exclude multi mods from compatibility checks. // checking compatibility of multi mods would try to flatten them and return incompatible mods.
// the loop below automatically marks all multi mods as not valid for gameplay anyway. // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first.
CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods); 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);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are valid as "required mods" in a multiplayer match session.
/// </summary>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidRequiredModsForMultiplayer(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? 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);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are valid as "free mods" in a multiplayer match session.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidFreeModsForMultiplayer(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
=> checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayerAsFreeMod && !(m is MultiMod), out invalidMods);
private static bool checkValid(IEnumerable<Mod> mods, Predicate<Mod> valid, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
invalidMods = null;
foreach (var mod in mods) foreach (var mod in mods)
{ {
if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) if (!valid(mod))
{ {
invalidMods ??= new List<Mod>(); invalidMods ??= new List<Mod>();
invalidMods.Add(mod); invalidMods.Add(mod);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Utils
var options = new SentryOptions var options = new SentryOptions
{ {
Dsn = "https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255", Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2",
Release = game.Version Release = game.Version
}; };

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="Realm" Version="10.10.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.430.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.509.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="Sentry" Version="3.14.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.430.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.509.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.430.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.509.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />