Merge remote-tracking branch 'upstream/HEAD' into dev

# Conflicts:
#	osu.Game/Screens/SelectV2/SongSelect.cs
#	osu.Game/osu.Game.csproj
This commit is contained in:
LA
2026-03-03 19:15:54 +08:00
51 changed files with 1423 additions and 1107 deletions

View File

@@ -5,8 +5,9 @@ on:
types: [opened]
permissions:
issues: read # to read the labels of any linked issue(s)
pull-requests: write # to put the found labels if any on the PR
issues: write
pull-requests: write
contents: read
jobs:
copy-labels:

View File

@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.209.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.303.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@@ -1,45 +1,64 @@
// 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.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Screens;
using osu.Game.Screens.Edit.Submission;
using osu.Game.Screens.Footer;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene
public partial class TestSceneBeatmapSubmissionOverlay : ScreenTestScene
{
private ScreenFooter footer = null!;
private TestBeatmapSubmissionOverlayScreen screen = null!;
[Cached]
private readonly BeatmapSubmissionSettings beatmapSubmissionSettings = new BeatmapSubmissionSettings();
[SetUpSteps]
public void SetUpSteps()
public override void SetUpSteps()
{
AddStep("add overlay", () =>
{
var receptor = new ScreenFooter.BackReceptor();
footer = new ScreenFooter(receptor);
base.SetUpSteps();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[]
{
(typeof(ScreenFooter), (object)footer),
(typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()),
},
Children = new Drawable[]
{
receptor,
new BeatmapSubmissionOverlay
{
State = { Value = Visibility.Visible, },
},
footer,
}
};
});
AddStep("push screen", () => LoadScreen(screen = new TestBeatmapSubmissionOverlayScreen()));
AddUntilStep("wait until screen is loaded", () => screen.IsLoaded, () => Is.True);
AddStep("show overlay", () => screen.Overlay.Show());
}
private partial class TestBeatmapSubmissionOverlayScreen : OsuScreen
{
public override bool ShowFooter => true;
public BeatmapSubmissionOverlay Overlay = null!;
private IDisposable? overlayRegistration;
[Resolved]
private IOverlayManager? overlayManager { get; set; }
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(Overlay = new BeatmapSubmissionOverlay());
}
protected override void LoadComplete()
{
base.LoadComplete();
overlayRegistration = overlayManager?.RegisterBlockingOverlay(Overlay);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
overlayRegistration?.Dispose();
}
}
}
}

View File

@@ -1,6 +1,8 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -15,6 +17,7 @@ using osu.Game.Database;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Lounge;
@@ -22,6 +25,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -35,7 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected IScreen CurrentScreen => multiplayerComponents.CurrentScreen;
protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen;
private BeatmapManager beatmaps = null!;
protected BeatmapManager Beatmaps { get; private set; } = null!;
private BeatmapSetInfo importedSet = null!;
private RulesetStore rulesets = null!;
@@ -49,7 +54,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapStore beatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm);
@@ -62,13 +67,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
importedSet = Beatmaps.GetAllUsableBeatmapSets().First();
InitialBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0);
OtherBeatmap = importedSet.Beatmaps.Last(b => b.Ruleset.OnlineID == 0);
});
@@ -118,6 +123,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
}
protected void AddBeatmapFromSongSelect(Func<BeatmapInfo> beatmap, RulesetInfo? ruleset = null, IReadOnlyList<Mod>? mods = null)
{
Screens.SelectV2.SongSelect? songSelect = null;
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.SelectV2.SongSelect) != null);
AddUntilStep("wait for loaded", () => songSelect.IsCurrentScreen() && !songSelect.AsNonNull().IsFiltering);
if (ruleset != null)
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);
if (mods != null)
AddStep($"set mods to {string.Join(",", mods.Select(m => m.Acronym))}", () => songSelect.AsNonNull().Mods.Value = mods);
AddStep("select other beatmap", () => songSelect.AsNonNull().Beatmap.Value = Beatmaps.GetWorkingBeatmap(beatmap()));
AddStep("confirm selection", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@@ -1,24 +1,16 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -45,10 +37,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestItemAddedToTheEndOfQueue()
{
addItem(() => OtherBeatmap);
AddBeatmapFromSongSelect(() => OtherBeatmap);
AddUntilStep("playlist has 2 items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2);
addItem(() => InitialBeatmap);
AddBeatmapFromSongSelect(() => InitialBeatmap);
AddUntilStep("playlist has 3 items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 3);
AddUntilStep("first item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
@@ -57,8 +49,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestNextItemSelectedAfterGameplayFinish()
{
addItem(() => OtherBeatmap);
addItem(() => InitialBeatmap);
AddBeatmapFromSongSelect(() => OtherBeatmap);
AddBeatmapFromSongSelect(() => InitialBeatmap);
RunGameplay();
@@ -74,8 +66,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestItemsNotClearedWhenSwitchToHostOnlyMode()
{
addItem(() => OtherBeatmap);
addItem(() => InitialBeatmap);
AddBeatmapFromSongSelect(() => OtherBeatmap);
AddBeatmapFromSongSelect(() => InitialBeatmap);
// Move to the "other" beatmap.
RunGameplay();
@@ -89,14 +81,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCorrectItemSelectedAfterNewItemAdded()
{
addItem(() => OtherBeatmap);
AddBeatmapFromSongSelect(() => OtherBeatmap);
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
}
[Test]
public void TestCorrectRulesetSelectedAfterNewItemAdded()
{
addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo);
AddBeatmapFromSongSelect(() => OtherBeatmap, new CatchRuleset().RulesetInfo);
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
@@ -113,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCorrectModsSelectedAfterNewItemAdded()
{
addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() });
AddBeatmapFromSongSelect(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() });
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
@@ -126,28 +118,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any());
AddStep("exit player", () => CurrentScreen.Exit());
}
private void addItem(Func<BeatmapInfo> beatmap, RulesetInfo? ruleset = null, IReadOnlyList<Mod>? mods = null)
{
Screens.Select.SongSelect? songSelect = null;
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded);
if (ruleset != null)
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);
if (mods != null)
AddStep($"set mods to {string.Join(",", mods.Select(m => m.Acronym))}", () => songSelect.AsNonNull().Mods.Value = mods);
AddStep("select other beatmap", () => songSelect.AsNonNull().FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
}
}

View File

@@ -7,14 +7,13 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Utils;
@@ -22,12 +21,13 @@ using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene
public partial class TestSceneFreeModSelectOverlay : ScreenTestScene
{
private FreeModSelectOverlay freeModSelectOverlay = null!;
private FooterButtonFreeMods footerButtonFreeMods = null!;
private ScreenFooter footer = null!;
private TestFreeModSelectOverlayScreen screen = null!;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
private readonly Bindable<IReadOnlyList<Mod>> freeMods = new Bindable<IReadOnlyList<Mod>>([]);
private FreeModSelectOverlay freeModSelectOverlay => screen.Overlay;
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGameBase)
@@ -35,6 +35,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
availableMods.BindTo(osuGameBase.AvailableMods);
}
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset selected mods", () => freeMods.Value = []);
}
[Test]
public void TestFreeModSelect()
{
@@ -44,11 +52,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
() => this.ChildrenOfType<ModPanel>()
.Where(panel => panel.IsPresent)
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
AddToggleStep("toggle visibility", visible =>
{
freeModSelectOverlay.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
});
}
[Test]
@@ -72,18 +75,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddStep("click select all button", navigateAndClick<SelectAllModsButton>);
AddStep("click select all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e");
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
void navigateAndClick<T>() where T : Drawable
{
InputManager.MoveMouseTo(this.ChildrenOfType<T>().Single());
InputManager.Click(MouseButton.Left);
}
}
[Test]
@@ -124,55 +125,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
}
[Test]
public void TestSelectAllViaFooterButtonThenDeselectFromOverlay()
{
createFreeModSelect();
AddAssert("overlay select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
AddStep("click footer select all button", () =>
{
InputManager.MoveMouseTo(footerButtonFreeMods);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
AddAssert("footer button displays all", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "all"));
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
}
private void createFreeModSelect()
{
AddStep("create free mod select screen", () => Child = new DependencyProvidingContainer
AddStep("create free mod select screen", () => LoadScreen(screen = new TestFreeModSelectOverlayScreen
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
freeModSelectOverlay = new FreeModSelectOverlay
{
State = { Value = Visibility.Visible }
},
footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Y = -ScreenFooter.HEIGHT,
FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods },
},
footer = new ScreenFooter(),
},
CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) },
});
FreeMods = { BindTarget = freeMods },
}));
AddUntilStep("wait until screen is loaded", () => screen.IsLoaded, () => Is.True);
AddStep("show overlay", () => freeModSelectOverlay.Show());
AddUntilStep("all column content loaded",
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& freeModSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
@@ -197,5 +157,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
return true;
}
private partial class TestFreeModSelectOverlayScreen : OsuScreen
{
public override bool ShowFooter => true;
public FreeModSelectOverlay Overlay = null!;
private IDisposable? overlayRegistration;
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>([]);
[Resolved]
private IOverlayManager? overlayManager { get; set; }
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(Overlay = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
overlayRegistration = overlayManager?.RegisterBlockingOverlay(Overlay);
}
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() =>
[
new FooterButtonFreeMods(Overlay)
{
FreeMods = { BindTarget = FreeMods },
},
];
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
overlayRegistration?.Dispose();
}
}
}
}

View File

@@ -5,6 +5,8 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
@@ -36,6 +38,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second playlist item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[1].ID);
}
[Test]
public void TestItemStillSelectedAfterChangeToSameBeatmap()
{
selectNewItem(() => InitialBeatmap);
AddUntilStep("playlist item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
public void TestSettingsUpdatedWhenChangingQueueMode()
{
@@ -47,14 +57,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("api room updated", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
}
[Test]
public void TestItemStillSelectedAfterChangeToSameBeatmap()
{
selectNewItem(() => InitialBeatmap);
AddUntilStep("playlist item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
public void TestItemStillSelectedAfterChangeToOtherBeatmap()
{
@@ -80,13 +82,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAddItemsAsHost()
{
addItem(() => OtherBeatmap);
AddBeatmapFromSongSelect(() => OtherBeatmap);
AddUntilStep("playlist contains two items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2);
}
private void selectNewItem(Func<BeatmapInfo> beatmap)
{
Screens.SelectV2.SongSelect? songSelect = null;
AddUntilStep("wait for playlist panels to load", () =>
{
var queueList = this.ChildrenOfType<MultiplayerQueueList>().Single();
@@ -99,26 +103,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.SelectV2.SongSelect) != null);
AddUntilStep("wait for loaded", () => songSelect.IsCurrentScreen() && !songSelect.AsNonNull().IsFiltering);
BeatmapInfo otherBeatmap = null!;
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddStep("select other beatmap", () => songSelect.AsNonNull().Beatmap.Value = Beatmaps.GetWorkingBeatmap(otherBeatmap = beatmap()));
AddStep("confirm selection", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
AddUntilStep("selected item is new beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == otherBeatmap.OnlineID);
}
private void addItem(Func<BeatmapInfo> beatmap)
{
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
}
}

View File

@@ -228,7 +228,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// edit playlist item
AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
waitForSongSelect();
// select beatmap
AddStep("Press select", () => InputManager.Key(Key.Enter));
@@ -451,7 +451,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
waitForSongSelect();
AddUntilStep("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.ClientRoom?.Playlist.First().BeatmapID);
@@ -492,7 +492,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
waitForSongSelect();
AddUntilStep("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.ClientRoom?.Playlist.First().RulesetID);
@@ -533,7 +533,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
waitForSongSelect();
AddUntilStep("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.ClientRoom.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
@@ -1051,7 +1051,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press edit on second item", () => this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(i => i.Item.RulesetID == 1)
.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().Single().TriggerClick());
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
waitForSongSelect();
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1);
AddStep("start match", () => multiplayerClient.StartMatch().WaitSafely());
@@ -1249,6 +1249,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => multiplayerClient.RoomJoined);
}
private void waitForSongSelect()
{
AddUntilStep("wait for song select", () =>
{
var songSelect = InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault();
return songSelect != null && songSelect.IsCurrentScreen() && !songSelect.IsFiltering;
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@@ -16,6 +16,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@@ -26,8 +27,8 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -64,7 +65,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
AddStep("create room", () => room = CreateDefaultRoom());
AddStep("create room", () =>
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
room = CreateDefaultRoom();
});
AddStep("join room", () => JoinRoom(room));
WaitForJoined();
}
@@ -80,7 +85,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room));
});
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && !songSelect.IsFiltering);
}
[Test]
@@ -101,19 +106,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
setUp();
AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
AddUntilStep("wait for filtering", () => !songSelect.IsFiltering);
AddStep("select beatmap",
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
() => songSelect.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
AddUntilStep("wait for ongoing operation to complete", () => !OnlinePlayDependencies.OngoingOperationTracker.InProgress.Value);
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
AddStep("confirm selection", () => songSelect.FinaliseSelection());
AddStep("confirm selection", () => InputManager.Key(Key.Enter));
AddUntilStep("song select exited", () => !songSelect.IsCurrentScreen());
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo((selectedBeatmap)));
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime);
}
@@ -133,10 +140,42 @@ namespace osu.Game.Tests.Visual.Multiplayer
// A previous test's mod overlay could still be fading out.
AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1);
AddStep("open free mod overlay", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeMods>().Single());
InputManager.Click(MouseButton.Left);
});
assertFreeModNotShown(allowedMod);
assertFreeModNotShown(requiredMod);
}
[Test]
public void TestFreeModsDisplayedOnEnter()
{
AddStep("set room freemods", () =>
{
var editedItem = MultiplayerClient.ClientRoom!.CurrentPlaylistItem.Clone();
editedItem.AllowedMods =
[
new APIMod(new OsuModHardRock()),
];
MultiplayerClient.EditPlaylistItem(editedItem);
});
setUp();
AddStep("open free mod overlay", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeMods>().Single());
InputManager.Click(MouseButton.Left);
});
assertFreeModShown(typeof(OsuModHardRock));
}
[Test]
public void TestChangeRulesetImmediatelyAfterLoadComplete()
{
@@ -154,16 +193,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
LoadScreen(songSelect);
});
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
AddStep("confirm selection", () => songSelect.FinaliseSelection());
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && !songSelect.IsFiltering);
AddStep("confirm selection", () => InputManager.Key(Key.Enter));
AddAssert("beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID, () => Is.EqualTo(1));
}
private void assertFreeModShown(Type type)
{
AddUntilStep($"{type.ReadableName()} displayed in freemod overlay",
() => this.ChildrenOfType<FreeModSelectOverlay>()
.Single()
.ChildrenOfType<ModPanel>()
.Where(panel => panel.Visible)
.Any(b => b.Mod.GetType() == type));
}
private void assertFreeModNotShown(Type type)
{
AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
AddUntilStep($"{type.ReadableName()} not displayed in freemod overlay",
() => this.ChildrenOfType<FreeModSelectOverlay>()
.Single()
.ChildrenOfType<ModPanel>()
@@ -185,12 +235,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new Bindable<IReadOnlyList<Mod>> FreeMods => base.FreeMods;
public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, PlaylistItem? itemToEdit = null)
: base(room, itemToEdit)
{
}
public void SelectBeatmap(BeatmapInfo beatmap) => SelectAndRun(beatmap, () => { });
}
}
}

View File

@@ -164,20 +164,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("freestyle enabled", () => songSelect.Freestyle.Value, () => Is.True);
AddStep("click icon in free mods button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeModsV2>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeMods>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select not visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("toggle freestyle off", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyleV2>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyle>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("freestyle disabled", () => songSelect.Freestyle.Value, () => Is.False);
AddStep("click icon in free mods button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeModsV2>().Single());
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreeMods>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

View File

@@ -8,9 +8,13 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
@@ -23,10 +27,13 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestFooterButtonsOnScreenTransitions()
{
PushAndConfirm(() => new TestScreenOne());
PushAndConfirm(() => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button One" }]
});
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
PushAndConfirm(() => new TestScreenTwo());
PushAndConfirm(() => new TestScreen { CreateButtons = () => [new ScreenFooterButton { Text = "Button Two" }] });
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
AddStep("exit screen", () => Game.ScreenStack.Exit());
@@ -40,7 +47,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
PushAndConfirm(() => new TestScreen(true));
PushAndConfirm(() => new TestScreen());
AddAssert("footer shown", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("old back button hidden", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Hidden));
@@ -69,10 +76,16 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
pushSubScreenAndConfirm(() => screen, () => new TestScreenOne());
pushSubScreenAndConfirm(() => screen, () => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button One" }]
});
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
pushSubScreenAndConfirm(() => screen, () => new TestScreenTwo());
pushSubScreenAndConfirm(() => screen, () => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button Two" }]
});
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
AddStep("exit sub screen", () => screen.ExitSubScreen());
@@ -92,10 +105,16 @@ namespace osu.Game.Tests.Visual.Navigation
TestScreenWithSubScreen screen = null!;
PushAndConfirm(() => screen = new TestScreenWithSubScreen());
pushSubScreenAndConfirm(() => screen, () => new TestScreenOne());
pushSubScreenAndConfirm(() => screen, () => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button One" }]
});
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
PushAndConfirm(() => new TestScreenTwo());
PushAndConfirm(() => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button Two" }]
});
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
AddStep("exit parent screen", () => Game.ScreenStack.Exit());
@@ -111,14 +130,23 @@ namespace osu.Game.Tests.Visual.Navigation
TestScreenWithSubScreen screen = null!;
PushAndConfirm(() => screen = new TestScreenWithSubScreen());
pushSubScreenAndConfirm(() => screen, () => new TestScreenOne());
pushSubScreenAndConfirm(() => screen, () => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button One" }]
});
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
PushAndConfirm(() => new TestScreenOne());
PushAndConfirm(() => new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button One" }]
});
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
// Can't use the helper method because the screen never loads
AddStep("Push new sub screen", () => screen.PushSubScreen(new TestScreenTwo()));
AddStep("Push new sub screen", () => screen.PushSubScreen(new TestScreen
{
CreateButtons = () => [new ScreenFooterButton { Text = "Button Two" }]
}));
AddWaitStep("wait for potential screen load", 5);
AddUntilStep("button one still shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
@@ -126,6 +154,83 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
}
/// <summary>
/// Tests clicking the back button while an overlay is open.
/// </summary>
[Test]
public void TestBackButtonWhenOverlayOpen()
{
TestScreen screen = null!;
PushAndConfirm(() =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
return screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () =>
[
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
],
};
});
AddStep("show overlay", () => screen.Overlay.Show());
AddAssert("overlay shown", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("press back", () => screenFooter.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
AddAssert("overlay hidden", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("screen still shown", () => screen.IsCurrentScreen(), () => Is.True);
}
/// <summary>
/// Tests clicking the back button on an overlay with `BackButtonPressed` being overridden.
/// </summary>
[Test]
public void TestBackButtonWithCustomBackButtonPressed()
{
TestScreen screen = null!;
TestShearedOverlayContainer overlay = null!;
PushAndConfirm(() =>
{
return screen = new TestScreen
{
Overlay = overlay = new TestShearedOverlayContainer(),
CreateButtons = () =>
[
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
],
};
});
AddStep("show overlay", () => screen.Overlay.Show());
AddAssert("overlay shown", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("set block count", () => overlay.BackButtonCount = 1);
AddStep("press back", () => screenFooter.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
AddAssert("overlay still shown", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("press back again", () => screenFooter.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
AddAssert("overlay hidden", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("screen still shown", () => screen.IsCurrentScreen(), () => Is.True);
}
private void pushSubScreenAndConfirm(Func<TestScreenWithSubScreen> target, Func<Screen> newScreen)
{
Screen screen = null!;
@@ -142,39 +247,45 @@ namespace osu.Game.Tests.Visual.Navigation
&& (previousScreen == null || previousScreen.GetChildScreen() == screen));
}
private partial class TestScreenOne : OsuScreen
{
public override bool ShowFooter => true;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => new[]
{
new ScreenFooterButton { Text = "Button One" },
};
}
private partial class TestScreenTwo : OsuScreen
{
public override bool ShowFooter => true;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => new[]
{
new ScreenFooterButton { Text = "Button Two" },
};
}
private partial class TestScreen : OsuScreen
{
public override bool ShowFooter { get; }
public TestScreen(bool footer)
public Func<IReadOnlyList<ScreenFooterButton>> CreateButtons = Array.Empty<ScreenFooterButton>;
public ShearedOverlayContainer Overlay = new TestShearedOverlayContainer();
private IDisposable? overlayRegistration;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Resolved]
private IOverlayManager? overlayManager { get; set; }
public TestScreen(bool showFooter = true)
{
ShowFooter = footer;
ShowFooter = showFooter;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(Overlay);
}
protected override void LoadComplete()
{
base.LoadComplete();
overlayRegistration = overlayManager?.RegisterBlockingOverlay(Overlay);
}
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => CreateButtons.Invoke();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
overlayRegistration?.Dispose();
}
}
@@ -196,5 +307,66 @@ namespace osu.Game.Tests.Visual.Navigation
public void ExitSubScreen() => SubScreenStack.Exit();
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
: base(OverlayColourScheme.Orange)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "Test overlay";
Header.Description = "An overlay that is made purely for testing purposes.";
}
public int BackButtonCount;
public override bool OnBackButton()
{
if (BackButtonCount > 0)
{
BackButtonCount--;
return true;
}
return false;
}
public override VisibilityContainer CreateFooterContent() => new TestFooterContent();
public partial class TestFooterContent : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new[]
{
new ShearedButton(200) { Text = "Action #1", Action = () => { } },
new ShearedButton(140) { Text = "Action #2", Action = () => { } },
}
};
}
protected override void PopIn()
{
this.MoveToY(0, 400, Easing.OutQuint)
.FadeIn(400, Easing.OutQuint);
}
protected override void PopOut()
{
this.MoveToY(-20f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
}
}
}
}
}

View File

@@ -72,6 +72,10 @@ namespace osu.Game.Tests.Visual.Online
Preview = @"https://b.ppy.sh/preview/12345.mp3",
PlayCount = 123,
FavouriteCount = 456,
NominationStatus = new BeatmapSetNominationStatus
{
Current = 2,
},
Submitted = DateTime.Now,
Ranked = DateTime.Now,
BPM = 111,

View File

@@ -17,13 +17,13 @@ namespace osu.Game.Tests.Visual.Playlists
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private readonly FooterButtonFreeModsV2 button;
private readonly FooterButtonFreeMods button;
public TestSceneFooterButtonFreeModsV2()
{
ModSelectOverlay modSelectOverlay;
Add(modSelectOverlay = new TestModSelectOverlay());
Add(button = new FooterButtonFreeModsV2(modSelectOverlay)
Add(button = new FooterButtonFreeMods(modSelectOverlay)
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,

View File

@@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual.Playlists
public TestSceneFooterButtonFreestyleV2()
{
Add(new FooterButtonFreestyleV2
Add(new FooterButtonFreestyle
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,

View File

@@ -30,7 +30,6 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
@@ -676,19 +675,9 @@ namespace osu.Game.Tests.Visual.Playlists
public TestPlaylistsScreen(PlaylistsRoomSubScreen screen)
{
ScreenFooter footer;
InternalChild = new DependencyProvidingContainer
InternalChild = Stack = new OnlinePlaySubScreenStack
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Stack = new OnlinePlaySubScreenStack
{
RelativeSizeAxes = Axes.Both
},
footer = new ScreenFooter(),
},
CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) },
RelativeSizeAxes = Axes.Both
};
Stack.Push(screen);

View File

@@ -1,301 +0,0 @@
// 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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
{
private DependencyProvidingContainer contentContainer = null!;
private ScreenFooter screenFooter = null!;
private UserModSelectOverlay modOverlay = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
screenFooter = new ScreenFooter();
Child = contentContainer = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ScreenFooter), screenFooter)
},
Children = new Drawable[]
{
modOverlay = new UserModSelectOverlay { ShowPresets = true },
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Depth = float.MinValue,
Child = screenFooter,
},
},
};
screenFooter.SetButtons(new ScreenFooterButton[]
{
new FooterButtonMods(modOverlay) { Current = SelectedMods },
new FooterButtonRandom(),
new FooterButtonOptions(),
});
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("show footer", () => screenFooter.Show());
}
/// <summary>
/// Transition when moving from a screen with no buttons to a screen with buttons.
/// </summary>
[Test]
public void TestButtonsIn()
{
}
/// <summary>
/// Transition when moving from a screen with buttons to a screen with no buttons.
/// </summary>
[Test]
public void TestButtonsOut()
{
AddStep("clear buttons", () => screenFooter.SetButtons(Array.Empty<ScreenFooterButton>()));
}
/// <summary>
/// Transition when moving from a screen with buttons to a screen with buttons.
/// </summary>
[Test]
public void TestReplaceButtons()
{
AddStep("replace buttons", () => screenFooter.SetButtons(new[]
{
new ScreenFooterButton { Text = "One", Action = () => { } },
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
}));
}
[Test]
public void TestExternalOverlayContent()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("set buttons", () => screenFooter.SetButtons(new[]
{
new ScreenFooterButton(externalOverlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
}));
AddWaitStep("wait for transition", 3);
AddStep("show overlay", () => externalOverlay.Show());
contentDisplayed();
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("hide overlay", () => externalOverlay.Hide());
contentHidden();
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
[Test]
public void TestTemporarilyShowFooter()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("hide footer", () => screenFooter.Hide());
AddStep("remove buttons", () => screenFooter.SetButtons(Array.Empty<ScreenFooterButton>()));
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("show external overlay", () => externalOverlay.Show());
AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible);
contentDisplayed();
AddStep("hide external overlay", () => externalOverlay.Hide());
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
contentHidden();
AddStep("show footer", () => screenFooter.Show());
AddAssert("content still hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
AddStep("show external overlay", () => externalOverlay.Show());
AddAssert("footer still visible", () => screenFooter.State.Value == Visibility.Visible);
AddStep("hide external overlay", () => externalOverlay.Hide());
AddAssert("footer still visible", () => screenFooter.State.Value == Visibility.Visible);
AddStep("hide footer", () => screenFooter.Hide());
AddStep("show external overlay", () => externalOverlay.Show());
}
[Test]
public void TestBackButton()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("hide footer", () => screenFooter.Hide());
AddStep("remove buttons", () => screenFooter.SetButtons(Array.Empty<ScreenFooterButton>()));
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("show external overlay", () => externalOverlay.Show());
AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible);
AddStep("press back", () => this.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
AddAssert("overlay hidden", () => externalOverlay.State.Value == Visibility.Hidden);
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
AddStep("show external overlay", () => externalOverlay.Show());
AddStep("set block count", () => externalOverlay.BackButtonCount = 1);
AddStep("press back", () => this.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
AddAssert("overlay still visible", () => externalOverlay.State.Value == Visibility.Visible);
AddAssert("footer still shown", () => screenFooter.State.Value == Visibility.Visible);
AddStep("press back again", () => this.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
AddAssert("overlay hidden", () => externalOverlay.State.Value == Visibility.Hidden);
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
}
[Test]
public void TestLoadOverlayAfterFooterIsDisplayed()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("show mod overlay", () => modOverlay.Show());
AddUntilStep("mod footer content shown", () => this.ChildrenOfType<ModSelectFooterContent>().SingleOrDefault()?.IsPresent, () => Is.True);
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddUntilStep("wait for load", () => externalOverlay.IsLoaded);
AddAssert("mod footer content still shown", () => this.ChildrenOfType<ModSelectFooterContent>().SingleOrDefault()?.IsPresent, () => Is.True);
AddAssert("external overlay content not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
AddStep("hide mod overlay", () => modOverlay.Hide());
AddUntilStep("mod footer content hidden", () => this.ChildrenOfType<ModSelectFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
AddAssert("external overlay content still not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
}
[Test]
public void TestButtonResizedAfterFooterIsDisplayed()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("set buttons", () => screenFooter.SetButtons(new[]
{
new ScreenFooterButton(externalOverlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
}));
AddWaitStep("wait for transition", 3);
AddStep("show overlay", () => externalOverlay.Show());
contentDisplayed();
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("resize active button", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(240, 300, Easing.OutQuint));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(116, 300, Easing.OutQuint));
AddStep("hide overlay", () => externalOverlay.Hide());
contentHidden();
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
private void contentHidden()
{
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
}
private void contentDisplayed()
{
AddUntilStep("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
: base(OverlayColourScheme.Orange)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "Test overlay";
Header.Description = "An overlay that is made purely for testing purposes.";
}
public int BackButtonCount;
public override bool OnBackButton()
{
if (BackButtonCount > 0)
{
BackButtonCount--;
return true;
}
return false;
}
public override VisibilityContainer CreateFooterContent() => new TestFooterContent();
public partial class TestFooterContent : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new[]
{
new ShearedButton(200) { Text = "Action #1", Action = () => { } },
new ShearedButton(140) { Text = "Action #2", Action = () => { } },
}
};
}
protected override void PopIn()
{
this.MoveToY(0, 400, Easing.OutQuint)
.FadeIn(400, Easing.OutQuint);
}
protected override void PopOut()
{
this.MoveToY(-20f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
}
}
}
}
}

View File

@@ -12,7 +12,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -23,17 +22,16 @@ using osu.Game.Overlays;
using osu.Game.Overlays.FirstRunSetup;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFirstRunSetupOverlay : OsuManualInputManagerTestScene
public partial class TestSceneFirstRunSetupOverlay : ScreenTestScene
{
private FirstRunSetupOverlay overlay;
private ScreenFooter footer;
private TestFirstRunSetupOverlayScreen screen = null!;
private FirstRunSetupOverlay overlay => screen.Overlay;
private readonly Mock<TestPerformerFromScreenRunner> performer = new Mock<TestPerformerFromScreenRunner>();
@@ -53,8 +51,10 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[SetUpSteps]
public void SetUpSteps()
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("setup dependencies", () =>
{
performer.Reset();
@@ -67,16 +67,16 @@ namespace osu.Game.Tests.Visual.UserInterface
.Callback((Notification n) => lastNotification = n);
});
createOverlay();
AddStep("reset first run", () => LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, true));
AddStep("show overlay", () => overlay.Show());
createScreen();
}
[Test]
public void TestBasic()
{
AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible);
AddAssert("footer visible", () => footer.State.Value == Visibility.Visible);
AddAssert("footer visible", () => ScreenFooter.State.Value == Visibility.Visible);
}
[Test]
@@ -92,7 +92,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("first run false", () => !LocalConfig.Get<bool>(OsuSetting.ShowFirstRunSetup));
createOverlay();
AddStep("exit screen", () => Stack.Exit());
createScreen();
AddWaitStep("wait some", 5);
@@ -146,7 +147,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (keyboard)
InputManager.Key(Key.Escape);
else
footer.BackButton.TriggerClick();
ScreenFooter.BackButton.TriggerClick();
}
return overlay.CurrentScreen is ScreenWelcome;
@@ -161,7 +162,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
else
{
AddStep("press back button", () => footer.BackButton.TriggerClick());
AddStep("press back button", () => ScreenFooter.BackButton.TriggerClick());
AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden);
}
}
@@ -204,25 +205,45 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);
}
private void createOverlay()
private void createScreen()
{
AddStep("add overlay", () =>
{
var receptor = new ScreenFooter.BackReceptor();
footer = new ScreenFooter(receptor);
AddStep("push screen", () => LoadScreen(screen = new TestFirstRunSetupOverlayScreen()));
AddUntilStep("wait until screen is loaded", () => screen.IsLoaded, () => Is.True);
}
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
overlay = new FirstRunSetupOverlay(),
footer,
}
};
});
private partial class TestFirstRunSetupOverlayScreen : OsuScreen
{
public override bool ShowFooter => true;
public FirstRunSetupOverlay Overlay = null!;
[CanBeNull]
private IDisposable overlayRegistration;
[CanBeNull]
[Resolved]
private IOverlayManager overlayManager { get; set; }
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(Overlay = new FirstRunSetupOverlay());
}
protected override void LoadComplete()
{
base.LoadComplete();
overlayRegistration = overlayManager?.RegisterBlockingOverlay(Overlay);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
overlayRegistration?.Dispose();
}
}
// interface mocks break hot reload, mocking this stub implementation instead works around it.

View File

@@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
@@ -25,6 +26,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Tests.Mods;
using osuTK;
@@ -33,17 +35,19 @@ using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModSelectOverlay : OsuManualInputManagerTestScene
public partial class TestSceneModSelectOverlay : ScreenTestScene
{
protected override bool UseFreshStoragePerRun => true;
private RulesetStore rulesetStore = null!;
private TestModSelectOverlay modSelectOverlay = null!;
private TestModSelectOverlayScreen screen = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
private ModSelectOverlay modSelectOverlay => screen.Overlay;
[BackgroundDependencyLoader]
private void load()
{
@@ -52,9 +56,10 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[SetUpSteps]
public void SetUpSteps()
public override void SetUpSteps()
{
AddStep("clear contents", Clear);
base.SetUpSteps();
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
@@ -97,29 +102,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createScreen()
{
AddStep("create screen", () =>
{
var receptor = new ScreenFooter.BackReceptor();
var footer = new ScreenFooter(receptor);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods },
},
footer,
}
};
});
AddStep("create screen", () => LoadScreen(screen = new TestModSelectOverlayScreen { SelectedMods = { BindTarget = SelectedMods } }));
AddUntilStep("wait until screen is loaded", () => screen.IsLoaded, () => Is.True);
waitForColumnLoad();
}
@@ -306,29 +290,30 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestSettingsNotCrossPolluting()
{
TestScreenWithTwoOverlays screenWithTwoOverlays = null!;
Bindable<IReadOnlyList<Mod>> selectedMods2 = null!;
ModSelectOverlay modSelectOverlay2 = null!;
createScreen();
AddStep("push screen", () =>
{
selectedMods2 = new Bindable<IReadOnlyList<Mod>>(new Mod[] { new OsuModDifficultyAdjust() });
LoadScreen(screen = screenWithTwoOverlays = new TestScreenWithTwoOverlays
{
SelectedMods = { BindTarget = SelectedMods },
SelectedMods2 = { BindTarget = selectedMods2 },
});
});
AddStep("wait until screen is loaded", () => screenWithTwoOverlays.IsCurrentScreen());
waitForColumnLoad();
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
AddStep("set setting", () => modSelectOverlay.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = 8);
AddStep("set setting", () => screenWithTwoOverlays.Overlay.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddStep("create second bindable", () => selectedMods2 = new Bindable<IReadOnlyList<Mod>>(new Mod[] { new OsuModDifficultyAdjust() }));
AddStep("create second overlay", () =>
{
Add(modSelectOverlay2 = new UserModSelectOverlay().With(d =>
{
d.Origin = Anchor.TopCentre;
d.Anchor = Anchor.TopCentre;
d.SelectedMods.BindTarget = selectedMods2;
}));
});
AddStep("show", () => modSelectOverlay2.Show());
AddStep("hide first overlay", () => screenWithTwoOverlays.Overlay.Hide());
AddStep("show second overlay", () => screenWithTwoOverlays.SecondOverlay.Show());
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
@@ -481,6 +466,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
AddStep("exit screen", () => Stack.Exit());
createScreen();
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
}
@@ -797,16 +783,11 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestColumnHidingOnIsValidChange()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
});
waitForColumnLoad();
createScreen();
changeRuleset(0);
AddStep("set filter for 2 columns", () => modSelectOverlay.IsValidMod = mod => mod.Type is ModType.DifficultyIncrease or ModType.Conversion);
AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("unset filter", () => modSelectOverlay.IsValidMod = _ => true);
@@ -816,9 +797,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("hide", () => modSelectOverlay.Hide());
AddStep("set filter for 3 columns", () => modSelectOverlay.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
|| mod.Type == ModType.Automation
|| mod.Type == ModType.Conversion);
AddStep("set filter for 3 columns", () => modSelectOverlay.IsValidMod = mod => mod.Type is ModType.DifficultyReduction or ModType.Automation or ModType.Conversion);
AddStep("show", () => modSelectOverlay.Show());
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
@@ -830,13 +809,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestColumnHidingOnTextFilterChange()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
createScreen();
changeRuleset(0);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
@@ -854,13 +827,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestHidingOverlayClearsTextSearch()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
createScreen();
changeRuleset(0);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
@@ -1019,8 +986,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
selectedMods = new Bindable<IReadOnlyList<Mod>>([]);
modSelectOverlay.SelectedMods.UnbindFrom(SelectedMods);
modSelectOverlay.SelectedMods.BindTo(selectedMods);
screen.SelectedMods.UnbindFrom(SelectedMods);
screen.SelectedMods.BindTo(selectedMods);
});
AddStep("activate PF", () => selectedMods.Value = [new OsuModPerfect()]);
@@ -1066,11 +1033,79 @@ namespace osu.Game.Tests.Visual.UserInterface
rulesetStore.Dispose();
}
private partial class TestModSelectOverlay : UserModSelectOverlay
private partial class TestModSelectOverlayScreen : OsuScreen
{
public TestModSelectOverlay()
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>();
public override bool ShowFooter => true;
public ModSelectOverlay Overlay = null!;
private IDisposable? firstOverlayRegistration;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Resolved]
protected IOverlayManager? OverlayManager { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
ShowPresets = true;
LoadComponent(Overlay = new UserModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods },
ShowPresets = true,
});
}
protected override void LoadComplete()
{
base.LoadComplete();
firstOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(Overlay);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
firstOverlayRegistration?.Dispose();
}
}
private partial class TestScreenWithTwoOverlays : TestModSelectOverlayScreen
{
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods2 = new Bindable<IReadOnlyList<Mod>>([]);
public ModSelectOverlay SecondOverlay = null!;
private IDisposable? secondOverlayRegistration;
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(SecondOverlay = new UserModSelectOverlay
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
SelectedMods = { BindTarget = SelectedMods2 },
});
}
protected override void LoadComplete()
{
base.LoadComplete();
secondOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(SecondOverlay);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
secondOverlayRegistration?.Dispose();
}
}

View File

@@ -0,0 +1,365 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneScreenFooter : ScreenTestScene
{
[Test]
public void TestButtonsIn()
{
AddStep("push empty screen", () => LoadScreen(new TestScreen()));
AddStep("push screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
}
[Test]
public void TestButtonsOut()
{
AddStep("push empty screen", () => LoadScreen(new TestScreen()));
AddStep("push screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
AddStep("exit screen", () => Stack.Exit());
}
[Test]
public void TestReplaceButtons()
{
AddStep("push first screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
AddStep("push second screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 4", Action = () => { } },
new ScreenFooterButton { Text = "Button 5", Action = () => { } },
new ScreenFooterButton { Text = "Button 6", Action = () => { } },
},
}));
}
[Test]
public void TestFooterVisibility()
{
TestScreen screen = null!;
TestScreen screenWithoutFooter = null!;
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("push screen", () => LoadScreen(screen = new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddAssert("footer shown", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("push screen with no footer", () => LoadScreen(screenWithoutFooter = new TestScreen(showFooter: false)));
AddUntilStep("wait until screen is loaded", () => screenWithoutFooter.IsCurrentScreen(), () => Is.True);
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddAssert("footer shown", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestExternalOverlayContent()
{
TestScreen screen = null!;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("show overlay", () => screen.Overlay.Show());
contentDisplayed();
AddAssert("other buttons hidden", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("hide overlay", () => screen.Overlay.Hide());
contentHidden();
AddAssert("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
[Test]
public void TestTemporarilyShowFooter()
{
TestScreen screen = null!;
AddStep("push screen", () => LoadScreen(screen = new TestScreen(showFooter: false)));
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("show overlay", () => screen.Overlay.Show());
AddAssert("footer shown", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
contentDisplayed();
AddStep("hide overlay", () => screen.Overlay.Hide());
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
contentHidden();
}
[Test]
public void TestShowOverlayHidesOtherOverlays()
{
TestScreen screen = null!;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
ModSelectOverlay secondOverlay = new ModSelectOverlay();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
SecondOverlay = secondOverlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new FooterButtonMods(secondOverlay),
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("show mods overlay", () => ScreenFooter.ChildrenOfType<FooterButtonMods>().First().TriggerClick());
AddUntilStep("wait until overlay is shown", () => screen.SecondOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("first button still visible", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First(b => b.Text == "One").Y, () => Is.EqualTo(0));
AddStep("show test overlay", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First(b => b.Text == "One").TriggerClick());
AddUntilStep("wait until overlay is shown", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("mod overlay is hidden", () => screen.SecondOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("hide test overlay", () => screen.Overlay.Hide());
contentHidden();
AddAssert("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
[Test]
public void TestButtonResizedAfterFooterIsDisplayed()
{
TestScreen screen = null!;
const float initial_width = 116;
const float width_increase = 124;
float secondButtonX = 0;
float overlayContentX = 0;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("save second button position", () => secondButtonX = ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).X);
AddStep("resize active button", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width + width_increase, 300, Easing.OutQuint));
AddUntilStep("second button moved", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).X, () => Is.EqualTo(secondButtonX + width_increase).Within(0.001));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width, 300, Easing.OutQuint));
AddUntilStep("second button moved back", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).X, () => Is.EqualTo(secondButtonX).Within(0.001));
AddStep("show overlay", () => screen.Overlay.Show());
contentDisplayed();
AddAssert("other buttons hidden", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("save overlay content position", () => overlayContentX = ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().First().Parent!.Parent!.X);
AddStep("resize active button", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width + width_increase, 300, Easing.OutQuint));
AddUntilStep("overlay content moved", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().First().Parent!.Parent!.X, () => Is.EqualTo(overlayContentX + width_increase).Within(0.001));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width, 300, Easing.OutQuint));
AddUntilStep("overlay content moved back", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().First().Parent!.Parent!.X, () => Is.EqualTo(overlayContentX).Within(0.001));
AddStep("hide overlay", () => screen.Overlay.Hide());
contentHidden();
AddUntilStep("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
private void contentHidden()
{
AddUntilStep("content hidden from footer", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
}
private void contentDisplayed()
{
AddUntilStep("content displayed in footer", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
}
private partial class TestScreen : OsuScreen
{
public override bool ShowFooter { get; }
public Func<IReadOnlyList<ScreenFooterButton>> CreateButtons = Array.Empty<ScreenFooterButton>;
public ShearedOverlayContainer Overlay = new TestShearedOverlayContainer();
public ShearedOverlayContainer SecondOverlay = new TestShearedOverlayContainer();
private IDisposable? overlayRegistration;
private IDisposable? secondOverlayRegistration;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Resolved]
private IOverlayManager? overlayManager { get; set; }
public TestScreen(bool showFooter = true)
{
ShowFooter = showFooter;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(Overlay);
LoadComponent(SecondOverlay);
}
protected override void LoadComplete()
{
base.LoadComplete();
overlayRegistration = overlayManager?.RegisterBlockingOverlay(Overlay);
secondOverlayRegistration = overlayManager?.RegisterBlockingOverlay(SecondOverlay);
}
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => CreateButtons.Invoke();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
overlayRegistration?.Dispose();
secondOverlayRegistration?.Dispose();
}
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
: base(OverlayColourScheme.Orange)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "Test overlay";
Header.Description = "An overlay that is made purely for testing purposes.";
}
public override VisibilityContainer CreateFooterContent() => new TestFooterContent();
public partial class TestFooterContent : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new[]
{
new ShearedButton(200) { Text = "Action #1", Action = () => { } },
new ShearedButton(140) { Text = "Action #2", Action = () => { } },
}
};
}
protected override void PopIn()
{
this.MoveToY(0, 400, Easing.OutQuint)
.FadeIn(400, Easing.OutQuint);
}
protected override void PopOut()
{
this.MoveToY(-20f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
}
}
}
}
}

View File

@@ -296,20 +296,20 @@ namespace osu.Game.Beatmaps.Drawables.Cards
return original;
}
statisticsContainer.Content[0][0] = withMargin(new FavouritesStatistic(BeatmapSet)
{
Current = FavouriteState,
});
statisticsContainer.Content[1][0] = withMargin(new PlayCountStatistic(BeatmapSet));
var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
if (hypesStatistic != null)
statisticsContainer.Content[0][1] = withMargin(hypesStatistic);
statisticsContainer.Content[0][0] = withMargin(hypesStatistic);
var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
if (nominationsStatistic != null)
statisticsContainer.Content[1][1] = withMargin(nominationsStatistic);
statisticsContainer.Content[1][0] = withMargin(nominationsStatistic);
statisticsContainer.Content[0][1] = withMargin(new PlayCountStatistic(BeatmapSet));
statisticsContainer.Content[1][1] = withMargin(new FavouritesStatistic(BeatmapSet)
{
Current = FavouriteState,
});
var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
if (dateStatistic != null)

View File

@@ -278,8 +278,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
if (nominationsStatistic != null)
yield return nominationsStatistic;
yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
yield return new PlayCountStatistic(BeatmapSet);
yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
if (dateStatistic != null)

View File

@@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
this.dateTime = dateTime;
Icon = FontAwesome.Regular.CheckCircle;
Icon = FontAwesome.Solid.CheckCircle;
Text = dateTime.ToLocalisedMediumDate();
}

View File

@@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
public PlayCountStatistic(IBeatmapSetOnlineInfo onlineInfo)
{
Icon = FontAwesome.Regular.PlayCircle;
Icon = FontAwesome.Solid.PlayCircle;
Text = onlineInfo.PlayCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelPlaycount(onlineInfo.PlayCount.ToLocalisableString(@"N0"));
}

View File

@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
using Realms;
namespace osu.Game.Database
@@ -87,6 +88,10 @@ namespace osu.Game.Database
public void AddFile(TModel item, Stream contents, string filename, Realm realm)
{
filename = filename.ToStandardisedPath();
if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filename))
throw new InvalidOperationException($@"Filename ""{filename}"" is not allowed.");
var existing = item.GetFile(filename);
if (existing != null)

View File

@@ -17,6 +17,7 @@ using osu.Game.Extensions;
using osu.Game.IO.Archives;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
using Realms;
namespace osu.Game.Database
@@ -221,7 +222,15 @@ namespace osu.Game.Database
foreach (string piece in realmFile.Filename.Split('/').Select(f => f.GetValidFilename()))
destinationPath = Path.Combine(destinationPath, piece);
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
string destinationDirectory = Path.GetDirectoryName(destinationPath)!;
if (!FilesystemSanityCheckHelpers.IsSubDirectory(parent: mountedPath, child: destinationDirectory))
{
Logger.Log($@"Skipping attempt to mount {realmFile.Filename} due to detected escape out of mounted path.", LoggingTarget.Database);
continue;
}
Directory.CreateDirectory(destinationDirectory);
// Consider using hard links here to make this instant.
using (var inStream = Files.Storage.GetStream(sourcePath))
@@ -361,6 +370,9 @@ namespace osu.Game.Database
// We intentionally delay adding to realm to avoid blocking on a write during disk operations.
foreach (var filenames in getShortenedFilenames(archive))
{
if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filenames.shortened))
throw new InvalidOperationException($@"Filename ""{filenames.original}"" is not allowed.");
using (Stream s = archive.GetStream(filenames.original))
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false, parameters.PreferHardLinks), filenames.shortened));
}

View File

@@ -33,6 +33,9 @@ namespace osu.Game.Graphics.Carousel
/// </summary>
protected partial class ScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler<GlobalAction>
{
public Action? OnPageUp { get; init; }
public Action? OnPageDown { get; init; }
public readonly Container Panels;
public void SetLayoutHeight(float height) => Panels.Height = height;
@@ -127,6 +130,22 @@ namespace osu.Game.Graphics.Carousel
protected override bool IsDragging => base.IsDragging || AbsoluteScrolling;
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.PageUp:
OnPageUp?.Invoke();
return true;
case Key.PageDown:
OnPageDown?.Invoke();
return true;
}
return base.OnKeyDown(e);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
@@ -201,6 +220,8 @@ namespace osu.Game.Graphics.Carousel
private readonly Drawable box;
private bool capturingMouseDown;
protected override float MinimumDimSize => SCROLL_BAR_WIDTH * 3;
private const float expanded_size_ratio = 2;
@@ -261,6 +282,7 @@ namespace osu.Game.Graphics.Carousel
{
if (!base.OnMouseDown(e)) return false;
capturingMouseDown = true;
updateVisuals(e);
return true;
}
@@ -275,13 +297,14 @@ namespace osu.Game.Graphics.Carousel
{
if (e.Button != MouseButton.Left) return;
capturingMouseDown = false;
updateVisuals(e);
base.OnMouseUp(e);
}
private void updateVisuals(MouseEvent e)
{
if (IsDragged || e.PressedButtons.Contains(MouseButton.Left))
if (capturingMouseDown)
box.FadeColour(highlightColour, 100);
else if (IsHovered)
box.FadeColour(hoverColour, 100);

View File

@@ -317,6 +317,8 @@ namespace osu.Game.Graphics.Carousel
{
Masking = false,
RelativeSizeAxes = Axes.Both,
OnPageUp = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Page, -1)),
OnPageDown = () => Scheduler.AddOnce(traverseFromKey, new TraversalOperation(TraversalType.Page, 1)),
};
Items.BindCollectionChanged((_, args) =>
@@ -538,26 +540,30 @@ namespace osu.Game.Graphics.Carousel
}
return false;
}
void traverseFromKey(TraversalOperation traversal)
private void traverseFromKey(TraversalOperation traversal)
{
switch (traversal.Type)
{
switch (traversal.Type)
{
case TraversalType.Keyboard:
traverseKeyboardSelection(traversal.Direction);
break;
case TraversalType.Keyboard:
traverseKeyboardSelection(traversal.Direction);
break;
case TraversalType.Set:
traverseSetSelection(traversal.Direction);
break;
case TraversalType.Page:
traverseKeyboardPage(traversal.Direction);
break;
case TraversalType.Group:
traverseGroupSelection(traversal.Direction);
break;
case TraversalType.Set:
traverseSetSelection(traversal.Direction);
break;
default:
throw new ArgumentOutOfRangeException();
}
case TraversalType.Group:
traverseGroupSelection(traversal.Direction);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
@@ -565,6 +571,7 @@ namespace osu.Game.Graphics.Carousel
{
Keyboard,
Set,
Page,
Group
}
@@ -622,6 +629,59 @@ namespace osu.Game.Graphics.Carousel
} while (newIndex != originalIndex);
}
/// <summary>
/// Performs a page-wise keyboard traversal in the carousel, moving the selection by approximately one "page" of items.
/// </summary>
/// <param name="direction">Positive for downwards, negative for upwards.</param>
private void traverseKeyboardPage(int direction)
{
if (carouselItems == null || carouselItems.Count == 0)
return;
int startIndex = currentKeyboardSelection.Index ?? (direction > 0 ? carouselItems.Count - 1 : 0);
// Compute the number of visible panels to treat as one page.
// Reduced by 50% to account for the search bar covering the top items.
int visiblePanelsCount = Math.Max(1, Scroll.Panels.Count / 2);
int visibleCount = 0;
int i = startIndex;
while (i >= 0 && i < carouselItems.Count)
{
i += direction;
if (i < 0 || i >= carouselItems.Count)
break;
var item = carouselItems[i];
if (!item.IsVisible)
continue;
visibleCount++;
if (visibleCount >= visiblePanelsCount)
{
setKeyboardSelection(item.Model);
ScrollToSelection();
playTraversalSound();
return;
}
}
// If we are at the beginning or end and there are not enough items left to scroll through a complete page, then we go to the last or first item.
var fallback = direction > 0
? carouselItems.LastOrDefault(x => x.IsVisible)
: carouselItems.FirstOrDefault(x => x.IsVisible);
if (fallback != null && !CheckModelEquality(fallback.Model, currentKeyboardSelection.Model))
{
setKeyboardSelection(fallback.Model);
ScrollToSelection();
playTraversalSound();
}
}
/// <summary>
/// Select the next valid group selection relative to a current selection.
/// This is generally for keyboard based traversal.

View File

@@ -157,7 +157,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Current = currentNumberInstantaneous,
OnCommit = () => current.Value = currentNumberInstantaneous.Value,
TooltipFormat = TooltipFormat,
TooltipFormat = s => TooltipFormat(s),
DisplayAsPercentage = DisplayAsPercentage,
PlaySamplesOnAdjust = PlaySamplesOnAdjust,
ResetToDefault = () =>

View File

@@ -236,9 +236,11 @@ Click to see what's new!", version);
public static LocalisableString ElevatedPrivileges(LocalisableString user) => new TranslatableString(getKey(@"elevated_privileges"), @"Running osu! as {0} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.", user);
/// <summary>
/// "Screenshot {0} saved!"
/// "Screenshot saved! Click to view.
/// {0}"
/// </summary>
public static LocalisableString ScreenshotSaved(string filename) => new TranslatableString(getKey(@"screenshot_saved"), @"Screenshot {0} saved!", filename);
public static LocalisableString ScreenshotSaved(string filename) => new TranslatableString(getKey(@"screenshot_saved"), @"Screenshot saved! Click to view.
{0}", filename);
/// <summary>
/// "The multiplayer server will be right back..."

View File

@@ -34,6 +34,7 @@ using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using CommonStrings = osu.Game.Localisation.CommonStrings;
using SongSelect = osu.Game.Screens.SelectV2.SongSelect;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Online.Leaderboards

View File

@@ -23,8 +23,19 @@ namespace osu.Game.Online.Matchmaking
/// <see cref="IMatchmakingServer.MatchmakingDeclineInvitation">declined</see>,
/// or ignored - in which case it will automatically be declined after a short timeout period.
/// </summary>
/// <remarks>
/// Provided for compatibility with older clients - can be removed 20260825.
/// </remarks>
Task MatchmakingRoomInvited();
/// <summary>
/// Signals that a match has been found and the local user is invited to it.
/// The invitation may be <see cref="IMatchmakingServer.MatchmakingAcceptInvitation">accepted</see>,
/// <see cref="IMatchmakingServer.MatchmakingDeclineInvitation">declined</see>,
/// or ignored - in which case it will automatically be declined after a short timeout period.
/// </summary>
Task MatchmakingRoomInvitedWithParams(MatchmakingRoomInvitationParams invitation);
/// <summary>
/// Signals that the matchmaking room is ready to be opened.
/// </summary>

View File

@@ -0,0 +1,16 @@
// 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.
using System;
using MessagePack;
namespace osu.Game.Online.Matchmaking
{
[MessagePackObject]
[Serializable]
public class MatchmakingRoomInvitationParams
{
[Key(0)]
public MatchmakingPoolType Type { get; set; }
}
}

View File

@@ -123,7 +123,7 @@ namespace osu.Game.Online.Multiplayer
public event Action? MatchmakingQueueJoined;
public event Action? MatchmakingQueueLeft;
public event Action? MatchmakingRoomInvited;
public event Action<MatchmakingRoomInvitationParams>? MatchmakingRoomInvited;
public event Action<long, string>? MatchmakingRoomReady;
public event Action<MatchmakingLobbyStatus>? MatchmakingLobbyStatusChanged;
public event Action<MatchmakingQueueStatus>? MatchmakingQueueStatusChanged;
@@ -1085,7 +1085,13 @@ namespace osu.Game.Online.Multiplayer
Task IMatchmakingClient.MatchmakingRoomInvited()
{
Scheduler.Add(() => MatchmakingRoomInvited?.Invoke());
// Compatibility with older servers.
return ((IMatchmakingClient)this).MatchmakingRoomInvitedWithParams(new MatchmakingRoomInvitationParams { Type = MatchmakingPoolType.QuickPlay });
}
Task IMatchmakingClient.MatchmakingRoomInvitedWithParams(MatchmakingRoomInvitationParams invitation)
{
Scheduler.Add(() => MatchmakingRoomInvited?.Invoke(invitation));
return Task.CompletedTask;
}

View File

@@ -76,6 +76,7 @@ namespace osu.Game.Online.Multiplayer
connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined);
connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft);
connection.On(nameof(IMatchmakingClient.MatchmakingRoomInvited), ((IMatchmakingClient)this).MatchmakingRoomInvited);
connection.On<MatchmakingRoomInvitationParams>(nameof(IMatchmakingClient.MatchmakingRoomInvitedWithParams), ((IMatchmakingClient)this).MatchmakingRoomInvitedWithParams);
connection.On<long, string>(nameof(IMatchmakingClient.MatchmakingRoomReady), ((IMatchmakingClient)this).MatchmakingRoomReady);
connection.On<MatchmakingLobbyStatus>(nameof(IMatchmakingClient.MatchmakingLobbyStatusChanged), ((IMatchmakingClient)this).MatchmakingLobbyStatusChanged);
connection.On<MatchmakingQueueStatus>(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged);

View File

@@ -71,8 +71,8 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Seasonal;
using osu.Game.Skinning;
using osu.Game.Updater;
@@ -774,7 +774,7 @@ namespace osu.Game
}
}, validScreens: new[]
{
typeof(SongSelect), typeof(Screens.SelectV2.SongSelect), typeof(IHandlePresentBeatmap)
typeof(SongSelect), typeof(IHandlePresentBeatmap)
});
}
@@ -877,7 +877,7 @@ namespace osu.Game
// which may not match the score, and thus crash.
IEnumerable<Type> validScreens =
Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
? new[] { typeof(SongSelect), typeof(Screens.SelectV2.SongSelect), typeof(DailyChallenge) }
? new[] { typeof(SongSelect), typeof(DailyChallenge) }
: Array.Empty<Type>();
PerformFromScreen(screen =>
@@ -1156,6 +1156,7 @@ namespace osu.Game
},
new PopoverContainer
{
// Ensure the footer is displayed above any content and/or overlays.
Depth = -1,
RelativeSizeAxes = Axes.Both,
Child = screenStackFooter = new ScreenStackFooter(ScreenStack, backReceptor)

View File

@@ -9,10 +9,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Extensions;
using osu.Game.Graphics;
@@ -32,7 +34,7 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_spacing = 2;
private readonly LinkFlowContainer infoContainer;
private readonly Statistic plays, favourites;
private readonly Statistic nominations, plays, favourites;
public readonly DifficultiesContainer Difficulties;
@@ -107,7 +109,14 @@ namespace osu.Game.Overlays.BeatmapSet
Margin = new MarginPadding { Top = 5 },
Children = new[]
{
plays = new Statistic(FontAwesome.Solid.PlayCircle),
nominations = new Statistic(FontAwesome.Solid.ThumbsUp)
{
TooltipText = BeatmapsetsStrings.ShowStatsNominations,
},
plays = new Statistic(FontAwesome.Solid.PlayCircle)
{
TooltipText = BeatmapsetsStrings.ShowStatsPlaycount,
},
favourites = new Statistic(FontAwesome.Solid.Heart),
},
},
@@ -176,8 +185,17 @@ namespace osu.Game.Overlays.BeatmapSet
// Else just choose the first available difficulty for now.
Beatmap.Value ??= Difficulties.FirstOrDefault()?.Beatmap;
if (beatmapSet?.Status == BeatmapOnlineStatus.Pending && beatmapSet.NominationStatus != null)
{
nominations.Show();
nominations.Value = beatmapSet.NominationStatus.Current;
}
else
nominations.Hide();
plays.Value = BeatmapSet?.PlayCount ?? 0;
favourites.Value = BeatmapSet?.FavouriteCount ?? 0;
favourites.TooltipText = BeatmapSet?.FavouriteCount > 0 ? BeatmapsetsStrings.ShowStatsFavourites : BeatmapsetsStrings.ShowStatsNoFavourites;
updateDifficultyButtons();
}
@@ -367,7 +385,7 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
private partial class Statistic : FillFlowContainer
private partial class Statistic : FillFlowContainer, IHasTooltip
{
private readonly OsuSpriteText text;
@@ -407,6 +425,8 @@ namespace osu.Game.Overlays.BeatmapSet
},
};
}
public LocalisableString TooltipText { get; set; }
}
public enum DifficultySelectorState

View File

@@ -14,7 +14,6 @@ using osu.Game.Localisation;
using osu.Game.Screens;
using osu.Game.Screens.SelectV2;
using osuTK;
using SongSelect = osu.Game.Screens.Select.SongSelect;
namespace osu.Game.Overlays.SkinEditor
{

View File

@@ -30,7 +30,7 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Screens.Edit.Submission

View File

@@ -1,146 +1,151 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreeMods : FooterButton
public partial class FooterButtonFreeMods : ScreenFooterButton
{
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>();
public readonly IBindable<bool> Freestyle = new Bindable<bool>();
protected override bool IsActive => FreeMods.Value.Count > 0;
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>([]);
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
public new Action Action
{
set => throw new NotSupportedException("The click action is handled by the button itself.");
}
private OsuSpriteText count = null!;
private Circle circle = null!;
private readonly FreeModSelectOverlay freeModSelectOverlay;
public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay)
{
this.freeModSelectOverlay = freeModSelectOverlay;
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = toggleAllFreeMods;
}
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Container modsWedge = null!;
private ModDisplay modDisplay = null!;
private Container modContainer = null!;
private FooterButtonMods.ModCountText overflowModCountDisplay = null!;
public FooterButtonFreeMods(ModSelectOverlay overlay)
: base(overlay)
{
}
[BackgroundDependencyLoader]
private void load()
{
ButtonContentContainer.AddRange(new[]
Text = OnlinePlayStrings.FooterButtonFreemods;
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
Add(modsWedge = new InputBlockingContainer
{
new Container
Y = -5f,
Depth = float.MaxValue,
Origin = Anchor.BottomLeft,
Shear = OsuGame.SHEAR,
CornerRadius = CORNER_RADIUS,
Size = new Vector2(BUTTON_WIDTH, FooterButtonMods.BAR_HEIGHT),
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
},
count = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5),
UseFullGlyphHeight = false,
}
}
Type = EdgeEffectType.Shadow,
Radius = 4,
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
},
new IconButton
Alpha = 0,
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
Icon = FontAwesome.Solid.Bars,
Enabled = { BindTarget = Enabled },
Action = () => freeModSelectOverlay.ToggleVisibility()
new Box
{
Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both,
},
modContainer = new Container
{
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
modDisplay = new ModDisplay(showExtendedInformation: true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -OsuGame.SHEAR,
Scale = new Vector2(0.5f),
Current = { BindTarget = FreeMods },
ExpansionMode = ExpansionMode.AlwaysContracted,
},
overflowModCountDisplay = new FooterButtonMods.ModCountText
{
Mods = { BindTarget = FreeMods },
},
}
},
}
});
SelectedColour = colours.Yellow;
DeselectedColour = SelectedColour.Opacity(0.5f);
Text = @"freemods";
TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip;
}
protected override void LoadComplete()
{
base.LoadComplete();
Freestyle.BindValueChanged(_ => updateModDisplay());
FreeMods.BindValueChanged(_ => updateModDisplay(), true);
Freestyle.BindValueChanged(f =>
{
Enabled.Value = !f.NewValue;
overflowModCountDisplay.CustomText = f.NewValue ? ModSelectOverlayStrings.AllMods.ToUpper() : (LocalisableString?)null;
}, true);
FreeMods.BindValueChanged(m =>
{
if (m.NewValue.Count == 0 && !Freestyle.Value)
modsWedge.FadeOut(300, Easing.OutExpo);
else
modsWedge.FadeIn(300, Easing.OutExpo);
}, true);
}
/// <summary>
/// Immediately toggle all free mods on/off.
/// </summary>
private void toggleAllFreeMods()
protected override void Update()
{
var availableMods = allAvailableAndValidMods.ToArray();
base.Update();
FreeMods.Value = FreeMods.Value.Count == availableMods.Length
? Array.Empty<Mod>()
: availableMods;
}
// If there are freemods selected but the display has no width, it's still loading.
// Don't update visibility in this state or we will cause an awkward flash.
if (FreeMods.Value.Count > 0 && Precision.AlmostEquals(modDisplay.DrawWidth, 0))
return;
private void updateModDisplay()
{
int currentCount = FreeMods.Value.Count;
bool showCountText =
// When freestyle is enabled this text shows "ALL MODS"
Freestyle.Value
// Standard flow where mods are overflowing so we show count text.
|| modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth;
if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value)
{
count.Text = "all";
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
circle.FadeColour(colours.Yellow, 200, Easing.OutQuint);
}
else if (currentCount > 0)
{
count.Text = $"{currentCount} mods";
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint);
}
if (showCountText)
overflowModCountDisplay.Show();
else
{
count.Text = "off";
count.FadeColour(colours.GrayF, 200, Easing.OutQuint);
circle.FadeColour(colours.Gray4, 200, Easing.OutQuint);
}
overflowModCountDisplay.Hide();
}
private IEnumerable<Mod> allAvailableAndValidMods => freeModSelectOverlay.AllAvailableMods
.Where(state => state.ValidForSelection.Value)
.Select(state => state.Mod);
}
}

View File

@@ -1,151 +0,0 @@
// 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.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreeModsV2 : ScreenFooterButton
{
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>([]);
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
public new Action Action
{
set => throw new NotSupportedException("The click action is handled by the button itself.");
}
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Container modsWedge = null!;
private ModDisplay modDisplay = null!;
private Container modContainer = null!;
private FooterButtonMods.ModCountText overflowModCountDisplay = null!;
public FooterButtonFreeModsV2(ModSelectOverlay overlay)
: base(overlay)
{
}
[BackgroundDependencyLoader]
private void load()
{
Text = OnlinePlayStrings.FooterButtonFreemods;
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
Add(modsWedge = new InputBlockingContainer
{
Y = -5f,
Depth = float.MaxValue,
Origin = Anchor.BottomLeft,
Shear = OsuGame.SHEAR,
CornerRadius = CORNER_RADIUS,
Size = new Vector2(BUTTON_WIDTH, FooterButtonMods.BAR_HEIGHT),
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 4,
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
},
Alpha = 0,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both,
},
modContainer = new Container
{
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
modDisplay = new ModDisplay(showExtendedInformation: true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -OsuGame.SHEAR,
Scale = new Vector2(0.5f),
Current = { BindTarget = FreeMods },
ExpansionMode = ExpansionMode.AlwaysContracted,
},
overflowModCountDisplay = new FooterButtonMods.ModCountText
{
Mods = { BindTarget = FreeMods },
},
}
},
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Freestyle.BindValueChanged(f =>
{
Enabled.Value = !f.NewValue;
overflowModCountDisplay.CustomText = f.NewValue ? ModSelectOverlayStrings.AllMods.ToUpper() : (LocalisableString?)null;
}, true);
FreeMods.BindValueChanged(m =>
{
if (m.NewValue.Count == 0 && !Freestyle.Value)
modsWedge.FadeOut(300, Easing.OutExpo);
else
modsWedge.FadeIn(300, Easing.OutExpo);
}, true);
}
protected override void Update()
{
base.Update();
// If there are freemods selected but the display has no width, it's still loading.
// Don't update visibility in this state or we will cause an awkward flash.
if (FreeMods.Value.Count > 0 && Precision.AlmostEquals(modDisplay.DrawWidth, 0))
return;
bool showCountText =
// When freestyle is enabled this text shows "ALL MODS"
Freestyle.Value
// Standard flow where mods are overflowing so we show count text.
|| modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth;
if (showCountText)
overflowModCountDisplay.Show();
else
overflowModCountDisplay.Hide();
}
}
}

View File

@@ -4,31 +4,23 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Screens.Select;
using osu.Game.Screens.Footer;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreestyle : FooterButton
public partial class FooterButtonFreestyle : ScreenFooterButton
{
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
protected override bool IsActive => Freestyle.Value;
public new Action Action
{
set => throw new NotSupportedException("The click action is handled by the button itself.");
}
private OsuSpriteText text = null!;
private Circle circle = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
@@ -41,61 +33,19 @@ namespace osu.Game.Screens.OnlinePlay
[BackgroundDependencyLoader]
private void load()
{
ButtonContentContainer.AddRange(new[]
{
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5),
UseFullGlyphHeight = false,
}
}
}
});
SelectedColour = colours.Yellow;
DeselectedColour = SelectedColour.Opacity(0.5f);
Text = @"freestyle";
TooltipText = MultiplayerMatchStrings.FreestyleButtonTooltip;
Text = OnlinePlayStrings.FooterButtonFreestyle;
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
}
protected override void LoadComplete()
{
base.LoadComplete();
Freestyle.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
if (Freestyle.Value)
Freestyle.BindValueChanged(active =>
{
text.Text = "on";
text.FadeColour(colours.Gray2, 200, Easing.OutQuint);
circle.FadeColour(colours.Yellow, 200, Easing.OutQuint);
}
else
{
text.Text = "off";
text.FadeColour(colours.GrayF, 200, Easing.OutQuint);
circle.FadeColour(colours.Gray4, 200, Easing.OutQuint);
}
OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
}, true);
}
}
}

View File

@@ -1,51 +0,0 @@
// 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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Screens.Footer;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreestyleV2 : ScreenFooterButton
{
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
public new Action Action
{
set => throw new NotSupportedException("The click action is handled by the button itself.");
}
[Resolved]
private OsuColour colours { get; set; } = null!;
public FooterButtonFreestyleV2()
{
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = () => Freestyle.Value = !Freestyle.Value;
}
[BackgroundDependencyLoader]
private void load()
{
Text = OnlinePlayStrings.FooterButtonFreestyle;
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
}
protected override void LoadComplete()
{
base.LoadComplete();
Freestyle.BindValueChanged(active =>
{
OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
}, true);
}
}
}

View File

@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Online.Matchmaking;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -94,15 +95,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
closeNotifications();
});
private void onMatchmakingRoomInvited() => Scheduler.Add(() =>
private void onMatchmakingRoomInvited(MatchmakingRoomInvitationParams invitation) => Scheduler.Add(() =>
{
CurrentState.Value = ScreenQueue.MatchmakingScreenState.PendingAccept;
if (backgroundNotification != null)
{
backgroundNotification.State = ProgressNotificationState.Completed;
backgroundNotification = null;
}
backgroundNotification?.Complete(invitation);
backgroundNotification = null;
});
private void onMatchmakingRoomReady(long roomId, string password) => Scheduler.Add(() =>
@@ -183,6 +181,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
return false;
};
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
return true;
};
matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found");
}
public void Complete(MatchmakingRoomInvitationParams invitation)
{
CompletionClickAction = () =>
{
client.MatchmakingAcceptInvitation().FireAndForget();
@@ -194,13 +203,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
return true;
};
CancelRequested = () =>
{
client.MatchmakingLeaveQueue().FireAndForget();
return true;
};
matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found");
State = ProgressNotificationState.Completed;
}
protected override Notification CreateCompletionNotification()

View File

@@ -23,9 +23,10 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Screens.Footer;
using osu.Game.Users;
using osu.Game.Utils;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
@@ -35,18 +36,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override string Title => ShortTitle.Humanize();
public override bool AllowEditing => false;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private OngoingOperationTracker operationTracker { get; set; } = null!;
[Resolved]
private IOverlayManager? overlayManager { get; set; }
private readonly Room room;
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private readonly PlaylistItem? itemToEdit;
private ModSelectOverlay modSelect = null!;
private LoadingLayer loadingLayer = null!;
private IDisposable? selectionOperation;
@@ -64,7 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private readonly PlaylistItem? initialItem;
private readonly FreeModSelectOverlay freeModSelect;
private FooterButton freeModsFooterButton = null!;
private IDisposable? freeModSelectOverlayRegistration;
@@ -80,6 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
initialItem = itemToEdit ?? room.Playlist.LastOrDefault();
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
LeftPadding = new MarginPadding { Top = CORNER_RADIUS_HIDE_OFFSET + Header.HEIGHT };
freeModSelect = new FreeModSelectOverlay
{
@@ -91,10 +94,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[BackgroundDependencyLoader]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
LoadComponent(freeModSelect);
AddInternal(loadingLayer = new LoadingLayer(true));
AddInternal(loadingLayer = new LoadingLayer(true)
{
BlockNonPositionalInput = true,
});
}
protected override void LoadComplete()
@@ -138,7 +142,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Ruleset.BindValueChanged(onRulesetChanged);
freestyle.BindValueChanged(onFreestyleChanged);
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect);
freeModSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(freeModSelect);
updateFooterButtons();
updateValidMods();
@@ -153,8 +157,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}, true);
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
private void onFreestyleChanged(ValueChangedEvent<bool> enabled)
{
updateFooterButtons();
@@ -184,12 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void updateFooterButtons()
{
if (freestyle.Value)
{
freeModsFooterButton.Enabled.Value = false;
freeModSelect.Hide();
}
else
freeModsFooterButton.Enabled.Value = true;
}
/// <summary>
@@ -206,11 +203,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!validFreeMods.SequenceEqual(FreeMods.Value))
FreeMods.Value = validFreeMods;
ModSelect.IsValidMod = isValidRequiredMod;
modSelect.IsValidMod = isValidRequiredMod;
freeModSelect.IsValidMod = isValidAllowedMod;
}
protected sealed override bool OnStart()
protected sealed override void OnStart()
{
var item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
@@ -220,7 +217,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Freestyle = freestyle.Value
};
return selectItem(item);
selectItem(item);
}
private bool selectItem(PlaylistItem item)
@@ -263,11 +260,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}, onError: _ =>
{
selectionOperation.Dispose();
Schedule(() =>
{
Carousel.AllowSelection = true;
});
});
}
else
@@ -296,31 +288,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return base.OnExiting(e);
}
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = isValidRequiredMod
};
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons()
{
var baseButtons = base.CreateSongSelectFooterButtons().ToList();
var buttons = base.CreateFooterButtons().ToList();
baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip;
buttons.Single(i => i is FooterButtonMods).TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip;
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
{
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect)
buttons.InsertRange(buttons.FindIndex(b => b is FooterButtonMods) + 1,
[
new FooterButtonFreeMods(freeModSelect)
{
FreeMods = { BindTarget = FreeMods },
Freestyle = { BindTarget = freestyle }
}, null),
(new FooterButtonFreestyle
},
new FooterButtonFreestyle
{
Freestyle = { BindTarget = freestyle }
}, null)
});
}
]);
return baseButtons;
return buttons;
}
/// <summary>

View File

@@ -211,12 +211,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
buttons.InsertRange(buttons.FindIndex(b => b is FooterButtonMods) + 1,
[
new FooterButtonFreeModsV2(freeModSelect)
new FooterButtonFreeMods(freeModSelect)
{
FreeMods = { BindTarget = freeMods },
Freestyle = { BindTarget = Freestyle }
},
new FooterButtonFreestyleV2
new FooterButtonFreestyle
{
Freestyle = { BindTarget = Freestyle }
}

View File

@@ -578,7 +578,7 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
/// <param name="beatmap">The beatmap which should be selected. If not provided, the current globally selected beatmap will be used.</param>
/// <param name="startAction">The action to perform if conditions are met to be able to proceed. May not be invoked if in an invalid state.</param>
public void SelectAndRun(BeatmapInfo beatmap, Action startAction)
protected void SelectAndRun(BeatmapInfo beatmap, Action startAction)
{
if (!this.IsCurrentScreen())
return;
@@ -773,11 +773,20 @@ namespace osu.Game.Screens.SelectV2
beginLooping();
}
Beatmap.BindValueChanged(updateVariousState, true);
}
private void updateVariousState(ValueChangedEvent<WorkingBeatmap> e)
{
if (!this.IsCurrentScreen())
return;
ensureGlobalBeatmapValid();
ensurePlayingSelected();
updateBackgroundDim();
fetchOnlineInfo(force: true);
updateWedgeVisibility();
fetchOnlineInfo(force: ReferenceEquals(e.OldValue, e.NewValue));
}
private void onLeavingScreen()
@@ -787,6 +796,8 @@ namespace osu.Game.Screens.SelectV2
if (manageCollectionsDialog?.FilteredBeatmapsProvider == getFilteredBeatmaps)
manageCollectionsDialog.FilteredBeatmapsProvider = null;
Beatmap.ValueChanged -= updateVariousState;
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
modSelectOverlay.Beatmap.UnbindFrom(Beatmap);

View File

@@ -165,11 +165,11 @@ namespace osu.Game.Skinning
var userSkins = realm.All<SkinInfo>()
.Where(s => !s.DeletePending && !s.Protected)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.AsEnumerable()
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.Select(s => s.ToLive(Realm));
foreach (var s in userSkins.Where(s => !s.Value.Protected))
foreach (var s in userSkins)
skins.Add(s);
});

View File

@@ -43,28 +43,29 @@ namespace osu.Game.Tests.Visual
base.Content.AddRange(new Drawable[]
{
backReceptor = new ScreenFooter.BackReceptor(),
Stack = new OsuScreenStack
{
Name = nameof(ScreenTestScene),
RelativeSizeAxes = Axes.Both
},
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Stack = new OsuScreenStack
{
Name = nameof(ScreenTestScene),
RelativeSizeAxes = Axes.Both
},
// TODO: is this ever used? it probably shouldn't be.
content = new Container { RelativeSizeAxes = Axes.Both },
overlayContent = new Container
{
RelativeSizeAxes = Axes.Both,
Child = DialogOverlay = new DialogOverlay()
},
screenStackFooter = new ScreenStackFooter(Stack, backReceptor)
{
BackButtonPressed = () => Stack.Exit()
BackButtonPressed = BackButtonPressed,
}
}
},
overlayContent = new Container
{
RelativeSizeAxes = Axes.Both,
Child = DialogOverlay = new DialogOverlay()
},
});
ScreenFooter = screenStackFooter.Footer;
@@ -73,6 +74,8 @@ namespace osu.Game.Tests.Visual
Stack.ScreenExited += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}");
}
protected virtual void BackButtonPressed() => Stack.Exit();
protected void LoadScreen(OsuScreen screen) => Stack.Push(screen);
[SetUpSteps]

View File

@@ -0,0 +1,37 @@
// 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.
using System;
using System.IO;
namespace osu.Game.Utils
{
public static class FilesystemSanityCheckHelpers
{
/// <summary>
/// Returns whether <paramref name="path"/> is potentially susceptible to path traversal style attacks.
/// </summary>
public static bool IncursPathTraversalRisk(string path)
=> path.Contains("../", StringComparison.Ordinal) || path.Contains("..\\", StringComparison.Ordinal) || Path.IsPathRooted(path);
/// <summary>
/// Returns whether <paramref name="child"/> is a subdirectory (direct or nested) of <paramref name="parent"/>.
/// </summary>
public static bool IsSubDirectory(string parent, string child)
{
// `Path.GetFullPath()` invocations are required to fully resolve the paths to unambiguous downwards-traversal-only paths.
var parentInfo = new DirectoryInfo(Path.GetFullPath(parent));
var childInfo = new DirectoryInfo(Path.GetFullPath(child));
while (childInfo != null)
{
if (parentInfo.FullName == childInfo.FullName)
return true;
childInfo = childInfo.Parent;
}
return false;
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project">
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>

View File

@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.209.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.303.0" />
</ItemGroup>
</Project>