mirror of
https://github.com/ppy/osu.git
synced 2026-05-25 05:22:48 +08:00
Compare commits
124 Commits
@@ -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
|
||||
@@ -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\**\*'
|
||||
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace osu.Game.Database
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool UseFixedEncoding => false;
|
||||
|
||||
protected override string FileExtension => @".osk";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-7
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user