1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-25 05:22:48 +08:00

Compare commits

..

124 Commits

65 changed files with 1467 additions and 601 deletions
+87
View File
@@ -0,0 +1,87 @@
name: Pack and nuget
on:
push:
tags:
- '*'
jobs:
notify_pending_production_deploy:
runs-on: ubuntu-latest
steps:
- name: Submit pending deployment notification
run: |
export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME"
export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID"
export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME:
[View Workflow Run]($URL)"
export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
BODY="$(jq --null-input '{
"embeds": [
{
"title": env.TITLE,
"color": 15098112,
"description": env.DESCRIPTION,
"url": env.URL,
"author": {
"name": env.GITHUB_ACTOR,
"icon_url": env.ACTOR_ICON
}
}
]
}')"
curl \
-H "Content-Type: application/json" \
-d "$BODY" \
"${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}"
pack:
name: Pack
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set artifacts directory
id: artifactsPath
run: echo "::set-output name=nuget_artifacts::${{github.workspace}}/artifacts"
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Pack
run: |
# Replace project references in templates with package reference, because they're included as source files.
dotnet remove Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game/osu.Game.csproj
dotnet remove Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj
dotnet remove Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game/osu.Game.csproj
dotnet remove Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj
dotnet add Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
dotnet add Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
dotnet add Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
dotnet add Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
# Pack
dotnet pack -c Release osu.Game /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
dotnet pack -c Release osu.Game.Rulesets.Osu /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
dotnet pack -c Release osu.Game.Rulesets.Taiko /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
dotnet pack -c Release osu.Game.Rulesets.Catch /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
dotnet pack -c Release osu.Game.Rulesets.Mania /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: osu
path: |
${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg
${{steps.artifactsPath.outputs.nuget_artifacts}}/*.snupkg
- name: Publish packages to nuget.org
run: dotnet nuget push ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
-32
View File
@@ -1,32 +0,0 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2022
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
before_build:
- cmd: dotnet --info # Useful when version mismatch between CI and local
- cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects
- cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects
- cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
after_build:
- ps: .\InspectCode.ps1
test:
assemblies:
except:
- '**\*Android*'
- '**\*iOS*'
- 'build\**\*'
-86
View File
@@ -1,86 +0,0 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
environment:
matrix:
- job_name: osu-game
- job_name: osu-ruleset
job_depends_on: osu-game
- job_name: taiko-ruleset
job_depends_on: osu-game
- job_name: catch-ruleset
job_depends_on: osu-game
- job_name: mania-ruleset
job_depends_on: osu-game
- job_name: templates
job_depends_on: osu-game
nuget:
project_feed: true
for:
-
matrix:
only:
- job_name: osu-game
build_script:
- cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: osu-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: taiko-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: catch-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: mania-ruleset
build_script:
- cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
-
matrix:
only:
- job_name: templates
build_script:
- cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
- cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
- cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
artifacts:
- path: '**\*.nupkg'
deploy:
- provider: Environment
name: nuget
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.512.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.604.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests;
using osu.Game.Tests.Visual;
using osuTK;
@@ -107,6 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
[FlakyTest]
public void TestVibrateWithoutSpinningOnCentreWithDoubleTime()
{
List<ReplayFrame> frames = new List<ReplayFrame>();
@@ -44,10 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
SnakingOut.BindTo(configSnakingOut);
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
BorderColour = GetBorderColour(skin);
}
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour;
protected virtual Color4 GetBorderColour(ISkinSource skin) => Color4.White;
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) => hitObjectAccentColour;
}
}
@@ -15,11 +15,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath();
protected override Color4 GetBorderColour(ISkinSource skin)
=> skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
{
// legacy skins use a constant value for slider track alpha, regardless of the source colour.
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(0.7f);
}
=> (skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour).Opacity(0.7f);
private partial class LegacyDrawableSliderPath : DrawableSliderPath
{
@@ -3,9 +3,7 @@
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.Testing;
using osu.Framework.Utils;
@@ -27,33 +25,34 @@ namespace osu.Game.Tests.Editing
public partial class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene
{
private TestHitObjectComposer composer = null!;
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
public TestSceneHitObjectComposerDistanceSnapping()
{
base.Content.Add(new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap
{
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
}),
Content
},
});
}
private EditorBeatmap editorBeatmap = null!;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = composer = new TestHitObjectComposer();
editorBeatmap = new EditorBeatmap(new OsuBeatmap
{
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
});
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap)
],
Children = new Drawable[]
{
editorBeatmap,
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = composer = new TestHitObjectComposer()
}
}
};
BeatDivisor.Value = 1;
@@ -247,16 +246,23 @@ namespace osu.Game.Tests.Editing
}
private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity),
() => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}",
() => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity),
() => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}",
() => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity),
() => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)",
() => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity),
() => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private partial class TestHitObjectComposer : OsuHitObjectComposer
{
+37 -4
View File
@@ -259,9 +259,6 @@ namespace osu.Game.Tests.Mods
new MultiplayerTestScenario(true, true, [new OsuModPerfect()], []),
new MultiplayerTestScenario(true, true, [new OsuModDoubleTime()], []),
new MultiplayerTestScenario(true, true, [new OsuModNightcore()], []),
new MultiplayerTestScenario(true, true, [new OsuModHidden()], []),
new MultiplayerTestScenario(true, true, [new OsuModFlashlight()], []),
new MultiplayerTestScenario(true, true, [new OsuModAccuracyChallenge()], []),
new MultiplayerTestScenario(true, true, [new OsuModDifficultyAdjust()], []),
new MultiplayerTestScenario(true, true, [new ModWindUp()], []),
new MultiplayerTestScenario(true, true, [new ModWindDown()], []),
@@ -347,8 +344,44 @@ namespace osu.Game.Tests.Mods
{
if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !commonAcronyms.Contains(mod.Acronym))
Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!");
// downgraded to warning, because there are valid reasons why they may still not be specified to be valid for freestyle as required
// (see `TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()` test case below).
if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym))
Assert.Fail($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets!");
Assert.Warn($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets.");
}
}
});
}
[Test]
public void TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()
{
Dictionary<(string firstMod, string secondMod), bool> compatibilityMap = new Dictionary<(string, string), bool>();
Assert.Multiple(() =>
{
for (int rulesetId = 0; rulesetId < 4; ++rulesetId)
{
var rulesetStore = new AssemblyRulesetStore();
var ruleset = rulesetStore.GetRuleset(rulesetId)!.CreateInstance();
var modsValidForFreestyleAsRequired = ruleset.CreateAllMods().Where(m => m.ValidForFreestyleAsRequiredMod).OrderBy(m => m.Acronym).ToList();
for (int i = 0; i < modsValidForFreestyleAsRequired.Count; i++)
{
for (int j = i; j < modsValidForFreestyleAsRequired.Count; ++j)
{
var first = modsValidForFreestyleAsRequired[i];
var second = modsValidForFreestyleAsRequired[j];
bool compatible = ModUtils.CheckCompatibleSet([first, second]);
if (!compatibilityMap.TryGetValue((first.Acronym, second.Acronym), out bool previousCompatible))
compatibilityMap[(first.Acronym, second.Acronym)] = compatible;
else if (previousCompatible != compatible)
Assert.Fail($"{first.Acronym} and {second.Acronym} declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} while not being consistently compatible in all four rulesets!");
}
}
}
});
@@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private bool seek;
[Test]
[FlakyTest]
public void TestAllSamplesStopDuringSeek()
{
DrawableSlider? slider = null;
@@ -69,6 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
[FlakyTest]
public void TestFadeOnIdle()
{
createTest();
@@ -144,7 +145,8 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
public void TestDoesntFadeOnMouseDown()
[FlakyTest]
public void TestDoesNotFadeOnMouseDown()
{
createTest();
@@ -303,6 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
[FlakyTest]
public void TestMostInSyncUserIsAudioSource()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
@@ -83,6 +84,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestCursorHidesWhenIdle()
{
AddStep("move mouse inside game bounds", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.TopLeft + new Vector2(20)));
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait until idle", () => Game.IsIdle.Value);
AddUntilStep("menu cursor hidden", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
@@ -793,7 +793,8 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect()));
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action!.Invoke());
AddWaitStep("wait two frames", 2);
ConfirmAtMainMenu();
}
[Test]
@@ -0,0 +1,157 @@
// 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.Extensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
{
/// <summary>
/// Tests copied out of `TestSceneScreenNavigation` which are specific to song select.
/// These are for SongSelectV2. Eventually, the tests in the above class should be deleted along with old song select.
/// </summary>
public partial class TestSceneSongSelectNavigation : OsuGameTestScene
{
[Test]
public void TestRetryFromResults()
{
var getOriginalPlayer = playToResults();
AddStep("attempt to retry", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
}
[Test]
public void TestPushSongSelectAndPressBackButtonImmediately()
{
AddStep("push song select", () => Game.ScreenStack.Push(new SoloSongSelect()));
// TODO: without this step, a critical bug will be hit, see inline comment in `OsuGame.handleBackButton`.
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect select && select.IsLoaded);
AddStep("press back button", () => Game.ChildrenOfType<ScreenBackButton>().First().Action!.Invoke());
ConfirmAtMainMenu();
}
[Test]
public void TestEditBeatmap()
{
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("open menu", () => InputManager.Key(Key.F3));
AddStep("trigger edit", () =>
{
// TODO: should be 5, not 4.
InputManager.Key(Key.Number4);
});
waitForScreen<Editor>();
pushEscape();
waitForScreen<SoloSongSelect>();
}
[TestCase(true)]
[TestCase(false)]
public void TestSongContinuesAfterExitPlayer(bool withUserPause)
{
Player? player = null;
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
if (withUserPause)
AddStep("pause", () => Game.Dependencies.Get<MusicController>().Stop(true));
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for fail", () => player?.GameplayState.HasFailed, () => Is.True);
AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
pushEscape();
AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying);
AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
}
private Func<Player> playToResults()
{
var player = playToCompletion();
AddUntilStep("wait for results", () => (Game.ScreenStack.CurrentScreen as ResultsScreen)?.IsLoaded == true);
return player;
}
private Func<Player> playToCompletion()
{
Player? player = null;
IWorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new SoloSongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } });
pushEnter();
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning);
AddStep("seek to near end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000));
AddUntilStep("wait for complete", () => player?.GameplayState.HasPassed, () => Is.True);
return () => player!;
}
private void waitForScreen<T>() where T : OsuScreen =>
AddUntilStep($"Wait for {typeof(T).ReadableName()}", () => Game.ScreenStack.CurrentScreen is T screen && screen.IsLoaded);
private void pushEnter() =>
AddStep("Press enter", () => InputManager.Key(Key.Enter));
private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape));
}
}
@@ -409,10 +409,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
protected override async Task<IEnumerable<CarouselItem>> FilterAsync(bool clearExistingPanels = false)
{
var items = await base.FilterAsync(clearExistingPanels);
var items = await base.FilterAsync(clearExistingPanels).ConfigureAwait(true);
if (FilterDelay != 0)
await Task.Delay(FilterDelay);
await Task.Delay(FilterDelay).ConfigureAwait(true);
PostFilterBeatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
return items;
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
@@ -94,6 +95,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
}
[Test]
public void TestHighChurnUpdatesStillShowsPanels()
{
ScheduledDelegate updateTask = null!;
AddBeatmaps(1, 1);
AddStep("start constantly updating beatmap in background", () =>
{
updateTask = Scheduler.AddDelayed(() => { BeatmapSets.ReplaceRange(0, 1, [BeatmapSets.First()]); }, 1, true);
});
CreateCarousel();
AddUntilStep("panels loaded", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.Empty);
AddStep("end task", () => updateTask.Cancel());
}
[Test]
[Explicit]
public void TestPerformanceWithManyBeatmaps()
@@ -203,10 +203,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
WaitForGroupSelection(0, 1);
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, CarouselItem.DEFAULT_HEIGHT / 2 + 1));
WaitForGroupSelection(0, 2);
ClickVisiblePanelWithOffset<PanelBeatmapSet>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
ClickVisiblePanelWithOffset<PanelBeatmapSet>(1, new Vector2(0, CarouselItem.DEFAULT_HEIGHT / 2 + 1));
WaitForGroupSelection(0, 5);
}
@@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK;
@@ -266,8 +265,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
// Clicks just above the first group panel should not actuate any action.
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 + 1)));
ClickVisiblePanelWithOffset<PanelBeatmapSet>(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 + BeatmapCarousel.SPACING + 1)));
AddAssert("no beatmaps visible", () => !GetVisiblePanels<PanelBeatmap>().Any());
@@ -278,14 +276,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForSelection(0, 0);
// Beatmap panels expand their selection area to cover holes from spacing.
ClickVisiblePanelWithOffset<PanelBeatmap>(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
ClickVisiblePanelWithOffset<PanelBeatmap>(0, new Vector2(0, -(PanelBeatmap.HEIGHT / 2 + 1)));
WaitForSelection(0, 0);
// Panels with higher depth will handle clicks in the gutters for simplicity.
ClickVisiblePanelWithOffset<PanelBeatmap>(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
ClickVisiblePanelWithOffset<PanelBeatmap>(2, new Vector2(0, (PanelBeatmap.HEIGHT / 2 + 1)));
WaitForSelection(0, 2);
ClickVisiblePanelWithOffset<PanelBeatmap>(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
ClickVisiblePanelWithOffset<PanelBeatmap>(2, new Vector2(0, -(PanelBeatmap.HEIGHT / 2 + 1)));
WaitForSelection(0, 2);
ClickVisiblePanelWithOffset<PanelBeatmap>(3, new Vector2(0, (PanelBeatmap.HEIGHT / 2 + 1)));
WaitForSelection(0, 3);
}
@@ -19,6 +19,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2
CreateCarousel();
}
[Test]
public void TestRandomObeysFiltering()
{
AddBeatmaps(2, 10, true);
ApplyToFilter("filter", c => c.SearchText = BeatmapSets[0].Beatmaps.Last().DifficultyName);
WaitForFiltering();
CheckDisplayedBeatmapSetsCount(1);
CheckDisplayedBeatmapsCount(1);
for (int i = 0; i < 10; i++)
{
nextRandom();
WaitForSelection(0, 9);
}
}
/// <summary>
/// Test random non-repeating algorithm
/// </summary>
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods;
@@ -105,6 +106,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2
void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen);
}
[Test]
public void TestInvalidRulesetDoesNotEnterGameplay()
{
var screensPushed = new List<IScreen>();
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(1);
LoadSongSelect();
AddStep("subscribe to screen pushed", () => Stack.ScreenPushed += onScreenPushed);
AddStep("change ruleset to taiko", () => Ruleset.Value = Rulesets.AvailableRulesets.Single(r => r.OnlineID == 1));
AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
AddUntilStep("wait for taiko beatmap selected", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
AddStep("change ruleset back and start gameplay immediately", () =>
{
Ruleset.Value = Rulesets.AvailableRulesets.Single(r => r.OnlineID == 0);
InputManager.MoveMouseTo(this.ChildrenOfType<OsuLogo>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("no screens pushed", () => screensPushed, () => Is.Empty);
AddStep("unsubscribe from screen pushed", () => Stack.ScreenPushed -= onScreenPushed);
AddUntilStep("wait for osu beatmap selected", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(0));
void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen);
}
#region Hotkeys
[Test]
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder;
@@ -65,6 +66,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("filter count is 0", () => filterOperationsCount, () => Is.EqualTo(0));
}
[Test]
public void TestFilterSingleResult_RetainsSelectedDifficulty()
{
LoadSongSelect();
ImportBeatmapForRuleset(0);
AddUntilStep("wait for single set", () => Carousel.ChildrenOfType<PanelBeatmapSet>().Count(), () => Is.EqualTo(1));
AddStep("select last difficulty", () =>
{
Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last());
});
AddStep("set filter text", () => filterTextBox.Current.Value = " ");
AddWaitStep("wait for debounce", 5);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddAssert("selection unchanged", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last()));
}
[Test]
public void TestFilterOnResumeAfterChange()
{
@@ -0,0 +1,22 @@
// 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.Game.Graphics;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneGhostIcon : OsuTestScene
{
public TestSceneGhostIcon()
{
Add(new GhostIcon
{
Size = new Vector2(64),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
}
}
-6
View File
@@ -39,12 +39,6 @@ namespace osu.Game.Tests
trackStore = audioManager.GetTrackStore(getZipReader());
}
~WaveformTestBeatmap()
{
// Remove the track store from the audio manager
trackStore?.Dispose();
}
private static Stream getStream() => TestResources.GetTestBeatmapStream();
private static ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream());
@@ -64,6 +64,20 @@ namespace osu.Game.Beatmaps
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";
/// <summary>
/// Whether gameplay is allowed for this beatmap with the provided ruleset (via conversion or direct compatibility).
/// </summary>
public static bool AllowGameplayWithRuleset(this IBeatmapInfo beatmap, RulesetInfo ruleset, bool allowConversion)
{
if (beatmap.Ruleset.ShortName == ruleset.ShortName)
return true;
if (allowConversion && beatmap.Ruleset.OnlineID == 0 && ruleset.OnlineID != 0)
return true;
return false;
}
/// <summary>
/// Get the beatmap info page URL, or <c>null</c> if unavailable.
/// </summary>
+7 -9
View File
@@ -124,18 +124,16 @@ namespace osu.Game.Beatmaps
Track.Looping = looping;
Track.RestartPoint = Metadata.PreviewTime;
if (Track.RestartPoint == -1)
if (!Track.IsLoaded)
{
if (!Track.IsLoaded)
{
// force length to be populated (https://github.com/ppy/osu-framework/issues/4202)
Track.Seek(Track.CurrentTime);
}
Track.RestartPoint = 0.4f * Track.Length;
// force length to be populated (https://github.com/ppy/osu-framework/issues/4202)
Track.Seek(Track.CurrentTime);
}
Track.RestartPoint += offsetFromPreviewPoint;
if (Track.RestartPoint < 0 || Track.RestartPoint > Track.Length)
Track.RestartPoint = 0.4f * Track.Length;
Track.RestartPoint = Math.Clamp(Track.RestartPoint + offsetFromPreviewPoint, 0, Track.Length);
}
/// <summary>
+2
View File
@@ -13,6 +13,8 @@ namespace osu.Game.Database
{
}
protected override bool UseFixedEncoding => false;
protected override string FileExtension => @".osk";
}
}
+55 -24
View File
@@ -12,6 +12,8 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Development;
using osu.Framework.Extensions.PolygonExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -147,13 +149,7 @@ namespace osu.Game.Graphics.Carousel
/// <summary>
/// Scroll carousel to the selected item if available.
/// </summary>
public void ScrollToSelection()
{
// TODO: this likely needs to be delayed until currentKeyboardSelection has a valid value.
// Early calls to `ScrollToSelection` will currently silently fail.
if (currentKeyboardSelection.CarouselItem != null)
Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop);
}
public void ScrollToSelection() => scrollToSelection.Invalidate();
/// <summary>
/// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect.
@@ -197,6 +193,8 @@ namespace osu.Game.Graphics.Carousel
if (clearExistingPanels)
filterReusesPanels.Invalidate();
filterAfterItemsChanged.Validate();
filterTask = performFilter();
filterTask.FireAndForget();
return filterTask;
@@ -280,7 +278,7 @@ namespace osu.Game.Graphics.Carousel
RelativeSizeAxes = Axes.Both,
};
Items.BindCollectionChanged((_, _) => FilterAsync());
Items.BindCollectionChanged((_, _) => filterAfterItemsChanged.Invalidate());
}
[BackgroundDependencyLoader]
@@ -303,22 +301,29 @@ namespace osu.Game.Graphics.Carousel
private Task<IEnumerable<CarouselItem>> filterTask = Task.FromResult(Enumerable.Empty<CarouselItem>());
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
/// <summary>
/// For background re-filters, ensure we wait for the previous filter operation to complete before starting another.
/// This avoids the carousel never updating its display in high churn scenarios.
/// </summary>
private readonly Cached filterAfterItemsChanged = new Cached();
private async Task<IEnumerable<CarouselItem>> performFilter()
{
Stopwatch stopwatch = Stopwatch.StartNew();
var cts = new CancellationTokenSource();
var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts);
await previousCancellationSource.CancelAsync().ConfigureAwait(false);
await previousCancellationSource.CancelAsync().ConfigureAwait(true);
if (DebounceDelay > 0)
{
log($"Filter operation queued, waiting for {DebounceDelay} ms debounce");
await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false);
await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true);
}
// Copy must be performed on update thread for now (see ConfigureAwait above).
// Could potentially be optimised in the future if it becomes an issue.
Debug.Assert(ThreadSafety.IsUpdateThread);
List<CarouselItem> items = new List<CarouselItem>(Items.Select(m => new CarouselItem(m)));
await Task.Run(async () =>
@@ -391,6 +396,15 @@ namespace osu.Game.Graphics.Carousel
offset += spacing;
item.CarouselYPosition = offset;
// ensure there are no input gaps where clicking will fall through the carousel.
// notably, only do this where there's positive spacing between panels (negative spacing means they overlap already and there is no gap to fill).
if (spacing > 0)
{
item.CarouselInputLenienceAbove = spacing / 2;
if (previousVisible != null)
previousVisible.CarouselInputLenienceBelow = item.CarouselInputLenienceAbove;
}
if (item.IsVisible)
{
offset += item.DrawHeight;
@@ -628,15 +642,19 @@ namespace osu.Game.Graphics.Carousel
{
var item = carouselItems[i];
updateItemYPosition(item, ref lastVisible, ref yPos);
if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!))
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i);
if (CheckModelEquality(item.Model, currentSelection.Model!))
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i);
updateItemYPosition(item, ref lastVisible, ref yPos);
}
// Update the total height of all items (to make the scroll container scrollable through the full height even though
// most items are not displayed / loaded).
Scroll.SetLayoutHeight(yPos + visibleHalfHeight);
// If a keyboard selection is currently made, we want to keep the view stable around the selection.
// That means that we should offset the immediate scroll position by any change in Y position for the selection.
if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition)
@@ -671,6 +689,12 @@ namespace osu.Game.Graphics.Carousel
/// </summary>
private readonly Cached filterReusesPanels = new Cached();
/// <summary>
/// Scrolling to selection relies on <see cref="currentKeyboardSelection"/> being fully populated.
/// This flag ensures it runs after <see cref="refreshAfterSelection"/> validates this.
/// </summary>
private readonly Cached scrollToSelection = new Cached();
protected override void Update()
{
base.Update();
@@ -724,6 +748,22 @@ namespace osu.Game.Graphics.Carousel
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
c.Expanded.Value = c.Item.IsExpanded;
}
if (!filterAfterItemsChanged.IsValid && !IsFiltering)
FilterAsync();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!scrollToSelection.IsValid)
{
if (currentKeyboardSelection.YPosition != null)
Scroll.ScrollTo(currentKeyboardSelection.YPosition.Value - visibleHalfHeight + BleedTop);
scrollToSelection.Validate();
}
}
protected virtual float GetPanelXOffset(Drawable panel)
@@ -815,6 +855,8 @@ namespace osu.Game.Graphics.Carousel
throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}");
carouselPanel.Item = item;
carouselPanel.DrawYPosition = item.CarouselYPosition;
Scroll.Add(drawable);
}
@@ -823,6 +865,7 @@ namespace osu.Game.Graphics.Carousel
// To make transitions of items appearing in the flow look good, do a pass and make sure newly added items spawn from
// just beneath the *current interpolated position* of the previous panel.
var orderedPanels = Scroll.Panels
.Where(p => Scroll.ScreenSpaceDrawQuad.Intersects(p.ScreenSpaceDrawQuad))
.OfType<ICarouselPanel>()
.Where(p => p.Item != null)
.OrderBy(p => p.Item!.CarouselYPosition)
@@ -838,21 +881,9 @@ namespace osu.Game.Graphics.Carousel
// It's usually off-screen anyway.
if (i > 0 && i < orderedPanels.Count - 1)
panel.DrawYPosition = orderedPanels[i - 1].DrawYPosition;
else
panel.DrawYPosition = panel.Item!.CarouselYPosition;
}
}
}
// Update the total height of all items (to make the scroll container scrollable through the full height even though
// most items are not displayed / loaded).
if (carouselItems.Count > 0)
{
var lastItem = carouselItems[^1];
Scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight));
}
else
Scroll.SetLayoutHeight(0);
}
private void expirePanel(Drawable panel)
@@ -20,10 +20,27 @@ namespace osu.Game.Graphics.Carousel
/// <summary>
/// The current Y position in the carousel.
///
/// This is managed by <see cref="Carousel{T}"/> and should not be set manually.
/// </summary>
public double CarouselYPosition { get; set; }
/// <summary>
/// The amount of input padding/lenience to be added to the area above this panel.
/// Calculated as half of the calculated spacing between this panel and the panel above it.
///
/// This is managed by <see cref="Carousel{T}"/> and should not be set manually.
/// </summary>
public float CarouselInputLenienceAbove { get; set; }
/// <summary>
/// The amount of input padding/lenience to be added to the area below this panel.
/// Calculated as half of the calculated spacing between this panel and the panel below it.
///
/// This is managed by <see cref="Carousel{T}"/> and should not be set manually.
/// </summary>
public float CarouselInputLenienceBelow { get; set; }
/// <summary>
/// The height this item will take when displayed. Defaults to <see cref="DEFAULT_HEIGHT"/>.
/// </summary>
+147
View File
@@ -0,0 +1,147 @@
// 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.Runtime.InteropServices;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics
{
/// <summary>
/// A (very cute) animated version of the <see cref="FontAwesome.Solid.Ghost"/> icon.
/// </summary>
public partial class GhostIcon : Drawable
{
private IShader ghostShader = null!;
/// <summary>
/// How long one complete loop of the ghost's animation takes, in milliseconds
/// </summary>
public float AnimationDuration = 2000;
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
ghostShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "Ghost");
}
protected override void Update()
{
base.Update();
Invalidate(Invalidation.DrawNode);
}
protected override DrawNode CreateDrawNode() => new GhostIconDrawNode(this);
private class GhostIconDrawNode : DrawNode
{
protected new GhostIcon Source => (GhostIcon)base.Source;
public GhostIconDrawNode(IDrawable source)
: base(source)
{
}
private Quad screenSpaceDrawQuad;
private Vector4 drawRectangle;
private Vector2 blend;
private IShader shader = null!;
private float time;
public override void ApplyState()
{
base.ApplyState();
screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad;
drawRectangle = new Vector4(0, 0, Source.DrawWidth, Source.DrawHeight);
shader = Source.ghostShader;
blend = new Vector2(Math.Min(Source.DrawWidth, Source.DrawHeight) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height));
time = (float)(Source.Time.Current / Source.AnimationDuration) % 1f;
}
private IUniformBuffer<GhostParameters>? ghostParametersBuffer;
private IVertexBatch<TexturedVertex2D>? quadBatch;
protected override void Draw(IRenderer renderer)
{
base.Draw(renderer);
if (!renderer.BindTexture(renderer.WhitePixel))
return;
quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(1, 2);
ghostParametersBuffer ??= renderer.CreateUniformBuffer<GhostParameters>();
ghostParametersBuffer.Data = new GhostParameters
{
Time = time
};
shader.Bind();
shader.BindUniformBlock("m_GhostParameters", ghostParametersBuffer);
var vertexAction = quadBatch.AddAction;
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.BottomLeft,
TexturePosition = new Vector2(0, 1),
TextureRect = drawRectangle,
BlendRange = blend,
Colour = DrawColourInfo.Colour.BottomLeft.SRGB,
});
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.BottomRight,
TexturePosition = new Vector2(1, 1),
TextureRect = drawRectangle,
BlendRange = blend,
Colour = DrawColourInfo.Colour.BottomRight.SRGB,
});
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.TopRight,
TexturePosition = new Vector2(1, 0),
TextureRect = drawRectangle,
BlendRange = blend,
Colour = DrawColourInfo.Colour.TopRight.SRGB,
});
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.TopLeft,
TexturePosition = Vector2.Zero,
TextureRect = drawRectangle,
BlendRange = blend,
Colour = DrawColourInfo.Colour.TopLeft.SRGB,
});
shader.Unbind();
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private record struct GhostParameters
{
public UniformFloat Time;
private UniformPadding12 pad;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
ghostParametersBuffer?.Dispose();
quadBatch?.Dispose();
}
}
}
}
@@ -10,7 +10,6 @@ namespace osu.Game.Graphics.UserInterface
{
public partial class ShearedToggleButton : ShearedButton
{
private Sample? sampleClick;
private Sample? sampleOff;
private Sample? sampleOn;
@@ -43,9 +42,8 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleClick = audio.Samples.Get(@"UI/default-select");
sampleOn = audio.Samples.Get(@"UI/dropdown-open");
sampleOff = audio.Samples.Get(@"UI/dropdown-close");
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@@ -72,8 +70,6 @@ namespace osu.Game.Graphics.UserInterface
private void playSample()
{
sampleClick?.Play();
if (PlayToggleSamples)
{
if (Active.Value)
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
@@ -78,13 +77,9 @@ namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
Flow.Height = 25;
Flow.Margin = new MarginPadding { Horizontal = 10, };
AddRangeInternal(new Drawable[]
AddInternal(new BackgroundLayer(0.5f)
{
new BackgroundLayer(0.5f)
{
Depth = 1
},
new HoverClickSounds(),
Depth = 1
});
Flow.Add(new SpriteIcon
@@ -34,6 +34,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
osuHeader.Dropdown = this;
osuHeader.LeftSideLabel = label;
}
AddInternal(new HoverClickSounds());
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
+3
View File
@@ -1306,6 +1306,9 @@ namespace osu.Game
private void handleBackButton()
{
// TODO: this is SUPER SUPER bad.
// It can potentially exit the wrong screen if screens are not loaded yet.
// ScreenFooter / ScreenBackButton should be aware of which screen it is currently being handled by.
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) return;
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton())) ScreenStack.Exit();
@@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => true;
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
-1
View File
@@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Restricted view area.";
public override bool Ranked => UsesDefaultConfiguration;
public override bool ValidForFreestyleAsRequiredMod => true;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public abstract BindableFloat SizeMultiplier { get; }
-1
View File
@@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModHidden;
public override ModType Type => ModType.DifficultyIncrease;
public override bool Ranked => UsesDefaultConfiguration;
public override bool ValidForFreestyleAsRequiredMod => true;
public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
@@ -58,6 +58,8 @@ namespace osu.Game.Screens.Footer
set => text.Text = value;
}
private readonly Container shearedContent;
private readonly SpriteText text;
private readonly SpriteIcon icon;
@@ -77,7 +79,7 @@ namespace osu.Game.Screens.Footer
Children = new Drawable[]
{
new Container
shearedContent = new Container
{
EdgeEffect = new EdgeEffectParameters
{
@@ -170,8 +172,8 @@ namespace osu.Game.Screens.Footer
FinishTransforms(true);
}
// use Content for tracking input as some buttons might be temporarily hidden with DisappearToBottom, and they become hidden by moving Content away from screen.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos);
// account for shear and buttons temporarily hidden with DisappearToBottom.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => shearedContent.ReceivePositionalInputAt(screenSpacePos);
public GlobalAction? Hotkey;
+94 -12
View File
@@ -14,11 +14,13 @@ 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.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -27,6 +29,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
@@ -39,11 +42,11 @@ using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Seasonal;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Screens.Menu
{
@@ -90,6 +93,8 @@ namespace osu.Game.Screens.Menu
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
private InputManager inputManager;
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false;
@@ -155,7 +160,7 @@ namespace osu.Game.Screens.Menu
{
skinEditor?.Show();
},
OnSolo = loadSoloSongSelect,
OnSolo = loadPreferredSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()),
OnPlaylists = () => this.Push(new Playlists()),
OnDailyChallenge = room =>
@@ -236,18 +241,23 @@ namespace osu.Game.Screens.Menu
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh");
loadSongSelectV2Samples(audio);
}
protected override void Update()
{
base.Update();
updateSongSelectV2HoldState();
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
private void loadSoloSongSelect()
{
if (GetContainingInputManager()!.CurrentState.Keyboard.ControlPressed)
this.Push(new SoloSongSelect());
else
this.Push(new PlaySongSelect());
}
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
@@ -453,7 +463,7 @@ namespace osu.Game.Screens.Menu
Beatmap.Value = beatmap;
Ruleset.Value = ruleset;
Schedule(loadSoloSongSelect);
Schedule(loadPreferredSongSelect);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@@ -477,6 +487,78 @@ namespace osu.Game.Screens.Menu
{
}
#region TEMPORARY: Song Select v2 easter egg
private const double required_hold_time = 500;
private double holdTime;
private bool ssv2Expanded;
private IDisposable ssv2Duck;
private Sample ssv2Sample;
private void loadPreferredSongSelect()
{
if (holdTime >= required_hold_time)
{
ssv2Sample?.Play();
this.Push(new SoloSongSelect());
}
else
this.Push(new PlaySongSelect());
}
private void loadSongSelectV2Samples(AudioManager audio)
{
ssv2Sample = audio.Samples.Get(@"UI/bss-complete");
}
private void updateSongSelectV2HoldState()
{
if (Buttons.State == ButtonSystemState.Play &&
inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) &&
inputManager.HoveredDrawables.Any(h => h is OsuLogo || (h is MainMenuButton b && b.TriggerKeys.Contains(Key.P))))
holdTime += Time.Elapsed;
else
{
var transformTarget = Game.ChildrenOfType<ScalingContainer>().First();
transformTarget.ScaleTo(1, 200, Easing.OutQuint)
.RotateTo(0, 200, Easing.OutQuint)
.FadeColour(OsuColour.Gray(1f), 200, Easing.OutQuint);
ssv2Duck?.Dispose();
ssv2Duck = null;
ssv2Expanded = false;
holdTime = 0;
}
if (holdTime >= required_hold_time && !ssv2Expanded)
{
var transformTarget = Game.ChildrenOfType<ScalingContainer>().First();
transformTarget.Anchor = Anchor.Centre;
transformTarget.Origin = Anchor.Centre;
transformTarget.ScaleTo(1.2f, 5000, Easing.OutPow10)
.RotateTo(2, 5000, Easing.OutPow10)
.FadeColour(Color4.BlueViolet, 10000, Easing.OutPow10);
ssv2Duck = musicController.Duck(new DuckParameters
{
DuckDuration = 2000,
DuckVolumeTo = 0.8f,
DuckCutoffTo = 500,
DuckEasing = Easing.OutQuint,
RestoreDuration = 200,
RestoreEasing = Easing.OutQuint
});
ssv2Expanded = true;
}
}
#endregion
private partial class MobileDisclaimerDialog : PopupDialog
{
public MobileDisclaimerDialog(Action confirmed)
@@ -2,31 +2,45 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class DefaultRankDisplay : Container, ISerialisableDrawable
public partial class DefaultRankDisplay : CompositeDrawable, ISerialisableDrawable
{
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
public bool UsesFixedAnchor { get; set; }
private readonly UpdateableRank rank;
private UpdateableRank rankDisplay = null!;
private SkinnableSound rankDownSample = null!;
private SkinnableSound rankUpSample = null!;
private IBindable<ScoreRank> rank = null!;
public DefaultRankDisplay()
{
Size = new Vector2(70, 35);
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
rank = new UpdateableRank(Scoring.ScoreRank.X)
rankDownSample = new SkinnableSound(new SampleInfo("Gameplay/rank-down")),
rankUpSample = new SkinnableSound(new SampleInfo("Gameplay/rank-up")),
rankDisplay = new UpdateableRank(ScoreRank.X)
{
RelativeSizeAxes = Axes.Both
},
@@ -37,9 +51,20 @@ namespace osu.Game.Screens.Play.HUD
{
base.LoadComplete();
rank.Rank = scoreProcessor.Rank.Value;
rank = scoreProcessor.Rank.GetBoundCopy();
rank.BindValueChanged(r =>
{
// Don't play rank-down sfx on quit/retry
if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F)
{
if (r.NewValue > rankDisplay.Rank)
rankUpSample.Play();
else
rankDownSample.Play();
}
scoreProcessor.Rank.BindValueChanged(v => rank.Rank = v.NewValue);
rankDisplay.Rank = r.NewValue;
}, true);
}
}
}
}
@@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -34,10 +35,12 @@ namespace osu.Game.Screens.Select.Leaderboards
isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
List<GameplayLeaderboardScore> newScores = new List<GameplayLeaderboardScore>();
if (globalScores != null)
{
foreach (var topScore in globalScores.AllScores.OrderByTotalScore())
scores.Add(new GameplayLeaderboardScore(topScore, false));
newScores.Add(new GameplayLeaderboardScore(topScore, false));
}
if (gameplayState != null)
@@ -48,9 +51,11 @@ namespace osu.Game.Screens.Select.Leaderboards
TotalScoreTiebreaker = long.MaxValue
};
localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
scores.Add(localScore);
newScores.Add(localScore);
}
scores.AddRange(newScores);
Scheduler.AddDelayed(sort, 1000, true);
}
+35 -26
View File
@@ -13,6 +13,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Threading;
using osu.Framework.Utils;
@@ -77,7 +78,8 @@ namespace osu.Game.Screens.SelectV2
}
else
{
if (top == CurrentSelectionItem || bottom == CurrentSelectionItem)
// `CurrentSelectionItem` cannot be used here because it may not be correctly set yet.
if (CurrentSelection != null && (CheckModelEquality(top.Model, CurrentSelection) || CheckModelEquality(bottom.Model, CurrentSelection)))
return SPACING * 2;
}
@@ -103,20 +105,20 @@ namespace osu.Game.Screens.SelectV2
private void load(BeatmapStore beatmapStore, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken)
{
setupPools();
setupBeatmaps(beatmapStore, cancellationToken);
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
loadSamples(audio);
config.BindWith(OsuSetting.RandomSelectAlgorithm, randomAlgorithm);
}
#region Beatmap source hookup
private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
protected override void LoadComplete()
{
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
base.LoadComplete();
detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true);
}
#region Beatmap source hookup
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
{
// TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider.
@@ -218,13 +220,7 @@ namespace osu.Game.Screens.SelectV2
return;
case BeatmapSetInfo setInfo:
// Selecting a set isn't valid let's re-select the first visible difficulty.
if (grouping.SetItems.TryGetValue(setInfo, out var items))
{
var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
RequestRecommendedSelection(beatmaps);
}
selectRecommendedDifficultyForBeatmapSet(setInfo);
return;
case BeatmapInfo beatmapInfo:
@@ -260,7 +256,9 @@ namespace osu.Game.Screens.SelectV2
if (containingGroup != null)
setExpandedGroup(containingGroup);
setExpandedSet(beatmapInfo);
if (grouping.BeatmapSetsGroupedTogether)
setExpandedSet(beatmapInfo);
break;
}
}
@@ -283,6 +281,16 @@ namespace osu.Game.Screens.SelectV2
setExpandedGroup(groupForReselection);
}
private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet)
{
// Selecting a set isn't valid let's re-select the first visible difficulty.
if (grouping.SetItems.TryGetValue(beatmapSet, out var items))
{
var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
RequestRecommendedSelection(beatmaps);
}
}
/// <summary>
/// If we don't have a selection and there's a single beatmap set returned, select it for the user.
/// </summary>
@@ -310,7 +318,12 @@ namespace osu.Game.Screens.SelectV2
}
}
RequestRecommendedSelection(items.Select(i => i.Model).OfType<BeatmapInfo>());
var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
if (beatmaps.Any(b => b.Equals(CurrentSelection as BeatmapInfo)))
return;
RequestRecommendedSelection(beatmaps);
}
protected override bool CheckValidForGroupSelection(CarouselItem item)
@@ -488,7 +501,7 @@ namespace osu.Game.Screens.SelectV2
private ScheduledDelegate? loadingDebounce;
public void Filter(FilterCriteria criteria)
public void Filter(FilterCriteria criteria, bool showLoadingImmediately = false)
{
bool resetDisplay = grouping.BeatmapSetsGroupedTogether != BeatmapCarouselFilterGrouping.ShouldGroupBeatmapsTogether(criteria);
@@ -496,9 +509,12 @@ namespace osu.Game.Screens.SelectV2
loadingDebounce ??= Scheduler.AddDelayed(() =>
{
if (loading.State.Value == Visibility.Visible)
return;
Scroll.FadeColour(OsuColour.Gray(0.5f), 1000, Easing.OutQuint);
loading.Show();
}, 250);
}, showLoadingImmediately ? 0 : 250);
FilterAsync(resetDisplay).ContinueWith(_ => Schedule(() =>
{
@@ -602,16 +618,9 @@ namespace osu.Game.Screens.SelectV2
if (carouselItems?.Any() != true)
return false;
// If set grouping is available, this is the fastest way to retrieve sets for randomisation.
// This is the fastest way to retrieve sets for randomisation.
ICollection<BeatmapSetInfo> visibleSets = grouping.SetItems.Keys;
// If not, we need to do an expensive copy.
//
// There's probably a more efficient way to handle this. Maybe the grouping filter should always expose grouped sets regardless
// as that process is done asynchronously.
if (!visibleSets.Any())
visibleSets = carouselItems.Select(i => i.Model).OfType<BeatmapInfo>().Select(b => b.BeatmapSet!).Distinct().ToList();
if (CurrentSelection is BeatmapInfo beatmapInfo)
{
randomSelectedBeatmaps.Add(beatmapInfo);
@@ -643,7 +652,7 @@ namespace osu.Game.Screens.SelectV2
if (CurrentSelectionItem != null)
playSpinSample(distanceBetween(carouselItems.First(i => !ReferenceEquals(i.Model, set)), CurrentSelectionItem), visibleSets.Count);
RequestRecommendedSelection(set.Beatmaps.Where(b => !b.Hidden));
selectRecommendedDifficultyForBeatmapSet(set);
return true;
}
@@ -76,15 +76,18 @@ namespace osu.Game.Screens.SelectV2
{
var beatmap = (BeatmapInfo)item.Model;
bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
if (newBeatmapSet)
{
if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems))
setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
}
if (BeatmapSetsGroupedTogether)
{
bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
if (newBeatmapSet)
{
if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems))
setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
if (groupItem != null)
groupItem.NestedItemCount++;
@@ -114,7 +117,7 @@ namespace osu.Game.Screens.SelectV2
currentGroupItems?.Add(i);
currentSetItems?.Add(i);
i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || currentSetItems == null));
i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || !BeatmapSetsGroupedTogether));
}
}
@@ -56,9 +56,7 @@ namespace osu.Game.Screens.SelectV2
private static bool checkCriteriaMatch(BeatmapInfo beatmap, FilterCriteria criteria)
{
bool match = criteria.Ruleset == null ||
beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName ||
(beatmap.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps);
bool match = criteria.Ruleset == null || beatmap.AllowGameplayWithRuleset(criteria.Ruleset!, criteria.AllowConvertedBeatmaps);
if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
{
@@ -3,12 +3,15 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
@@ -68,6 +71,8 @@ namespace osu.Game.Screens.SelectV2
protected partial class TabItem : TabItem<T>
{
private Sample? selectSample;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -78,7 +83,7 @@ namespace osu.Game.Screens.SelectV2
{
AutoSizeAxes = Axes.Both;
Children = new[]
Children = new Drawable[]
{
Text = new OsuSpriteText
{
@@ -87,15 +92,24 @@ namespace osu.Game.Screens.SelectV2
Text = value.ToString(),
Font = OsuFont.Style.Body,
},
new HoverSounds(HoverSampleSet.TabSelect)
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
selectSample = audio.Samples.Get(@"UI/tabselect-select");
}
protected override void LoadComplete()
{
base.LoadComplete();
updateDisplay();
}
protected override void OnActivatedByUser() => selectSample?.Play();
protected override void OnActivated() => updateDisplay();
protected override void OnDeactivated() => updateDisplay();
@@ -370,6 +370,7 @@ namespace osu.Game.Screens.SelectV2
},
new TrianglesV2
{
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -75,7 +76,7 @@ namespace osu.Game.Screens.SelectV2
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
private const float personal_best_height = 100;
private const float personal_best_height = 112;
[BackgroundDependencyLoader]
private void load()
@@ -192,33 +193,40 @@ namespace osu.Game.Screens.SelectV2
private bool initialFetchComplete;
private ScheduledDelegate? refetchOperation;
private void refetchScores()
{
SetScores(Array.Empty<ScoreInfo>());
if (beatmap.IsDefault)
refetchOperation?.Cancel();
refetchOperation = Scheduler.AddDelayed(() =>
{
SetState(LeaderboardState.NoneSelected);
return;
}
SetScores(Array.Empty<ScoreInfo>());
SetState(LeaderboardState.Retrieving);
if (beatmap.IsDefault)
{
SetState(LeaderboardState.NoneSelected);
return;
}
var fetchBeatmapInfo = beatmap.Value.BeatmapInfo;
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
SetState(LeaderboardState.Retrieving);
// For now, we forcefully refresh to keep things simple.
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
// (like returning from gameplay after setting a new score, returning to song select after main menu).
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true);
var fetchBeatmapInfo = beatmap.Value.BeatmapInfo;
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
if (!initialFetchComplete)
{
// only bind this after the first fetch to avoid reading stale scores.
fetchedScores.BindTo(leaderboardManager.Scores);
fetchedScores.BindValueChanged(_ => updateScores(), true);
initialFetchComplete = true;
}
// For now, we forcefully refresh to keep things simple.
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
// (like returning from gameplay after setting a new score, returning to song select after main menu).
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null),
forceRefresh: true);
if (!initialFetchComplete)
{
// only bind this after the first fetch to avoid reading stale scores.
fetchedScores.BindTo(leaderboardManager.Scores);
fetchedScores.BindValueChanged(_ => updateScores(), true);
initialFetchComplete = true;
}
}, initialFetchComplete ? 200 : 0);
}
private void updateScores()
@@ -54,7 +54,7 @@ namespace osu.Game.Screens.SelectV2
private ILinkHandler? linkHandler { get; set; }
[Resolved]
private SongSelect? songSelect { get; set; }
private ISongSelect? songSelect { get; set; }
[BackgroundDependencyLoader]
private void load()
@@ -165,7 +165,7 @@ namespace osu.Game.Screens.SelectV2
Colour = colourProvider.Background4,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
},
new HoverClickSounds(HoverSampleSet.Button),
new HoverClickSounds(),
};
}
@@ -194,7 +194,7 @@ namespace osu.Game.Screens.SelectV2
public partial class TagsOverflowPopover : OsuPopover
{
private readonly string[] tags;
private readonly SongSelect? songSelect;
private readonly ISongSelect? songSelect;
public TagsOverflowPopover(string[] tags, SongSelect? songSelect)
{
@@ -64,7 +64,7 @@ namespace osu.Game.Screens.SelectV2
private Statistic bpmStatistic = null!;
[Resolved]
private SongSelect? songSelect { get; set; }
private ISongSelect? songSelect { get; set; }
[Resolved]
private LocalisationManager localisation { get; set; } = null!;
@@ -147,7 +147,8 @@ namespace osu.Game.Screens.SelectV2
new ShearAligningWrapper(statisticsFlow = new FillFlowContainer
{
Shear = -OsuGame.SHEAR,
AutoSizeAxes = Axes.Both,
AutoSizeAxes = Axes.X,
Height = 30,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0f),
Children = new Drawable[]
@@ -96,7 +96,7 @@ namespace osu.Game.Screens.SelectV2
Shear = -OsuGame.SHEAR,
AlwaysPresent = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Height = 20,
Margin = new MarginPadding { Vertical = 5f },
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
@@ -159,7 +159,7 @@ namespace osu.Game.Screens.SelectV2
{
Shear = -OsuGame.SHEAR,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Height = 53,
Padding = new MarginPadding { Bottom = border_weight, Right = border_weight },
Child = new Container
{
@@ -137,7 +137,7 @@ namespace osu.Game.Screens.SelectV2
return;
float flowWidth = statisticsFlow[0].Width * statisticsFlow.Count + statisticsFlow.Spacing.X * (statisticsFlow.Count - 1);
bool tiny = !autoSize && DrawWidth < flowWidth;
bool tiny = !autoSize && DrawWidth < flowWidth - 20;
if (displayedTinyStatistics != tiny)
{
@@ -179,7 +179,11 @@ namespace osu.Game.Screens.SelectV2
}
else
{
statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty { Value = d });
statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty
{
AccentColour = accentColour,
Value = d
});
updateStatisticsSizing();
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ namespace osu.Game.Screens.SelectV2
// taken from draw visualiser. used for carousel alignment purposes.
public const float HEIGHT_FROM_SCREEN_TOP = 141 - corner_radius;
private const float corner_radius = 8;
private const float corner_radius = 10;
private SongSelectSearchTextBox searchTextBox = null!;
private ShearedToggleButton showConvertedBeatmapsButton = null!;
+7 -1
View File
@@ -29,10 +29,16 @@ namespace osu.Game.Screens.SelectV2
void ManageCollections();
/// <summary>
/// Present the provided score at the results screen.
/// Opens results screen with the given score.
/// This assumes active beatmap and ruleset selection matches the score.
/// </summary>
void PresentScore(ScoreInfo score);
/// <summary>
/// Set the current filter text query to the provided string.
/// </summary>
void Search(string query);
/// <summary>
/// Gets relevant actionable items for beatmap context menus, based on the type of song select.
/// </summary>
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.SelectV2
private LinkFlowContainer textFlow = null!;
private SpriteIcon icon = null!;
private GhostIcon icon = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
@@ -71,13 +71,16 @@ namespace osu.Game.Screens.SelectV2
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
icon = new SpriteIcon
new Container
{
Icon = FontAwesome.Solid.Ghost,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(10),
Size = new Vector2(50),
Child = icon = new GhostIcon
{
RelativeSizeAxes = Axes.Both,
},
},
new OsuSpriteText
{
@@ -101,6 +104,17 @@ namespace osu.Game.Screens.SelectV2
};
}
protected override void LoadComplete()
{
base.LoadComplete();
icon.Loop(t =>
t.MoveToY(-10, 2000, Easing.InOutSine)
.Then()
.MoveToY(0, 2000, Easing.InOutSine)
);
}
protected override void PopIn()
{
this.FadeIn(600, Easing.OutQuint);
@@ -121,9 +135,6 @@ namespace osu.Game.Screens.SelectV2
this.ScaleTo(0.9f)
.ScaleTo(1f, 1000, Easing.OutQuint);
icon.ScaleTo(new Vector2(-1, 1))
.ScaleTo(new Vector2(1, 1), 500, Easing.InOutSine);
textFlow.FadeInFromZero(800, Easing.OutQuint);
textFlow.Clear();
+90 -17
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,8 +14,10 @@ using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
@@ -37,10 +40,13 @@ namespace osu.Game.Screens.SelectV2
private Container backgroundLayerHorizontalPadding = null!;
private Container backgroundContainer = null!;
private Container iconContainer = null!;
private Box activationFlash = null!;
private Box hoverLayer = null!;
private Box keyboardSelectionLayer = null!;
private Box selectionLayer = null!;
private Drawable activationFlash = null!;
private Drawable hoverLayer = null!;
private Drawable keyboardSelectionLayer = null!;
private PulsatingBox selectionLayer = null!;
public Container TopLevelContent { get; private set; } = null!;
@@ -68,10 +74,22 @@ namespace osu.Game.Screens.SelectV2
}
}
// content is offset by PanelXOffset, make sure we only handle input at the actual visible
// offset region.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
TopLevelContent.ReceivePositionalInputAt(screenSpacePos);
public sealed override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
if (item == null)
return TopLevelContent.ReceivePositionalInputAt(screenSpacePos);
var inputRectangle = TopLevelContent.DrawRectangle;
// Cover the gaps introduced by the spacing between panels so that user mis-aims don't result in no-ops.
inputRectangle = inputRectangle.Inflate(new MarginPadding
{
Top = item.CarouselInputLenienceAbove,
Bottom = item.CarouselInputLenienceBelow,
});
return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos));
}
[Resolved]
private BeatmapCarousel? carousel { get; set; }
@@ -97,7 +115,7 @@ namespace osu.Game.Screens.SelectV2
Hollow = true,
Radius = 2,
},
Children = new Drawable[]
Children = new[]
{
new BufferedContainer
{
@@ -150,11 +168,11 @@ namespace osu.Game.Screens.SelectV2
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
},
selectionLayer = new Box
selectionLayer = new PulsatingBox
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Width = 0.6f,
Width = 0.8f,
Blending = BlendingParameters.Additive,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -162,7 +180,7 @@ namespace osu.Game.Screens.SelectV2
keyboardSelectionLayer = new Box
{
Alpha = 0,
Colour = colourProvider.Highlight1.Opacity(0.1f),
Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1.Opacity(0.1f), colourProvider.Highlight1.Opacity(0.4f)),
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
},
@@ -180,6 +198,51 @@ namespace osu.Game.Screens.SelectV2
backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4);
}
public partial class PulsatingBox : BeatSyncedContainer
{
public double FlashOffset;
private readonly Box box;
public PulsatingBox()
{
EarlyActivationMilliseconds = 50;
InternalChildren = new Drawable[]
{
box = new Box
{
RelativeSizeAxes = Axes.Both,
},
};
}
private int separation = 1;
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (beatIndex % separation != 0)
return;
double length = timingPoint.BeatLength;
separation = 1;
while (length < 500)
{
length *= 2;
separation *= 2;
}
box
.Delay(FlashOffset)
.FadeTo(0.8f, length / 6, Easing.Out)
.Then()
.FadeTo(0.4f, length, Easing.Out);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
@@ -199,7 +262,11 @@ namespace osu.Game.Screens.SelectV2
KeyboardSelected.BindValueChanged(selected =>
{
if (selected.NewValue)
keyboardSelectionLayer.FadeIn(100, Easing.OutQuint);
{
keyboardSelectionLayer.FadeIn(80, Easing.Out)
.Then()
.FadeTo(0.5f, 2000, Easing.OutQuint);
}
else
keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint);
@@ -211,8 +278,14 @@ namespace osu.Game.Screens.SelectV2
{
base.PrepareForUse();
// Slightly offset the flash animation based on the panel depth.
// This assumes a minimum depth of -2 (groups).
selectionLayer.FlashOffset = (2 + Item!.DepthLayer) * 50;
updateAccentColour();
updateXOffset();
updateXOffset(animated: false);
updateSelectedState(animated: false);
this.FadeIn(DURATION, Easing.OutQuint);
}
@@ -257,13 +330,13 @@ namespace osu.Game.Screens.SelectV2
selectionLayer.FadeOut(200, Easing.OutQuint);
}
private void updateXOffset()
private void updateXOffset(bool animated = true)
{
float x = PanelXOffset + corner_radius;
if (!Expanded.Value && !Selected.Value)
{
if (this is PanelBeatmap)
if (this is PanelBeatmap || this is PanelBeatmapStandalone)
x += active_x_offset * 2;
else
x += active_x_offset * 4;
@@ -272,7 +345,7 @@ namespace osu.Game.Screens.SelectV2
if (!KeyboardSelected.Value)
x += active_x_offset;
TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint);
TopLevelContent.MoveToX(x, animated ? DURATION : 0, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
-13
View File
@@ -72,19 +72,6 @@ namespace osu.Game.Screens.SelectV2
PanelXOffset = 60;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
var inputRectangle = TopLevelContent.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(TopLevelContent.ToLocalSpace(screenSpacePos));
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@@ -71,22 +71,6 @@ namespace osu.Game.Screens.SelectV2
private OsuSpriteText authorText = null!;
private FillFlowContainer mainFill = null!;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
var inputRectangle = TopLevelContent.DrawRectangle;
if (Selected.Value)
{
// 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 * 2 });
}
return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos));
}
public PanelBeatmapStandalone()
{
PanelXOffset = 20;
+137 -68
View File
@@ -1,101 +1,170 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.PolygonExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class PanelSetBackground : ModelBackedDrawable<WorkingBeatmap>
public partial class PanelSetBackground : BufferedContainer
{
protected override double TransformDuration => 400;
[Resolved]
private BeatmapCarousel? beatmapCarousel { get; set; }
private Sprite? sprite;
private WorkingBeatmap? working;
private CancellationTokenSource? loadCancellation;
private double timeSinceUnpool;
public WorkingBeatmap? Beatmap
{
get => Model;
set => Model = value;
get => working;
set
{
if (value == working)
return;
working = value;
loadCancellation?.Cancel();
loadCancellation = null;
sprite?.Expire();
sprite = null;
timeSinceUnpool = 0;
}
}
protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model);
private partial class BackgroundSprite : CompositeDrawable
public PanelSetBackground()
// TODO: for performance reasons we may want this to be true.
// Setting to true will require that the buffered portion is moved to a child such that `FadeIn`/`FadeOut` transforms
// still work.
: base(cachedFrameBuffer: false)
{
private readonly WorkingBeatmap? working;
RelativeSizeAxes = Axes.Both;
}
public BackgroundSprite(WorkingBeatmap? working)
protected override void Update()
{
base.Update();
loadContentIfRequired();
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
this.working = working;
new FillFlowContainer
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
Shear = new Vector2(0.8f, 0),
Children = new[]
{
// The left half with no gradient applied
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
Width = 0.4f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0.3f)),
Width = 0.2f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0.2f)),
// Slightly more than 1.0 in total to account for shear.
Width = 0.45f,
},
}
},
};
}
RelativeSizeAxes = Axes.Both;
private void loadContentIfRequired()
{
// A load is already in progress if the cancellation token is non-null.
if (loadCancellation != null || working == null)
return;
if (beatmapCarousel != null)
{
Quad containingSsdq = beatmapCarousel.ScreenSpaceDrawQuad;
// One may ask why we are not using `DelayedLoadWrapper` for this delayed load logic.
//
// - Using `DelayedLoadWrapper` would only allow us to load content when on screen, but we want to preload while panels are off-screen.
// This allows a more seamless experience when a user is scrolling at a moderate speed, as we are loading in backgrounds before they
// enter the visible viewport.
// - By using a slightly customised formula to decide when to start the load, we can coerce the loading of backgrounds into an order that
// prioritises panels which are closest to the centre of the screen. Basically, we want to load backgrounds "outwards" from the visual
// centre to give the user the best experience possible.
float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100;
timeSinceUnpool += Time.Elapsed;
// We only trigger a load after this set has been in an updating state for a set amount of time.
if (timeSinceUnpool <= timeUpdatingBeforeLoad)
return;
}
loadCancellation = new CancellationTokenSource();
LoadComponentAsync(new PanelBeatmapBackground(working)
{
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
}, s =>
{
AddInternal(sprite = s);
bool spriteOnScreen = beatmapCarousel?.ScreenSpaceDrawQuad.Intersects(sprite.ScreenSpaceDrawQuad) != false;
sprite.FadeInFromZero(spriteOnScreen ? 400 : 0, Easing.OutQuint);
}, loadCancellation.Token);
}
public partial class PanelBeatmapBackground : Sprite
{
private readonly IWorkingBeatmap working;
public PanelBeatmapBackground(IWorkingBeatmap working)
{
ArgumentNullException.ThrowIfNull(working);
this.working = working;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
private void load()
{
var texture = working?.GetPanelBackground();
if (texture != null)
{
InternalChildren = new Drawable[]
{
new Sprite
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
Texture = texture,
},
new FillFlowContainer
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
Shear = new Vector2(0.8f, 0),
Children = new[]
{
// The left half with no gradient applied
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
Width = 0.4f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0.3f)),
Width = 0.2f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0.2f)),
// Slightly more than 1.0 in total to account for shear.
Width = 0.45f,
},
}
},
};
}
else
{
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
};
}
Texture = working.GetPanelBackground();
}
}
}
+7 -14
View File
@@ -58,7 +58,7 @@ namespace osu.Game.Screens.SelectV2
public override IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap)
{
yield return new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => SelectAndStart(beatmap)) { Icon = FontAwesome.Solid.Check };
yield return new OsuMenuItem(ButtonSystemStrings.Play.ToSentence(), MenuItemType.Highlighted, () => SelectAndRun(beatmap, OnStart)) { Icon = FontAwesome.Solid.Check };
yield return new OsuMenuItem(ButtonSystemStrings.Edit.ToSentence(), MenuItemType.Standard, () => edit(beatmap)) { Icon = FontAwesome.Solid.PencilAlt };
yield return new OsuMenuItemSpacer();
@@ -85,13 +85,9 @@ namespace osu.Game.Screens.SelectV2
yield return new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => beatmaps.Hide(beatmap));
}
protected override bool OnStart()
protected override void OnStart()
{
if (playerLoader != null) return false;
if (!this.IsCurrentScreen()) return false;
if (Beatmap.IsDefault) return false;
FinaliseSelection();
if (playerLoader != null) return;
modsAtGameplayStart = Mods.Value;
@@ -106,7 +102,7 @@ namespace osu.Game.Screens.SelectV2
{
Text = NotificationsStrings.NoAutoplayMod
});
return false;
return;
}
var mods = Mods.Value.Append(autoInstance).ToArray();
@@ -120,7 +116,6 @@ namespace osu.Game.Screens.SelectV2
sampleConfirmSelection?.Play();
this.Push(playerLoader = new PlayerLoader(createPlayer));
return true;
Player createPlayer()
{
@@ -143,12 +138,10 @@ namespace osu.Game.Screens.SelectV2
private void edit(BeatmapInfo beatmap)
{
FinaliseSelection();
if (!this.IsCurrentScreen())
return;
// Forced refetch is important here to guarantee correct invalidation across all difficulties.
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true);
this.Push(new EditorLoader());
SelectAndRun(beatmap, () => this.Push(new EditorLoader()));
}
public override void OnResuming(ScreenTransitionEvent e)
+237 -132
View File
@@ -92,6 +92,7 @@ namespace osu.Game.Screens.SelectV2
private BeatmapTitleWedge titleWedge = null!;
private BeatmapDetailsArea detailsArea = null!;
private FillFlowContainer wedgesContainer = null!;
private Box rightGradientBackground = null!;
private NoResultsPlaceholder noResultsPlaceholder = null!;
@@ -132,8 +133,8 @@ namespace osu.Game.Screens.SelectV2
new Box
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0f)),
Width = 0.6f,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0f)),
},
new Container
{
@@ -204,8 +205,10 @@ namespace osu.Game.Screens.SelectV2
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
rightGradientBackground = new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.0f), Color4.Black.Opacity(0.5f)),
RelativeSizeAxes = Axes.Both,
},
@@ -224,7 +227,7 @@ namespace osu.Game.Screens.SelectV2
BleedTop = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5,
BleedBottom = ScreenFooter.HEIGHT + 5,
RelativeSizeAxes = Axes.Both,
RequestPresentBeatmap = _ => OnStart(),
RequestPresentBeatmap = b => SelectAndRun(b, OnStart),
RequestSelection = selectBeatmap,
RequestRecommendedSelection = selectRecommendedBeatmap,
NewItemsPresented = newItemsPresented,
@@ -260,10 +263,11 @@ namespace osu.Game.Screens.SelectV2
}
/// <summary>
/// Called when a selection is made.
/// Called when a selection is made to progress away from the song select screen.
///
/// This is the default action which should be provided to <see cref="SelectAndRun"/>.
/// </summary>
/// <returns>If a resultant action occurred that takes the user away from SongSelect.</returns>
protected abstract bool OnStart();
protected abstract void OnStart();
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => new ScreenFooterButton[]
{
@@ -292,13 +296,24 @@ namespace osu.Game.Screens.SelectV2
modSelectOverlay.State.BindValueChanged(v =>
{
Debug.Assert(this.IsCurrentScreen());
if (!this.IsCurrentScreen())
return;
logo?.ScaleTo(v.NewValue == Visibility.Visible ? 0f : logo_scale, 400, Easing.OutQuint)
.FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint);
});
Beatmap.BindValueChanged(_ => updateSelection());
Beatmap.BindValueChanged(_ =>
{
if (!this.IsCurrentScreen())
return;
ensureGlobalBeatmapValid();
ensurePlayingSelected(true);
updateBackgroundDim();
updateWedgeVisibility();
});
}
protected override void Update()
@@ -319,7 +334,7 @@ namespace osu.Game.Screens.SelectV2
/// Ensures some music is playing for the current track.
/// Will resume playback from a manual user pause if the track has changed.
/// </summary>
private void ensurePlayingSelected()
private void ensurePlayingSelected(bool restart)
{
if (!ControlGlobalMusic)
return;
@@ -331,7 +346,7 @@ namespace osu.Game.Screens.SelectV2
if (!track.IsRunning && (music.UserPauseRequested != true || isNewTrack))
{
Logger.Log($"Song select decided to {nameof(ensurePlayingSelected)}");
music.Play(true);
music.Play(restart);
}
lastTrack.SetTarget(track);
@@ -367,21 +382,57 @@ namespace osu.Game.Screens.SelectV2
private void ensureTrackLooping(IWorkingBeatmap beatmap, TrackChangeDirection changeDirection)
=> beatmap.PrepareTrackForPreview(true);
private IDisposable? trackDuck;
private void attachTrackDuckingIfShould()
{
bool shouldDuck = noResultsPlaceholder.State.Value == Visibility.Visible;
if (shouldDuck && trackDuck == null)
trackDuck = music.Duck(new DuckParameters { DuckVolumeTo = 1, DuckCutoffTo = 500 });
}
private void detachTrackDucking()
{
trackDuck?.Dispose();
trackDuck = null;
}
#endregion
#region Selection handling
/// <summary>
/// Immediately flush any pending selection. Should be run before performing final actions such as leaving the screen.
/// </summary>
protected void FinaliseSelection()
{
if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting)
selectionDebounce.RunTask();
}
private ScheduledDelegate? selectionDebounce;
/// <summary>
/// Finalises selection on the given <see cref="BeatmapInfo"/> and runs the provided action if possible.
/// </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)
{
selectionDebounce?.Cancel();
if (!this.IsCurrentScreen())
return;
// `ensureGlobalBeatmapValid` also performs this checks, but it will change the active selection on fail.
// By checking locally first, we can correctly perform a no-op rather than changing selection.
if (!checkBeatmapValidForSelection(beatmap, carousel.Criteria))
return;
// Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific).
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true);
if (Beatmap.IsDefault)
return;
if (!ensureGlobalBeatmapValid())
return;
startAction();
}
private void selectRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
{
selectBeatmap(difficultyRecommender?.GetRecommendedBeatmap(beatmaps) ?? beatmaps.First());
@@ -389,37 +440,65 @@ namespace osu.Game.Screens.SelectV2
private void selectBeatmap(BeatmapInfo beatmap)
{
if (beatmap.BeatmapSet!.Protected)
if (!this.IsCurrentScreen())
return;
carousel.CurrentSelection = beatmap;
// Debounce consideration is to avoid beatmap churn on key repeat selection.
selectionDebounce?.Cancel();
selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE);
}
private void updateSelection() => Scheduler.AddOnce(() =>
private bool ensureGlobalBeatmapValid()
{
var beatmap = Beatmap.Value;
if (!this.IsCurrentScreen())
return false;
carousel.CurrentSelection = beatmap.BeatmapInfo;
// While filtering, let's not ever attempt to change selection.
// This will be resolved after the filter completes, see `newItemsPresented`.
bool carouselStateIsValid = filterDebounce?.State != ScheduledDelegate.RunState.Waiting && !carousel.IsFiltering;
if (!carouselStateIsValid)
return false;
if (this.IsCurrentScreen())
ensurePlayingSelected();
// Refetch to be confident that the current selection is still valid. It may have been deleted or hidden.
var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true);
bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, filterControl.CreateCriteria());
// If not the current screen, this will be applied in OnResuming.
if (this.IsCurrentScreen())
if (Beatmap.IsDefault || !validSelection)
{
ApplyToBackground(backgroundModeBeatmap =>
{
backgroundModeBeatmap.BlurAmount.Value = 0;
backgroundModeBeatmap.Beatmap = beatmap;
backgroundModeBeatmap.IgnoreUserSettings.Value = true;
backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f;
backgroundModeBeatmap.FadeColour(Color4.White, 250);
});
validSelection = carousel.NextRandom();
if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting)
selectionDebounce?.RunTask();
}
});
if (validSelection)
carousel.CurrentSelection = Beatmap.Value.BeatmapInfo;
else
Beatmap.SetDefault();
return validSelection;
}
private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria)
{
if (criteria == null)
return false;
if (!beatmap.AllowGameplayWithRuleset(Ruleset.Value, criteria.AllowConvertedBeatmaps))
return false;
if (beatmap.Hidden)
return false;
if (beatmap.BeatmapSet == null)
return false;
if (beatmap.BeatmapSet.Protected || beatmap.BeatmapSet.DeletePending)
return false;
return true;
}
#endregion
@@ -430,24 +509,7 @@ namespace osu.Game.Screens.SelectV2
base.OnEntering(e);
this.FadeIn();
titleWedge.Show();
detailsArea.Show();
filterControl.Show();
modSelectOverlay.Beatmap.BindTo(Beatmap);
modSelectOverlay.SelectedMods.BindTo(Mods);
beginLooping();
// force reselection if entering song select with a protected beatmap
if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected)
{
if (!carousel.NextRandom())
Beatmap.SetDefault();
}
else
updateSelection();
onArrivingAtScreen();
}
public override void OnResuming(ScreenTransitionEvent e)
@@ -455,41 +517,25 @@ namespace osu.Game.Screens.SelectV2
base.OnResuming(e);
this.FadeIn(fade_duration, Easing.OutQuint);
onArrivingAtScreen();
carousel.VisuallyFocusSelected = false;
ensureGlobalBeatmapValid();
titleWedge.Show();
detailsArea.Show();
filterControl.Show();
modSelectOverlay.Beatmap.BindTo(Beatmap);
// required due to https://github.com/ppy/osu-framework/issues/3218
modSelectOverlay.SelectedMods.Disabled = false;
modSelectOverlay.SelectedMods.BindTo(Mods);
beginLooping();
if (Beatmap.Value.BeatmapInfo.BeatmapSet!.Protected)
Beatmap.SetDefault();
else
updateSelection();
if (ControlGlobalMusic)
{
// restart playback on returning to song select, regardless.
// not sure this should be a permanent thing (we may want to leave a user pause paused even on returning)
music.ResetTrackAdjustments();
music.Play(requestedByUser: true);
}
}
public override void OnSuspending(ScreenTransitionEvent e)
{
this.FadeOut(fade_duration, Easing.OutQuint);
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
modSelectOverlay.Beatmap.UnbindFrom(Beatmap);
titleWedge.Hide();
detailsArea.Hide();
filterControl.Hide();
carousel.VisuallyFocusSelected = true;
endLooping();
this.FadeOut(fade_duration, Easing.OutQuint);
onLeavingScreen();
base.OnSuspending(e);
}
@@ -497,16 +543,42 @@ namespace osu.Game.Screens.SelectV2
public override bool OnExiting(ScreenExitEvent e)
{
this.FadeOut(fade_duration, Easing.OutQuint);
titleWedge.Hide();
detailsArea.Hide();
filterControl.Hide();
endLooping();
onLeavingScreen();
return base.OnExiting(e);
}
private void onArrivingAtScreen()
{
modSelectOverlay.Beatmap.BindTo(Beatmap);
// required due to https://github.com/ppy/osu-framework/issues/3218
modSelectOverlay.SelectedMods.Disabled = false;
modSelectOverlay.SelectedMods.BindTo(Mods);
carousel.VisuallyFocusSelected = false;
updateWedgeVisibility();
beginLooping();
attachTrackDuckingIfShould();
ensureGlobalBeatmapValid();
ensurePlayingSelected(false);
updateBackgroundDim();
}
private void onLeavingScreen()
{
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
modSelectOverlay.Beatmap.UnbindFrom(Beatmap);
updateWedgeVisibility();
endLooping();
detachTrackDucking();
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
@@ -525,7 +597,7 @@ namespace osu.Game.Screens.SelectV2
logo.Action = () =>
{
OnStart();
SelectAndRun(Beatmap.Value.BeatmapInfo, OnStart);
return false;
};
}
@@ -546,27 +618,62 @@ namespace osu.Game.Screens.SelectV2
logo.FadeOut(120, Easing.Out);
}
private void updateWedgeVisibility()
{
// Ensure we don't show an invalid selection before the carousel has finished initially filtering.
// This avoids a flicker of a placeholder or invalid beatmap before a proper selection.
//
// After the carousel finishes filtering, it will attempt a selection then call this method again.
if (!carouselItemsPresented && !checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria()))
return;
if (carousel.VisuallyFocusSelected)
{
titleWedge.Hide();
detailsArea.Hide();
filterControl.Hide();
}
else
{
titleWedge.Show();
detailsArea.Show();
filterControl.Show();
}
}
private void updateBackgroundDim() => ApplyToBackground(backgroundModeBeatmap =>
{
backgroundModeBeatmap.BlurAmount.Value = 0;
backgroundModeBeatmap.Beatmap = Beatmap.Value;
backgroundModeBeatmap.IgnoreUserSettings.Value = true;
backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f;
// Required to undo results screen dimming the background.
// Probably needs more thought because this needs to be in every `ApplyToBackground` currently to restore sane defaults.
backgroundModeBeatmap.FadeColour(Color4.White, 250);
});
#endregion
#region Filtering
private bool carouselItemsPresented;
private const double filter_delay = 250;
private ScheduledDelegate? filterDebounce;
/// <summary>
/// Set the query to the search text box.
/// </summary>
/// <param name="query">The string to search.</param>
public void Search(string query) => filterControl.Search(query);
private void criteriaChanged(FilterCriteria criteria)
{
// The first filter needs to be applied immediately as this triggers the initial carousel load.
double filterDelay = filterDebounce == null ? 0 : filter_delay;
filterDebounce?.Cancel();
filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filterDelay);
// The first filter needs to be applied immediately as this triggers the initial carousel load.
bool isFirstFilter = filterDebounce == null;
// Criteria change may have included a ruleset change which made the current selection invalid.
bool isSelectionValid = checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, criteria);
filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria, !isSelectionValid); }, isFirstFilter || !isSelectionValid ? 0 : filter_delay);
}
private void newItemsPresented(IEnumerable<CarouselItem> carouselItems)
@@ -574,35 +681,40 @@ namespace osu.Game.Screens.SelectV2
if (carousel.Criteria == null)
return;
carouselItemsPresented = true;
int count = carousel.MatchedBeatmapsCount;
if (count == 0)
{
noResultsPlaceholder.Show();
noResultsPlaceholder.Filter = carousel.Criteria;
}
else
noResultsPlaceholder.Hide();
updateNoResultsPlaceholder();
// Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
// but also in this case we want support for formatting a number within a string).
filterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match";
// Refetch to be confident that the current selection is still valid. It may have been deleted or hidden.
var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true);
bool currentBeatmapNotValid = currentBeatmap.BeatmapInfo.Hidden || currentBeatmap.BeatmapSetInfo?.DeletePending == true;
ensureGlobalBeatmapValid();
// If all results are filtered away don't deselect the current global beatmap selection...
if (!carouselItems.Any())
updateWedgeVisibility();
}
private void updateNoResultsPlaceholder()
{
int count = carousel.MatchedBeatmapsCount;
if (count == 0)
{
// ...unless it has been deleted or hidden
if (currentBeatmapNotValid)
Beatmap.SetDefault();
return;
}
noResultsPlaceholder.Show();
noResultsPlaceholder.Filter = carousel.Criteria!;
if (Beatmap.IsDefault || currentBeatmapNotValid)
carousel.NextRandom();
attachTrackDuckingIfShould();
rightGradientBackground.ResizeWidthTo(3, 1000, Easing.OutPow10);
}
else
{
noResultsPlaceholder.Hide();
detachTrackDucking();
rightGradientBackground.ResizeWidthTo(1, 400, Easing.OutPow10);
}
}
#endregion
@@ -656,11 +768,11 @@ namespace osu.Game.Screens.SelectV2
#endregion
/// <summary>
/// Opens results screen with the given score.
/// This assumes active beatmap and ruleset selection matches the score.
/// </summary>
public void PresentScore(ScoreInfo score)
#region Implementation of ISongSelect
void ISongSelect.Search(string query) => filterControl.Search(query);
void ISongSelect.PresentScore(ScoreInfo score)
{
Debug.Assert(Beatmap.Value.BeatmapInfo.Equals(score.BeatmapInfo));
Debug.Assert(Ruleset.Value.Equals(score.Ruleset));
@@ -668,14 +780,7 @@ namespace osu.Game.Screens.SelectV2
this.Push(new SoloResultsScreen(score));
}
/// <summary>
/// Finalises selection on the given <see cref="BeatmapInfo"/>.
/// </summary>
public void SelectAndStart(BeatmapInfo beatmap)
{
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
OnStart();
}
#endregion
#region Beatmap management
@@ -687,7 +792,7 @@ namespace osu.Game.Screens.SelectV2
public virtual IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap)
{
yield return new OsuMenuItem("Select", MenuItemType.Highlighted, () => SelectAndStart(beatmap))
yield return new OsuMenuItem("Select", MenuItemType.Highlighted, () => SelectAndRun(beatmap, OnStart))
{
Icon = FontAwesome.Solid.Check
};
+11 -5
View File
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Skinning
@@ -20,13 +22,15 @@ namespace osu.Game.Skinning
[Resolved]
private ISkinSource source { get; set; } = null!;
private readonly Sprite rank;
private readonly Sprite rankDisplay;
private IBindable<ScoreRank> rank = null!;
public LegacyRankDisplay()
{
AutoSizeAxes = Axes.Both;
AddInternal(rank = new Sprite
AddInternal(rankDisplay = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -35,11 +39,12 @@ namespace osu.Game.Skinning
protected override void LoadComplete()
{
scoreProcessor.Rank.BindValueChanged(v =>
rank = scoreProcessor.Rank.GetBoundCopy();
rank.BindValueChanged(r =>
{
var texture = source.GetTexture($"ranking-{v.NewValue}-small");
var texture = source.GetTexture($"ranking-{r.NewValue}-small");
rank.Texture = texture;
rankDisplay.Texture = texture;
if (texture != null)
{
@@ -57,6 +62,7 @@ namespace osu.Game.Skinning
.Expire();
}
}, true);
FinishTransforms(true);
}
}
+1 -9
View File
@@ -341,8 +341,6 @@ namespace osu.Game.Tests.Visual
{
private readonly Track track;
private readonly TrackVirtualStore store;
/// <summary>
/// Create an instance which creates a <see cref="TestBeatmap"/> for the provided ruleset when requested.
/// </summary>
@@ -372,7 +370,7 @@ namespace osu.Game.Tests.Visual
if (referenceClock != null)
{
store = new TrackVirtualStore(referenceClock);
var store = new TrackVirtualStore(referenceClock);
audio.AddItem(store);
track = store.GetVirtual(trackLength);
}
@@ -385,12 +383,6 @@ namespace osu.Game.Tests.Visual
LoadTrack();
}
~ClockBackedTestWorkingBeatmap()
{
// Remove the track store from the audio manager
store?.Dispose();
}
protected override Track GetBeatmapTrack() => track;
public override bool TryTransferTrack(WorkingBeatmap target)
+2 -2
View File
@@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.512.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.425.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.604.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.605.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" />
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.512.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.604.1" />
</ItemGroup>
</Project>