1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 22: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,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2022.1.0-eap10",
"version": "2022.1.1",
"commands": [
"jb"
]
@ -27,4 +27,4 @@
]
}
}
}
}

View File

@ -31,7 +31,7 @@ jobs:
uses: actions/cache@v3
with:
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
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -52,10 +52,10 @@
</ItemGroup>
<ItemGroup>
<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 Label="Transitive Dependencies">
<!-- 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>
</Project>

View File

@ -4,8 +4,6 @@
using System;
using System.IO;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
using osu.Framework;
using osu.Framework.Development;
@ -63,8 +61,6 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
{
host.ExceptionThrown += handleException;
if (!host.IsPrimaryInstance)
{
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();
});
}
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" />
</ItemGroup>
<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="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />

View File

@ -8,7 +8,7 @@
<ItemGroup>
<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" />
</ItemGroup>

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -113,7 +113,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public void TestCursorInCentre()
{
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]

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>

View File

@ -122,6 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
new OsuModEasy(),
new OsuModHardRock(),
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;
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.
if (effectiveMissCount > 0)
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 osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@ -85,6 +86,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
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)
{
if (BaseObject is Slider currentSlider)

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
@ -17,13 +19,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public Flashlight(Mod[] 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;
protected override double DecayWeight => 1.0;
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 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.
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;
}
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);

View File

@ -50,6 +50,11 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load(OsuConfigManager config)
{
hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations);
hitAnimations.BindValueChanged(_ =>
{
foreach (var d in HitObjectContainer.AliveObjects)
d.RefreshStateTransforms();
});
}
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) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
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)
{
osuObject.TimeFadeIn = osuObject.TimePreempt * fade_in_duration_multiplier;
osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER;
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
applyFadeInAdjustment(nested);
}
@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Mods
static (double fadeStartTime, double fadeDuration) getParameters(OsuHitObject hitObject)
{
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)
double longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime;

View File

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

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</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]
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 osu.Framework.Audio.Track;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Tests.Rulesets.Mods
{
@ -16,11 +18,14 @@ namespace osu.Game.Tests.Rulesets.Mods
private const double duration = 9000;
private TrackVirtual track;
private OsuPlayfield playfield;
[SetUp]
public void SetUp()
{
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)]
@ -80,8 +85,8 @@ namespace osu.Game.Tests.Rulesets.Mods
private void seekTrackAndUpdateMod(ModTimeRamp mod, double time)
{
track.Seek(time);
// update the mod via a fake playfield to re-calculate the current rate.
mod.Update(null);
playfield.Clock.ProcessFrame();
mod.Update(playfield);
}
private static Beatmap createSingleSpinnerBeatmap()

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
@ -66,6 +67,13 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestPopoverHasFocus()
{
clickDifficultyPiece(0);
velocityPopoverHasFocus();
}
[Test]
public void TestSingleSelection()
{
@ -133,6 +141,15 @@ namespace osu.Game.Tests.Visual.Editing
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}", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
@ -151,6 +168,7 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover()
{
AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
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]
public void TestSingleSelection()
{
@ -173,14 +180,23 @@ namespace osu.Game.Tests.Visual.Editing
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);
});
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}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
@ -215,8 +231,9 @@ namespace osu.Game.Tests.Visual.Editing
private void dismissPopover()
{
AddStep("unfocus textbox", () => 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", () =>

View File

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

View File

@ -1,21 +1,105 @@
// 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 TestSceneFreeModSelectOverlay : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
private FreeModSelectOverlay freeModSelectOverlay;
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 }
};
});
});
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());
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);

View File

@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertHasFreeModButton(Type type, bool hasButton = true)
{
AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
() => this.ChildrenOfType<FreeModSelectScreen>()
() => this.ChildrenOfType<FreeModSelectOverlay>()
.Single()
.ChildrenOfType<ModPanel>()
.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.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -171,10 +172,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contents loaded",
() => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
AddUntilStep("mod select contains only double time mod",
() => this.ChildrenOfType<UserModSelectScreen>()
() => this.ChildrenOfType<UserModSelectOverlay>()
.SingleOrDefault()?
.ChildrenOfType<ModPanel>()
.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.Screens.Menu;
using osu.Game.Skinning;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.Navigation
{
@ -33,7 +32,6 @@ namespace osu.Game.Tests.Visual.Navigation
private IReadOnlyList<Type> requiredGameDependencies => new[]
{
typeof(OsuGame),
typeof(SentryLogger),
typeof(OsuLogo),
typeof(IdleTracker),
typeof(OnScreenDisplay),

View File

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

View File

@ -25,7 +25,6 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly Bindable<Channel> selected = new Bindable<Channel>();
private OsuSpriteText selectorText;
private OsuSpriteText selectedText;
private OsuSpriteText leaveText;
private ChannelList channelList;
@ -43,21 +42,12 @@ namespace osu.Game.Tests.Visual.Online
Height = 0.7f,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
selectorText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
},
new Drawable[]
{
selectedText = new OsuSpriteText
@ -89,7 +79,6 @@ namespace osu.Game.Tests.Visual.Online
channelList.OnRequestSelect += channel =>
{
channelList.SelectorActive.Value = false;
selected.Value = channel;
};
@ -101,12 +90,6 @@ namespace osu.Game.Tests.Visual.Online
channelList.RemoveChannel(channel);
};
channelList.SelectorActive.BindValueChanged(change =>
{
selectorText.Text = $"Channel Selector Active: {change.NewValue}";
selected.Value = null;
}, true);
selected.BindValueChanged(change =>
{
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]
public void TestShowHide()
{
@ -158,20 +171,17 @@ namespace osu.Game.Tests.Visual.Online
public void TestChannelSelection()
{
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("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden);
AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden);
AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1);
AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1);
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
}
[Test]
public void TestSearchInListing()
{
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");
AddUntilStep("Only channel 2 visibile", () =>
{
@ -263,6 +273,7 @@ namespace osu.Game.Tests.Visual.Online
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
}
[Test]
@ -285,8 +296,7 @@ namespace osu.Game.Tests.Visual.Online
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
}
[Test]
@ -310,8 +320,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
}
[Test]
@ -331,6 +340,7 @@ namespace osu.Game.Tests.Visual.Online
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
}
[Test]
@ -351,6 +361,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
}
[Test]
@ -375,11 +386,14 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
}
private Visibility listingVisibility =>
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value;
private bool listingIsVisible =>
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
private Visibility loadingVisibility =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value;
private bool loadingIsVisible =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value == Visibility.Visible;
private bool channelIsVisible =>
!listingIsVisible && !loadingIsVisible;
private DrawableChannel currentDrawableChannel =>
chatOverlay.ChildrenOfType<DrawableChannel>().Single();

View File

@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking
score.Accuracy = accuracy;
score.Rank = rank;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
loadResultsScreen(() => screen = createResultsScreen(score));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
UnrankedSoloResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createUnrankedSoloResultsScreen()));
loadResultsScreen(() => screen = createUnrankedSoloResultsScreen());
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
ScorePanel expandedPanel = null;
@ -231,7 +231,7 @@ namespace osu.Game.Tests.Visual.Ranking
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);
@ -255,7 +255,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
loadResultsScreen(() => screen = createResultsScreen());
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
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 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);
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 UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo());

View File

@ -1008,7 +1008,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
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);

View File

@ -4,9 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
@ -25,9 +27,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{
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;
@ -37,8 +39,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private void load()
{
Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage));
Dependencies.CacheAs(performer.Object);
Dependencies.CacheAs(notificationOverlay.Object);
Dependencies.CacheAs<IPerformFromScreenRunner>(performer.Object);
Dependencies.CacheAs<INotificationOverlay>(notificationOverlay.Object);
}
[SetUpSteps]
@ -72,7 +74,6 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
[Ignore("Enable when first run setup is being displayed on first run.")]
public void TestDoesntOpenOnSecondRun()
{
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("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.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Linq;
using NUnit.Framework;
@ -12,11 +14,9 @@ using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
@ -41,20 +41,16 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = new ModColumn(modType, false)
{
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]
public void TestMultiSelection()
{
ModColumn column = null;
ModColumn column = null!;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -62,7 +58,8 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = column = new ModColumn(ModType.DifficultyIncrease, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
}
});
@ -91,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestFiltering()
{
TestModColumn column = null;
TestModColumn column = null!;
AddStep("create content", () => Child = new Container
{
@ -100,30 +97,31 @@ namespace osu.Game.Tests.Visual.UserInterface
Child = column = new TestModColumn(ModType.Fun, true)
{
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);
clickToggle();
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
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));
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);
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("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("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
@ -138,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestKeyboardSelection()
{
ModColumn column = null;
ModColumn column = null!;
AddStep("create content", () => Child = new Container
{
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 })
{
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));
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));
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));
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));
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
@ -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.
using System;
@ -10,45 +10,210 @@ 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.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
[Description("mod select and icon display")]
public class TestSceneModSelectOverlay : OsuTestScene
[TestFixture]
public class TestSceneModSelectOverlay : OsuManualInputManagerTestScene
{
private RulesetStore rulesets;
private ModDisplay modDisplay;
private TestModSelectOverlay modSelect;
[Resolved]
private RulesetStore rulesetStore { get; set; }
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
createDisplay(() => new TestModSelectOverlay());
});
private UserModSelectOverlay modSelectOverlay;
[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>
@ -58,10 +223,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSettingsNotCrossPolluting()
{
Bindable<IReadOnlyList<Mod>> selectedMods2 = null;
ModSelectOverlay modSelectOverlay2 = null;
createScreen();
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);
@ -69,7 +236,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("create second overlay", () =>
{
Add(modSelect = new TestModSelectOverlay().With(d =>
Add(modSelectOverlay2 = new UserModSelectOverlay().With(d =>
{
d.Origin = 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 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 } };
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", () => modSelect.DeselectAllButton.TriggerClick());
AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
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);
}
[Test]
public void TestAnimationFlushOnClose()
{
createScreen();
changeRuleset(0);
AddStep("Select all fun mods", () =>
{
modSelect.ModSectionsContainer
.Single(c => c.ModType == ModType.DifficultyIncrease)
.SelectAll();
modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
.SelectAll();
});
AddUntilStep("many mods selected", () => modDisplay.Current.Value.Count >= 5);
AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
AddStep("trigger deselect and close overlay", () =>
{
modSelect.ModSectionsContainer
.Single(c => c.ModType == ModType.DifficultyIncrease)
.DeselectAll();
modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
.DeselectAll();
modSelect.Hide();
modSelectOverlay.Hide();
});
AddAssert("all mods deselected", () => modDisplay.Current.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>());
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);
@ -173,42 +308,42 @@ namespace osu.Game.Tests.Visual.UserInterface
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);
AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
changeRuleset(0);
AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 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 = modSelect.GetModButton(SelectedMods.Value.Single());
return ((OsuModDoubleTime)button.SelectedMod).SpeedChange.Value == 1.01;
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);
AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
createScreen();
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 overlayButtonMod = null;
createScreen();
changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () =>
{
var button = modSelect.GetModButton(SelectedMods.Value.Single());
overlayButtonMod = button.SelectedMod;
return overlayButtonMod.GetType() == external.GetType();
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.Contains(overlayButtonMod));
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(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));
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", () => 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));
AddUntilStep("double time not visible", () => modSelect.ChildrenOfType<ModButton>().All(b => !b.Mods.Any(m => m is OsuModDoubleTime)));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = 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", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
AddStep("make double time valid again", () => modSelect.IsValidMod = m => true);
AddUntilStep("double time visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModDoubleTime)));
AddAssert("nightcore still visible", () => modSelect.ChildrenOfType<ModButton>().Any(b => b.Mods.Any(m => m is OsuModNightcore)));
AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = m => true);
AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
AddAssert("nightcore still visible", () => modSelectOverlay.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", () => 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));
AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2);
AddStep("make NF invalid", () => modSelectOverlay.IsValidMod = m => !(m is ModNoFail));
AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
}
[Test]
public void TestUnimplementedModIsUnselectable()
{
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);
checkSelected(mod);
createScreen();
changeRuleset(0);
selectPrevious(mod);
checkNotSelected(mod);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
selectNext(mod);
selectNext(mod);
checkNotSelected(mod);
selectPrevious(mod);
selectPrevious(mod);
checkNotSelected(mod);
}
private void testMultiMod(MultiMod multiMod)
{
foreach (var mod in multiMod.Mods)
AddStep("click deselect all button", () =>
{
selectNext(mod);
checkSelected(mod);
}
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;
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
InputManager.Click(MouseButton.Left);
});
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); });
waitForLoad();
}
createScreen();
changeRuleset(0);
private void changeTestRuleset(RulesetInfo rulesetInfo)
{
AddStep($"change ruleset to {rulesetInfo.Name}", () => { Ruleset.Value = rulesetInfo; });
waitForLoad();
}
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);
private void waitForLoad() =>
AddUntilStep("wait for icons to load", () => modSelect.AllLoaded);
private void checkNotSelected(Mod mod)
{
AddAssert($"check {mod.Name} is not selected", () =>
AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
AddStep("click back button", () =>
{
var button = modSelect.GetModButton(mod);
return modSelect.SelectedMods.Value.All(m => m.GetType() != mod.GetType()) && button.SelectedMod?.GetType() != mod.GetType();
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().First());
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 =>
{
d.Origin = Anchor.BottomCentre;
d.Anchor = Anchor.BottomCentre;
d.SelectedMods.BindTarget = SelectedMods;
}),
modDisplay = new ModDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Position = new Vector2(-5, 25),
Current = { BindTarget = modSelect.SelectedMods }
}
};
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", () => modSelectOverlay.IsValidMod = _ => true);
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 =>
base.ModSectionsContainer;
AddStep("change to ruleset without all mod types", () => Ruleset.Value = TestCustomisableModRuleset.CreateTestRulesetInfo());
AddUntilStep("1 column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
public ModButton GetModButton(Mod mod)
{
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;
changeRuleset(0);
AddAssert("5 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 5);
}
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
{
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneRoundedButton : OsuTestScene
public class TestSceneRoundedButton : ThemeComparisonTestScene
{
[Test]
public void TestBasic()
private readonly BindableBool enabled = new BindableBool(true);
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
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.DarkGray
},
button = new RoundedButton
{
Width = 400,
Text = "Test button",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = () => { }
}
}
});
[Test]
public void TestDisabled()
{
AddToggleStep("toggle disabled", disabled => enabled.Value = !disabled);
}
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="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<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 Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Moq" Version="4.17.2" />

View File

@ -6,7 +6,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.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" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
@ -164,6 +166,20 @@ namespace osu.Game.Configuration
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)
: 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>
/// Write changes to realm.
/// </summary>

View File

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

View File

@ -184,14 +184,12 @@ namespace osu.Game.Graphics.UserInterface
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);
if (IsPreSelected || IsSelected)
Background.FadeIn(100, Easing.OutQuint);
else
Background.FadeOut(600, Easing.OutQuint);
}
protected override void UpdateForegroundColour()

View File

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

View File

@ -5,9 +5,9 @@ using osu.Framework.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>
/// "Mod Select"
@ -26,4 +26,4 @@ namespace osu.Game.Localisation
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.
/// </summary>
/// <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>
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;
@ -439,7 +440,7 @@ namespace osu.Game.Online.Chat
case ChannelType.Multiplayer:
// join is implicit. happens when you join a multiplayer game.
// this will probably change in the future.
joinChannel(channel, fetchInitialMessages);
joinChannel(channel, fetchInitialMessages, setCurrent);
return channel;
case ChannelType.PM:
@ -460,7 +461,7 @@ namespace osu.Game.Online.Chat
default:
var req = new JoinChannelRequest(channel);
req.Success += () => joinChannel(channel, fetchInitialMessages);
req.Success += () => joinChannel(channel, fetchInitialMessages, setCurrent);
req.Failure += ex => LeaveChannel(channel);
api.Queue(req);
return channel;
@ -472,7 +473,8 @@ namespace osu.Game.Online.Chat
this.fetchInitialMessages(channel);
}
CurrentChannel.Value ??= channel;
if (setCurrent)
CurrentChannel.Value ??= channel;
return channel;
}

View File

@ -14,6 +14,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -56,6 +57,8 @@ using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Graphics;
using Sentry;
using Logger = osu.Framework.Logging.Logger;
namespace osu.Game
{
@ -258,7 +261,7 @@ namespace osu.Game
{
dependencies.CacheAs(this);
dependencies.Cache(SentryLogger);
SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
@ -1197,6 +1200,15 @@ namespace osu.Game
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)
{
case IntroScreen intro:

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@ -85,6 +86,8 @@ namespace osu.Game
public bool IsDeployedBuild => AssemblyVersion.Major > 0;
internal const string BUILD_SUFFIX = "lazer";
public virtual string Version
{
get
@ -93,7 +96,7 @@ namespace osu.Game
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
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>
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()
{
Name = @"osu!";
allowableExceptions = UnhandledExceptionsBeforeCrash;
}
[BackgroundDependencyLoader]
@ -408,6 +423,8 @@ namespace osu.Game
LocalConfig ??= UseDevelopmentServer
? new DevelopmentOsuConfigManager(Storage)
: new OsuConfigManager(Storage);
host.ExceptionThrown += onExceptionThrown;
}
/// <summary>
@ -505,6 +522,23 @@ namespace osu.Game
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)
{
base.Dispose(isDisposing);
@ -514,6 +548,9 @@ namespace osu.Game
LocalConfig?.Dispose();
realm?.Dispose();
if (Host != null)
Host.ExceptionThrown -= onExceptionThrown;
}
}
}

View File

@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -19,11 +18,9 @@ namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelList : Container
{
public Action<Channel>? OnRequestSelect;
public Action<Channel?>? OnRequestSelect;
public Action<Channel>? OnRequestLeave;
public readonly BindableBool SelectorActive = new BindableBool();
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private ChannelListItemFlow publicChannelFlow = null!;
@ -56,7 +53,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
new ChannelListSelector
{
Margin = new MarginPadding { Bottom = 10 },
SelectorActive = { BindTarget = SelectorActive },
Action = () => OnRequestSelect?.Invoke(null),
},
privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"),
},
@ -73,7 +70,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
ChannelListItem item = new ChannelListItem(channel);
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
item.SelectorActive.BindTarget = SelectorActive;
ChannelListItemFlow flow = getFlowForChannel(channel);
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 SelectorActive = new BindableBool();
private Box hoverBox = null!;
private Box selectBox = null!;
private OsuSpriteText text = null!;
@ -127,7 +125,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
base.LoadComplete();
selectedChannel.BindValueChanged(_ => updateState(), true);
SelectorActive.BindValueChanged(_ => updateState(), true);
Unread.BindValueChanged(_ => updateState(), true);
}
@ -163,7 +160,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
private void updateState()
{
bool selected = selectedChannel.Value == Channel && !SelectorActive.Value;
bool selected = selectedChannel.Value == Channel;
if (selected)
selectBox.FadeIn(300, Easing.OutQuint);

View File

@ -12,17 +12,19 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelListSelector : OsuClickableContainer
{
public readonly BindableBool SelectorActive = new BindableBool();
private Box hoverBox = null!;
private Box selectBox = null!;
private OsuSpriteText text = null!;
[Resolved]
private Bindable<Channel> currentChannel { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@ -69,9 +71,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
{
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);
selectBox.FadeIn(300, Easing.OutQuint);
@ -82,8 +86,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
selectBox.FadeOut(200, Easing.OutQuint);
}
}, true);
Action = () => SelectorActive.Value = true;
}
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 chat_bar_height = 60;
private readonly BindableBool selectorActive = new BindableBool();
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@ -100,7 +98,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Y,
Width = side_bar_width,
Padding = new MarginPadding { Top = top_bar_height },
SelectorActive = { BindTarget = selectorActive },
},
new Container
{
@ -137,7 +134,6 @@ namespace osu.Game.Overlays
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Padding = new MarginPadding { Left = side_bar_width },
ShowSearch = { BindTarget = selectorActive },
},
};
}
@ -146,8 +142,6 @@ namespace osu.Game.Overlays
{
base.LoadComplete();
loading.Show();
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
@ -157,21 +151,14 @@ namespace osu.Game.Overlays
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true);
channelList.OnRequestSelect += channel =>
{
// Manually selecting a channel should dismiss the selector
selectorActive.Value = false;
channelManager.CurrentChannel.Value = channel;
};
channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = 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);
textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
textBar.OnChatMessageCommitted += handleChatMessage;
selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true);
}
/// <summary>
@ -191,8 +178,6 @@ namespace osu.Game.Overlays
channelManager.CurrentChannel.Value = channel;
}
selectorActive.Value = false;
channel.HighlightedMessage.Value = message;
Show();
@ -252,28 +237,25 @@ namespace osu.Game.Overlays
{
Channel? newChannel = channel.NewValue;
loading.Show();
// Channel is null when leaving the currently selected channel
if (newChannel == null)
{
// Find another channel to switch to
newChannel = channelManager.JoinedChannels.FirstOrDefault(c => c != channel.OldValue);
if (newChannel == null)
selectorActive.Value = true;
else
currentChannel.Value = newChannel;
return;
// null channel denotes that we should be showing the listing.
channelListing.State.Value = Visibility.Visible;
textBar.ShowSearch.Value = true;
}
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
else
{
currentChannelContainer.Clear();
currentChannelContainer.Add(loaded);
loading.Hide();
});
channelListing.State.Value = Visibility.Hidden;
textBar.ShowSearch.Value = false;
loading.Show();
LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded =>
{
currentChannelContainer.Clear();
currentChannelContainer.Add(loaded);
loading.Hide();
});
}
}
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 const float CONTENT_FONT_SIZE = 16;
protected const float HEADER_FONT_SIZE = 24;
[Resolved]
protected OverlayColourProvider OverlayColourProvider { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
const float header_size = 40;
const float spacing = 20;
InternalChildren = new Drawable[]
@ -33,23 +36,29 @@ namespace osu.Game.Overlays.FirstRunSetup
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Children = new Drawable[]
Masking = false,
Child = new Container
{
new OsuSpriteText
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 30 },
Children = new Drawable[]
{
Text = this.GetLocalisableDescription(),
Font = OsuFont.Default.With(size: header_size),
Colour = OverlayColourProvider.Light1,
new OsuSpriteText
{
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);
this
.FadeInFromZero(500)
.FadeInFromZero(100)
.MoveToX(offset)
.MoveToX(0, 500, Easing.OutQuint);
}
@ -68,7 +77,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
base.OnResuming(e);
this
.FadeInFromZero(500)
.FadeInFromZero(100)
.MoveToX(0, 500, Easing.OutQuint);
}

View File

@ -46,11 +46,11 @@ namespace osu.Game.Overlays.FirstRunSetup
[BackgroundDependencyLoader(permitNulls: true)]
private void load(LegacyImportManager? legacyImportManager)
{
Vector2 buttonSize = new Vector2(500, 60);
Vector2 buttonSize = new Vector2(400, 50);
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,
Text = FirstRunSetupBeatmapScreenStrings.Description,
@ -63,7 +63,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Height = 30,
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,
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,
Text = FirstRunSetupBeatmapScreenStrings.TutorialDescription,
@ -89,7 +89,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.TutorialButton,
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,
Text = FirstRunSetupBeatmapScreenStrings.BundledDescription,
@ -105,7 +105,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.BundledButton,
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,
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,
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,

View File

@ -9,7 +9,7 @@ using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
@ -22,11 +22,11 @@ namespace osu.Game.Overlays.FirstRunSetup
private SearchContainer<SettingsSection> searchContainer;
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
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,
RelativeSizeAxes = Axes.X,
@ -50,7 +50,7 @@ namespace osu.Game.Overlays.FirstRunSetup
{
new[]
{
new TriangleButton
new RoundedButton
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
@ -59,10 +59,11 @@ namespace osu.Game.Overlays.FirstRunSetup
Action = applyStandard,
},
Empty(),
new DangerousTriangleButton
new RoundedButton
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
BackgroundColour = colours.Pink3,
Text = FirstRunSetupOverlayStrings.ClassicDefaults,
RelativeSizeAxes = Axes.X,
Action = applyClassic

View File

@ -35,9 +35,11 @@ namespace osu.Game.Overlays.FirstRunSetup
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
const float screen_width = 640;
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,
RelativeSizeAxes = Axes.X,
@ -54,7 +56,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.None,
Size = new Vector2(960, 960 / 16f * 9 / 2),
Size = new Vector2(screen_width, screen_width / 16f * 9 / 2),
Children = new Drawable[]
{
new GridContainer
@ -123,6 +125,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private class SampleScreenContainer : CompositeDrawable
{
private readonly OsuScreen screen;
// Minimal isolation from main game.
[Cached]
@ -142,6 +145,12 @@ namespace osu.Game.Overlays.FirstRunSetup
public override bool PropagatePositionalInputSubTree => false;
public override bool PropagateNonPositionalInputSubTree => false;
public SampleScreenContainer(OsuScreen screen)
{
this.screen = screen;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
{
@ -149,13 +158,8 @@ namespace osu.Game.Overlays.FirstRunSetup
Beatmap.Value.LoadTrack();
Ruleset.Value = rulesets.AvailableRulesets.First();
}
public SampleScreenContainer(Screen screen)
{
OsuScreenStack stack;
RelativeSizeAxes = Axes.Both;
OsuLogo logo;
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[]
{
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,
RelativeSizeAxes = Axes.X,

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@ -62,12 +63,15 @@ namespace osu.Game.Overlays
typeof(ScreenBehaviour),
};
private Container stackContainer = null!;
private Container screenContent = null!;
private Bindable<OverlayActivation>? overlayActivationMode;
private Container content = null!;
private LoadingSpinner loading = null!;
private ScheduledDelegate? loadingShowDelegate;
public FirstRunSetupOverlay()
: base(OverlayColourScheme.Purple)
{
@ -86,36 +90,48 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
Padding = new MarginPadding { Bottom = 20, },
Child = new GridContainer
{
Horizontal = 70 * 1.2f,
Bottom = 20,
},
Child = new InputBlockingContainer
{
Masking = true,
CornerRadius = 14,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
ColumnDimensions = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6,
},
stackContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Vertical = 20,
Horizontal = 70,
},
}
new Dimension(),
new Dimension(minSize: 640, maxSize: 800),
new Dimension(),
},
},
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);
// TODO: uncomment when happy with the whole flow.
// if (showFirstRunSetup.Value) Show();
if (showFirstRunSetup.Value) Show();
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@ -269,7 +284,7 @@ namespace osu.Game.Overlays
{
Debug.Assert(currentStepIndex == null);
stackContainer.Child = stack = new ScreenStack
screenContent.Child = stack = new ScreenStack
{
RelativeSizeAxes = Axes.Both,
};
@ -300,12 +315,20 @@ namespace osu.Game.Overlays
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
{
// TODO: uncomment when happy with the whole flow.
// showFirstRunSetup.Value = false;
showFirstRunSetup.Value = false;
currentStepIndex = null;
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]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
public IncompatibilityDisplayingModPanel(ModState modState)
: base(modState)
{
}
public IncompatibilityDisplayingModPanel(Mod 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -25,7 +24,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@ -38,20 +36,30 @@ namespace osu.Game.Overlays.Mods
public readonly ModType ModType;
private Func<Mod, bool>? filter;
private IReadOnlyList<ModState> availableMods = Array.Empty<ModState>();
/// <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.
/// Sets the list of mods to show in this column.
/// </summary>
public Func<Mod, bool>? Filter
public IReadOnlyList<ModState> AvailableMods
{
get => filter;
get => availableMods;
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();
if (IsLoaded)
asyncLoadPanels();
}
}
@ -60,44 +68,12 @@ namespace osu.Game.Overlays.Mods
/// </summary>
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 virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod);
protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod);
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 Box headerBackground;
private readonly Container contentContainer;
@ -257,12 +233,8 @@ namespace osu.Game.Overlays.Mods
}
[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);
if (toggleAllCheckbox != null)
@ -280,6 +252,7 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete();
toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true);
asyncLoadPanels();
}
private void updateToggleAllText()
@ -288,34 +261,21 @@ namespace osu.Game.Overlays.Mods
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 void asyncLoadPanels()
{
cancellationTokenSource?.Cancel();
var panels = createPanels();
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)));
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(_ =>
{
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()
{
foreach (var panel in panelFlow)
{
panel.Active.Value = SelectedMods.Contains(panel.Mod);
panel.ApplyFilter(Filter);
}
allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value);
Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1;
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{
toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0;
toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0;
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
private const double initial_multiple_selection_delay = 120;
@ -455,7 +335,7 @@ namespace osu.Game.Overlays.Mods
{
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);
}
@ -466,7 +346,7 @@ namespace osu.Game.Overlays.Mods
{
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);
}
@ -553,10 +433,10 @@ namespace osu.Game.Overlays.Mods
int index = Array.IndexOf(toggleKeys, e.Key);
if (index < 0) return false;
var panel = panelFlow.ElementAtOrDefault(index);
if (panel == null || panel.Filtered.Value) return false;
var modState = availableMods.ElementAtOrDefault(index);
if (modState == null || modState.Filtered.Value) return false;
panel.Active.Toggle();
modState.Active.Toggle();
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 Mod Mod { get; }
public BindableBool Active { get; } = new BindableBool();
public BindableBool Filtered { get; } = new BindableBool();
public Mod Mod => modState.Mod;
public BindableBool Active => modState.Active;
public BindableBool Filtered => modState.Filtered;
private readonly ModState modState;
protected readonly Box Background;
protected readonly Container SwitchContainer;
@ -55,9 +57,9 @@ namespace osu.Game.Overlays.Mods
private Sample? sampleOff;
private Sample? sampleOn;
public ModPanel(Mod mod)
public ModPanel(ModState modState)
{
Mod = mod;
this.modState = modState;
RelativeSizeAxes = Axes.X;
Height = 42;
@ -79,7 +81,7 @@ namespace osu.Game.Overlays.Mods
SwitchContainer = new Container
{
RelativeSizeAxes = Axes.Y,
Child = new ModSwitchSmall(mod)
Child = new ModSwitchSmall(Mod)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -115,7 +117,7 @@ namespace osu.Game.Overlays.Mods
{
new OsuSpriteText
{
Text = mod.Name,
Text = Mod.Name,
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
@ -125,7 +127,7 @@ namespace osu.Game.Overlays.Mods
},
new OsuSpriteText
{
Text = mod.Description,
Text = Mod.Description,
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Truncate = true,
@ -141,6 +143,11 @@ namespace osu.Game.Overlays.Mods
Action = Active.Toggle;
}
public ModPanel(Mod mod)
: this(new ModState(mod))
{
}
[BackgroundDependencyLoader(true)]
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.
// 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 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)
: base(type)
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 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>
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowTrackAdjustments"/> is <c>true</c>.
/// </summary>
@ -385,6 +387,7 @@ namespace osu.Game.Overlays
/// </remarks>
public void ResetTrackAdjustments()
{
// todo: we probably want a helper method rather than this.
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Balance);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Frequency);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo);
@ -392,8 +395,10 @@ namespace osu.Game.Overlays
if (allowTrackAdjustments)
{
CurrentTrack.BindAdjustments(modTrackAdjustments = new AudioAdjustments());
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)
{
// 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)
content.AutoSizeAxes = Axes.Y;
else

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Audio;
using osu.Framework.Audio;
namespace osu.Game.Rulesets.Mods
{
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
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.
// 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
{
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
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.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
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.
private const double rate_change_on_miss = 0.95d;
private ITrack track;
private IAdjustableAudioComponent track;
private double targetRate = 1d;
/// <summary>
@ -141,7 +139,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.BindValueChanged(adjustPitchChanged);
}
public void ApplyToTrack(ITrack track)
public void ApplyToTrack(IAdjustableAudioComponent track)
{
this.track = track;
@ -151,7 +149,7 @@ namespace osu.Game.Rulesets.Mods
recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count));
}
public void ApplyToSample(DrawableSample sample)
public void ApplyToSample(IAdjustableAudioComponent sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
@ -210,7 +208,6 @@ namespace osu.Game.Rulesets.Mods
private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting)
{
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), 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.
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Mods
}, true);
}
public override void ApplyToTrack(ITrack track)
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);

View File

@ -3,7 +3,6 @@
using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@ -71,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
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);
}

View File

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

View File

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

View File

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

View File

@ -448,7 +448,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary>
/// Reapplies the current <see cref="ArmedState"/>.
/// </summary>
protected void RefreshStateTransforms() => updateState(State.Value, true);
public void RefreshStateTransforms() => updateState(State.Value, true);
/// <summary>
/// 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);
// We need a non-zero travel vector in order to find a valid direction.
if (travelVector == Vector2.Zero)
return (StartPosition, StartTime);
travelVector = new Vector2(0, -1);
float travelLength = travelVector.Length;
// FindSnappedDistance will always round down, but we want to potentially round upwards.
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
// to allow for snapping at a non-multiplied ratio.
float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier);

View File

@ -122,6 +122,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
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));
}
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 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