1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-22 20:12:56 +08:00

Merge branch 'master' into bss/the-actual-submission

This commit is contained in:
Dean Herbert 2025-02-07 17:44:08 +09:00
commit 6335228fb0
No known key found for this signature in database
58 changed files with 839 additions and 365 deletions

View File

@ -173,7 +173,7 @@ namespace osu.Desktop
new Button
{
Label = "View beatmap",
Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
}
};
}

View File

@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
[Solo]
public void TestCommitPlacementViaRightClick()
{
Playfield playfield = null!;

View File

@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -234,6 +235,31 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestNoSubmissionWhenScoreZero()
{
prepareTestAPI(true);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
AddStep("add fake non-scoring hit", () =>
{
Player.ScoreProcessor.RevertResult(Player.Results.First());
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new IgnoreJudgement())
{
Type = HitResult.IgnoreHit,
});
});
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestSubmissionOnExit()
{

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus
new APIMenuImage
{
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023",
Url = $@"{API.Endpoints.WebsiteUrl}/home/news/2023-12-21-project-loved-december-2023",
}
}
});

View File

@ -165,7 +165,6 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
[Solo]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{
prepareBeatmap();

View File

@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestLink()
{
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/");
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/");
AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Main_page");
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/FAQ");
AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Writing");
AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Formatting");
}
[Test]

View File

@ -62,12 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
});
AddToggleStep("toggle legacy classic skin", v =>
{
if (skins != null)
skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default;
});
}
[SetUp]
@ -84,6 +78,16 @@ namespace osu.Game.Tests.Visual.Ranking
}));
}
[Test]
public void TestLegacySkin()
{
AddToggleStep("toggle legacy classic skin", v =>
{
if (skins != null)
skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default;
});
}
private int onlineScoreID = 1;
[TestCase(1, ScoreRank.X, 0)]

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;
@ -16,10 +18,10 @@ using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
@ -53,16 +55,6 @@ namespace osu.Game.Tests.Visual.SongSelect
Scheduler.AddDelayed(updateStats, 100, true);
}
[SetUpSteps]
public virtual void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Sort = SortMode.Title });
}
protected void CreateCarousel()
{
AddStep("create components", () =>
@ -146,6 +138,9 @@ namespace osu.Game.Tests.Visual.SongSelect
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
protected void WaitForGroupSelection(int group, int panel)
{
AddUntilStep($"selected is group{group} panel{panel}", () =>
@ -171,6 +166,15 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
protected IEnumerable<T> GetVisiblePanels<T>()
where T : Drawable
{
return Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y);
}
protected void ClickVisiblePanel<T>(int index)
where T : Drawable
{
@ -185,17 +189,63 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
protected void ClickVisiblePanelWithOffset<T>(int index, Vector2 positionOffsetFromCentre)
where T : Drawable
{
AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () =>
{
var panel = Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y)
.ElementAt(index);
InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
}
/// <summary>
/// Add requested beatmap sets count to list.
/// </summary>
/// <param name="count">The count of beatmap sets to add.</param>
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () =>
/// <param name="randomMetadata">Whether to randomise the metadata to make groupings more uniform.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () =>
{
for (int i = 0; i < count; i++)
BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)));
{
var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4));
if (randomMetadata)
{
char randomCharacter = getRandomCharacter();
var metadata = new BeatmapMetadata
{
// Create random metadata, then we can check if sorting works based on these
Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9),
Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}",
Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) },
};
foreach (var beatmap in beatmapSetInfo.Beatmaps)
beatmap.Metadata = metadata.DeepClone();
}
BeatmapSets.Add(beatmapSetInfo);
}
});
private static long randomCharPointer;
private static char getRandomCharacter()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*";
return chars[(int)((randomCharPointer++ / 2) % chars.Length)];
}
protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());
protected void RemoveFirstBeatmap() =>

View File

@ -2,100 +2,65 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
{
/// <summary>
/// Currently covers adding and removing of items and scrolling.
/// If we add more tests here, these two categories can likely be split out into separate scenes.
/// Covers common steps which can be used for manual testing.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene
{
[Test]
[Explicit]
public void TestBasics()
{
AddBeatmaps(1);
CreateCarousel();
RemoveAllBeatmaps();
AddBeatmaps(10, randomMetadata: true);
AddBeatmaps(10);
AddBeatmaps(1);
}
[Test]
[Explicit]
public void TestSorting()
{
SortBy(new FilterCriteria { Sort = SortMode.Artist });
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
}
[Test]
[Explicit]
public void TestRemovals()
{
RemoveFirstBeatmap();
RemoveAllBeatmaps();
}
[Test]
public void TestOffScreenLoading()
{
AddStep("disable masking", () => Scroll.Masking = false);
AddStep("enable masking", () => Scroll.Masking = true);
}
[Test]
public void TestAddRemoveOneByOne()
[Explicit]
public void TestAddRemoveRepeatedOps()
{
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
}
[Test]
public void TestSorting()
[Explicit]
public void TestMasking()
{
AddBeatmaps(10);
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Sort = SortMode.Artist });
}
[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
{
Quad positionBefore = default;
AddBeatmaps(10);
WaitForDrawablePanels();
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2));
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
{
Quad positionBefore = default;
AddBeatmaps(10);
WaitForDrawablePanels();
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last());
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
AddStep("disable masking", () => Scroll.Masking = false);
AddStep("enable masking", () => Scroll.Masking = true);
}
[Test]

View File

@ -0,0 +1,177 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
AddBeatmaps(10, 3, true);
WaitForDrawablePanels();
}
[Test]
public void TestOpenCloseGroupWithNoSelectionMouse()
{
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
}
[Test]
public void TestOpenCloseGroupWithNoSelectionKeyboard()
{
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
SelectNextPanel();
Select();
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
CheckNoSelection();
Select();
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
CheckNoSelection();
}
[Test]
public void TestCarouselRemembersSelection()
{
SelectNextGroup();
object? selection = null;
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
AddBeatmaps(10);
WaitForDrawablePanels();
CheckHasSelection();
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}
[Test]
public void TestGroupSelectionOnHeader()
{
SelectNextGroup();
WaitForGroupSelection(0, 1);
SelectPrevPanel();
SelectPrevPanel();
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
SelectPrevGroup();
WaitForGroupSelection(0, 1);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
SelectPrevGroup();
WaitForGroupSelection(0, 1);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
}
[Test]
public void TestKeyboardSelection()
{
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
CheckNoSelection();
// open first group
Select();
CheckNoSelection();
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
SelectNextPanel();
Select();
WaitForGroupSelection(3, 1);
SelectNextGroup();
WaitForGroupSelection(3, 5);
SelectNextGroup();
WaitForGroupSelection(4, 1);
SelectPrevGroup();
WaitForGroupSelection(3, 5);
SelectNextGroup();
WaitForGroupSelection(4, 1);
SelectNextGroup();
WaitForGroupSelection(4, 5);
SelectNextGroup();
WaitForGroupSelection(0, 1);
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextGroup();
WaitForGroupSelection(0, 1);
SelectNextPanel();
SelectNextGroup();
WaitForGroupSelection(1, 1);
}
}
}

View File

@ -8,27 +8,27 @@ using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene
{
public override void SetUpSteps()
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
AddBeatmaps(10, 3);
WaitForDrawablePanels();
}
[Test]
public void TestOpenCloseGroupWithNoSelectionMouse()
{
AddBeatmaps(10, 5);
WaitForDrawablePanels();
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
@ -44,86 +44,79 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestOpenCloseGroupWithNoSelectionKeyboard()
{
AddBeatmaps(10, 5);
WaitForDrawablePanels();
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
SelectNextPanel();
Select();
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
CheckNoSelection();
Select();
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
CheckNoSelection();
GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
}
[Test]
public void TestCarouselRemembersSelection()
{
AddBeatmaps(10);
WaitForDrawablePanels();
SelectNextGroup();
object? selection = null;
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
AddBeatmaps(10);
WaitForDrawablePanels();
CheckHasSelection();
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null);
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}
[Test]
public void TestGroupSelectionOnHeader()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
SelectNextGroup();
WaitForGroupSelection(0, 0);
SelectPrevPanel();
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
SelectPrevGroup();
WaitForGroupSelection(2, 9);
WaitForGroupSelection(0, 0);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
SelectPrevGroup();
WaitForGroupSelection(0, 0);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
}
[Test]
public void TestKeyboardSelection()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
@ -154,5 +147,28 @@ namespace osu.Game.Tests.Visual.SongSelect
SelectPrevGroup();
WaitForGroupSelection(2, 9);
}
[Test]
public void TestInputHandlingWithinGaps()
{
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
// Clicks just above the first group panel should not actuate any action.
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1)));
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2)));
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
CheckNoSelection();
// Beatmap panels expand their selection area to cover holes from spacing.
ClickVisiblePanelWithOffset<BeatmapPanel>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForGroupSelection(0, 0);
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForGroupSelection(0, 1);
}
}
}

View File

@ -5,14 +5,25 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Sort = SortMode.Title });
}
/// <summary>
/// Keyboard selection via up and down arrows doesn't actually change the selection until
/// the select key is pressed.
@ -77,28 +88,26 @@ namespace osu.Game.Tests.Visual.SongSelect
object? selection = null;
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
AddBeatmaps(10);
WaitForDrawablePanels();
CheckHasSelection();
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}
[Test]
@ -141,7 +150,11 @@ namespace osu.Game.Tests.Visual.SongSelect
SelectPrevPanel();
SelectPrevGroup();
WaitForSelection(0, 0);
WaitForSelection(1, 0);
SelectPrevPanel();
SelectNextGroup();
WaitForSelection(1, 0);
}
[Test]
@ -194,6 +207,36 @@ namespace osu.Game.Tests.Visual.SongSelect
CheckNoSelection();
}
[Test]
public void TestInputHandlingWithinGaps()
{
AddBeatmaps(2, 5);
WaitForDrawablePanels();
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
// Clicks just above the first group panel should not actuate any action.
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1)));
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2)));
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
WaitForSelection(0, 0);
// Beatmap panels expand their selection area to cover holes from spacing.
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForSelection(0, 0);
// Panels with higher depth will handle clicks in the gutters for simplicity.
ClickVisiblePanelWithOffset<BeatmapPanel>(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForSelection(0, 2);
ClickVisiblePanelWithOffset<BeatmapPanel>(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForSelection(0, 3);
}
private void checkSelectionIterating(bool isIterating)
{
object? selection = null;

View File

@ -0,0 +1,65 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria());
AddBeatmaps(10);
WaitForDrawablePanels();
}
[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
{
Quad positionBefore = default;
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
{
Quad positionBefore = default;
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last());
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
}
}

View File

@ -1239,7 +1239,6 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[Solo]
public void TestHardDeleteHandledCorrectly()
{
createSongSelect();

View File

@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null)
return null;
return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
}
}
}

View File

@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps
return null;
if (ruleset != null)
return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
}
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Online.API
private readonly Queue<APIRequest> queue = new Queue<APIRequest>();
public EndpointConfiguration EndpointConfiguration { get; }
public EndpointConfiguration Endpoints { get; }
/// <summary>
/// The API response version.
@ -73,7 +73,7 @@ namespace osu.Game.Online.API
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly Logger log;
public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpoints, string versionHash)
{
this.game = game;
this.config = config;
@ -87,13 +87,13 @@ namespace osu.Game.Online.API
APIVersion = now.Year * 10000 + now.Month * 100 + now.Day;
}
EndpointConfiguration = endpointConfiguration;
Endpoints = endpoints;
NotificationsClient = setUpNotificationsClient();
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl);
authentication = new OAuth(endpoints.APIClientID, endpoints.APIClientSecret, Endpoints.APIUrl);
log = Logger.GetLogger(LoggingTarget.Network);
log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}");
log.Add($@"API endpoint root: {Endpoints.APIUrl}");
log.Add($@"API request version: {APIVersion}");
ProvidedUsername = config.Get<string>(OsuSetting.Username);
@ -405,7 +405,7 @@ namespace osu.Game.Online.API
var req = new RegistrationRequest
{
Url = $@"{EndpointConfiguration.APIEndpointUrl}/users",
Url = $@"{Endpoints.APIUrl}/users",
Method = HttpMethod.Post,
Username = username,
Email = email,

View File

@ -71,7 +71,7 @@ namespace osu.Game.Online.API
protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri);
protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}";
protected virtual string Uri => $@"{API!.Endpoints.APIUrl}/api/v2/{Target}";
protected IAPIProvider? API;

View File

@ -41,10 +41,10 @@ namespace osu.Game.Online.API
public string ProvidedUsername => LocalUser.Value.Username;
public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration
public EndpointConfiguration Endpoints { get; } = new EndpointConfiguration
{
APIEndpointUrl = "http://localhost",
WebsiteRootUrl = "http://localhost",
APIUrl = "http://localhost",
WebsiteUrl = "http://localhost",
};
public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd"));

View File

@ -53,7 +53,7 @@ namespace osu.Game.Online.API
/// <summary>
/// Holds configuration for online endpoints.
/// </summary>
EndpointConfiguration EndpointConfiguration { get; }
EndpointConfiguration Endpoints { get; }
/// <summary>
/// The version of the API.

View File

@ -15,10 +15,10 @@ namespace osu.Game.Online.API.Requests
get
{
// can be removed once the service has been successfully deployed to production
if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null)
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}";
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}";
}
}

View File

@ -21,10 +21,10 @@ namespace osu.Game.Online.API.Requests
get
{
// can be removed once the service has been successfully deployed to production
if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null)
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets";
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets";
}
}

View File

@ -14,10 +14,10 @@ namespace osu.Game.Online.API.Requests
get
{
// can be removed once the service has been successfully deployed to production
if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null)
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}";
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}";
}
}

View File

@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat
if (url.StartsWith('/'))
{
url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}";
url = $"{api.Endpoints.WebsiteUrl}{url}";
isTrustedDomain = true;
}
else
{
isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal);
isTrustedDomain = url.StartsWith(api.Endpoints.WebsiteUrl, StringComparison.Ordinal);
}
if (!url.CheckIsValidUrl())

View File

@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat
string getBeatmapPart()
{
return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle;
return beatmapOnlineID > 0 ? $"[{api.Endpoints.WebsiteUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle;
}
string getRulesetPart()

View File

@ -7,12 +7,12 @@ namespace osu.Game.Online
{
public DevelopmentEndpointConfiguration()
{
WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh";
WebsiteUrl = APIUrl = @"https://dev.ppy.sh";
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
APIClientID = "5";
SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
MetadataUrl = $@"{APIUrl}/signalr/metadata";
}
}
}

View File

@ -8,16 +8,6 @@ namespace osu.Game.Online
/// </summary>
public class EndpointConfiguration
{
/// <summary>
/// The base URL for the website. Does not include a trailing slash.
/// </summary>
public string WebsiteRootUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the main (osu-web) API. Does not include a trailing slash.
/// </summary>
public string APIEndpointUrl { get; set; } = string.Empty;
/// <summary>
/// The OAuth client secret.
/// </summary>
@ -29,23 +19,33 @@ namespace osu.Game.Online
public string APIClientID { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR spectator server.
/// The base URL for the website. Does not include a trailing slash.
/// </summary>
public string SpectatorEndpointUrl { get; set; } = string.Empty;
public string WebsiteUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR multiplayer server.
/// The endpoint for the main (osu-web) API. Does not include a trailing slash.
/// </summary>
public string MultiplayerEndpointUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR metadata server.
/// </summary>
public string MetadataEndpointUrl { get; set; } = string.Empty;
public string APIUrl { get; set; } = string.Empty;
/// <summary>
/// The root URL for the service handling beatmap submission. Does not include a trailing slash.
/// </summary>
public string? BeatmapSubmissionServiceUrl { get; set; }
/// <summary>
/// The endpoint for the SignalR spectator server.
/// </summary>
public string SpectatorUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR multiplayer server.
/// </summary>
public string MultiplayerUrl { get; set; } = string.Empty;
/// <summary>
/// The endpoint for the SignalR metadata server.
/// </summary>
public string MetadataUrl { get; set; } = string.Empty;
}
}

View File

@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.OnlineID > 0)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}")));
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}")));
if (Score.Files.Count > 0)
{

View File

@ -47,7 +47,7 @@ namespace osu.Game.Online.Metadata
public OnlineMetadataClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MetadataEndpointUrl;
endpoint = endpoints.MetadataUrl;
}
[BackgroundDependencyLoader]

View File

@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer
public OnlineMultiplayerClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MultiplayerEndpointUrl;
endpoint = endpoints.MultiplayerUrl;
}
[BackgroundDependencyLoader]

View File

@ -7,12 +7,12 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
WebsiteUrl = APIUrl = @"https://osu.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
SpectatorUrl = "https://spectator.ppy.sh/spectator";
MultiplayerUrl = "https://spectator.ppy.sh/multiplayer";
MetadataUrl = "https://spectator.ppy.sh/metadata";
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Online.Spectator
public OnlineSpectatorClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorEndpointUrl;
endpoint = endpoints.SpectatorUrl;
}
[BackgroundDependencyLoader]

View File

@ -295,7 +295,7 @@ namespace osu.Game
EndpointConfiguration endpoints = CreateEndpoints();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl;
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
frameworkLocale.BindValueChanged(_ => updateLanguage());

View File

@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments
private void copyUrl()
{
clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}");
clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}");
onScreenDisplay?.Display(new CopyUrlToast());
}

View File

@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login
}
};
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset");
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.Endpoints.WebsiteUrl}/home/password-reset");
password.OnCommit += (_, _) => performLogin();

View File

@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login
explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset");
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.Endpoints.WebsiteUrl}/home/password-reset");
explainText.AddText(". You can also ");
explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () =>
{

View File

@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header
}
topLinkContainer.AddText("Contributed ");
topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/users/{user.Id}/posts", creationParameters: embolden);
addSpacer(topLinkContainer);
topLinkContainer.AddText("Posted ");
topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden);
topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/comments?user_id={user.Id}", creationParameters: embolden);
string websiteWithoutProtocol = user.Website;

View File

@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
Texture = textures.Get(banner.Image),
};
Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}");
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/tournaments/{banner.TournamentId}");
}
protected override void LoadComplete()

View File

@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header
cover.User = user;
avatar.User = user;
usernameText.Text = user?.Username ?? string.Empty;
openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}";
openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}";
userFlag.CountryCode = user?.CountryCode ?? default;
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);

View File

@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
private void addBeatmapsetLink()
=> content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont());
private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull();
private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoints.WebsiteUrl}{url}").Argument.AsNonNull();
private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular)
=> OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true);

View File

@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki
Padding = new MarginPadding(padding),
Child = new WikiPanelMarkdownContainer(isFullWidth)
{
CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/",
CurrentPath = $@"{api.Endpoints.WebsiteUrl}/wiki/",
Text = text,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y

View File

@ -167,7 +167,7 @@ namespace osu.Game.Overlays
}
else
{
LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown));
LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/{path.Value}/", response.Markdown));
}
}
@ -176,7 +176,7 @@ namespace osu.Game.Overlays
wikiData.Value = null;
path.Value = "error";
LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/",
LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/",
$"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH})."));
}

View File

@ -1264,7 +1264,7 @@ namespace osu.Game.Screens.Edit
bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset())
|| (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset()));
bool submissionAvailable = api.EndpointConfiguration.BeatmapSubmissionServiceUrl != null;
bool submissionAvailable = api.Endpoints.BeatmapSubmissionServiceUrl != null;
if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable)
{

View File

@ -299,7 +299,7 @@ namespace osu.Game.Screens.Edit.Submission
uploadStep.SetCompleted();
if (configManager.Get<bool>(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission))
game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}");
game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}");
await updateLocalBeatmap().ConfigureAwait(true);
};
@ -326,7 +326,7 @@ namespace osu.Game.Screens.Edit.Submission
uploadStep.SetCompleted();
if (configManager.Get<bool>(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission))
game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}");
game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}");
await updateLocalBeatmap().ConfigureAwait(true);
};

View File

@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.MappingHelpForumDescription,
ButtonText = BeatmapSubmissionStrings.MappingHelpForum,
Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"),
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/56"),
},
new FormButton
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription,
ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum,
Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"),
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/60"),
},
},
});

View File

@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return items.ToArray();
string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}";
string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}";
}
}

View File

@ -284,6 +284,13 @@ namespace osu.Game.Screens.Play
return Task.CompletedTask;
}
// zero scores should also never be submitted.
if (score.ScoreInfo.TotalScore == 0)
{
Logger.Log("Zero score, skipping score submission");
return Task.CompletedTask;
}
// mind the timing of this.
// once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background,
// so all exceptional circumstances that would disallow submission must be handled above.

View File

@ -11,13 +11,15 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
@ -67,7 +69,7 @@ namespace osu.Game.Screens.Ranking.Contracted
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 1,
Offset = new Vector2(0, 4)
Offset = new Vector2(0, 2)
},
Children = new Drawable[]
{
@ -100,10 +102,10 @@ namespace osu.Game.Screens.Ranking.Contracted
CornerRadius = 20,
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Colour = Color4.Black.Opacity(0.15f),
Type = EdgeEffectType.Shadow,
Radius = 8,
Offset = new Vector2(0, 4),
Offset = new Vector2(0, 1),
}
},
new OsuSpriteText
@ -134,14 +136,33 @@ namespace osu.Game.Screens.Ranking.Contracted
createStatistic(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, $"{score.Accuracy.FormatAccuracy()}"),
}
},
new ModFlowDisplay
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Current = { Value = score.Mods },
IconScale = 0.5f,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(3),
ChildrenEnumerable =
[
new DifficultyIcon(score.BeatmapInfo!, score.Ruleset)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(20),
TooltipType = DifficultyIconTooltipType.Extended,
Margin = new MarginPadding { Right = 2 }
},
..
score.Mods.AsOrdered().Select(m => new ModIcon(m)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Scale = new Vector2(0.3f),
Margin = new MarginPadding { Top = -6 }
})
]
}
}
}

View File

@ -41,7 +41,6 @@ namespace osu.Game.Screens.Ranking.Expanded
private readonly List<StatisticDisplay> statisticDisplays = new List<StatisticDisplay>();
private FillFlowContainer starAndModDisplay;
private RollingCounter<long> scoreCounter;
[Resolved]
@ -139,12 +138,35 @@ namespace osu.Game.Screens.Ranking.Expanded
Alpha = 0,
AlwaysPresent = true
},
starAndModDisplay = new FillFlowContainer
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new DifficultyIcon(beatmap, score.Ruleset)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(20),
TooltipType = DifficultyIconTooltipType.Extended,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
ExpansionMode = ExpansionMode.AlwaysExpanded,
Scale = new Vector2(0.5f),
Current = { Value = score.Mods }
}
}
},
new FillFlowContainer
{
@ -225,29 +247,6 @@ namespace osu.Game.Screens.Ranking.Expanded
if (score.Date != default)
AddInternal(new PlayedOnText(score.Date));
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
if (starDifficulty != null)
{
starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
});
}
if (score.Mods.Any())
{
starAndModDisplay.Add(new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
ExpansionMode = ExpansionMode.AlwaysExpanded,
Scale = new Vector2(0.5f),
Current = { Value = score.Mods }
});
}
}
protected override void LoadComplete()

View File

@ -20,12 +20,23 @@ namespace osu.Game.Screens.SelectV2
[Cached]
public partial class BeatmapCarousel : Carousel<BeatmapInfo>
{
public const float SPACING = 5f;
private IBindableList<BeatmapSetInfo> detachedBeatmaps = null!;
private readonly LoadingLayer loading;
private readonly BeatmapCarouselFilterGrouping grouping;
protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom)
{
if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo)
// Beatmap difficulty panels do not overlap with themselves or any other panel.
return SPACING;
return -SPACING;
}
public BeatmapCarousel()
{
DebounceDelay = 100;
@ -95,11 +106,9 @@ namespace osu.Game.Screens.SelectV2
private GroupDefinition? lastSelectedGroup;
private BeatmapInfo? lastSelectedBeatmap;
protected override bool HandleItemSelected(object? model)
protected override void HandleItemActivated(CarouselItem item)
{
base.HandleItemSelected(model);
switch (model)
switch (item.Model)
{
case GroupDefinition group:
// Special case collapsing an open group.
@ -107,37 +116,42 @@ namespace osu.Game.Screens.SelectV2
{
setExpansionStateOfGroup(lastSelectedGroup, false);
lastSelectedGroup = null;
return false;
return;
}
setExpandedGroup(group);
return false;
return;
case BeatmapSetInfo setInfo:
// Selecting a set isn't valid let's re-select the first difficulty.
CurrentSelection = setInfo.Beatmaps.First();
return false;
return;
case BeatmapInfo beatmapInfo:
// If we have groups, we need to account for them.
if (Criteria.SplitOutDifficulties)
{
// Find the containing group. There should never be too many groups so iterating is efficient enough.
GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
if (group != null)
setExpandedGroup(group);
}
else
{
setExpandedSet(beatmapInfo);
}
return true;
CurrentSelection = beatmapInfo;
return;
}
}
return true;
protected override void HandleItemSelected(object? model)
{
base.HandleItemSelected(model);
switch (model)
{
case BeatmapSetInfo:
case GroupDefinition:
throw new InvalidOperationException("Groups should never become selected");
case BeatmapInfo beatmapInfo:
// Find any containing group. There should never be too many groups so iterating is efficient enough.
GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
if (containingGroup != null)
setExpandedGroup(containingGroup);
setExpandedSet(beatmapInfo);
break;
}
}
protected override bool CheckValidForGroupSelection(CarouselItem item)
@ -148,7 +162,7 @@ namespace osu.Game.Screens.SelectV2
return true;
case BeatmapInfo:
return Criteria.SplitOutDifficulties;
return !grouping.BeatmapSetsGroupedTogether;
case GroupDefinition:
return false;
@ -170,12 +184,46 @@ namespace osu.Game.Screens.SelectV2
{
if (grouping.GroupItems.TryGetValue(group, out var items))
{
foreach (var i in items)
if (expanded)
{
if (i.Model is GroupDefinition)
i.IsExpanded = expanded;
else
i.IsVisible = expanded;
foreach (var i in items)
{
switch (i.Model)
{
case GroupDefinition:
i.IsExpanded = true;
break;
case BeatmapSetInfo set:
// Case where there are set headers, header should be visible
// and items should use the set's expanded state.
i.IsVisible = true;
setExpansionStateOfSetItems(set, i.IsExpanded);
break;
default:
// Case where there are no set headers, all items should be visible.
if (!grouping.BeatmapSetsGroupedTogether)
i.IsVisible = true;
break;
}
}
}
else
{
foreach (var i in items)
{
switch (i.Model)
{
case GroupDefinition:
i.IsExpanded = false;
break;
default:
i.IsVisible = false;
break;
}
}
}
}
}

View File

@ -14,6 +14,8 @@ namespace osu.Game.Screens.SelectV2
{
public class BeatmapCarouselFilterGrouping : ICarouselFilter
{
public bool BeatmapSetsGroupedTogether { get; private set; }
/// <summary>
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
/// </summary>
@ -36,8 +38,6 @@ namespace osu.Game.Screens.SelectV2
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
{
bool groupSetsTogether;
setItems.Clear();
groupItems.Clear();
@ -48,12 +48,39 @@ namespace osu.Game.Screens.SelectV2
switch (criteria.Group)
{
default:
groupSetsTogether = true;
BeatmapSetsGroupedTogether = true;
newItems.AddRange(items);
break;
case GroupMode.Artist:
BeatmapSetsGroupedTogether = true;
char groupChar = (char)0;
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
var b = (BeatmapInfo)item.Model;
char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]);
if (beatmapFirstChar > groupChar)
{
groupChar = beatmapFirstChar;
var groupDefinition = new GroupDefinition($"{groupChar}");
var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT };
newItems.Add(groupItem);
groupItems[groupDefinition] = new HashSet<CarouselItem> { groupItem };
}
newItems.Add(item);
}
break;
case GroupMode.Difficulty:
groupSetsTogether = false;
BeatmapSetsGroupedTogether = false;
int starGroup = int.MinValue;
foreach (var item in items)
@ -66,7 +93,12 @@ namespace osu.Game.Screens.SelectV2
{
starGroup = (int)Math.Floor(b.StarRating);
var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *");
var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT };
var groupItem = new CarouselItem(groupDefinition)
{
DrawHeight = GroupPanel.HEIGHT,
DepthLayer = -2
};
newItems.Add(groupItem);
groupItems[groupDefinition] = new HashSet<CarouselItem> { groupItem };
@ -81,7 +113,7 @@ namespace osu.Game.Screens.SelectV2
// Add set headers wherever required.
CarouselItem? lastItem = null;
if (groupSetsTogether)
if (BeatmapSetsGroupedTogether)
{
for (int i = 0; i < newItems.Count; i++)
{
@ -91,11 +123,16 @@ namespace osu.Game.Screens.SelectV2
if (item.Model is BeatmapInfo beatmap)
{
bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID);
bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
if (newBeatmapSet)
{
var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT };
var setItem = new CarouselItem(beatmap.BeatmapSet!)
{
DrawHeight = BeatmapSetPanel.HEIGHT,
DepthLayer = -1
};
setItems[beatmap.BeatmapSet!] = new HashSet<CarouselItem> { setItem };
newItems.Insert(i, setItem);
i++;

View File

@ -24,6 +24,19 @@ namespace osu.Game.Screens.SelectV2
private Box activationFlash = null!;
private OsuSpriteText text = null!;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
var inputRectangle = DrawRectangle;
// Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
//
// Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly
// larger hit target.
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING });
return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
}
[BackgroundDependencyLoader]
private void load()
{
@ -86,13 +99,7 @@ namespace osu.Game.Screens.SelectV2
protected override bool OnClick(ClickEvent e)
{
if (carousel.CurrentSelection != Item!.Model)
{
carousel.CurrentSelection = Item!.Model;
return true;
}
carousel.TryActivateSelection();
carousel.Activate(Item!);
return true;
}

View File

@ -1,7 +1,6 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -83,7 +82,7 @@ namespace osu.Game.Screens.SelectV2
protected override bool OnClick(ClickEvent e)
{
carousel.CurrentSelection = Item!.Model;
carousel.Activate(Item!);
return true;
}
@ -98,8 +97,6 @@ namespace osu.Game.Screens.SelectV2
public void Activated()
{
// sets should never be activated.
throw new InvalidOperationException();
}
#endregion

View File

@ -16,6 +16,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Logging;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
@ -50,11 +51,6 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public float DistanceOffscreenToPreload { get; set; }
/// <summary>
/// Vertical space between panel layout. Negative value can be used to create an overlapping effect.
/// </summary>
protected float SpacingBetweenPanels { get; set; } = -5;
/// <summary>
/// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter.
/// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations.
@ -94,28 +90,46 @@ namespace osu.Game.Screens.SelectV2
public object? CurrentSelection
{
get => currentSelection.Model;
set => setSelection(value);
set
{
if (currentSelection.Model != value)
{
HandleItemSelected(value);
if (currentSelection.Model != null)
HandleItemDeselected(currentSelection.Model);
currentKeyboardSelection = new Selection(value);
currentSelection = currentKeyboardSelection;
selectionValid.Invalidate();
}
else if (currentKeyboardSelection.Model != value)
{
// Even if the current selection matches, let's ensure the keyboard selection is reset
// to the newly selected object. This matches user expectations (for now).
currentKeyboardSelection = currentSelection;
selectionValid.Invalidate();
}
}
}
/// <summary>
/// Activate the current selection, if a selection exists and matches keyboard selection.
/// If keyboard selection does not match selection, this will transfer the selection on first invocation.
/// Activate the specified item.
/// </summary>
public void TryActivateSelection()
/// <param name="item"></param>
public void Activate(CarouselItem item)
{
if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
{
CurrentSelection = currentKeyboardSelection.Model;
return;
}
(GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated();
HandleItemActivated(item);
if (currentSelection.CarouselItem != null)
{
(GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated();
HandleItemActivated(currentSelection.CarouselItem);
}
selectionValid.Invalidate();
}
/// <summary>
/// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect.
/// </summary>
protected virtual float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) => 0f;
#endregion
#region Properties and methods concerning implementations
@ -176,30 +190,28 @@ namespace osu.Game.Screens.SelectV2
protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true;
/// <summary>
/// Called when an item is "selected".
/// Called after an item becomes the <see cref="CurrentSelection"/>.
/// Should be used to handle any group expansion, item visibility changes, etc.
/// </summary>
/// <returns>Whether the item should be selected.</returns>
protected virtual bool HandleItemSelected(object? model) => true;
protected virtual void HandleItemSelected(object? model) { }
/// <summary>
/// Called when an item is "deselected".
/// Called when the <see cref="CurrentSelection"/> changes to a new selection.
/// Should be used to handle any group expansion, item visibility changes, etc.
/// </summary>
protected virtual void HandleItemDeselected(object? model)
{
}
protected virtual void HandleItemDeselected(object? model) { }
/// <summary>
/// Called when an item is "activated".
/// Called when an item is activated via user input (keyboard traversal or a mouse click).
/// </summary>
/// <remarks>
/// An activated item should for instance:
/// - Open or close a folder
/// - Start gameplay on a beatmap difficulty.
/// An activated item should decide to perform an action, such as:
/// - Change its expanded state (and show / hide children items).
/// - Set the item to the <see cref="CurrentSelection"/>.
/// - Start gameplay on a beatmap difficulty if already selected.
/// </remarks>
/// <param name="item">The carousel item which was activated.</param>
protected virtual void HandleItemActivated(CarouselItem item)
{
}
protected virtual void HandleItemActivated(CarouselItem item) { }
#endregion
@ -255,7 +267,7 @@ namespace osu.Game.Screens.SelectV2
}
log("Updating Y positions");
updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels);
updateYPositions(items, visibleHalfHeight);
}
catch (OperationCanceledException)
{
@ -281,17 +293,26 @@ namespace osu.Game.Screens.SelectV2
void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}");
}
private static void updateYPositions(IEnumerable<CarouselItem> carouselItems, float offset, float spacing)
private void updateYPositions(IEnumerable<CarouselItem> carouselItems, float offset)
{
CarouselItem? previousVisible = null;
foreach (var item in carouselItems)
updateItemYPosition(item, ref offset, spacing);
updateItemYPosition(item, ref previousVisible, ref offset);
}
private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing)
private void updateItemYPosition(CarouselItem item, ref CarouselItem? previousVisible, ref float offset)
{
float spacing = previousVisible == null || !item.IsVisible ? 0 : GetSpacingBetweenPanels(previousVisible, item);
offset += spacing;
item.CarouselYPosition = offset;
if (item.IsVisible)
offset += item.DrawHeight + spacing;
{
offset += item.DrawHeight;
previousVisible = item;
}
}
#endregion
@ -303,7 +324,8 @@ namespace osu.Game.Screens.SelectV2
switch (e.Action)
{
case GlobalAction.Select:
TryActivateSelection();
if (currentKeyboardSelection.CarouselItem != null)
Activate(currentKeyboardSelection.CarouselItem);
return true;
case GlobalAction.SelectNext:
@ -372,32 +394,29 @@ namespace osu.Game.Screens.SelectV2
// If the user has a different keyboard selection and requests
// group selection, first transfer the keyboard selection to actual selection.
if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
{
TryActivateSelection();
// There's a chance this couldn't resolve, at which point continue with standard traversal.
if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem)
return;
Activate(currentKeyboardSelection.CarouselItem);
return;
}
int originalIndex;
int newIndex;
if (currentSelection.Index == null)
if (currentKeyboardSelection.Index == null)
{
// If there's no current selection, start from either end of the full list.
newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0;
}
else
{
newIndex = originalIndex = currentSelection.Index.Value;
newIndex = originalIndex = currentKeyboardSelection.Index.Value;
// As a second special case, if we're group selecting backwards and the current selection isn't a group,
// make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early.
if (direction < 0)
{
while (!CheckValidForGroupSelection(carouselItems[newIndex]))
while (newIndex > 0 && !CheckValidForGroupSelection(carouselItems[newIndex]))
newIndex--;
}
}
@ -411,7 +430,7 @@ namespace osu.Game.Screens.SelectV2
if (CheckValidForGroupSelection(newItem))
{
setSelection(newItem.Model);
HandleItemActivated(newItem);
return;
}
} while (newIndex != originalIndex);
@ -426,23 +445,6 @@ namespace osu.Game.Screens.SelectV2
private Selection currentKeyboardSelection = new Selection();
private Selection currentSelection = new Selection();
private void setSelection(object? model)
{
if (currentSelection.Model == model)
return;
if (HandleItemSelected(model))
{
if (currentSelection.Model != null)
HandleItemDeselected(currentSelection.Model);
currentKeyboardSelection = new Selection(model);
currentSelection = currentKeyboardSelection;
}
selectionValid.Invalidate();
}
private void setKeyboardSelection(object? model)
{
currentKeyboardSelection = new Selection(model);
@ -468,7 +470,7 @@ namespace osu.Game.Screens.SelectV2
return;
}
float spacing = SpacingBetweenPanels;
CarouselItem? lastVisible = null;
int count = carouselItems.Count;
Selection prevKeyboard = currentKeyboardSelection;
@ -480,7 +482,7 @@ namespace osu.Game.Screens.SelectV2
{
var item = carouselItems[i];
updateItemYPosition(item, ref yPos, spacing);
updateItemYPosition(item, ref lastVisible, ref yPos);
if (ReferenceEquals(item.Model, currentKeyboardSelection.Model))
currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i);
@ -548,6 +550,8 @@ namespace osu.Game.Screens.SelectV2
updateDisplayedRange(range);
}
double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0;
foreach (var panel in scroll.Panels)
{
var c = (ICarouselPanel)panel;
@ -556,8 +560,8 @@ namespace osu.Game.Screens.SelectV2
if (c.Item == null)
continue;
double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0;
scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos));
float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight);
scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth);
if (c.DrawYPosition != c.Item.CarouselYPosition)
c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed);
@ -676,6 +680,15 @@ namespace osu.Game.Screens.SelectV2
carouselPanel.Expanded.Value = false;
}
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
// handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed).
if (invalidation.HasFlag(Invalidation.DrawSize))
selectionValid.Invalidate();
return base.OnInvalidate(invalidation, source);
}
#endregion
#region Internal helper classes

View File

@ -29,6 +29,11 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public float DrawHeight { get; set; } = DEFAULT_HEIGHT;
/// <summary>
/// Defines the display depth relative to other <see cref="CarouselItem"/>s.
/// </summary>
public int DepthLayer { get; set; }
/// <summary>
/// Whether this item is visible or hidden.
/// </summary>

View File

@ -1,7 +1,6 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -96,7 +95,7 @@ namespace osu.Game.Screens.SelectV2
protected override bool OnClick(ClickEvent e)
{
carousel.CurrentSelection = Item!.Model;
carousel.Activate(Item!);
return true;
}
@ -111,8 +110,6 @@ namespace osu.Game.Screens.SelectV2
public void Activated()
{
// sets should never be activated.
throw new InvalidOperationException();
}
#endregion

View File

@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray()));
if (score.OnlineID > 0)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}")));
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}")));
if (score.Files.Count <= 0) return items.ToArray();

View File

@ -41,7 +41,7 @@ namespace osu.Game.Utils
{
this.game = game;
if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal))
if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal))
return;
sentrySession = SentrySdk.Init(options =>