1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 00:47:24 +08:00

Merge branch 'master' into new-chat-drawable-channel

This commit is contained in:
Dean Herbert 2022-05-12 22:36:19 +09:00
commit 8760e5d884
115 changed files with 2238 additions and 4061 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"jetbrains.resharper.globaltools": { "jetbrains.resharper.globaltools": {
"version": "2022.1.0-eap10", "version": "2022.1.1",
"commands": [ "commands": [
"jb" "jb"
] ]
@ -27,4 +27,4 @@
] ]
} }
} }
} }

View File

@ -31,7 +31,7 @@ jobs:
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: ${{ github.workspace }}/inspectcode path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }} key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }}
- name: Dotnet code style - name: Dotnet code style
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

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

View File

@ -4,8 +4,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc; using osu.Desktop.LegacyIpc;
using osu.Framework; using osu.Framework;
using osu.Framework.Development; using osu.Framework.Development;
@ -63,8 +61,6 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
{ {
host.ExceptionThrown += handleException;
if (!host.IsPrimaryInstance) if (!host.IsPrimaryInstance)
{ {
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
@ -131,23 +127,5 @@ namespace osu.Desktop
// tools.SetProcessAppUserModelId(); // tools.SetProcessAppUserModelId();
}); });
} }
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
/// <summary>
/// Allow a maximum of one unhandled exception, per second of execution.
/// </summary>
/// <param name="arg"></param>
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
}
} }
} }

View File

@ -24,7 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.23-gc8da1a" /> <PackageReference Include="Clowd.Squirrel" Version="2.9.40" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />

View File

@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="nunit" Version="3.13.2" /> <PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
</ItemGroup> </ItemGroup>

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -113,7 +113,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public void TestCursorInCentre() public void TestCursorInCentre()
{ {
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position))); AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
assertSnappedDistance(0); assertSnappedDistance(beat_length);
}
[Test]
public void TestCursorAlmostInCentre()
{
AddStep("move mouse to almost centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position) + new Vector2(1)));
assertSnappedDistance(beat_length);
} }
[Test] [Test]

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -122,6 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
new OsuModEasy(), new OsuModEasy(),
new OsuModHardRock(), new OsuModHardRock(),
new OsuModFlashlight(), new OsuModFlashlight(),
new MultiMod(new OsuModFlashlight(), new OsuModHidden())
}; };
} }
} }

View File

@ -219,9 +219,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0; double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
if (score.Mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));

View File

@ -4,6 +4,7 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
@ -85,6 +86,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
setDistances(clockRate); setDistances(clockRate);
} }
public double OpacityAt(double time, bool hidden)
{
if (time > BaseObject.StartTime)
{
// Consider a hitobject as being invisible when its start time is passed.
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
// but this is an approximation and such a case is unlikely to be hit where this function is used.
return 0.0;
}
double fadeInStartTime = BaseObject.StartTime - BaseObject.TimePreempt;
double fadeInDuration = BaseObject.TimeFadeIn;
if (hidden)
{
// Taken from OsuModHidden.
double fadeOutStartTime = BaseObject.StartTime - BaseObject.TimePreempt + BaseObject.TimeFadeIn;
double fadeOutDuration = BaseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
return Math.Min
(
Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0),
1.0 - Math.Clamp((time - fadeOutStartTime) / fadeOutDuration, 0.0, 1.0)
);
}
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
}
private void setDistances(double clockRate) private void setDistances(double clockRate)
{ {
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -17,13 +19,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public Flashlight(Mod[] mods) public Flashlight(Mod[] mods)
: base(mods) : base(mods)
{ {
hidden = mods.Any(m => m is OsuModHidden);
} }
private double skillMultiplier => 0.07; private double skillMultiplier => 0.05;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0; protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
private readonly bool hidden;
private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2;
private double currentStrain; private double currentStrain;
private double strainValueOf(DifficultyHitObject current) private double strainValueOf(DifficultyHitObject current)
@ -61,13 +69,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// We also want to nerf stacks so that only the first object of the stack is accounted for. // We also want to nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0); double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; // Bonus based on how visible the object is.
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
} }
lastObj = currentObj; lastObj = currentObj;
} }
return Math.Pow(smallDistNerf * result, 2.0); result = Math.Pow(smallDistNerf * result, 2.0);
// Additional bonus for Hidden due to there being no approach circles.
if (hidden)
result *= 1.0 + hidden_bonus;
return result;
} }
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);

View File

@ -50,6 +50,11 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations); hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations);
hitAnimations.BindValueChanged(_ =>
{
foreach (var d in HitObjectContainer.AliveObjects)
d.RefreshStateTransforms();
});
} }
protected override void OnNewDrawableHitObject(DrawableHitObject d) protected override void OnNewDrawableHitObject(DrawableHitObject d)

View File

@ -27,8 +27,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
private const double fade_in_duration_multiplier = 0.4; public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
private const double fade_out_duration_multiplier = 0.3; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick);
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
static void applyFadeInAdjustment(OsuHitObject osuObject) static void applyFadeInAdjustment(OsuHitObject osuObject)
{ {
osuObject.TimeFadeIn = osuObject.TimePreempt * fade_in_duration_multiplier; osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER;
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>()) foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
applyFadeInAdjustment(nested); applyFadeInAdjustment(nested);
} }
@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Mods
static (double fadeStartTime, double fadeDuration) getParameters(OsuHitObject hitObject) static (double fadeStartTime, double fadeDuration) getParameters(OsuHitObject hitObject)
{ {
double fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn; double fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn;
double fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier; double fadeOutDuration = hitObject.TimePreempt * FADE_OUT_DURATION_MULTIPLIER;
// new duration from completed fade in to end (before fading out) // new duration from completed fade in to end (before fading out)
double longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime; double longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime;

View File

@ -195,16 +195,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public void UpdateProgress(double completionProgress) public void UpdateProgress(double completionProgress)
{ {
var newPos = drawableSlider.HitObject.CurvePositionAt(completionProgress); Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
var diff = lastPosition.HasValue ? lastPosition.Value - newPos : newPos - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
if (diff == Vector2.Zero)
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
if (diff.LengthFast < 0.01f)
return; return;
Position = newPos;
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = Position;
lastPosition = newPos;
} }
private class FollowCircleContainer : CircularContainer private class FollowCircleContainer : CircularContainer

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup> </ItemGroup>

View File

@ -136,6 +136,37 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestAddFileToAsyncImportedBeatmap()
{
RunTestWithRealm((realm, storage) =>
{
BeatmapSetInfo? detachedSet = null;
using (var importer = new BeatmapModelManager(realm, storage))
using (new RealmRulesetStore(realm, storage))
{
Task.Run(async () =>
{
Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
// ReSharper disable once AccessToDisposedClosure
beatmapSet = await importer.Import(reader);
Assert.NotNull(beatmapSet);
Debug.Assert(beatmapSet != null);
// Intentionally detach on async thread as to not trigger a refresh on the main thread.
beatmapSet.PerformRead(s => detachedSet = s.Detach());
}).WaitSafely();
Debug.Assert(detachedSet != null);
importer.AddFile(detachedSet, new MemoryStream(), "test");
}
});
}
[Test] [Test]
public void TestImportBeatmapThenCleanup() public void TestImportBeatmapThenCleanup()
{ {

View File

@ -0,0 +1,36 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods
{
public class ModSettingsTest
{
[Test]
public void TestModSettingsUnboundWhenCopied()
{
var original = new OsuModDoubleTime();
var copy = (OsuModDoubleTime)original.DeepClone();
original.SpeedChange.Value = 2;
Assert.That(original.SpeedChange.Value, Is.EqualTo(2.0));
Assert.That(copy.SpeedChange.Value, Is.EqualTo(1.5));
}
[Test]
public void TestMultiModSettingsUnboundWhenCopied()
{
var original = new MultiMod(new OsuModDoubleTime());
var copy = (MultiMod)original.DeepClone();
((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2;
Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0));
Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5));
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
{
public class TestCustomisableModRuleset : Ruleset
{
public static RulesetInfo CreateTestRulesetInfo() => new TestCustomisableModRuleset().RulesetInfo;
public override IEnumerable<Mod> GetModsFor(ModType type)
{
if (type == ModType.Conversion)
{
return new Mod[]
{
new TestModCustomisable1(),
new TestModCustomisable2()
};
}
return Array.Empty<Mod>();
}
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description { get; } = "test";
public override string ShortName { get; } = "tst";
public class TestModCustomisable1 : TestModCustomisable
{
public override string Name => "Customisable Mod 1";
public override string Acronym => "CM1";
}
public class TestModCustomisable2 : TestModCustomisable
{
public override string Name => "Customisable Mod 2";
public override string Acronym => "CM2";
public override bool RequiresConfiguration => true;
}
public abstract class TestModCustomisable : Mod, IApplicableMod
{
public override double ScoreMultiplier => 1.0;
public override string Description => "This is a customisable test mod.";
public override ModType Type => ModType.Conversion;
[SettingSource("Sample float", "Change something for a mod")]
public BindableFloat SliderBindable { get; } = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 7
};
[SettingSource("Sample bool", "Clicking this changes a setting")]
public BindableBool TickBindable { get; } = new BindableBool();
}
}
}

View File

@ -3,9 +3,11 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Tests.Rulesets.Mods namespace osu.Game.Tests.Rulesets.Mods
{ {
@ -16,11 +18,14 @@ namespace osu.Game.Tests.Rulesets.Mods
private const double duration = 9000; private const double duration = 9000;
private TrackVirtual track; private TrackVirtual track;
private OsuPlayfield playfield;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
track = new TrackVirtual(20_000); track = new TrackVirtual(20_000);
// define a fake playfield to re-calculate the current rate by ModTimeRamp.Update(Playfield).
playfield = new OsuPlayfield { Clock = new FramedClock(track) };
} }
[TestCase(0, 1)] [TestCase(0, 1)]
@ -80,8 +85,8 @@ namespace osu.Game.Tests.Rulesets.Mods
private void seekTrackAndUpdateMod(ModTimeRamp mod, double time) private void seekTrackAndUpdateMod(ModTimeRamp mod, double time)
{ {
track.Seek(time); track.Seek(time);
// update the mod via a fake playfield to re-calculate the current rate. playfield.Clock.ProcessFrame();
mod.Update(null); mod.Update(playfield);
} }
private static Beatmap createSingleSpinnerBeatmap() private static Beatmap createSingleSpinnerBeatmap()

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -66,6 +67,13 @@ namespace osu.Game.Tests.Visual.Editing
}); });
} }
[Test]
public void TestPopoverHasFocus()
{
clickDifficultyPiece(0);
velocityPopoverHasFocus();
}
[Test] [Test]
public void TestSingleSelection() public void TestSingleSelection()
{ {
@ -133,6 +141,15 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
private void velocityPopoverHasFocus() => AddUntilStep("velocity popover textbox focused", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
var textbox = slider?.ChildrenOfType<OsuTextBox>().Single();
return textbox?.HasFocus == true;
});
private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () => private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () =>
{ {
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault(); var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
@ -151,6 +168,7 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover() private void dismissPopover()
{ {
AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape)); AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent)); AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
} }

View File

@ -57,6 +57,13 @@ namespace osu.Game.Tests.Visual.Editing
}); });
} }
[Test]
public void TestPopoverHasFocus()
{
clickSamplePiece(0);
samplePopoverHasFocus();
}
[Test] [Test]
public void TestSingleSelection() public void TestSingleSelection()
{ {
@ -173,14 +180,23 @@ namespace osu.Game.Tests.Visual.Editing
samplePopoverHasSingleBank("normal"); samplePopoverHasSingleBank("normal");
} }
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{ {
var difficultyPiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(difficultyPiece); InputManager.MoveMouseTo(samplePiece);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
var textbox = slider?.ChildrenOfType<OsuTextBox>().Single();
return textbox?.HasFocus == true;
});
private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () => private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
{ {
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault(); var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
@ -215,8 +231,9 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover() private void dismissPopover()
{ {
AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape)); AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent)); AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Any(popover => popover.IsPresent));
} }
private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () => private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () =>

View File

@ -24,7 +24,7 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual.UserInterface; using osu.Game.Tests.Mods;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
new Drawable[] new Drawable[]
{ {
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{ {
Recorder = recorder = new TestReplayRecorder(new Score Recorder = recorder = new TestReplayRecorder(new Score
{ {
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}, },
new Drawable[] new Drawable[]
{ {
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{ {
ReplayInputHandler = new TestFramedReplayInputHandler(replay) ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{ {

View File

@ -1,57 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneReplaySettingsOverlay : OsuTestScene
{
public TestSceneReplaySettingsOverlay()
{
ExampleContainer container;
Add(new PlayerSettingsOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
State = { Value = Visibility.Visible }
});
Add(container = new ExampleContainer());
AddStep(@"Add button", () => container.Add(new TriangleButton
{
RelativeSizeAxes = Axes.X,
Text = @"Button",
}));
AddStep(@"Add checkbox", () => container.Add(new PlayerCheckbox
{
LabelText = "Checkbox",
}));
AddStep(@"Add textbox", () => container.Add(new FocusedTextBox
{
RelativeSizeAxes = Axes.X,
Height = 30,
PlaceholderText = "Textbox",
HoldFocus = false,
}));
}
private class ExampleContainer : PlayerSettingsGroup
{
public ExampleContainer()
: base("example")
{
}
}
}
}

View File

@ -27,8 +27,8 @@ using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Mods;
using osu.Game.Tests.Visual.Spectator; using osu.Game.Tests.Visual.Spectator;
using osu.Game.Tests.Visual.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
new Drawable[] new Drawable[]
{ {
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{ {
Recorder = recorder = new TestReplayRecorder Recorder = recorder = new TestReplayRecorder
{ {
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}, },
new Drawable[] new Drawable[]
{ {
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{ {
Clock = new FramedClock(manualClock), Clock = new FramedClock(manualClock),
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)

View File

@ -1,21 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneFreeModSelectOverlay : MultiplayerTestScene public class TestSceneFreeModSelectOverlay : MultiplayerTestScene
{ {
[SetUp] private FreeModSelectOverlay freeModSelectOverlay;
public new void Setup() => Schedule(() => private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGameBase)
{ {
Child = new FreeModSelectOverlay availableMods.BindTo(osuGameBase.AvailableMods);
}
[Test]
public void TestFreeModSelect()
{
createFreeModSelect();
AddUntilStep("all visible mods are playable",
() => this.ChildrenOfType<ModPanel>()
.Where(panel => panel.IsPresent)
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
AddToggleStep("toggle visibility", visible =>
{
if (freeModSelectOverlay != null)
freeModSelectOverlay.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
});
}
[Test]
public void TestCustomisationNotAvailable()
{
createFreeModSelect();
AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
AddWaitStep("wait some", 3);
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
}
[Test]
public void TestSelectDeselectAll()
{
createFreeModSelect();
AddStep("click select all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
}
private void createFreeModSelect()
{
AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay
{ {
State = { Value = Visibility.Visible } State = { Value = Visibility.Visible }
}; });
}); AddUntilStep("all column content loaded",
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& freeModSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
}
private bool assertAllAvailableModsSelected()
{
var allAvailableMods = availableMods.Value
.SelectMany(pair => pair.Value)
.Where(mod => mod.UserPlayable && mod.HasImplementation)
.ToList();
foreach (var availableMod in allAvailableMods)
{
if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
return false;
}
return true;
}
} }
} }

View File

@ -1,105 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneFreeModSelectScreen : MultiplayerTestScene
{
private FreeModSelectScreen freeModSelectScreen;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGameBase)
{
availableMods.BindTo(osuGameBase.AvailableMods);
}
[Test]
public void TestFreeModSelect()
{
createFreeModSelect();
AddUntilStep("all visible mods are playable",
() => this.ChildrenOfType<ModPanel>()
.Where(panel => panel.IsPresent)
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
AddToggleStep("toggle visibility", visible =>
{
if (freeModSelectScreen != null)
freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
});
}
[Test]
public void TestCustomisationNotAvailable()
{
createFreeModSelect();
AddStep("select difficulty adjust", () => freeModSelectScreen.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
AddWaitStep("wait some", 3);
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
}
[Test]
public void TestSelectDeselectAll()
{
createFreeModSelect();
AddStep("click select all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !freeModSelectScreen.SelectedMods.Value.Any());
}
private void createFreeModSelect()
{
AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen
{
State = { Value = Visibility.Visible }
});
AddUntilStep("all column content loaded",
() => freeModSelectScreen.ChildrenOfType<ModColumn>().Any()
&& freeModSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
}
private bool assertAllAvailableModsSelected()
{
var allAvailableMods = availableMods.Value
.SelectMany(pair => pair.Value)
.Where(mod => mod.UserPlayable && mod.HasImplementation)
.ToList();
foreach (var availableMod in allAvailableMods)
{
if (freeModSelectScreen.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
return false;
}
return true;
}
}
}

View File

@ -627,7 +627,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectScreen>().Single().State.Value == Visibility.Hidden); AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);

View File

@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertHasFreeModButton(Type type, bool hasButton = true) private void assertHasFreeModButton(Type type, bool hasButton = true)
{ {
AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
() => this.ChildrenOfType<FreeModSelectScreen>() () => this.ChildrenOfType<FreeModSelectOverlay>()
.Single() .Single()
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.Where(panel => !panel.Filtered.Value) .Where(panel => !panel.Filtered.Value)

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -171,10 +172,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contents loaded", AddUntilStep("mod select contents loaded",
() => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded)); () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
AddUntilStep("mod select contains only double time mod", AddUntilStep("mod select contains only double time mod",
() => this.ChildrenOfType<UserModSelectScreen>() () => this.ChildrenOfType<UserModSelectOverlay>()
.SingleOrDefault()? .SingleOrDefault()?
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
} }
[Test]
public void TestNextPlaylistItemSelectedAfterCompletion()
{
AddStep("add two playlist items", () =>
{
SelectedRoom.Value.Playlist.AddRange(new[]
{
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
},
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
});
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("change user to loaded", () => MultiplayerClient.ChangeState(MultiplayerUserState.Loaded));
AddUntilStep("user playing", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Playing);
AddStep("abort gameplay", () => MultiplayerClient.AbortGameplay());
AddUntilStep("last playlist item selected", () =>
{
var lastItem = this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.APIRoom?.Playlist.Last().ID);
return lastItem.IsSelectedItem;
});
}
} }
} }

View File

@ -23,7 +23,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
@ -33,7 +32,6 @@ namespace osu.Game.Tests.Visual.Navigation
private IReadOnlyList<Type> requiredGameDependencies => new[] private IReadOnlyList<Type> requiredGameDependencies => new[]
{ {
typeof(OsuGame), typeof(OsuGame),
typeof(SentryLogger),
typeof(OsuLogo), typeof(OsuLogo),
typeof(IdleTracker), typeof(IdleTracker),
typeof(OnScreenDisplay), typeof(OnScreenDisplay),

View File

@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Navigation
public class TestPlaySongSelect : PlaySongSelect public class TestPlaySongSelect : PlaySongSelect
{ {
public ModSelectScreen ModSelectOverlay => ModSelect; public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;

View File

@ -25,7 +25,6 @@ namespace osu.Game.Tests.Visual.Online
[Cached] [Cached]
private readonly Bindable<Channel> selected = new Bindable<Channel>(); private readonly Bindable<Channel> selected = new Bindable<Channel>();
private OsuSpriteText selectorText;
private OsuSpriteText selectedText; private OsuSpriteText selectedText;
private OsuSpriteText leaveText; private OsuSpriteText leaveText;
private ChannelList channelList; private ChannelList channelList;
@ -43,21 +42,12 @@ namespace osu.Game.Tests.Visual.Online
Height = 0.7f, Height = 0.7f,
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.Absolute, 20),
new Dimension(), new Dimension(),
}, },
Content = new[] Content = new[]
{ {
new Drawable[]
{
selectorText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
},
new Drawable[] new Drawable[]
{ {
selectedText = new OsuSpriteText selectedText = new OsuSpriteText
@ -89,7 +79,6 @@ namespace osu.Game.Tests.Visual.Online
channelList.OnRequestSelect += channel => channelList.OnRequestSelect += channel =>
{ {
channelList.SelectorActive.Value = false;
selected.Value = channel; selected.Value = channel;
}; };
@ -101,12 +90,6 @@ namespace osu.Game.Tests.Visual.Online
channelList.RemoveChannel(channel); channelList.RemoveChannel(channel);
}; };
channelList.SelectorActive.BindValueChanged(change =>
{
selectorText.Text = $"Channel Selector Active: {change.NewValue}";
selected.Value = null;
}, true);
selected.BindValueChanged(change => selected.BindValueChanged(change =>
{ {
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}"; selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";

View File

@ -121,6 +121,19 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
[Test]
public void TestBasic()
{
AddStep("Show overlay with channel", () =>
{
chatOverlay.Show();
Channel joinedChannel = channelManager.JoinChannel(testChannel1);
channelManager.CurrentChannel.Value = joinedChannel;
});
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
AddUntilStep("Channel is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
}
[Test] [Test]
public void TestShowHide() public void TestShowHide()
{ {
@ -158,20 +171,17 @@ namespace osu.Game.Tests.Visual.Online
public void TestChannelSelection() public void TestChannelSelection()
{ {
AddStep("Show overlay", () => chatOverlay.Show()); AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); AddAssert("Listing is visible", () => listingIsVisible);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden); AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden);
AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1);
AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1);
} }
[Test] [Test]
public void TestSearchInListing() public void TestSearchInListing()
{ {
AddStep("Show overlay", () => chatOverlay.Show()); AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); AddAssert("Listing is visible", () => listingIsVisible);
AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2"); AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
AddUntilStep("Only channel 2 visibile", () => AddUntilStep("Only channel 2 visibile", () =>
{ {
@ -263,6 +273,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
}); });
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
} }
[Test] [Test]
@ -285,8 +296,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
}); });
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2); AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
} }
[Test] [Test]
@ -310,8 +320,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2)); AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2); AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
} }
[Test] [Test]
@ -331,6 +340,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
}); });
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
} }
[Test] [Test]
@ -351,6 +361,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null); AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
} }
[Test] [Test]
@ -375,11 +386,14 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
} }
private Visibility listingVisibility => private bool listingIsVisible =>
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value; chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
private Visibility loadingVisibility => private bool loadingIsVisible =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value; chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value == Visibility.Visible;
private bool channelIsVisible =>
!listingIsVisible && !loadingIsVisible;
private DrawableChannel currentDrawableChannel => private DrawableChannel currentDrawableChannel =>
chatOverlay.ChildrenOfType<DrawableChannel>().Single(); chatOverlay.ChildrenOfType<DrawableChannel>().Single();

View File

@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking
score.Accuracy = accuracy; score.Accuracy = accuracy;
score.Rank = rank; score.Rank = rank;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score))); loadResultsScreen(() => screen = createResultsScreen(score));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null); AddAssert("retry overlay present", () => screen.RetryOverlay != null);
} }
@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
UnrankedSoloResultsScreen screen = null; UnrankedSoloResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createUnrankedSoloResultsScreen())); loadResultsScreen(() => screen = createUnrankedSoloResultsScreen());
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null); AddAssert("retry overlay present", () => screen.RetryOverlay != null);
} }
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
ScorePanel expandedPanel = null; ScorePanel expandedPanel = null;
@ -231,7 +231,7 @@ namespace osu.Game.Tests.Visual.Ranking
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), tcs.Task))); loadResultsScreen(() => screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), tcs.Task));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for loaded", () => screen.IsLoaded);
@ -255,7 +255,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value); AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
@ -276,7 +276,7 @@ namespace osu.Game.Tests.Visual.Ranking
var ruleset = new RulesetWithNoPerformanceCalculator(); var ruleset = new RulesetWithNoPerformanceCalculator();
var score = TestResources.CreateTestScoreInfo(ruleset.RulesetInfo); var score = TestResources.CreateTestScoreInfo(ruleset.RulesetInfo);
AddStep("load results", () => Child = new TestResultsContainer(createResultsScreen(score))); loadResultsScreen(() => createResultsScreen(score));
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddAssert("PP displayed as 0", () => AddAssert("PP displayed as 0", () =>
@ -287,6 +287,22 @@ namespace osu.Game.Tests.Visual.Ranking
}); });
} }
private void loadResultsScreen(Func<ResultsScreen> createResults)
{
ResultsScreen results = null;
AddStep("load results", () => Child = new TestResultsContainer(results = createResults()));
// expanded panel should be centered the moment results screen is loaded
// but can potentially be scrolled away on certain specific load scenarios.
// see: https://github.com/ppy/osu/issues/18226
AddUntilStep("expanded panel in centre of screen", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, results.ScreenSpaceDrawQuad.Centre.X, 1);
});
}
private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo()); private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo());
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo()); private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo());

View File

@ -1008,7 +1008,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public WorkingBeatmap CurrentBeatmap => Beatmap.Value; public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
public new BeatmapCarousel Carousel => base.Carousel; public new BeatmapCarousel Carousel => base.Carousel;
public new ModSelectScreen ModSelect => base.ModSelect; public new ModSelectOverlay ModSelect => base.ModSelect;
public new void PresentScore(ScoreInfo score) => base.PresentScore(score); public new void PresentScore(ScoreInfo score) => base.PresentScore(score);

View File

@ -4,9 +4,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -25,9 +27,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private FirstRunSetupOverlay overlay; private FirstRunSetupOverlay overlay;
private readonly Mock<IPerformFromScreenRunner> performer = new Mock<IPerformFromScreenRunner>(); private readonly Mock<TestPerformerFromScreenRunner> performer = new Mock<TestPerformerFromScreenRunner>();
private readonly Mock<INotificationOverlay> notificationOverlay = new Mock<INotificationOverlay>(); private readonly Mock<TestNotificationOverlay> notificationOverlay = new Mock<TestNotificationOverlay>();
private Notification lastNotification; private Notification lastNotification;
@ -37,8 +39,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private void load() private void load()
{ {
Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage));
Dependencies.CacheAs(performer.Object); Dependencies.CacheAs<IPerformFromScreenRunner>(performer.Object);
Dependencies.CacheAs(notificationOverlay.Object); Dependencies.CacheAs<INotificationOverlay>(notificationOverlay.Object);
} }
[SetUpSteps] [SetUpSteps]
@ -72,7 +74,6 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
[Test] [Test]
[Ignore("Enable when first run setup is being displayed on first run.")]
public void TestDoesntOpenOnSecondRun() public void TestDoesntOpenOnSecondRun()
{ {
AddStep("set first run", () => LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, true)); AddStep("set first run", () => LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, true));
@ -196,5 +197,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible); AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps); AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps);
} }
// interface mocks break hot reload, mocking this stub implementation instead works around it.
// see: https://github.com/moq/moq4/issues/1252
[UsedImplicitly]
public class TestNotificationOverlay : INotificationOverlay
{
public virtual void Post(Notification notification)
{
}
public virtual void Hide()
{
}
public virtual IBindable<int> UnreadCount => null;
}
// interface mocks break hot reload, mocking this stub implementation instead works around it.
// see: https://github.com/moq/moq4/issues/1252
[UsedImplicitly]
public class TestPerformerFromScreenRunner : IPerformFromScreenRunner
{
public virtual void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null)
{
}
}
} }
} }

View File

@ -1,64 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModButton : OsuTestScene
{
public TestSceneModButton()
{
Children = new Drawable[]
{
new ModButton(new MultiMod(new TestMod1(), new TestMod2(), new TestMod3(), new TestMod4()))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
};
}
private class TestMod1 : TestMod
{
public override string Name => "Test mod 1";
public override string Acronym => "M1";
}
private class TestMod2 : TestMod
{
public override string Name => "Test mod 2";
public override string Acronym => "M2";
public override IconUsage? Icon => FontAwesome.Solid.Exclamation;
}
private class TestMod3 : TestMod
{
public override string Name => "Test mod 3";
public override string Acronym => "M3";
public override IconUsage? Icon => FontAwesome.Solid.ArrowRight;
}
private class TestMod4 : TestMod
{
public override string Name => "Test mod 4";
public override string Acronym => "M4";
}
private abstract class TestMod : Mod, IApplicableMod
{
public override double ScoreMultiplier => 1.0;
public override string Description => "This is a test mod.";
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -12,11 +14,9 @@ using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko; using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -41,20 +41,16 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = new ModColumn(modType, false) Child = new ModColumn(modType, false)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(modType)
} }
}); });
AddStep("change ruleset to osu!", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
} }
[Test] [Test]
public void TestMultiSelection() public void TestMultiSelection()
{ {
ModColumn column = null; ModColumn column = null!;
AddStep("create content", () => Child = new Container AddStep("create content", () => Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -62,7 +58,8 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = column = new ModColumn(ModType.DifficultyIncrease, true) Child = column = new ModColumn(ModType.DifficultyIncrease, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
} }
}); });
@ -91,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestFiltering() public void TestFiltering()
{ {
TestModColumn column = null; TestModColumn column = null!;
AddStep("create content", () => Child = new Container AddStep("create content", () => Child = new Container
{ {
@ -100,30 +97,31 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = column = new TestModColumn(ModType.Fun, true) Child = column = new TestModColumn(ModType.Fun, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.Fun)
} }
}); });
AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2); AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2);
clickToggle(); clickToggle();
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value));
AddStep("unset filter", () => column.Filter = null); AddStep("unset filter", () => setFilter(null));
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value)); AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value));
AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value); AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2); AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2);
AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value); AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
AddStep("filter out everything", () => column.Filter = _ => false); AddStep("filter out everything", () => setFilter(_ => false));
AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Filtered.Value)); AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Filtered.Value));
AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
AddStep("inset filter", () => column.Filter = null); AddStep("inset filter", () => setFilter(null));
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value)); AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value));
AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent); AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
@ -138,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestKeyboardSelection() public void TestKeyboardSelection()
{ {
ModColumn column = null; ModColumn column = null!;
AddStep("create content", () => Child = new Container AddStep("create content", () => Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -146,7 +144,8 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.DifficultyReduction)
} }
}); });
@ -158,7 +157,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press W again", () => InputManager.Key(Key.W)); AddStep("press W again", () => InputManager.Key(Key.W));
AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
AddStep("set filter to NF", () => column.Filter = mod => mod.Acronym == "NF"); AddStep("set filter to NF", () => setFilter(mod => mod.Acronym == "NF"));
AddStep("press W", () => InputManager.Key(Key.W)); AddStep("press W", () => InputManager.Key(Key.W));
AddAssert("NF panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddAssert("NF panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
@ -166,12 +165,18 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press W again", () => InputManager.Key(Key.W)); AddStep("press W again", () => InputManager.Key(Key.W));
AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
AddStep("filter out everything", () => column.Filter = _ => false); AddStep("filter out everything", () => setFilter(_ => false));
AddStep("press W", () => InputManager.Key(Key.W)); AddStep("press W", () => InputManager.Key(Key.W));
AddAssert("NF panel not selected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddAssert("NF panel not selected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
AddStep("clear filter", () => column.Filter = null); AddStep("clear filter", () => setFilter(null));
}
private void setFilter(Func<Mod, bool>? filter)
{
foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods)
modState.Filtered.Value = filter?.Invoke(modState.Mod) == false;
} }
private class TestModColumn : ModColumn private class TestModColumn : ModColumn
@ -183,5 +188,13 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
} }
} }
private static ModState[] getExampleModsFor(ModType modType)
{
return new OsuRuleset().GetModsFor(modType)
.SelectMany(ModUtils.FlattenMod)
.Select(mod => new ModState(mod))
.ToArray();
}
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
@ -10,45 +10,210 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Mods;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
[Description("mod select and icon display")] [TestFixture]
public class TestSceneModSelectOverlay : OsuTestScene public class TestSceneModSelectOverlay : OsuManualInputManagerTestScene
{ {
private RulesetStore rulesets; [Resolved]
private ModDisplay modDisplay; private RulesetStore rulesetStore { get; set; }
private TestModSelectOverlay modSelect;
[BackgroundDependencyLoader] private UserModSelectOverlay modSelectOverlay;
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
createDisplay(() => new TestModSelectOverlay());
});
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("show", () => modSelect.Show()); AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
}
private void createScreen()
{
AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
}
[Test]
public void TestStateChange()
{
createScreen();
AddStep("toggle state", () => modSelectOverlay.ToggleVisibility());
}
[Test]
public void TestPreexistingSelection()
{
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
createScreen();
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
}
[Test]
public void TestExternalSelection()
{
createScreen();
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
}
[Test]
public void TestRulesetChange()
{
createScreen();
changeRuleset(0);
changeRuleset(1);
changeRuleset(2);
changeRuleset(3);
}
[Test]
public void TestIncompatibilityToggling()
{
createScreen();
changeRuleset(0);
AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value);
AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value);
AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value);
}
[Test]
public void TestDimmedState()
{
createScreen();
changeRuleset(0);
AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value));
ModColumn lastColumn = null;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
AddStep("request scroll to last column", () =>
{
var lastDimContainer = this.ChildrenOfType<ModSelectOverlay.ColumnDimContainer>().Last();
lastColumn = lastDimContainer.Column;
lastDimContainer.RequestScroll?.Invoke(lastDimContainer);
});
AddUntilStep("column undimmed", () => lastColumn.Active.Value);
AddStep("click panel", () =>
{
InputManager.MoveMouseTo(lastColumn.ChildrenOfType<ModPanel>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("panel selected", () => lastColumn.ChildrenOfType<ModPanel>().First().Active.Value);
}
[Test]
public void TestCustomisationToggleState()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
assertCustomisationToggleState(disabled: false, active: false);
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via toggle", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ShearedToggleButton>().Single());
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
assertCustomisationToggleState(disabled: false, active: false);
AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
assertCustomisationToggleState(disabled: false, active: false);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
}
[Test]
public void TestDismissCustomisationViaDimmedArea()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType<ModSettingsArea>().Single()));
AddStep("move mouse to dimmed area", () =>
{
InputManager.MoveMouseTo(new Vector2(
modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.X,
(modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.Y + modSelectOverlay.ScreenSpaceDrawQuad.BottomLeft.Y) / 2));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
assertCustomisationToggleState(disabled: false, active: false);
AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModPanel>().First()));
AddAssert("first mod panel is hovered", () => modSelectOverlay.ChildrenOfType<ModPanel>().First().IsHovered);
} }
/// <summary> /// <summary>
@ -58,10 +223,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSettingsNotCrossPolluting() public void TestSettingsNotCrossPolluting()
{ {
Bindable<IReadOnlyList<Mod>> selectedMods2 = null; Bindable<IReadOnlyList<Mod>> selectedMods2 = null;
ModSelectOverlay modSelectOverlay2 = null;
createScreen();
AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
AddStep("set setting", () => modSelect.ChildrenOfType<SettingsSlider<float>>().First().Current.Value = 8); AddStep("set setting", () => modSelectOverlay.ChildrenOfType<SettingsSlider<float>>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8); AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
@ -69,7 +236,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("create second overlay", () => AddStep("create second overlay", () =>
{ {
Add(modSelect = new TestModSelectOverlay().With(d => Add(modSelectOverlay2 = new UserModSelectOverlay().With(d =>
{ {
d.Origin = Anchor.TopCentre; d.Origin = Anchor.TopCentre;
d.Anchor = Anchor.TopCentre; d.Anchor = Anchor.TopCentre;
@ -77,7 +244,7 @@ namespace osu.Game.Tests.Visual.UserInterface
})); }));
}); });
AddStep("show", () => modSelect.Show()); AddStep("show", () => modSelectOverlay2.Show());
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8); AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null); AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
@ -88,83 +255,51 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } }; var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; }); AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2); AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
AddStep("deselect", () => modSelect.DeselectAllButton.TriggerClick()); AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0); AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).TriggerClick()); AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true); AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
} }
[Test] [Test]
public void TestAnimationFlushOnClose() public void TestAnimationFlushOnClose()
{ {
createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("Select all fun mods", () => AddStep("Select all fun mods", () =>
{ {
modSelect.ModSectionsContainer modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease) .Single(c => c.ModType == ModType.DifficultyIncrease)
.SelectAll(); .SelectAll();
}); });
AddUntilStep("many mods selected", () => modDisplay.Current.Value.Count >= 5); AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
AddStep("trigger deselect and close overlay", () => AddStep("trigger deselect and close overlay", () =>
{ {
modSelect.ModSectionsContainer modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease) .Single(c => c.ModType == ModType.DifficultyIncrease)
.DeselectAll(); .DeselectAll();
modSelect.Hide(); modSelectOverlay.Hide();
}); });
AddAssert("all mods deselected", () => modDisplay.Current.Value.Count == 0); AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestOsuMods()
{
changeRuleset(0);
var osu = new OsuRuleset();
var easierMods = osu.GetModsFor(ModType.DifficultyReduction);
var harderMods = osu.GetModsFor(ModType.DifficultyIncrease);
var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
testSingleMod(noFailMod);
testMultiMod(doubleTimeMod);
testIncompatibleMods(easy, hardRock);
testDeselectAll(easierMods.Where(m => !(m is MultiMod)));
}
[Test]
public void TestManiaMods()
{
changeRuleset(3);
var mania = new ManiaRuleset();
testModsWithSameBaseType(
mania.CreateMod<ManiaModFadeIn>(),
mania.CreateMod<ManiaModHidden>());
} }
[Test] [Test]
public void TestRulesetChanges() public void TestRulesetChanges()
{ {
createScreen();
changeRuleset(0); changeRuleset(0);
var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
@ -173,42 +308,42 @@ namespace osu.Game.Tests.Visual.UserInterface
changeRuleset(0); changeRuleset(0);
AddAssert("ensure mods still selected", () => modDisplay.Current.Value.SingleOrDefault(m => m is OsuModNoFail) != null); AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
changeRuleset(3); changeRuleset(3);
AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0); AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
changeRuleset(0); changeRuleset(0);
AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0); AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
} }
[Test] [Test]
public void TestExternallySetCustomizedMod() public void TestExternallySetCustomizedMod()
{ {
createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("ensure button is selected and customized accordingly", () => AddAssert("ensure button is selected and customized accordingly", () =>
{ {
var button = modSelect.GetModButton(SelectedMods.Value.Single()); var button = getPanelForMod(SelectedMods.Value.Single().GetType());
return ((OsuModDoubleTime)button.SelectedMod).SpeedChange.Value == 1.01; return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01;
}); });
} }
[Test] [Test]
public void TestSettingsAreRetainedOnReload() public void TestSettingsAreRetainedOnReload()
{ {
createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay())); createScreen();
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
} }
@ -218,236 +353,170 @@ namespace osu.Game.Tests.Visual.UserInterface
Mod external = new OsuModDoubleTime(); Mod external = new OsuModDoubleTime();
Mod overlayButtonMod = null; Mod overlayButtonMod = null;
createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; }); AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () => AddAssert("ensure button is selected", () =>
{ {
var button = modSelect.GetModButton(SelectedMods.Value.Single()); var button = getPanelForMod(SelectedMods.Value.Single().GetType());
overlayButtonMod = button.SelectedMod; overlayButtonMod = button.Mod;
return overlayButtonMod.GetType() == external.GetType(); return button.Active.Value;
}); });
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
AddAssert("mod instance doesn't match", () => external != overlayButtonMod); AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1); AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod)); AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod)));
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external)); AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external)));
}
[Test]
public void TestNonStacked()
{
changeRuleset(0);
AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
AddStep("show", () => modSelect.Show());
AddAssert("ensure all buttons are spread out", () => modSelect.ChildrenOfType<ModButton>().All(m => m.Mods.Length <= 1));
} }
[Test] [Test]
public void TestChangeIsValidChangesButtonVisibility() public void TestChangeIsValidChangesButtonVisibility()
{ {
createScreen();
changeRuleset(0); changeRuleset(0);
AddAssert("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
AddUntilStep("double time not visible", () => modSelect.ChildrenOfType<ModButton>().All(b => !b.Mods.Any(m => m is OsuModDoubleTime))); AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore))); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
AddStep("make double time valid again", () => modSelect.IsValidMod = m => true); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = m => true);
AddUntilStep("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore))); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
} }
[Test] [Test]
public void TestChangeIsValidPreservesSelection() public void TestChangeIsValidPreservesSelection()
{ {
createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddStep("make NF invalid", () => modSelect.IsValidMod = m => !(m is ModNoFail)); AddStep("make NF invalid", () => modSelectOverlay.IsValidMod = m => !(m is ModNoFail));
AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2); AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
} }
[Test] [Test]
public void TestUnimplementedModIsUnselectable() public void TestUnimplementedModIsUnselectable()
{ {
var testRuleset = new TestUnimplementedModOsuRuleset(); var testRuleset = new TestUnimplementedModOsuRuleset();
changeTestRuleset(testRuleset.RulesetInfo);
var conversionMods = testRuleset.GetModsFor(ModType.Conversion); createScreen();
var unimplementedMod = conversionMods.FirstOrDefault(m => m is TestUnimplementedMod); AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
waitForColumnLoad();
testUnimplementedMod(unimplementedMod); AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
} }
private void testSingleMod(Mod mod) [Test]
public void TestDeselectAllViaButton()
{ {
selectNext(mod); createScreen();
checkSelected(mod); changeRuleset(0);
selectPrevious(mod); AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
checkNotSelected(mod); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
selectNext(mod); AddStep("click deselect all button", () =>
selectNext(mod);
checkNotSelected(mod);
selectPrevious(mod);
selectPrevious(mod);
checkNotSelected(mod);
}
private void testMultiMod(MultiMod multiMod)
{
foreach (var mod in multiMod.Mods)
{ {
selectNext(mod); InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
checkSelected(mod); InputManager.Click(MouseButton.Left);
}
for (int index = multiMod.Mods.Length - 1; index >= 0; index--)
selectPrevious(multiMod.Mods[index]);
foreach (var mod in multiMod.Mods)
checkNotSelected(mod);
}
private void testUnimplementedMod(Mod mod)
{
selectNext(mod);
checkNotSelected(mod);
}
private void testIncompatibleMods(Mod modA, Mod modB)
{
selectNext(modA);
checkSelected(modA);
checkNotSelected(modB);
selectNext(modB);
checkSelected(modB);
checkNotSelected(modA);
selectPrevious(modB);
checkNotSelected(modA);
checkNotSelected(modB);
}
private void testDeselectAll(IEnumerable<Mod> mods)
{
foreach (var mod in mods)
selectNext(mod);
AddAssert("check for any selection", () => modSelect.SelectedMods.Value.Any());
AddStep("deselect all", () => modSelect.DeselectAllButton.Action.Invoke());
AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any());
}
private void testModsWithSameBaseType(Mod modA, Mod modB)
{
selectNext(modA);
checkSelected(modA);
selectNext(modB);
checkSelected(modB);
// Backwards
selectPrevious(modA);
checkSelected(modA);
}
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
private void checkSelected(Mod mod)
{
AddAssert($"check {mod.Name} is selected", () =>
{
var button = modSelect.GetModButton(mod);
return modSelect.SelectedMods.Value.SingleOrDefault(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected;
}); });
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
} }
private void changeRuleset(int? onlineId) [Test]
public void TestCloseViaBackButton()
{ {
AddStep($"change ruleset to {(onlineId?.ToString() ?? "none")}", () => { Ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == onlineId); }); createScreen();
waitForLoad(); changeRuleset(0);
}
private void changeTestRuleset(RulesetInfo rulesetInfo) AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
{ assertCustomisationToggleState(disabled: false, active: true);
AddStep($"change ruleset to {rulesetInfo.Name}", () => { Ruleset.Value = rulesetInfo; }); AddAssert("back button disabled", () => !this.ChildrenOfType<ShearedButton>().First().Enabled.Value);
waitForLoad();
}
private void waitForLoad() => AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for icons to load", () => modSelect.AllLoaded); AddStep("click back button", () =>
private void checkNotSelected(Mod mod)
{
AddAssert($"check {mod.Name} is not selected", () =>
{ {
var button = modSelect.GetModButton(mod); InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().First());
return modSelect.SelectedMods.Value.All(m => m.GetType() != mod.GetType()) && button.SelectedMod?.GetType() != mod.GetType(); InputManager.Click(MouseButton.Left);
}); });
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
} }
private void createDisplay(Func<TestModSelectOverlay> createOverlayFunc) [Test]
public void TestColumnHiding()
{ {
Children = new Drawable[] AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay
{ {
modSelect = createOverlayFunc().With(d => RelativeSizeAxes = Axes.Both,
{ State = { Value = Visibility.Visible },
d.Origin = Anchor.BottomCentre; SelectedMods = { BindTarget = SelectedMods },
d.Anchor = Anchor.BottomCentre; IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
d.SelectedMods.BindTarget = SelectedMods; });
}), waitForColumnLoad();
modDisplay = new ModDisplay changeRuleset(0);
{
Anchor = Anchor.TopRight, AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
Origin = Anchor.TopRight,
Position = new Vector2(-5, 25), AddStep("unset filter", () => modSelectOverlay.IsValidMod = _ => true);
Current = { BindTarget = modSelect.SelectedMods } AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
}
}; AddStep("filter out everything", () => modSelectOverlay.IsValidMod = _ => false);
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("hide", () => modSelectOverlay.Hide());
AddStep("set filter for 3 columns", () => modSelectOverlay.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
|| mod.Type == ModType.Automation
|| mod.Type == ModType.Conversion);
AddStep("show", () => modSelectOverlay.Show());
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
} }
private class TestModSelectOverlay : UserModSelectOverlay [Test]
public void TestColumnHidingOnRulesetChange()
{ {
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods; createScreen();
public bool AllLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); changeRuleset(0);
AddAssert("5 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 5);
public new FillFlowContainer<ModSection> ModSectionsContainer => AddStep("change to ruleset without all mod types", () => Ruleset.Value = TestCustomisableModRuleset.CreateTestRulesetInfo());
base.ModSectionsContainer; AddUntilStep("1 column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
public ModButton GetModButton(Mod mod) changeRuleset(0);
{ AddAssert("5 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 5);
var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type);
return section.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
}
public new TriangleButton DeselectAllButton => base.DeselectAllButton;
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
public new Color4 HighMultiplierColour => base.HighMultiplierColour;
} }
private class TestNonStackedModSelectOverlay : TestModSelectOverlay private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectOverlay.ChildrenOfType<ModColumn>().Any() && modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
private void changeRuleset(int id)
{ {
protected override bool Stacked => false; AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id));
waitForColumnLoad();
} }
private void assertCustomisationToggleState(bool disabled, bool active)
{
ShearedToggleButton getToggle() => modSelectOverlay.ChildrenOfType<ShearedToggleButton>().Single();
AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled);
AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active);
}
private ModPanel getPanelForMod(Type modType)
=> modSelectOverlay.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
private class TestUnimplementedMod : Mod private class TestUnimplementedMod : Mod
{ {
public override string Name => "Unimplemented mod"; public override string Name => "Unimplemented mod";

View File

@ -1,525 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneModSelectScreen : OsuManualInputManagerTestScene
{
[Resolved]
private RulesetStore rulesetStore { get; set; }
private UserModSelectScreen modSelectScreen;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
}
private void createScreen()
{
AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
}
[Test]
public void TestStateChange()
{
createScreen();
AddStep("toggle state", () => modSelectScreen.ToggleVisibility());
}
[Test]
public void TestPreexistingSelection()
{
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
createScreen();
AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectScreen.ChildrenOfType<ISettingsItem>().Any());
}
[Test]
public void TestExternalSelection()
{
createScreen();
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModAlternate(), new OsuModDaycore() });
AddUntilStep("two panels active", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectScreen.ChildrenOfType<ISettingsItem>().Any());
}
[Test]
public void TestRulesetChange()
{
createScreen();
changeRuleset(0);
changeRuleset(1);
changeRuleset(2);
changeRuleset(3);
}
[Test]
public void TestIncompatibilityToggling()
{
createScreen();
changeRuleset(0);
AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value);
AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value);
AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value);
}
[Test]
public void TestDimmedState()
{
createScreen();
changeRuleset(0);
AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value));
ModColumn lastColumn = null;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
AddStep("request scroll to last column", () =>
{
var lastDimContainer = this.ChildrenOfType<ModSelectScreen.ColumnDimContainer>().Last();
lastColumn = lastDimContainer.Column;
lastDimContainer.RequestScroll?.Invoke(lastDimContainer);
});
AddUntilStep("column undimmed", () => lastColumn.Active.Value);
AddStep("click panel", () =>
{
InputManager.MoveMouseTo(lastColumn.ChildrenOfType<ModPanel>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("panel selected", () => lastColumn.ChildrenOfType<ModPanel>().First().Active.Value);
}
[Test]
public void TestCustomisationToggleState()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
assertCustomisationToggleState(disabled: false, active: false);
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via toggle", () =>
{
InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType<ShearedToggleButton>().Single());
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
assertCustomisationToggleState(disabled: false, active: false);
AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
assertCustomisationToggleState(disabled: false, active: false);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
}
[Test]
public void TestDismissCustomisationViaDimmedArea()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType<ModSettingsArea>().Single()));
AddStep("move mouse to dimmed area", () =>
{
InputManager.MoveMouseTo(new Vector2(
modSelectScreen.ScreenSpaceDrawQuad.TopLeft.X,
(modSelectScreen.ScreenSpaceDrawQuad.TopLeft.Y + modSelectScreen.ScreenSpaceDrawQuad.BottomLeft.Y) / 2));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
assertCustomisationToggleState(disabled: false, active: false);
AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType<ModPanel>().First()));
AddAssert("first mod panel is hovered", () => modSelectScreen.ChildrenOfType<ModPanel>().First().IsHovered);
}
/// <summary>
/// Ensure that two mod overlays are not cross polluting via central settings instances.
/// </summary>
[Test]
public void TestSettingsNotCrossPolluting()
{
Bindable<IReadOnlyList<Mod>> selectedMods2 = null;
ModSelectScreen modSelectScreen2 = null;
createScreen();
AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
AddStep("set setting", () => modSelectScreen.ChildrenOfType<SettingsSlider<float>>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddStep("create second bindable", () => selectedMods2 = new Bindable<IReadOnlyList<Mod>>(new Mod[] { new OsuModDifficultyAdjust() }));
AddStep("create second overlay", () =>
{
Add(modSelectScreen2 = new UserModSelectScreen().With(d =>
{
d.Origin = Anchor.TopCentre;
d.Anchor = Anchor.TopCentre;
d.SelectedMods.BindTarget = selectedMods2;
}));
});
AddStep("show", () => modSelectScreen2.Show());
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
}
[Test]
public void TestSettingsResetOnDeselection()
{
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
createScreen();
changeRuleset(0);
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
}
[Test]
public void TestAnimationFlushOnClose()
{
createScreen();
changeRuleset(0);
AddStep("Select all fun mods", () =>
{
modSelectScreen.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
.SelectAll();
});
AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
AddStep("trigger deselect and close overlay", () =>
{
modSelectScreen.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
.DeselectAll();
modSelectScreen.Hide();
});
AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestRulesetChanges()
{
createScreen();
changeRuleset(0);
var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; });
changeRuleset(0);
AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
changeRuleset(3);
AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
changeRuleset(0);
AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestExternallySetCustomizedMod()
{
createScreen();
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("ensure button is selected and customized accordingly", () =>
{
var button = getPanelForMod(SelectedMods.Value.Single().GetType());
return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01;
});
}
[Test]
public void TestSettingsAreRetainedOnReload()
{
createScreen();
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
createScreen();
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
}
[Test]
public void TestExternallySetModIsReplacedByOverlayInstance()
{
Mod external = new OsuModDoubleTime();
Mod overlayButtonMod = null;
createScreen();
changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () =>
{
var button = getPanelForMod(SelectedMods.Value.Single().GetType());
overlayButtonMod = button.Mod;
return button.Active.Value;
});
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod)));
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external)));
}
[Test]
public void TestChangeIsValidChangesButtonVisibility()
{
createScreen();
changeRuleset(0);
AddAssert("double time visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
AddStep("make double time invalid", () => modSelectScreen.IsValidMod = m => !(m is OsuModDoubleTime));
AddUntilStep("double time not visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
AddStep("make double time valid again", () => modSelectScreen.IsValidMod = m => true);
AddUntilStep("double time visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
}
[Test]
public void TestChangeIsValidPreservesSelection()
{
createScreen();
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddStep("make NF invalid", () => modSelectScreen.IsValidMod = m => !(m is ModNoFail));
AddAssert("DT + HD still selected", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
}
[Test]
public void TestUnimplementedModIsUnselectable()
{
var testRuleset = new TestUnimplementedModOsuRuleset();
createScreen();
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
waitForColumnLoad();
AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
}
[Test]
public void TestDeselectAllViaButton()
{
createScreen();
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
}
[Test]
public void TestCloseViaBackButton()
{
createScreen();
changeRuleset(0);
AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true);
AddAssert("back button disabled", () => !this.ChildrenOfType<ShearedButton>().First().Enabled.Value);
AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
AddStep("click back button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select hidden", () => modSelectScreen.State.Value == Visibility.Hidden);
}
[Test]
public void TestColumnHiding()
{
AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("unset filter", () => modSelectScreen.IsValidMod = _ => true);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("filter out everything", () => modSelectScreen.IsValidMod = _ => false);
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("hide", () => modSelectScreen.Hide());
AddStep("set filter for 3 columns", () => modSelectScreen.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
|| mod.Type == ModType.Automation
|| mod.Type == ModType.Conversion);
AddStep("show", () => modSelectScreen.Show());
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectScreen.ChildrenOfType<ModColumn>().Any() && modSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
private void changeRuleset(int id)
{
AddStep($"set ruleset to {id}", () => Ruleset.Value = rulesetStore.GetRuleset(id));
waitForColumnLoad();
}
private void assertCustomisationToggleState(bool disabled, bool active)
{
ShearedToggleButton getToggle() => modSelectScreen.ChildrenOfType<ShearedToggleButton>().Single();
AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled);
AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active);
}
private ModPanel getPanelForMod(Type modType)
=> modSelectScreen.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
private class TestUnimplementedMod : Mod
{
public override string Name => "Unimplemented mod";
public override string Acronym => "UM";
public override string Description => "A mod that is not implemented.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Conversion;
}
private class TestUnimplementedModOsuRuleset : OsuRuleset
{
public override string ShortName => "unimplemented";
public override IEnumerable<Mod> GetModsFor(ModType type)
{
if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
return base.GetModsFor(type);
}
}
}
}

View File

@ -1,238 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModSettings : OsuManualInputManagerTestScene
{
private TestModSelectOverlay modSelect;
private readonly Mod testCustomisableMod = new TestModCustomisable1();
private readonly Mod testCustomisableAutoOpenMod = new TestModCustomisable2();
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
Ruleset.Value = CreateTestRulesetInfo();
});
[Test]
public void TestButtonShowsOnCustomisableMod()
{
createModSelect();
openModSelect();
AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value);
AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded);
AddStep("select mod", () => modSelect.SelectMod(testCustomisableMod));
AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
AddStep("open Customisation", () => modSelect.CustomiseButton.TriggerClick());
AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableMod));
AddAssert("controls hidden", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
}
[Test]
public void TestButtonShowsOnModAlreadyAdded()
{
AddStep("set active mods", () => SelectedMods.Value = new List<Mod> { testCustomisableMod });
createModSelect();
AddAssert("mods still active", () => SelectedMods.Value.Count == 1);
openModSelect();
AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
}
[Test]
public void TestCustomisationMenuVisibility()
{
createModSelect();
openModSelect();
AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
AddStep("select mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.State.Value == Visibility.Visible);
AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden);
}
[Test]
public void TestModSettingsUnboundWhenCopied()
{
OsuModDoubleTime original = null;
OsuModDoubleTime copy = null;
AddStep("create mods", () =>
{
original = new OsuModDoubleTime();
copy = (OsuModDoubleTime)original.DeepClone();
});
AddStep("change property", () => original.SpeedChange.Value = 2);
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, original.SpeedChange.Value));
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
}
[Test]
public void TestMultiModSettingsUnboundWhenCopied()
{
MultiMod original = null;
MultiMod copy = null;
AddStep("create mods", () =>
{
original = new MultiMod(new OsuModDoubleTime());
copy = (MultiMod)original.DeepClone();
});
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
}
[Test]
public void TestCustomisationMenuNoClickthrough()
{
createModSelect();
openModSelect();
AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.State.Value == Visibility.Visible);
AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
}
private void createModSelect()
{
AddStep("create mod select", () =>
{
Child = modSelect = new TestModSelectOverlay
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
SelectedMods = { BindTarget = SelectedMods }
};
});
}
private void openModSelect()
{
AddStep("open", () => modSelect.Show());
AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded);
}
private class TestModSelectOverlay : UserModSelectOverlay
{
public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer;
public new TriangleButton CustomiseButton => base.CustomiseButton;
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
public ModButton GetModButton(Mod mod)
{
return ModSectionsContainer.ChildrenOfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
}
public void SelectMod(Mod mod) =>
GetModButton(mod).SelectNext(1);
public void SetModSettingsWidth(float newWidth) =>
ModSettingsContainer.Parent.Width = newWidth;
}
public static RulesetInfo CreateTestRulesetInfo() => new TestCustomisableModRuleset().RulesetInfo;
public class TestCustomisableModRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type)
{
if (type == ModType.Conversion)
{
return new Mod[]
{
new TestModCustomisable1(),
new TestModCustomisable2()
};
}
return Array.Empty<Mod>();
}
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description { get; } = "test";
public override string ShortName { get; } = "tst";
}
private class TestModCustomisable1 : TestModCustomisable
{
public override string Name => "Customisable Mod 1";
public override string Acronym => "CM1";
}
private class TestModCustomisable2 : TestModCustomisable
{
public override string Name => "Customisable Mod 2";
public override string Acronym => "CM2";
public override bool RequiresConfiguration => true;
}
private abstract class TestModCustomisable : Mod, IApplicableMod
{
public override double ScoreMultiplier => 1.0;
public override string Description => "This is a customisable test mod.";
public override ModType Type => ModType.Conversion;
[SettingSource("Sample float", "Change something for a mod")]
public BindableFloat SliderBindable { get; } = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 7
};
[SettingSource("Sample bool", "Clicking this changes a setting")]
public BindableBool TickBindable { get; } = new BindableBool();
}
}
}

View File

@ -1,44 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Testing;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneRoundedButton : OsuTestScene public class TestSceneRoundedButton : ThemeComparisonTestScene
{ {
[Test] private readonly BindableBool enabled = new BindableBool(true);
public void TestBasic()
protected override Drawable CreateContent() => new RoundedButton
{ {
RoundedButton button = null; Width = 400,
Text = "Test button",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Enabled = { BindTarget = enabled },
};
AddStep("create button", () => Child = new Container [Test]
{ public void TestDisabled()
RelativeSizeAxes = Axes.Both, {
Children = new Drawable[] AddToggleStep("toggle disabled", disabled => enabled.Value = !disabled);
{ }
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.DarkGray
},
button = new RoundedButton
{
Width = 400,
Text = "Test button",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = () => { }
}
}
});
AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { }); [Test]
public void TestBackgroundColour()
{
AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red));
AddAssert("first button has correct colour", () => Cell(0, 1).ChildrenOfType<RoundedButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Highlight1);
} }
} }
} }

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneSettingsToolboxGroup : OsuManualInputManagerTestScene
{
private SettingsToolboxGroup group;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = group = new SettingsToolboxGroup("example")
{
Children = new Drawable[]
{
new RoundedButton
{
RelativeSizeAxes = Axes.X,
Text = @"Button",
Enabled = { Value = true },
},
new OsuCheckbox
{
LabelText = @"Checkbox",
},
new OutlinedTextBox
{
RelativeSizeAxes = Axes.X,
Height = 30,
PlaceholderText = @"Textbox",
}
},
};
});
[Test]
public void TestClickExpandButtonMultipleTimes()
{
AddAssert("group expanded by default", () => group.Expanded.Value);
AddStep("click expand button multiple times", () =>
{
InputManager.MoveMouseTo(group.ChildrenOfType<IconButton>().Single());
Scheduler.AddDelayed(() => InputManager.Click(MouseButton.Left), 100);
Scheduler.AddDelayed(() => InputManager.Click(MouseButton.Left), 200);
Scheduler.AddDelayed(() => InputManager.Click(MouseButton.Left), 300);
});
AddAssert("group contracted", () => !group.Expanded.Value);
}
}
}

View File

@ -5,7 +5,7 @@
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="Moq" Version="4.17.2" />

View File

@ -6,7 +6,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking; using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -164,6 +166,20 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorHitAnimations, false); SetDefault(OsuSetting.EditorHitAnimations, false);
} }
public IDictionary<OsuSetting, string> GetLoggableState() =>
new Dictionary<OsuSetting, string>(ConfigStore.Where(kvp => !keyContainsPrivateInformation(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString()));
private static bool keyContainsPrivateInformation(OsuSetting argKey)
{
switch (argKey)
{
case OsuSetting.Token:
return true;
}
return false;
}
public OsuConfigManager(Storage storage) public OsuConfigManager(Storage storage)
: base(storage) : base(storage)
{ {

View File

@ -344,6 +344,26 @@ namespace osu.Game.Database
} }
} }
/// <summary>
/// Write changes to realm.
/// </summary>
/// <param name="action">The work to run.</param>
public T Write<T>(Func<Realm, T> action)
{
if (ThreadSafety.IsUpdateThread)
{
total_writes_update.Value++;
return Realm.Write(action);
}
else
{
total_writes_async.Value++;
using (var realm = getRealmInstance())
return realm.Write(action);
}
}
/// <summary> /// <summary>
/// Write changes to realm. /// Write changes to realm.
/// </summary> /// </summary>

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -33,9 +32,12 @@ namespace osu.Game.Graphics.UserInterface
private Color4? backgroundColour; private Color4? backgroundColour;
/// <summary>
/// Sets a custom background colour to this button, replacing the provided default.
/// </summary>
public Color4 BackgroundColour public Color4 BackgroundColour
{ {
get => backgroundColour ?? Color4.White; get => backgroundColour ?? defaultBackgroundColour;
set set
{ {
backgroundColour = value; backgroundColour = value;
@ -43,6 +45,23 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private Color4 defaultBackgroundColour;
/// <summary>
/// Sets a default background colour to this button.
/// </summary>
protected Color4 DefaultBackgroundColour
{
get => defaultBackgroundColour;
set
{
defaultBackgroundColour = value;
if (backgroundColour == null)
Background.FadeColour(value);
}
}
protected override Container<Drawable> Content { get; } protected override Container<Drawable> Content { get; }
protected Box Hover; protected Box Hover;
@ -89,8 +108,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
if (backgroundColour == null) DefaultBackgroundColour = colours.BlueDark;
BackgroundColour = colours.BlueDark;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -106,10 +124,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (Enabled.Value) if (Enabled.Value)
{ Background.FlashColour(BackgroundColour.Lighten(0.4f), 200);
Debug.Assert(backgroundColour != null);
Background.FlashColour(backgroundColour.Value.Lighten(0.4f), 200);
}
return base.OnClick(e); return base.OnClick(e);
} }

View File

@ -184,14 +184,12 @@ namespace osu.Game.Graphics.UserInterface
protected override void UpdateBackgroundColour() protected override void UpdateBackgroundColour()
{ {
if (!IsPreSelected && !IsSelected)
{
Background.FadeOut(600, Easing.OutQuint);
return;
}
Background.FadeIn(100, Easing.OutQuint);
Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint); Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint);
if (IsPreSelected || IsSelected)
Background.FadeIn(100, Easing.OutQuint);
else
Background.FadeOut(600, Easing.OutQuint);
} }
protected override void UpdateForegroundColour() protected override void UpdateForegroundColour()

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -29,8 +28,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours) private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
{ {
if (BackgroundColour == Color4.White) DefaultBackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -5,9 +5,9 @@ using osu.Framework.Localisation;
namespace osu.Game.Localisation namespace osu.Game.Localisation
{ {
public static class ModSelectScreenStrings public static class ModSelectOverlayStrings
{ {
private const string prefix = @"osu.Game.Resources.Localisation.ModSelectScreen"; private const string prefix = @"osu.Game.Resources.Localisation.ModSelectOverlay";
/// <summary> /// <summary>
/// "Mod Select" /// "Mod Select"
@ -26,4 +26,4 @@ namespace osu.Game.Localisation
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -420,10 +420,11 @@ namespace osu.Game.Online.Chat
/// Joins a channel if it has not already been joined. Must be called from the update thread. /// Joins a channel if it has not already been joined. Must be called from the update thread.
/// </summary> /// </summary>
/// <param name="channel">The channel to join.</param> /// <param name="channel">The channel to join.</param>
/// <param name="setCurrent">Set the channel to join as the current channel if the current channel is null.</param>
/// <returns>The joined channel. Note that this may not match the parameter channel as it is a backed object.</returns> /// <returns>The joined channel. Note that this may not match the parameter channel as it is a backed object.</returns>
public Channel JoinChannel(Channel channel) => joinChannel(channel, true); public Channel JoinChannel(Channel channel, bool setCurrent = true) => joinChannel(channel, true, setCurrent);
private Channel joinChannel(Channel channel, bool fetchInitialMessages = false) private Channel joinChannel(Channel channel, bool fetchInitialMessages = false, bool setCurrent = true)
{ {
if (channel == null) return null; if (channel == null) return null;
@ -439,7 +440,7 @@ namespace osu.Game.Online.Chat
case ChannelType.Multiplayer: case ChannelType.Multiplayer:
// join is implicit. happens when you join a multiplayer game. // join is implicit. happens when you join a multiplayer game.
// this will probably change in the future. // this will probably change in the future.
joinChannel(channel, fetchInitialMessages); joinChannel(channel, fetchInitialMessages, setCurrent);
return channel; return channel;
case ChannelType.PM: case ChannelType.PM:
@ -460,7 +461,7 @@ namespace osu.Game.Online.Chat
default: default:
var req = new JoinChannelRequest(channel); var req = new JoinChannelRequest(channel);
req.Success += () => joinChannel(channel, fetchInitialMessages); req.Success += () => joinChannel(channel, fetchInitialMessages, setCurrent);
req.Failure += ex => LeaveChannel(channel); req.Failure += ex => LeaveChannel(channel);
api.Queue(req); api.Queue(req);
return channel; return channel;
@ -472,7 +473,8 @@ namespace osu.Game.Online.Chat
this.fetchInitialMessages(channel); this.fetchInitialMessages(channel);
} }
CurrentChannel.Value ??= channel; if (setCurrent)
CurrentChannel.Value ??= channel;
return channel; return channel;
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -56,6 +57,8 @@ using osu.Game.Updater;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK.Graphics; using osuTK.Graphics;
using Sentry;
using Logger = osu.Framework.Logging.Logger;
namespace osu.Game namespace osu.Game
{ {
@ -258,7 +261,7 @@ namespace osu.Game
{ {
dependencies.CacheAs(this); dependencies.CacheAs(this);
dependencies.Cache(SentryLogger); SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 }); dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
@ -1197,6 +1200,15 @@ namespace osu.Game
private void screenChanged(IScreen current, IScreen newScreen) private void screenChanged(IScreen current, IScreen newScreen)
{ {
SentrySdk.ConfigureScope(scope =>
{
scope.Contexts[@"screen stack"] = new
{
Current = newScreen?.GetType().ReadableName(),
Previous = current?.GetType().ReadableName(),
};
});
switch (newScreen) switch (newScreen)
{ {
case IntroScreen intro: case IntroScreen intro:

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -85,6 +86,8 @@ namespace osu.Game
public bool IsDeployedBuild => AssemblyVersion.Major > 0; public bool IsDeployedBuild => AssemblyVersion.Major > 0;
internal const string BUILD_SUFFIX = "lazer";
public virtual string Version public virtual string Version
{ {
get get
@ -93,7 +96,7 @@ namespace osu.Game
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release"); return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
var version = AssemblyVersion; var version = AssemblyVersion;
return $@"{version.Major}.{version.Minor}.{version.Build}-lazer"; return $@"{version.Major}.{version.Minor}.{version.Build}-{BUILD_SUFFIX}";
} }
} }
@ -180,9 +183,21 @@ namespace osu.Game
/// </summary> /// </summary>
protected DatabaseContextFactory EFContextFactory { get; private set; } protected DatabaseContextFactory EFContextFactory { get; private set; }
/// <summary>
/// Number of unhandled exceptions to allow before aborting execution.
/// </summary>
/// <remarks>
/// When an unhandled exception is encountered, an internal count will be decremented.
/// If the count hits zero, the game will crash.
/// Each second, the count is incremented until reaching the value specified.
/// </remarks>
protected virtual int UnhandledExceptionsBeforeCrash => DebugUtils.IsDebugBuild ? 0 : 1;
public OsuGameBase() public OsuGameBase()
{ {
Name = @"osu!"; Name = @"osu!";
allowableExceptions = UnhandledExceptionsBeforeCrash;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -408,6 +423,8 @@ namespace osu.Game
LocalConfig ??= UseDevelopmentServer LocalConfig ??= UseDevelopmentServer
? new DevelopmentOsuConfigManager(Storage) ? new DevelopmentOsuConfigManager(Storage)
: new OsuConfigManager(Storage); : new OsuConfigManager(Storage);
host.ExceptionThrown += onExceptionThrown;
} }
/// <summary> /// <summary>
@ -505,6 +522,23 @@ namespace osu.Game
AvailableMods.Value = dict; AvailableMods.Value = dict;
} }
private int allowableExceptions;
/// <summary>
/// Allows a maximum of one unhandled exception, per second of execution.
/// </summary>
private bool onExceptionThrown(Exception _)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
@ -514,6 +548,9 @@ namespace osu.Game
LocalConfig?.Dispose(); LocalConfig?.Dispose();
realm?.Dispose(); realm?.Dispose();
if (Host != null)
Host.ExceptionThrown -= onExceptionThrown;
} }
} }
} }

View File

@ -6,7 +6,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -19,11 +18,9 @@ namespace osu.Game.Overlays.Chat.ChannelList
{ {
public class ChannelList : Container public class ChannelList : Container
{ {
public Action<Channel>? OnRequestSelect; public Action<Channel?>? OnRequestSelect;
public Action<Channel>? OnRequestLeave; public Action<Channel>? OnRequestLeave;
public readonly BindableBool SelectorActive = new BindableBool();
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>(); private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private ChannelListItemFlow publicChannelFlow = null!; private ChannelListItemFlow publicChannelFlow = null!;
@ -56,7 +53,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
new ChannelListSelector new ChannelListSelector
{ {
Margin = new MarginPadding { Bottom = 10 }, Margin = new MarginPadding { Bottom = 10 },
SelectorActive = { BindTarget = SelectorActive }, Action = () => OnRequestSelect?.Invoke(null),
}, },
privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"), privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"),
}, },
@ -73,7 +70,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
ChannelListItem item = new ChannelListItem(channel); ChannelListItem item = new ChannelListItem(channel);
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
item.SelectorActive.BindTarget = SelectorActive;
ChannelListItemFlow flow = getFlowForChannel(channel); ChannelListItemFlow flow = getFlowForChannel(channel);
channelMap.Add(channel, item); channelMap.Add(channel, item);

View File

@ -31,8 +31,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
public readonly BindableBool Unread = new BindableBool(); public readonly BindableBool Unread = new BindableBool();
public readonly BindableBool SelectorActive = new BindableBool();
private Box hoverBox = null!; private Box hoverBox = null!;
private Box selectBox = null!; private Box selectBox = null!;
private OsuSpriteText text = null!; private OsuSpriteText text = null!;
@ -127,7 +125,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
base.LoadComplete(); base.LoadComplete();
selectedChannel.BindValueChanged(_ => updateState(), true); selectedChannel.BindValueChanged(_ => updateState(), true);
SelectorActive.BindValueChanged(_ => updateState(), true);
Unread.BindValueChanged(_ => updateState(), true); Unread.BindValueChanged(_ => updateState(), true);
} }
@ -163,7 +160,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
private void updateState() private void updateState()
{ {
bool selected = selectedChannel.Value == Channel && !SelectorActive.Value; bool selected = selectedChannel.Value == Channel;
if (selected) if (selected)
selectBox.FadeIn(300, Easing.OutQuint); selectBox.FadeIn(300, Easing.OutQuint);

View File

@ -12,17 +12,19 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.Chat.ChannelList namespace osu.Game.Overlays.Chat.ChannelList
{ {
public class ChannelListSelector : OsuClickableContainer public class ChannelListSelector : OsuClickableContainer
{ {
public readonly BindableBool SelectorActive = new BindableBool();
private Box hoverBox = null!; private Box hoverBox = null!;
private Box selectBox = null!; private Box selectBox = null!;
private OsuSpriteText text = null!; private OsuSpriteText text = null!;
[Resolved]
private Bindable<Channel> currentChannel { get; set; } = null!;
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
@ -69,9 +71,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
{ {
base.LoadComplete(); base.LoadComplete();
SelectorActive.BindValueChanged(selector => currentChannel.BindValueChanged(channel =>
{ {
if (selector.NewValue) // This logic should be handled by the chat overlay rather than this component.
// Selected state should be moved to an abstract class and shared with ChannelListItem.
if (channel.NewValue == null)
{ {
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
selectBox.FadeIn(300, Easing.OutQuint); selectBox.FadeIn(300, Easing.OutQuint);
@ -82,8 +86,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
selectBox.FadeOut(200, Easing.OutQuint); selectBox.FadeOut(200, Easing.OutQuint);
} }
}, true); }, true);
Action = () => SelectorActive.Value = true;
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -50,8 +50,6 @@ namespace osu.Game.Overlays
private const float side_bar_width = 190; private const float side_bar_width = 190;
private const float chat_bar_height = 60; private const float chat_bar_height = 60;
private readonly BindableBool selectorActive = new BindableBool();
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -100,7 +98,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = side_bar_width, Width = side_bar_width,
Padding = new MarginPadding { Top = top_bar_height }, Padding = new MarginPadding { Top = top_bar_height },
SelectorActive = { BindTarget = selectorActive },
}, },
new Container new Container
{ {
@ -137,7 +134,6 @@ namespace osu.Game.Overlays
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Padding = new MarginPadding { Left = side_bar_width }, Padding = new MarginPadding { Left = side_bar_width },
ShowSearch = { BindTarget = selectorActive },
}, },
}; };
} }
@ -146,8 +142,6 @@ namespace osu.Game.Overlays
{ {
base.LoadComplete(); base.LoadComplete();
loading.Show();
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight); config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
@ -157,21 +151,14 @@ namespace osu.Game.Overlays
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true); channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true); channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true);
channelList.OnRequestSelect += channel => channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel;
{
// Manually selecting a channel should dismiss the selector
selectorActive.Value = false;
channelManager.CurrentChannel.Value = channel;
};
channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel); channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel); channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel, false);
channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel); channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms; textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
textBar.OnChatMessageCommitted += handleChatMessage; textBar.OnChatMessageCommitted += handleChatMessage;
selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true);
} }
/// <summary> /// <summary>
@ -191,8 +178,6 @@ namespace osu.Game.Overlays
channelManager.CurrentChannel.Value = channel; channelManager.CurrentChannel.Value = channel;
} }
selectorActive.Value = false;
channel.HighlightedMessage.Value = message; channel.HighlightedMessage.Value = message;
Show(); Show();
@ -252,28 +237,25 @@ namespace osu.Game.Overlays
{ {
Channel? newChannel = channel.NewValue; Channel? newChannel = channel.NewValue;
loading.Show();
// Channel is null when leaving the currently selected channel
if (newChannel == null) if (newChannel == null)
{ {
// Find another channel to switch to // null channel denotes that we should be showing the listing.
newChannel = channelManager.JoinedChannels.FirstOrDefault(c => c != channel.OldValue); channelListing.State.Value = Visibility.Visible;
textBar.ShowSearch.Value = true;
if (newChannel == null)
selectorActive.Value = true;
else
currentChannel.Value = newChannel;
return;
} }
else
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
{ {
currentChannelContainer.Clear(); channelListing.State.Value = Visibility.Hidden;
currentChannelContainer.Add(loaded); textBar.ShowSearch.Value = false;
loading.Hide();
}); loading.Show();
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
{
currentChannelContainer.Clear();
currentChannelContainer.Add(loaded);
loading.Hide();
});
}
} }
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)

View File

@ -19,13 +19,16 @@ namespace osu.Game.Overlays.FirstRunSetup
protected FillFlowContainer Content { get; private set; } protected FillFlowContainer Content { get; private set; }
protected const float CONTENT_FONT_SIZE = 16;
protected const float HEADER_FONT_SIZE = 24;
[Resolved] [Resolved]
protected OverlayColourProvider OverlayColourProvider { get; private set; } protected OverlayColourProvider OverlayColourProvider { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
const float header_size = 40;
const float spacing = 20; const float spacing = 20;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@ -33,23 +36,29 @@ namespace osu.Game.Overlays.FirstRunSetup
new OsuScrollContainer(Direction.Vertical) new OsuScrollContainer(Direction.Vertical)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false, Masking = false,
Children = new Drawable[] Child = new Container
{ {
new OsuSpriteText RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 30 },
Children = new Drawable[]
{ {
Text = this.GetLocalisableDescription(), new OsuSpriteText
Font = OsuFont.Default.With(size: header_size), {
Colour = OverlayColourProvider.Light1, Text = this.GetLocalisableDescription(),
Font = OsuFont.TorusAlternate.With(size: HEADER_FONT_SIZE),
Colour = OverlayColourProvider.Light1,
},
Content = new FillFlowContainer
{
Y = HEADER_FONT_SIZE + spacing,
Spacing = new Vector2(spacing),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
}, },
Content = new FillFlowContainer
{
Y = header_size + spacing,
Spacing = new Vector2(spacing),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
}, },
} }
}; };
@ -59,7 +68,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{ {
base.OnEntering(e); base.OnEntering(e);
this this
.FadeInFromZero(500) .FadeInFromZero(100)
.MoveToX(offset) .MoveToX(offset)
.MoveToX(0, 500, Easing.OutQuint); .MoveToX(0, 500, Easing.OutQuint);
} }
@ -68,7 +77,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{ {
base.OnResuming(e); base.OnResuming(e);
this this
.FadeInFromZero(500) .FadeInFromZero(100)
.MoveToX(0, 500, Easing.OutQuint); .MoveToX(0, 500, Easing.OutQuint);
} }

View File

@ -46,11 +46,11 @@ namespace osu.Game.Overlays.FirstRunSetup
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(LegacyImportManager? legacyImportManager) private void load(LegacyImportManager? legacyImportManager)
{ {
Vector2 buttonSize = new Vector2(500, 60); Vector2 buttonSize = new Vector2(400, 50);
Content.Children = new Drawable[] Content.Children = new Drawable[]
{ {
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Colour = OverlayColourProvider.Content1, Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.Description, Text = FirstRunSetupBeatmapScreenStrings.Description,
@ -63,7 +63,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Height = 30, Height = 30,
Children = new Drawable[] Children = new Drawable[]
{ {
currentlyLoadedBeatmaps = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24, weight: FontWeight.SemiBold)) currentlyLoadedBeatmaps = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: HEADER_FONT_SIZE, weight: FontWeight.SemiBold))
{ {
Colour = OverlayColourProvider.Content2, Colour = OverlayColourProvider.Content2,
TextAnchor = Anchor.Centre, TextAnchor = Anchor.Centre,
@ -73,7 +73,7 @@ namespace osu.Game.Overlays.FirstRunSetup
}, },
} }
}, },
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Colour = OverlayColourProvider.Content1, Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.TutorialDescription, Text = FirstRunSetupBeatmapScreenStrings.TutorialDescription,
@ -89,7 +89,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.TutorialButton, Text = FirstRunSetupBeatmapScreenStrings.TutorialButton,
Action = downloadTutorial Action = downloadTutorial
}, },
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Colour = OverlayColourProvider.Content1, Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.BundledDescription, Text = FirstRunSetupBeatmapScreenStrings.BundledDescription,
@ -105,7 +105,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.BundledButton, Text = FirstRunSetupBeatmapScreenStrings.BundledButton,
Action = downloadBundled Action = downloadBundled
}, },
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Colour = OverlayColourProvider.Content1, Colour = OverlayColourProvider.Content1,
Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.", Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.",
@ -131,7 +131,7 @@ namespace osu.Game.Overlays.FirstRunSetup
})); }));
} }
}, },
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Colour = OverlayColourProvider.Content1, Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps, Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,

View File

@ -9,7 +9,7 @@ using osu.Framework.Localisation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections; using osu.Game.Overlays.Settings.Sections;
@ -22,11 +22,11 @@ namespace osu.Game.Overlays.FirstRunSetup
private SearchContainer<SettingsSection> searchContainer; private SearchContainer<SettingsSection> searchContainer;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuColour colours)
{ {
Content.Children = new Drawable[] Content.Children = new Drawable[]
{ {
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Text = FirstRunSetupOverlayStrings.BehaviourDescription, Text = FirstRunSetupOverlayStrings.BehaviourDescription,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -50,7 +50,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{ {
new[] new[]
{ {
new TriangleButton new RoundedButton
{ {
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
@ -59,10 +59,11 @@ namespace osu.Game.Overlays.FirstRunSetup
Action = applyStandard, Action = applyStandard,
}, },
Empty(), Empty(),
new DangerousTriangleButton new RoundedButton
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
BackgroundColour = colours.Pink3,
Text = FirstRunSetupOverlayStrings.ClassicDefaults, Text = FirstRunSetupOverlayStrings.ClassicDefaults,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Action = applyClassic Action = applyClassic

View File

@ -35,9 +35,11 @@ namespace osu.Game.Overlays.FirstRunSetup
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
const float screen_width = 640;
Content.Children = new Drawable[] Content.Children = new Drawable[]
{ {
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Text = FirstRunSetupOverlayStrings.UIScaleDescription, Text = FirstRunSetupOverlayStrings.UIScaleDescription,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -54,7 +56,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Size = new Vector2(960, 960 / 16f * 9 / 2), Size = new Vector2(screen_width, screen_width / 16f * 9 / 2),
Children = new Drawable[] Children = new Drawable[]
{ {
new GridContainer new GridContainer
@ -123,6 +125,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private class SampleScreenContainer : CompositeDrawable private class SampleScreenContainer : CompositeDrawable
{ {
private readonly OsuScreen screen;
// Minimal isolation from main game. // Minimal isolation from main game.
[Cached] [Cached]
@ -142,6 +145,12 @@ namespace osu.Game.Overlays.FirstRunSetup
public override bool PropagatePositionalInputSubTree => false; public override bool PropagatePositionalInputSubTree => false;
public override bool PropagateNonPositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false;
public SampleScreenContainer(OsuScreen screen)
{
this.screen = screen;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets) private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
{ {
@ -149,13 +158,8 @@ namespace osu.Game.Overlays.FirstRunSetup
Beatmap.Value.LoadTrack(); Beatmap.Value.LoadTrack();
Ruleset.Value = rulesets.AvailableRulesets.First(); Ruleset.Value = rulesets.AvailableRulesets.First();
}
public SampleScreenContainer(Screen screen)
{
OsuScreenStack stack; OsuScreenStack stack;
RelativeSizeAxes = Axes.Both;
OsuLogo logo; OsuLogo logo;
Padding = new MarginPadding(5); Padding = new MarginPadding(5);
@ -189,7 +193,8 @@ namespace osu.Game.Overlays.FirstRunSetup
}, },
}; };
stack.Push(screen); // intentionally load synchronously so it is included in the initial load of the first run screen.
stack.PushSynchronously(screen);
} }
} }
} }

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{ {
Content.Children = new Drawable[] Content.Children = new Drawable[]
{ {
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Text = FirstRunSetupOverlayStrings.WelcomeDescription, Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -62,12 +63,15 @@ namespace osu.Game.Overlays
typeof(ScreenBehaviour), typeof(ScreenBehaviour),
}; };
private Container stackContainer = null!; private Container screenContent = null!;
private Bindable<OverlayActivation>? overlayActivationMode; private Bindable<OverlayActivation>? overlayActivationMode;
private Container content = null!; private Container content = null!;
private LoadingSpinner loading = null!;
private ScheduledDelegate? loadingShowDelegate;
public FirstRunSetupOverlay() public FirstRunSetupOverlay()
: base(OverlayColourScheme.Purple) : base(OverlayColourScheme.Purple)
{ {
@ -86,36 +90,48 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding Padding = new MarginPadding { Bottom = 20, },
Child = new GridContainer
{ {
Horizontal = 70 * 1.2f, Anchor = Anchor.Centre,
Bottom = 20, Origin = Anchor.Centre,
},
Child = new InputBlockingContainer
{
Masking = true,
CornerRadius = 14,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] ColumnDimensions = new[]
{ {
new Box new Dimension(),
{ new Dimension(minSize: 640, maxSize: 800),
RelativeSizeAxes = Axes.Both, new Dimension(),
Colour = ColourProvider.Background6,
},
stackContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Vertical = 20,
Horizontal = 70,
},
}
}, },
}, Content = new[]
{
new[]
{
Empty(),
new InputBlockingContainer
{
Masking = true,
CornerRadius = 14,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6,
},
loading = new LoadingSpinner(),
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 20 },
Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, },
},
},
},
Empty(),
},
}
}
}, },
}); });
@ -171,8 +187,7 @@ namespace osu.Game.Overlays
config.BindWith(OsuSetting.ShowFirstRunSetup, showFirstRunSetup); config.BindWith(OsuSetting.ShowFirstRunSetup, showFirstRunSetup);
// TODO: uncomment when happy with the whole flow. if (showFirstRunSetup.Value) Show();
// if (showFirstRunSetup.Value) Show();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@ -269,7 +284,7 @@ namespace osu.Game.Overlays
{ {
Debug.Assert(currentStepIndex == null); Debug.Assert(currentStepIndex == null);
stackContainer.Child = stack = new ScreenStack screenContent.Child = stack = new ScreenStack
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };
@ -300,12 +315,20 @@ namespace osu.Game.Overlays
if (currentStepIndex < steps.Length) if (currentStepIndex < steps.Length)
{ {
stack.Push((Screen)Activator.CreateInstance(steps[currentStepIndex.Value])); var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]);
loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200);
nextScreen.OnLoadComplete += _ =>
{
loadingShowDelegate?.Cancel();
loading.Hide();
};
stack.Push(nextScreen);
} }
else else
{ {
// TODO: uncomment when happy with the whole flow. showFirstRunSetup.Value = false;
// showFirstRunSetup.Value = false;
currentStepIndex = null; currentStepIndex = null;
Hide(); Hide();
} }

View File

@ -1,66 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class IncompatibilityDisplayingModButton : ModButton
{
private readonly CompositeDrawable incompatibleIcon;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
public IncompatibilityDisplayingModButton(Mod mod)
: base(mod)
{
ButtonContent.Add(incompatibleIcon = new IncompatibleIcon
{
Anchor = Anchor.BottomRight,
Origin = Anchor.Centre,
Position = new Vector2(-13),
});
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateCompatibility), true);
}
protected override void DisplayMod(Mod mod)
{
base.DisplayMod(mod);
Scheduler.AddOnce(updateCompatibility);
}
private void updateCompatibility()
{
var m = SelectedMod ?? Mods.First();
bool isIncompatible = false;
if (selectedMods.Value.Count > 0 && !selectedMods.Value.Contains(m))
isIncompatible = !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(m));
if (isIncompatible)
incompatibleIcon.Show();
else
incompatibleIcon.Hide();
}
public override ITooltip<Mod> GetCustomTooltip() => new IncompatibilityDisplayingTooltip();
}
}

View File

@ -19,6 +19,11 @@ namespace osu.Game.Overlays.Mods
[Resolved] [Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
public IncompatibilityDisplayingModPanel(ModState modState)
: base(modState)
{
}
public IncompatibilityDisplayingModPanel(Mod mod) public IncompatibilityDisplayingModPanel(Mod mod)
: base(mod) : base(mod)
{ {

View File

@ -1,64 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public class IncompatibleIcon : VisibilityContainer, IHasTooltip
{
private Circle circle;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Size = new Vector2(20);
State.Value = Visibility.Hidden;
Alpha = 0;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
},
new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(0.6f),
Icon = FontAwesome.Solid.Slash,
Colour = Color4.White,
Shadow = true,
}
};
}
protected override void PopIn()
{
this.FadeIn(200, Easing.OutQuint);
circle.FlashColour(Color4.Red, 500, Easing.OutQuint);
this.ScaleTo(1.8f).Then().ScaleTo(1, 500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(200, Easing.OutQuint);
this.ScaleTo(0.8f, 200, Easing.In);
}
public LocalisableString TooltipText => "Incompatible with current selected mods";
}
}

View File

@ -1,319 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using System;
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// Represents a clickable button which can cycle through one of more mods.
/// </summary>
public class ModButton : ModButtonEmpty, IHasCustomTooltip<Mod>
{
private ModIcon foregroundIcon;
private ModIcon backgroundIcon;
private readonly SpriteText text;
private readonly Container<ModIcon> iconsContainer;
/// <summary>
/// Fired when the selection changes.
/// </summary>
public Action<Mod> SelectionChanged;
public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
private const Easing mod_switch_easing = Easing.InOutSine;
private const double mod_switch_duration = 120;
// A selected index of -1 means not selected.
private int selectedIndex = -1;
/// <summary>
/// Change the selected mod index of this button.
/// </summary>
/// <param name="newIndex">The new index.</param>
/// <param name="resetSettings">Whether any settings applied to the mod should be reset on selection.</param>
/// <returns>Whether the selection changed.</returns>
private bool changeSelectedIndex(int newIndex, bool resetSettings = true)
{
if (newIndex == selectedIndex) return false;
int direction = newIndex < selectedIndex ? -1 : 1;
bool beforeSelected = Selected;
Mod previousSelection = SelectedMod ?? Mods[0];
if (newIndex >= Mods.Length)
newIndex = -1;
else if (newIndex < -1)
newIndex = Mods.Length - 1;
if (newIndex >= 0 && !Mods[newIndex].HasImplementation)
return false;
selectedIndex = newIndex;
Mod newSelection = SelectedMod ?? Mods[0];
if (resetSettings)
newSelection.ResetSettingsToDefaults();
Schedule(() =>
{
if (beforeSelected != Selected)
{
iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic);
iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic);
}
if (previousSelection != newSelection)
{
const float rotate_angle = 16;
foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing);
backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing);
backgroundIcon.Mod = newSelection;
using (BeginDelayedSequence(mod_switch_duration))
{
foregroundIcon
.RotateTo(-rotate_angle * direction)
.RotateTo(0f, mod_switch_duration, mod_switch_easing);
backgroundIcon
.RotateTo(rotate_angle * direction)
.RotateTo(0f, mod_switch_duration, mod_switch_easing);
Schedule(() => DisplayMod(newSelection));
}
}
foregroundIcon.Selected.Value = Selected;
});
SelectionChanged?.Invoke(SelectedMod);
return true;
}
public bool Selected => selectedIndex != -1;
private Color4 selectedColour;
public Color4 SelectedColour
{
get => selectedColour;
set
{
if (value == selectedColour) return;
selectedColour = value;
if (Selected) foregroundIcon.Colour = value;
}
}
private Mod mod;
protected readonly Container ButtonContent;
public Mod Mod
{
get => mod;
set
{
mod = value;
if (mod == null)
{
Mods = Array.Empty<Mod>();
Alpha = 0;
}
else
{
Mods = (mod as MultiMod)?.Mods ?? new[] { mod };
Alpha = 1;
}
createIcons();
if (Mods.Length > 0)
{
DisplayMod(Mods[0]);
}
}
}
public Mod[] Mods { get; private set; }
public virtual Mod SelectedMod => Mods.ElementAtOrDefault(selectedIndex);
protected override bool OnMouseDown(MouseDownEvent e)
{
ButtonContent.ScaleTo(0.9f, 800, Easing.Out);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
ButtonContent.ScaleTo(1, 500, Easing.OutElastic);
// only trigger the event if we are inside the area of the button
if (Contains(e.ScreenSpaceMousePosition))
{
switch (e.Button)
{
case MouseButton.Right:
SelectNext(-1);
break;
}
}
}
protected override bool OnClick(ClickEvent e)
{
SelectNext(1);
return true;
}
/// <summary>
/// Select the next available mod in a specified direction.
/// </summary>
/// <param name="direction">1 for forwards, -1 for backwards.</param>
public void SelectNext(int direction)
{
int start = selectedIndex + direction;
// wrap around if we are at an extremity.
if (start >= Mods.Length)
start = -1;
else if (start < -1)
start = Mods.Length - 1;
for (int i = start; i < Mods.Length && i >= 0; i += direction)
{
if (SelectAt(i))
return;
}
Deselect();
}
/// <summary>
/// Select the mod at the provided index.
/// </summary>
/// <param name="index">The index to select.</param>
/// <param name="resetSettings">Whether any settings applied to the mod should be reset on selection.</param>
/// <returns>Whether the selection changed.</returns>
public bool SelectAt(int index, bool resetSettings = true)
{
if (!Mods[index].HasImplementation) return false;
changeSelectedIndex(index, resetSettings);
return true;
}
public void Deselect() => changeSelectedIndex(-1);
protected virtual void DisplayMod(Mod mod)
{
if (backgroundIcon != null)
backgroundIcon.Mod = foregroundIcon.Mod;
foregroundIcon.Mod = mod;
text.Text = mod.Name;
Colour = mod.HasImplementation ? Color4.White : Color4.Gray;
}
private void createIcons()
{
iconsContainer.Clear();
if (Mods.Length > 1)
{
iconsContainer.AddRange(new[]
{
backgroundIcon = new ModIcon(Mods[1], false)
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Position = new Vector2(1.5f),
},
foregroundIcon = new ModIcon(Mods[0], false)
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Position = new Vector2(-1.5f),
},
});
}
else
{
iconsContainer.Add(foregroundIcon = new ModIcon(Mod, false)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
});
}
}
public ModButton(Mod mod)
{
Children = new Drawable[]
{
new Container
{
Size = new Vector2(77f, 80f),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Children = new Drawable[]
{
ButtonContent = new Container
{
Children = new Drawable[]
{
iconsContainer = new Container<ModIcon>
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
},
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
}
},
text = new OsuSpriteText
{
Y = 75,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 18)
},
new HoverSounds()
};
Mod = mod;
}
public virtual ITooltip<Mod> GetCustomTooltip() => new ModButtonTooltip();
public Mod TooltipContent => SelectedMod ?? Mods.FirstOrDefault();
}
}

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// A mod button used exclusively for providing an empty space the size of a mod button.
/// </summary>
public class ModButtonEmpty : Container
{
public ModButtonEmpty()
{
Size = new Vector2(100f);
AlwaysPresent = true;
}
}
}

View File

@ -13,7 +13,6 @@ using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -25,7 +24,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -38,20 +36,30 @@ namespace osu.Game.Overlays.Mods
public readonly ModType ModType; public readonly ModType ModType;
private Func<Mod, bool>? filter; private IReadOnlyList<ModState> availableMods = Array.Empty<ModState>();
/// <summary> /// <summary>
/// A function determining whether each mod in the column should be displayed. /// Sets the list of mods to show in this column.
/// A return value of <see langword="true"/> means that the mod is not filtered and therefore its corresponding panel should be displayed.
/// A return value of <see langword="false"/> means that the mod is filtered out and therefore its corresponding panel should be hidden.
/// </summary> /// </summary>
public Func<Mod, bool>? Filter public IReadOnlyList<ModState> AvailableMods
{ {
get => filter; get => availableMods;
set set
{ {
filter = value; Debug.Assert(value.All(mod => mod.Mod.Type == ModType));
availableMods = value;
foreach (var mod in availableMods)
{
mod.Active.BindValueChanged(_ => updateState());
mod.Filtered.BindValueChanged(_ => updateState());
}
updateState(); updateState();
if (IsLoaded)
asyncLoadPanels();
} }
} }
@ -60,44 +68,12 @@ namespace osu.Game.Overlays.Mods
/// </summary> /// </summary>
public Bindable<bool> Active = new BindableBool(true); public Bindable<bool> Active = new BindableBool(true);
private readonly Bindable<bool> allFiltered = new BindableBool();
/// <summary>
/// True if all of the panels in this column have been filtered out by the current <see cref="Filter"/>.
/// </summary>
public IBindable<bool> AllFiltered => allFiltered;
/// <summary>
/// List of mods marked as selected in this column.
/// </summary>
/// <remarks>
/// Note that the mod instances returned by this property are owned solely by this column
/// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances).
/// </remarks>
public IReadOnlyList<Mod> SelectedMods { get; private set; } = Array.Empty<Mod>();
/// <summary>
/// Invoked when a mod panel has been selected interactively by the user.
/// </summary>
public event Action? SelectionChangedByUser;
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod); protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod);
private readonly Key[]? toggleKeys; private readonly Key[]? toggleKeys;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
/// <summary>
/// All mods that are available for the current ruleset in this particular column.
/// </summary>
/// <remarks>
/// Note that the mod instances in this list are owned solely by this column
/// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances).
/// </remarks>
private IReadOnlyList<Mod> localAvailableMods = Array.Empty<Mod>();
private readonly TextFlowContainer headerText; private readonly TextFlowContainer headerText;
private readonly Box headerBackground; private readonly Box headerBackground;
private readonly Container contentContainer; private readonly Container contentContainer;
@ -257,12 +233,8 @@ namespace osu.Game.Overlays.Mods
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) private void load(OverlayColourProvider colourProvider, OsuColour colours)
{ {
availableMods.BindTo(game.AvailableMods);
updateLocalAvailableMods(asyncLoadContent: false);
availableMods.BindValueChanged(_ => updateLocalAvailableMods(asyncLoadContent: true));
headerBackground.Colour = accentColour = colours.ForModType(ModType); headerBackground.Colour = accentColour = colours.ForModType(ModType);
if (toggleAllCheckbox != null) if (toggleAllCheckbox != null)
@ -280,6 +252,7 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete(); base.LoadComplete();
toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true); toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true);
asyncLoadPanels();
} }
private void updateToggleAllText() private void updateToggleAllText()
@ -288,34 +261,21 @@ namespace osu.Game.Overlays.Mods
toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll; toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll;
} }
private void updateLocalAvailableMods(bool asyncLoadContent)
{
var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty<Mod>())
.Select(m => m.DeepClone())
.ToList();
if (newMods.SequenceEqual(localAvailableMods))
return;
localAvailableMods = newMods;
if (asyncLoadContent)
asyncLoadPanels();
else
onPanelsLoaded(createPanels());
}
private CancellationTokenSource? cancellationTokenSource; private CancellationTokenSource? cancellationTokenSource;
private void asyncLoadPanels() private void asyncLoadPanels()
{ {
cancellationTokenSource?.Cancel(); cancellationTokenSource?.Cancel();
var panels = createPanels(); var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)));
Task? loadTask; Task? loadTask;
latestLoadTask = loadTask = LoadComponentsAsync(panels, onPanelsLoaded, (cancellationTokenSource = new CancellationTokenSource()).Token); latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
{
panelFlow.ChildrenEnumerable = loaded;
updateState();
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ => loadTask.ContinueWith(_ =>
{ {
if (loadTask == latestLoadTask) if (loadTask == latestLoadTask)
@ -323,97 +283,17 @@ namespace osu.Game.Overlays.Mods
}); });
} }
private IEnumerable<ModPanel> createPanels()
{
var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)));
return panels;
}
private void onPanelsLoaded(IEnumerable<ModPanel> loaded)
{
panelFlow.ChildrenEnumerable = loaded;
updateState();
foreach (var panel in panelFlow)
{
panel.Active.BindValueChanged(_ => panelStateChanged(panel));
}
}
private void updateState() private void updateState()
{ {
foreach (var panel in panelFlow) Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1;
{
panel.Active.Value = SelectedMods.Contains(panel.Mod);
panel.ApplyFilter(Filter);
}
allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value);
if (toggleAllCheckbox != null && !SelectionAnimationRunning) if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{ {
toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0;
toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
} }
} }
/// <summary>
/// This flag helps to determine the source of changes to <see cref="SelectedMods"/>.
/// If the value is false, then <see cref="SelectedMods"/> are changing due to a user selection on the UI.
/// If the value is true, then <see cref="SelectedMods"/> are changing due to an external <see cref="SetSelection"/> call.
/// </summary>
private bool externalSelectionUpdateInProgress;
private void panelStateChanged(ModPanel panel)
{
if (externalSelectionUpdateInProgress)
return;
var newSelectedMods = panel.Active.Value
? SelectedMods.Append(panel.Mod)
: SelectedMods.Except(panel.Mod.Yield());
SelectedMods = newSelectedMods.ToArray();
updateState();
SelectionChangedByUser?.Invoke();
}
/// <summary>
/// Adjusts the set of selected mods in this column to match the passed in <paramref name="mods"/>.
/// </summary>
/// <remarks>
/// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state.
/// <see cref="ModSelectScreen"/> uses this to substitute any external mod references in <see cref="ModSelectScreen.SelectedMods"/>
/// to references that are owned by this column.
/// </remarks>
internal void SetSelection(IReadOnlyList<Mod> mods)
{
externalSelectionUpdateInProgress = true;
var newSelection = new List<Mod>();
foreach (var mod in localAvailableMods)
{
var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == mod.GetType());
if (matchingSelectedMod != null)
{
mod.CopyFrom(matchingSelectedMod);
newSelection.Add(mod);
}
else
{
mod.ResetSettingsToDefaults();
}
}
SelectedMods = newSelection;
updateState();
externalSelectionUpdateInProgress = false;
}
#region Bulk select / deselect #region Bulk select / deselect
private const double initial_multiple_selection_delay = 120; private const double initial_multiple_selection_delay = 120;
@ -455,7 +335,7 @@ namespace osu.Game.Overlays.Mods
{ {
pendingSelectionOperations.Clear(); pendingSelectionOperations.Clear();
foreach (var button in panelFlow.Where(b => !b.Active.Value && !b.Filtered.Value)) foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value))
pendingSelectionOperations.Enqueue(() => button.Active.Value = true); pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
} }
@ -466,7 +346,7 @@ namespace osu.Game.Overlays.Mods
{ {
pendingSelectionOperations.Clear(); pendingSelectionOperations.Clear();
foreach (var button in panelFlow.Where(b => b.Active.Value && !b.Filtered.Value)) foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value))
pendingSelectionOperations.Enqueue(() => button.Active.Value = false); pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
} }
@ -553,10 +433,10 @@ namespace osu.Game.Overlays.Mods
int index = Array.IndexOf(toggleKeys, e.Key); int index = Array.IndexOf(toggleKeys, e.Key);
if (index < 0) return false; if (index < 0) return false;
var panel = panelFlow.ElementAtOrDefault(index); var modState = availableMods.ElementAtOrDefault(index);
if (panel == null || panel.Filtered.Value) return false; if (modState == null || modState.Filtered.Value) return false;
panel.Active.Toggle(); modState.Active.Toggle();
return true; return true;
} }

View File

@ -1,54 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModControlSection : CompositeDrawable
{
protected FillFlowContainer FlowContent;
public readonly Mod Mod;
public ModControlSection(Mod mod, IEnumerable<Drawable> modControls)
{
Mod = mod;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
FlowContent = new FillFlowContainer
{
Margin = new MarginPadding { Top = 30 },
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
ChildrenEnumerable = modControls
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AddRangeInternal(new Drawable[]
{
new OsuSpriteText
{
Text = Mod.Name,
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Colour = colours.Yellow,
},
FlowContent
});
}
}
}

View File

@ -28,9 +28,11 @@ namespace osu.Game.Overlays.Mods
{ {
public class ModPanel : OsuClickableContainer public class ModPanel : OsuClickableContainer
{ {
public Mod Mod { get; } public Mod Mod => modState.Mod;
public BindableBool Active { get; } = new BindableBool(); public BindableBool Active => modState.Active;
public BindableBool Filtered { get; } = new BindableBool(); public BindableBool Filtered => modState.Filtered;
private readonly ModState modState;
protected readonly Box Background; protected readonly Box Background;
protected readonly Container SwitchContainer; protected readonly Container SwitchContainer;
@ -55,9 +57,9 @@ namespace osu.Game.Overlays.Mods
private Sample? sampleOff; private Sample? sampleOff;
private Sample? sampleOn; private Sample? sampleOn;
public ModPanel(Mod mod) public ModPanel(ModState modState)
{ {
Mod = mod; this.modState = modState;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 42; Height = 42;
@ -79,7 +81,7 @@ namespace osu.Game.Overlays.Mods
SwitchContainer = new Container SwitchContainer = new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Child = new ModSwitchSmall(mod) Child = new ModSwitchSmall(Mod)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -115,7 +117,7 @@ namespace osu.Game.Overlays.Mods
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = mod.Name, Text = Mod.Name,
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding Margin = new MarginPadding
@ -125,7 +127,7 @@ namespace osu.Game.Overlays.Mods
}, },
new OsuSpriteText new OsuSpriteText
{ {
Text = mod.Description, Text = Mod.Description,
Font = OsuFont.Default.With(size: 12), Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true, Truncate = true,
@ -141,6 +143,11 @@ namespace osu.Game.Overlays.Mods
Action = Active.Toggle; Action = Active.Toggle;
} }
public ModPanel(Mod mod)
: this(new ModState(mod))
{
}
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler) private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler)
{ {

View File

@ -1,261 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
using osuTK.Input;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using Humanizer;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Mods
{
public class ModSection : CompositeDrawable
{
private readonly Drawable header;
public FillFlowContainer<ModButtonEmpty> ButtonsContainer { get; }
protected IReadOnlyList<ModButton> Buttons { get; private set; } = Array.Empty<ModButton>();
public Action<Mod> Action;
public Key[] ToggleKeys;
public readonly ModType ModType;
public IEnumerable<Mod> SelectedMods => Buttons.Select(b => b.SelectedMod).Where(m => m != null);
private CancellationTokenSource modsLoadCts;
protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0;
/// <summary>
/// True when all mod icons have completed loading.
/// </summary>
public bool ModIconsLoaded { get; private set; } = true;
public IEnumerable<Mod> Mods
{
set
{
var modContainers = value.Select(m =>
{
if (m == null)
return new ModButtonEmpty();
return CreateModButton(m).With(b =>
{
b.SelectionChanged = mod =>
{
ModButtonStateChanged(mod);
Action?.Invoke(mod);
};
});
}).ToArray();
modsLoadCts?.Cancel();
if (modContainers.Length == 0)
{
ModIconsLoaded = true;
header.Hide();
Hide();
return;
}
ModIconsLoaded = false;
LoadComponentsAsync(modContainers, c =>
{
ModIconsLoaded = true;
ButtonsContainer.ChildrenEnumerable = c;
}, (modsLoadCts = new CancellationTokenSource()).Token);
Buttons = modContainers.OfType<ModButton>().ToArray();
header.FadeIn(200);
this.FadeIn(200);
}
}
protected virtual void ModButtonStateChanged(Mod mod)
{
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed) return false;
if (ToggleKeys != null)
{
int index = Array.IndexOf(ToggleKeys, e.Key);
if (index > -1 && index < Buttons.Count)
Buttons[index].SelectNext(e.ShiftPressed ? -1 : 1);
}
return base.OnKeyDown(e);
}
private const double initial_multiple_selection_delay = 120;
private double selectionDelay = initial_multiple_selection_delay;
private double lastSelection;
private readonly Queue<Action> pendingSelectionOperations = new Queue<Action>();
protected override void Update()
{
base.Update();
if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay)
{
if (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
{
dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
selectionDelay = Math.Max(30, selectionDelay * 0.8f);
lastSelection = Time.Current;
}
else
{
// reset the selection delay after all animations have been completed.
// this will cause the next action to be immediately performed.
selectionDelay = initial_multiple_selection_delay;
}
}
}
/// <summary>
/// Selects all mods.
/// </summary>
public void SelectAll()
{
pendingSelectionOperations.Clear();
foreach (var button in Buttons.Where(b => !b.Selected))
pendingSelectionOperations.Enqueue(() => button.SelectAt(0));
}
/// <summary>
/// Deselects all mods.
/// </summary>
public void DeselectAll()
{
pendingSelectionOperations.Clear();
DeselectTypes(Buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null));
}
/// <summary>
/// Deselect one or more mods in this section.
/// </summary>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
/// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
/// <param name="newSelection">If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in <paramref name="modTypes"/>.</param>
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false, Mod newSelection = null)
{
foreach (var button in Buttons)
{
if (button.SelectedMod == null) continue;
if (button.SelectedMod == newSelection)
continue;
foreach (var type in modTypes)
{
if (type.IsInstanceOfType(button.SelectedMod))
{
if (immediate)
button.Deselect();
else
pendingSelectionOperations.Enqueue(button.Deselect);
}
}
}
}
/// <summary>
/// Updates all buttons with the given list of selected mods.
/// </summary>
/// <param name="newSelectedMods">The new list of selected mods to select.</param>
public void UpdateSelectedButtons(IReadOnlyList<Mod> newSelectedMods)
{
foreach (var button in Buttons)
updateButtonSelection(button, newSelectedMods);
}
private void updateButtonSelection(ModButton button, IReadOnlyList<Mod> newSelectedMods)
{
foreach (var mod in newSelectedMods)
{
int index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType());
if (index < 0)
continue;
var buttonMod = button.Mods[index];
// as this is likely coming from an external change, ensure the settings of the mod are in sync.
buttonMod.CopyFrom(mod);
button.SelectAt(index, false);
return;
}
button.Deselect();
}
public ModSection(ModType type)
{
ModType = type;
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
Origin = Anchor.TopCentre;
Anchor = Anchor.TopCentre;
InternalChildren = new[]
{
header = CreateHeader(type.Humanize(LetterCasing.Title)),
ButtonsContainer = new FillFlowContainer<ModButtonEmpty>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Spacing = new Vector2(50f, 0f),
Margin = new MarginPadding
{
Top = 20,
},
AlwaysPresent = true
},
};
}
protected virtual Drawable CreateHeader(string text) => new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = text
};
protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod);
/// <summary>
/// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
/// </summary>
public void FlushPendingSelections()
{
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
dequeuedAction();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,664 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Lists;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public abstract class ModSelectScreen : ShearedOverlayContainer, ISamplePlaybackDisabler
{
protected const int BUTTON_WIDTH = 200;
[Cached]
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; private set; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private Func<Mod, bool> isValidMod = m => true;
/// <summary>
/// A function determining whether each mod in the column should be displayed.
/// A return value of <see langword="true"/> means that the mod is not filtered and therefore its corresponding panel should be displayed.
/// A return value of <see langword="false"/> means that the mod is filtered out and therefore its corresponding panel should be hidden.
/// </summary>
public Func<Mod, bool> IsValidMod
{
get => isValidMod;
set
{
isValidMod = value ?? throw new ArgumentNullException(nameof(value));
if (IsLoaded)
updateAvailableMods();
}
}
/// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowTotalMultiplier => true;
protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys);
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
protected virtual IEnumerable<ShearedButton> CreateFooterButtons() => createDefaultFooterButtons();
private readonly BindableBool customisationVisible = new BindableBool();
private ModSettingsArea modSettingsArea = null!;
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private ShearedButton backButton = null!;
private DifficultyMultiplierDisplay? multiplierDisplay;
private ShearedToggleButton? customisationButton;
protected ModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Header.Title = ModSelectScreenStrings.ModSelectTitle;
Header.Description = ModSelectScreenStrings.ModSelectDescription;
AddRange(new Drawable[]
{
new ClickToReturnContainer
{
RelativeSizeAxes = Axes.Both,
HandleMouse = { BindTarget = customisationVisible },
OnClicked = () => customisationVisible.Value = false
},
modSettingsArea = new ModSettingsArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = 0
}
});
MainAreaContent.AddRange(new Drawable[]
{
new Container
{
Padding = new MarginPadding
{
Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
columnScroll = new ColumnScrollContainer
{
RelativeSizeAxes = Axes.Both,
Masking = false,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = columnFlow = new ColumnFlowContainer
{
Direction = FillDirection.Horizontal,
Shear = new Vector2(SHEAR, 0),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 70 },
Children = new[]
{
createModColumnContent(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }),
createModColumnContent(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }),
createModColumnContent(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }),
createModColumnContent(ModType.Conversion),
createModColumnContent(ModType.Fun)
}
}
}
}
}
});
if (ShowTotalMultiplier)
{
MainAreaContent.Add(new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.X,
Height = DifficultyMultiplierDisplay.HEIGHT,
Margin = new MarginPadding { Horizontal = 100 },
Child = multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
});
}
FooterContent.Child = footerButtonFlow = new FillFlowContainer<ShearedButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding
{
Vertical = PADDING,
Horizontal = 70
},
Spacing = new Vector2(10),
ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH)
{
Text = CommonStrings.Back,
Action = Hide,
DarkerColour = colours.Pink2,
LighterColour = colours.Pink1
})
};
}
protected override void LoadComplete()
{
base.LoadComplete();
State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true);
((IBindable<IReadOnlyList<Mod>>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
SelectedMods.BindValueChanged(val =>
{
updateMultiplier();
updateCustomisation(val);
updateSelectionFromBindable();
}, true);
foreach (var column in columnFlow.Columns)
{
column.SelectionChangedByUser += updateBindableFromSelection;
}
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
updateAvailableMods();
// Start scrolled slightly to the right to give the user a sense that
// there is more horizontal content available.
ScheduleAfterChildren(() =>
{
columnScroll.ScrollTo(200, false);
columnScroll.ScrollToStart();
});
}
/// <summary>
/// Select all visible mods in all columns.
/// </summary>
protected void SelectAll()
{
foreach (var column in columnFlow.Columns)
column.SelectAll();
}
/// <summary>
/// Deselect all visible mods in all columns.
/// </summary>
protected void DeselectAll()
{
foreach (var column in columnFlow.Columns)
column.DeselectAll();
}
private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null)
{
var column = CreateModColumn(modType, toggleKeys).With(column =>
{
column.Filter = IsValidMod;
// spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden.
column.Margin = new MarginPadding { Right = 10 };
});
return new ColumnDimContainer(column)
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140),
};
}
private ShearedButton[] createDefaultFooterButtons()
=> new[]
{
customisationButton = new ShearedToggleButton(BUTTON_WIDTH)
{
Text = ModSelectScreenStrings.ModCustomisation,
Active = { BindTarget = customisationVisible }
},
new ShearedButton(BUTTON_WIDTH)
{
Text = CommonStrings.DeselectAll,
Action = DeselectAll
}
};
private void updateMultiplier()
{
if (multiplierDisplay == null)
return;
double multiplier = 1.0;
foreach (var mod in SelectedMods.Value)
multiplier *= mod.ScoreMultiplier;
multiplierDisplay.Current.Value = multiplier;
}
private void updateAvailableMods()
{
foreach (var column in columnFlow.Columns)
column.Filter = m => m.HasImplementation && isValidMod.Invoke(m);
}
private void updateCustomisation(ValueChangedEvent<IReadOnlyList<Mod>> valueChangedEvent)
{
if (customisationButton == null)
return;
bool anyCustomisableMod = false;
bool anyModWithRequiredCustomisationAdded = false;
foreach (var mod in SelectedMods.Value)
{
anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
}
if (anyCustomisableMod)
{
customisationVisible.Disabled = false;
if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value)
customisationVisible.Value = true;
}
else
{
if (customisationVisible.Value)
customisationVisible.Value = false;
customisationVisible.Disabled = true;
}
}
private void updateCustomisationVisualState()
{
const double transition_duration = 300;
MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic);
foreach (var button in footerButtonFlow)
{
if (button != customisationButton)
button.Enabled.Value = !customisationVisible.Value;
}
float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0;
modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic);
}
private void updateSelectionFromBindable()
{
// `SelectedMods` may contain mod references that come from external sources.
// to ensure isolation, first pull in the potentially-external change into the mod columns...
foreach (var column in columnFlow.Columns)
column.SetSelection(SelectedMods.Value);
// and then, when done, replace the potentially-external mod references in `SelectedMods` with ones we own.
updateBindableFromSelection();
}
private void updateBindableFromSelection()
{
var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray();
// the following guard intends to check cases where we've already replaced potentially-external mod references with our own and avoid endless recursion.
// TODO: replace custom comparer with System.Collections.Generic.ReferenceEqualityComparer when fully on .NET 6
if (candidateSelection.SequenceEqual(SelectedMods.Value, new FuncEqualityComparer<Mod>(ReferenceEquals)))
return;
SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection);
}
#region Transition handling
private const float distance = 700;
protected override void PopIn()
{
const double fade_in_duration = 400;
base.PopIn();
multiplierDisplay?
.Delay(fade_in_duration * 0.65f)
.FadeIn(fade_in_duration / 2, Easing.OutQuint)
.ScaleTo(1, fade_in_duration, Easing.OutElastic);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++)
{
var column = columnFlow[i].Column;
double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30;
double duration = column.AllFiltered.Value ? 0 : fade_in_duration;
float startingYPosition = 0;
if (!column.AllFiltered.Value)
startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
column.TopLevelContent
.MoveToY(startingYPosition)
.Delay(delay)
.MoveToY(0, duration, Easing.OutQuint)
.FadeIn(duration, Easing.OutQuint);
if (!column.AllFiltered.Value)
nonFilteredColumnCount += 1;
}
}
protected override void PopOut()
{
const double fade_out_duration = 500;
base.PopOut();
multiplierDisplay?
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
.ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++)
{
var column = columnFlow[i].Column;
double duration = column.AllFiltered.Value ? 0 : fade_out_duration;
float newYPosition = 0;
if (!column.AllFiltered.Value)
newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
column.FlushPendingSelections();
column.TopLevelContent
.MoveToY(newYPosition, duration, Easing.OutQuint)
.FadeOut(duration, Easing.OutQuint);
if (!column.AllFiltered.Value)
nonFilteredColumnCount += 1;
}
}
#endregion
#region Input handling
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.Back:
// Pressing the back binding should only go back one step at a time.
hideOverlay(false);
return true;
// This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection:
case GlobalAction.Select:
{
// Pressing toggle or select should completely hide the overlay in one shot.
hideOverlay(true);
return true;
}
}
return base.OnPressed(e);
void hideOverlay(bool immediate)
{
if (customisationVisible.Value)
{
Debug.Assert(customisationButton != null);
customisationButton.TriggerClick();
if (!immediate)
return;
}
backButton.TriggerClick();
}
}
#endregion
#region Sample playback control
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool(true);
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
#endregion
/// <summary>
/// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility.
/// </summary>
internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{
public ColumnScrollContainer()
: base(Direction.Horizontal)
{
}
protected override void Update()
{
base.Update();
// the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space.
// note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns.
float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent);
float rightVisibleBound = leftVisibleBound + DrawWidth;
// if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass.
// this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past.
float leftMovementBound = Math.Min(Current, Target);
float rightMovementBound = Math.Max(Current, Target) + DrawWidth;
foreach (var column in Child)
{
// DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear,
// so we have to manually compensate.
var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent);
var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent);
bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound)
&& Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X);
bool isBeingScrolledToward = Precision.AlmostBigger(topLeft.X, leftMovementBound)
&& Precision.DefinitelyBigger(rightMovementBound, bottomRight.X);
column.Active.Value = isCurrentlyVisible || isBeingScrolledToward;
}
}
}
/// <summary>
/// Manages padding and layout of mod columns.
/// </summary>
internal class ColumnFlowContainer : FillFlowContainer<ColumnDimContainer>
{
public IEnumerable<ModColumn> Columns => Children.Select(dimWrapper => dimWrapper.Column);
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
public ColumnFlowContainer()
{
AddLayout(drawSizeLayout);
}
public override void Add(ColumnDimContainer dimContainer)
{
base.Add(dimContainer);
Debug.Assert(dimContainer != null);
dimContainer.Column.Shear = Vector2.Zero;
}
protected override void Update()
{
base.Update();
if (!drawSizeLayout.IsValid)
{
Padding = new MarginPadding
{
Left = DrawHeight * SHEAR,
Bottom = 10
};
drawSizeLayout.Validate();
}
}
}
/// <summary>
/// Encapsulates a column and provides dim and input blocking based on an externally managed "active" state.
/// </summary>
internal class ColumnDimContainer : Container
{
public ModColumn Column { get; }
/// <summary>
/// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen.
/// </summary>
public readonly Bindable<bool> Active = new BindableBool();
/// <summary>
/// Invoked when the column is clicked while not active, requesting a scroll to be performed to bring it on-screen.
/// </summary>
public Action<ColumnDimContainer>? RequestScroll { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
public ColumnDimContainer(ModColumn column)
{
Child = Column = column;
column.Active.BindTo(Active);
}
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ => updateState());
Column.AllFiltered.BindValueChanged(_ => updateState(), true);
FinishTransforms();
}
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning;
private void updateState()
{
Colour4 targetColour;
Column.Alpha = Column.AllFiltered.Value ? 0 : 1;
if (Column.Active.Value)
targetColour = Colour4.White;
else
targetColour = IsHovered ? colours.GrayC : colours.Gray8;
this.FadeColour(targetColour, 800, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
{
if (!Active.Value)
RequestScroll?.Invoke(this);
return true;
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
updateState();
return Active.Value;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
}
/// <summary>
/// A container which blocks and handles input, managing the "return from customisation" state change.
/// </summary>
private class ClickToReturnContainer : Container
{
public BindableBool HandleMouse { get; } = new BindableBool();
public Action? OnClicked { get; set; }
public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value;
protected override bool Handle(UIEvent e)
{
if (!HandleMouse.Value)
return base.Handle(e);
switch (e)
{
case ClickEvent _:
OnClicked?.Invoke();
return true;
case MouseEvent _:
return true;
}
return base.Handle(e);
}
}
}
}

View File

@ -1,111 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public class ModSettingsContainer : VisibilityContainer
{
public readonly IBindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public IBindable<bool> HasSettingsForSelection => hasSettingsForSelection;
private readonly Bindable<bool> hasSettingsForSelection = new Bindable<bool>();
private readonly FillFlowContainer<ModControlSection> modSettingsContent;
private readonly Container content;
private const double transition_duration = 400;
public ModSettingsContainer()
{
RelativeSizeAxes = Axes.Both;
Child = content = new Container
{
Masking = true,
CornerRadius = 10,
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
X = 1,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(0, 0, 0, 192)
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = modSettingsContent = new FillFlowContainer<ModControlSection>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Padding = new MarginPadding(20),
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedMods.BindValueChanged(modsChanged, true);
}
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
modSettingsContent.Clear();
foreach (var mod in mods.NewValue)
{
var settings = mod.CreateSettingsControls().ToList();
if (settings.Count > 0)
modSettingsContent.Add(new ModControlSection(mod, settings));
}
bool hasSettings = modSettingsContent.Count > 0;
if (!hasSettings)
Hide();
hasSettingsForSelection.Value = hasSettings;
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
protected override void PopIn()
{
this.FadeIn(transition_duration, Easing.OutQuint);
content.MoveToX(0, transition_duration, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(transition_duration, Easing.OutQuint);
content.MoveToX(1, transition_duration, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,35 @@
// 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.Bindables;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// Wrapper class used to store the current state of a mod shown on the <see cref="ModSelectOverlay"/>.
/// Used primarily to decouple data from drawable logic.
/// </summary>
public class ModState
{
/// <summary>
/// The mod that whose state this instance describes.
/// </summary>
public Mod Mod { get; }
/// <summary>
/// Whether the mod is currently selected.
/// </summary>
public BindableBool Active { get; } = new BindableBool();
/// <summary>
/// Whether the mod is currently filtered out due to not matching imposed criteria.
/// </summary>
public BindableBool Filtered { get; } = new BindableBool();
public ModState(Mod mod)
{
Mod = mod;
}
}
}

View File

@ -1,30 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public class UserModSelectOverlay : ModSelectOverlay public class UserModSelectOverlay : ModSelectOverlay
{ {
protected override void OnModSelected(Mod mod) public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{ {
base.OnModSelected(mod);
foreach (var section in ModSectionsContainer.Children)
section.DeselectTypes(mod.IncompatibleMods, true, mod);
} }
protected override ModSection CreateModSection(ModType type) => new UserModSection(type); protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys);
private class UserModSection : ModSection protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection)
{ {
public UserModSection(ModType type) var addedMods = newSelection.Except(oldSelection);
: base(type) var removedMods = oldSelection.Except(newSelection);
IEnumerable<Mod> modsAfterRemoval = newSelection.Except(removedMods).ToList();
// the preference is that all new mods should override potential incompatible old mods.
// in general that's a bit difficult to compute if more than one mod is added at a time,
// so be conservative and just remove all mods that aren't compatible with any one added mod.
foreach (var addedMod in addedMods)
{
if (!ModUtils.CheckCompatibleSet(modsAfterRemoval.Append(addedMod), out var invalidMods))
modsAfterRemoval = modsAfterRemoval.Except(invalidMods);
modsAfterRemoval = modsAfterRemoval.Append(addedMod).ToList();
}
return modsAfterRemoval.ToList();
}
private class UserModColumn : ModColumn
{
public UserModColumn(ModType modType, bool allowBulkSelection, [CanBeNull] Key[] toggleKeys = null)
: base(modType, allowBulkSelection, toggleKeys)
{ {
} }
protected override ModButton CreateModButton(Mod mod) => new IncompatibilityDisplayingModButton(mod); protected override ModPanel CreateModPanel(ModState modState) => new IncompatibilityDisplayingModPanel(modState);
} }
} }
} }

View File

@ -1,53 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public class UserModSelectScreen : ModSelectScreen
{
public UserModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{
}
protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys);
protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection)
{
var addedMods = newSelection.Except(oldSelection);
var removedMods = oldSelection.Except(newSelection);
IEnumerable<Mod> modsAfterRemoval = newSelection.Except(removedMods).ToList();
// the preference is that all new mods should override potential incompatible old mods.
// in general that's a bit difficult to compute if more than one mod is added at a time,
// so be conservative and just remove all mods that aren't compatible with any one added mod.
foreach (var addedMod in addedMods)
{
if (!ModUtils.CheckCompatibleSet(modsAfterRemoval.Append(addedMod), out var invalidMods))
modsAfterRemoval = modsAfterRemoval.Except(invalidMods);
modsAfterRemoval = modsAfterRemoval.Append(addedMod).ToList();
}
return modsAfterRemoval.ToList();
}
private class UserModColumn : ModColumn
{
public UserModColumn(ModType modType, bool allowBulkSelection, [CanBeNull] Key[] toggleKeys = null)
: base(modType, allowBulkSelection, toggleKeys)
{
}
protected override ModPanel CreateModPanel(Mod mod) => new IncompatibilityDisplayingModPanel(mod);
}
}
}

View File

@ -377,6 +377,8 @@ namespace osu.Game.Overlays
} }
} }
private AudioAdjustments modTrackAdjustments;
/// <summary> /// <summary>
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowTrackAdjustments"/> is <c>true</c>. /// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowTrackAdjustments"/> is <c>true</c>.
/// </summary> /// </summary>
@ -385,6 +387,7 @@ namespace osu.Game.Overlays
/// </remarks> /// </remarks>
public void ResetTrackAdjustments() public void ResetTrackAdjustments()
{ {
// todo: we probably want a helper method rather than this.
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Balance); CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Balance);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Frequency); CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Frequency);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo); CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo);
@ -392,8 +395,10 @@ namespace osu.Game.Overlays
if (allowTrackAdjustments) if (allowTrackAdjustments)
{ {
CurrentTrack.BindAdjustments(modTrackAdjustments = new AudioAdjustments());
foreach (var mod in mods.Value.OfType<IApplicableToTrack>()) foreach (var mod in mods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(CurrentTrack); mod.ApplyToTrack(modTrackAdjustments);
} }
} }
} }

View File

@ -156,6 +156,10 @@ namespace osu.Game.Overlays
private void updateExpandedState(ValueChangedEvent<bool> expanded) private void updateExpandedState(ValueChangedEvent<bool> expanded)
{ {
// clearing transforms is necessary to avoid a previous height transform
// potentially continuing to get processed while content has changed to autosize.
content.ClearTransforms();
if (expanded.NewValue) if (expanded.NewValue)
content.AutoSizeAxes = Axes.Y; content.AutoSizeAxes = Axes.Y;
else else

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Audio; using osu.Framework.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public interface IApplicableToSample : IApplicableMod public interface IApplicableToSample : IApplicableMod
{ {
void ApplyToSample(DrawableSample sample); void ApplyToSample(IAdjustableAudioComponent sample);
} }
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio.Track; using osu.Framework.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public interface IApplicableToTrack : IApplicableMod public interface IApplicableToTrack : IApplicableMod
{ {
void ApplyToTrack(ITrack track); void ApplyToTrack(IAdjustableAudioComponent track);
} }
} }

View File

@ -5,9 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -79,7 +77,7 @@ namespace osu.Game.Rulesets.Mods
// Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast. // Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast.
private const double rate_change_on_miss = 0.95d; private const double rate_change_on_miss = 0.95d;
private ITrack track; private IAdjustableAudioComponent track;
private double targetRate = 1d; private double targetRate = 1d;
/// <summary> /// <summary>
@ -141,7 +139,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.BindValueChanged(adjustPitchChanged); AdjustPitch.BindValueChanged(adjustPitchChanged);
} }
public void ApplyToTrack(ITrack track) public void ApplyToTrack(IAdjustableAudioComponent track)
{ {
this.track = track; this.track = track;
@ -151,7 +149,7 @@ namespace osu.Game.Rulesets.Mods
recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count)); recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count));
} }
public void ApplyToSample(DrawableSample sample) public void ApplyToSample(IAdjustableAudioComponent sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }
@ -210,7 +208,6 @@ namespace osu.Game.Rulesets.Mods
private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting) private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting)
{ {
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Mods
}, true); }, true);
} }
public override void ApplyToTrack(ITrack track) public override void ApplyToTrack(IAdjustableAudioComponent track)
{ {
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);

View File

@ -3,7 +3,6 @@
using System.Linq; using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
InverseMuting.BindValueChanged(i => MuteComboCount.MinValue = i.NewValue ? 1 : 0, true); InverseMuting.BindValueChanged(i => MuteComboCount.MinValue = i.NewValue ? 1 : 0, true);
} }
public void ApplyToTrack(ITrack track) public void ApplyToTrack(IAdjustableAudioComponent track)
{ {
track.AddAdjustment(AdjustableProperty.Volume, mainVolumeAdjust); track.AddAdjustment(AdjustableProperty.Volume, mainVolumeAdjust);
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mods
}, true); }, true);
} }
public override void ApplyToTrack(ITrack track) public override void ApplyToTrack(IAdjustableAudioComponent track)
{ {
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);

View File

@ -3,9 +3,7 @@
using System; using System;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -15,12 +13,12 @@ namespace osu.Game.Rulesets.Mods
public abstract BindableNumber<double> SpeedChange { get; } public abstract BindableNumber<double> SpeedChange { get; }
public virtual void ApplyToTrack(ITrack track) public virtual void ApplyToTrack(IAdjustableAudioComponent track)
{ {
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
public virtual void ApplyToSample(DrawableSample sample) public virtual void ApplyToSample(IAdjustableAudioComponent sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }

View File

@ -4,9 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Mods
Precision = 0.01, Precision = 0.01,
}; };
private ITrack track; private IAdjustableAudioComponent track;
protected ModTimeRamp() protected ModTimeRamp()
{ {
@ -55,7 +53,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.BindValueChanged(applyPitchAdjustment); AdjustPitch.BindValueChanged(applyPitchAdjustment);
} }
public void ApplyToTrack(ITrack track) public void ApplyToTrack(IAdjustableAudioComponent track)
{ {
this.track = track; this.track = track;
@ -63,7 +61,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange(); AdjustPitch.TriggerChange();
} }
public void ApplyToSample(DrawableSample sample) public void ApplyToSample(IAdjustableAudioComponent sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }
@ -90,7 +88,7 @@ namespace osu.Game.Rulesets.Mods
public virtual void Update(Playfield playfield) public virtual void Update(Playfield playfield)
{ {
applyRateAdjustment(track.CurrentTime); applyRateAdjustment(playfield.Clock.CurrentTime);
} }
/// <summary> /// <summary>

View File

@ -448,7 +448,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Reapplies the current <see cref="ArmedState"/>. /// Reapplies the current <see cref="ArmedState"/>.
/// </summary> /// </summary>
protected void RefreshStateTransforms() => updateState(State.Value, true); public void RefreshStateTransforms() => updateState(State.Value, true);
/// <summary> /// <summary>
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time. /// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.

View File

@ -76,14 +76,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 travelVector = (position - StartPosition); Vector2 travelVector = (position - StartPosition);
// We need a non-zero travel vector in order to find a valid direction.
if (travelVector == Vector2.Zero) if (travelVector == Vector2.Zero)
return (StartPosition, StartTime); travelVector = new Vector2(0, -1);
float travelLength = travelVector.Length; float travelLength = travelVector.Length;
// FindSnappedDistance will always round down, but we want to potentially round upwards. // FindSnappedDistance will always round down, but we want to potentially round upwards.
travelLength += DistanceBetweenTicks / 2; travelLength += DistanceBetweenTicks / 2;
// We never want to snap towards zero.
if (travelLength < DistanceBetweenTicks)
travelLength = DistanceBetweenTicks;
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
// to allow for snapping at a non-multiplied ratio. // to allow for snapping at a non-multiplied ratio.
float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier);

View File

@ -122,6 +122,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
beatmap.EndChange(); beatmap.EndChange();
}); });
} }
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(sliderVelocitySlider));
}
} }
} }
} }

View File

@ -126,6 +126,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue));
} }
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume));
}
private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null; private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null;
private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? (int?)relevantControlPoints.First().SampleVolume : null; private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? (int?)relevantControlPoints.First().SampleVolume : null;

Some files were not shown because too many files have changed in this diff Show More