diff --git a/.editorconfig b/.editorconfig index be5652954b..840fa98334 100644 --- a/.editorconfig +++ b/.editorconfig @@ -191,4 +191,5 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error +dotnet_diagnostic.OLOC001.words_in_name = 5 dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/osu.Android.props b/osu.Android.props index 8d79eb94a8..ff6499631d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index e317a44bc3..eb9045d9ce 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -123,7 +123,12 @@ namespace osu.Desktop tools.RemoveUninstallerRegistryEntry(); }, onEveryRun: (version, tools, firstRun) => { - tools.SetProcessAppUserModelId(); + // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently + // causes the right-click context menu to function incorrectly. + // + // This may turn out to be non-required after an alternative solution is implemented. + // see https://github.com/clowd/Clowd.Squirrel/issues/24 + // tools.SetProcessAppUserModelId(); }); } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 346a09cac8..ab61b14ac4 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -5,10 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps public void ApplyPositionOffsets(IBeatmap beatmap) { - var rng = new FastRandom(RNG_SEED); + var rng = new LegacyRandom(RNG_SEED); float? lastPosition = null; double lastStartTime = 0; @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps initialiseHyperDash(beatmap); } - private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) + private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, LegacyRandom rng) { float offsetPosition = hitObject.OriginalX; double startTime = hitObject.StartTime; @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps /// The position which the offset should be applied to. /// The maximum offset, cannot exceed 20px. /// The random number generator. - private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng) + private static void applyRandomOffset(ref float position, double maxOffset, LegacyRandom rng) { bool right = rng.NextBool(); float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))); diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index b2a555f89d..04b522b404 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -90,6 +90,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new LegacyHitExplosion(); return null; + + default: + throw new UnsupportedSkinComponentException(component); } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 47e0e6d7b1..207c6907c8 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -11,8 +11,8 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Beatmaps.Patterns; -using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; +using osu.Game.Utils; using osuTK; namespace osu.Game.Rulesets.Mania.Beatmaps @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private readonly int originalTargetColumns; // Internal for testing purposes - internal FastRandom Random { get; private set; } + internal LegacyRandom Random { get; private set; } private Pattern lastPattern = new Pattern(); @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps IBeatmapDifficultyInfo difficulty = original.Difficulty; int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); - Random = new FastRandom(seed); + Random = new LegacyRandom(seed); return base.ConvertBeatmap(original, cancellationToken); } @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator { - public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 5f8b58d94d..dafe65f415 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -8,12 +8,12 @@ using System.Linq; using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; +using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { convertType = PatternType.None; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index f816a70ab3..2265d3d347 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using System.Linq; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly int endTime; private readonly PatternType convertType; - public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 53b059b4e2..41d4c9322b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -9,11 +9,11 @@ using osuTK; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly PatternType convertType; - public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, + public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index eaf0ea0f2b..d5689c047a 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -5,8 +5,8 @@ using System; using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; +using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { @@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The random number generator to use. /// - protected readonly FastRandom Random; + protected readonly LegacyRandom Random; /// /// The beatmap which is being converted from. /// protected readonly IBeatmap OriginalBeatmap; - protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(hitObject, beatmap, previousPattern) { if (random == null) throw new ArgumentNullException(nameof(random)); diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index ab6bd78ece..31550a8105 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; + private const double release_threshold = 24; protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; @@ -37,31 +38,43 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills var maniaCurrent = (ManiaDifficultyHitObject)current; double endTime = maniaCurrent.EndTime; int column = maniaCurrent.BaseObject.Column; + double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information double holdFactor = 1.0; // Factor to all additional strains in case something else is held double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + bool isOverlapping = false; // Fill up the holdEndTimes array for (int i = 0; i < holdEndTimes.Length; ++i) { - // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... - if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) - holdAddition = 1.0; - - // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 - if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1)) - holdAddition = 0; + // The current note is overlapped if a previous note or end is overlapping the current note body + isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1); // We give a slight bonus to everything if something is held meanwhile if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) holdFactor = 1.25; + closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i])); + // Decay individual strains individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base); } holdEndTimes[column] = endTime; + // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending. + // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away. + // holdAddition + // ^ + // 1.0 + - - - - - -+----------- + // | / + // 0.5 + - - - - -/ Sigmoid Curve + // | /| + // 0.0 +--------+-+---------------> Release Difference / ms + // release_threshold + if (isOverlapping) + holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime))); + // Increase individual strain in own column individualStrains[column] += 2.0 * holdFactor; individualStrain = individualStrains[column]; diff --git a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs deleted file mode 100644 index a9cd7f2476..0000000000 --- a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; - -namespace osu.Game.Rulesets.Mania.MathUtils -{ - /// - /// A PRNG specified in http://heliosphan.org/fastrandom.html. - /// - internal class FastRandom - { - private const double int_to_real = 1.0 / (int.MaxValue + 1.0); - private const uint int_mask = 0x7FFFFFFF; - private const uint y = 842502087; - private const uint z = 3579807591; - private const uint w = 273326509; - - internal uint X { get; private set; } - internal uint Y { get; private set; } = y; - internal uint Z { get; private set; } = z; - internal uint W { get; private set; } = w; - - public FastRandom(int seed) - { - X = (uint)seed; - } - - public FastRandom() - : this(Environment.TickCount) - { - } - - /// - /// Generates a random unsigned integer within the range [, ). - /// - /// The random value. - public uint NextUInt() - { - uint t = X ^ (X << 11); - X = Y; - Y = Z; - Z = W; - return W = W ^ (W >> 19) ^ t ^ (t >> 8); - } - - /// - /// Generates a random integer value within the range [0, ). - /// - /// The random value. - public int Next() => (int)(int_mask & NextUInt()); - - /// - /// Generates a random integer value within the range [0, ). - /// - /// The upper bound. - /// The random value. - public int Next(int upperBound) => (int)(NextDouble() * upperBound); - - /// - /// Generates a random integer value within the range [, ). - /// - /// The lower bound of the range. - /// The upper bound of the range. - /// The random value. - public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound)); - - /// - /// Generates a random double value within the range [0, 1). - /// - /// The random value. - public double NextDouble() => int_to_real * Next(); - - private uint bitBuffer; - private int bitIndex = 32; - - /// - /// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls. - /// - /// The random value. - public bool NextBool() - { - if (bitIndex == 32) - { - bitBuffer = NextUInt(); - bitIndex = 1; - - return (bitBuffer & 1) == 1; - } - - bitIndex++; - return ((bitBuffer >>= 1) & 1) == 1; - } - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 431bd77402..315b4444c2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -116,9 +116,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); - } - break; + default: + throw new UnsupportedSkinComponentException(component); + } } return base.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index d673b7a6ac..a40ae611d8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -70,7 +70,9 @@ namespace osu.Game.Rulesets.Osu.Tests var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo()); tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; - Child = new SkinProvidingContainer(tintingSkin) + var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap); + + Child = new SkinProvidingContainer(provider) { RelativeSizeAxes = Axes.Both, Child = dho = new DrawableSlider(prepareObject(new Slider diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index 0403e81229..429fe30fc5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -9,6 +9,10 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModSuddenDeath : ModSuddenDeath { - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] + { + typeof(OsuModAutopilot), + typeof(OsuModTarget), + }).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 5285380097..4fab9b6a5a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSuddenDeath) }; [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] public Bindable Seed { get; } = new Bindable diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 900ad6f6d3..572185e6e1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -35,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinComponents.FollowPoint: return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false); + case OsuSkinComponents.SliderScorePoint: + return this.GetAnimation(component.LookupName, false, false); + case OsuSkinComponents.SliderFollowCircle: var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); if (followCircle != null) @@ -123,6 +126,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinComponents.ApproachCircle: return new LegacyApproachCircle(); + + default: + throw new UnsupportedSkinComponentException(component); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index a4c0381d16..a638019e69 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI }, new SettingsCheckbox { + ClassicDefault = false, LabelText = "Snaking out sliders", Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) }, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index bbc8f0abea..af5921b0fb 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -57,6 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); + case TaikoSkinComponents.Swell: + // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601). + return null; + case TaikoSkinComponents.HitTarget: if (GetTexture("taikobigcircle") != null) return new TaikoLegacyHitTarget(); @@ -119,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.Mascot: return new DrawableTaikoMascot(); + + default: + throw new UnsupportedSkinComponentException(component); } } diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 296c5cef76..a354464a8e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -160,6 +160,40 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(1); } + [Test] + public void TestFailConditions() + { + var beatmap = createBeatmap(0, 1000); + createProcessor(beatmap); + + AddStep("setup fail conditions", () => processor.FailConditions += ((_, result) => result.Type == HitResult.Miss)); + + AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); + AddAssert("not failed", () => !processor.HasFailed); + AddStep("apply miss hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Miss })); + AddAssert("failed", () => processor.HasFailed); + } + + [TestCase(HitResult.Miss)] + [TestCase(HitResult.Meh)] + public void TestMultipleFailConditions(HitResult resultApplied) + { + var beatmap = createBeatmap(0, 1000); + createProcessor(beatmap); + + AddStep("setup multiple fail conditions", () => + { + processor.FailConditions += ((_, result) => result.Type == HitResult.Miss); + processor.FailConditions += ((_, result) => result.Type == HitResult.Meh); + }); + + AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); + AddAssert("not failed", () => !processor.HasFailed); + + AddStep($"apply {resultApplied.ToString().ToLower()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); + AddAssert("failed", () => processor.HasFailed); + } + [Test] public void TestBonusObjectsExcludedFromDrain() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 79d7bb366d..bf491db45a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -21,7 +21,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { + AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index a224a78531..ab5d766609 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -85,7 +85,10 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); pauseAndConfirm(); + AddAssert("player not playing", () => !Player.LocalUserPlaying.Value); + resumeAndConfirm(); + AddUntilStep("player playing", () => Player.LocalUserPlaying.Value); } [Test] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs new file mode 100644 index 0000000000..8c96ec699f --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneButtonSystemNavigation.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneButtonSystemNavigation : OsuGameTestScene + { + private ButtonSystem buttons => ((MainMenu)Game.ScreenStack.CurrentScreen).ChildrenOfType().Single(); + + [Test] + public void TestGlobalActionHasPriority() + { + AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial); + + // triggering the cookie in the initial state with any key should only happen if no other action is bound to that key. + // here, F10 is bound to GlobalAction.ToggleGameplayMouseButtons. + AddStep("press F10", () => InputManager.Key(Key.F10)); + AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial); + + AddStep("press P", () => InputManager.Key(Key.P)); + AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); + } + + [Test] + public void TestShortcutKeys() + { + AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial); + + AddStep("press P", () => InputManager.Key(Key.P)); + AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); + + AddStep("press P", () => InputManager.Key(Key.P)); + AddAssert("state is play", () => buttons.State == ButtonSystemState.Play); + + AddStep("press P", () => InputManager.Key(Key.P)); + AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 347b4b6c54..b7a74dcd27 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -59,8 +59,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); + DismissAnyNotifications(); return player != null; }); @@ -73,12 +72,6 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); } - private void clickMouseInCentre() - { - InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); - InputManager.Click(MouseButton.Left); - } - private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel .ChildrenOfType() .FirstOrDefault(s => s.Ruleset.ShortName == "osu"); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs index 22a00a3e5a..2662b3930c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs @@ -89,18 +89,11 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); + DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value); } - - private void clickMouseInCentre() - { - InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); - InputManager.Click(MouseButton.Left); - } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 0b7242eee0..a1f41d4caf 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -16,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Overlays.Settings; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -24,12 +22,10 @@ using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; -using osu.Game.Skinning.Editor; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; @@ -71,73 +67,6 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); } - [Test] - public void TestEditComponentDuringGameplay() - { - Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); - - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - - SkinEditor skinEditor = null; - - AddStep("open skin editor", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.PressKey(Key.ShiftLeft); - InputManager.Key(Key.S); - InputManager.ReleaseKey(Key.ControlLeft); - InputManager.ReleaseKey(Key.ShiftLeft); - }); - - AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType().FirstOrDefault()) != null); - - AddStep("Click gameplay scene button", () => - { - skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay").TriggerClick(); - }); - - AddUntilStep("wait for player", () => - { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); - return Game.ScreenStack.CurrentScreen is Player; - }); - - BarHitErrorMeter hitErrorMeter = null; - - AddUntilStep("select bar hit error blueprint", () => - { - var blueprint = skinEditor.ChildrenOfType().FirstOrDefault(b => b.Item is BarHitErrorMeter); - - if (blueprint == null) - return false; - - hitErrorMeter = (BarHitErrorMeter)blueprint.Item; - skinEditor.SelectedComponents.Clear(); - skinEditor.SelectedComponents.Add(blueprint.Item); - return true; - }); - - AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault); - - AddStep("hover first slider", () => - { - InputManager.MoveMouseTo( - skinEditor.ChildrenOfType().First() - .ChildrenOfType>().First() - .ChildrenOfType>().First() - ); - }); - - AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left)); - - AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); - } - [Test] public void TestRetryCountIncrements() { @@ -155,8 +84,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); + DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); @@ -280,8 +208,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); + DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); @@ -596,8 +523,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => { - // dismiss any notifications that may appear (ie. muted notification). - clickMouseInCentre(); + DismissAnyNotifications(); return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); @@ -607,12 +533,6 @@ namespace osu.Game.Tests.Visual.Navigation return () => player; } - private void clickMouseInCentre() - { - InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre); - InputManager.Click(MouseButton.Left); - } - private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs new file mode 100644 index 0000000000..d3aeba2c0f --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs @@ -0,0 +1,136 @@ +// Copyright (c) ppy Pty Ltd . 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.Extensions; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning.Editor; +using osu.Game.Tests.Beatmaps.IO; +using osuTK.Input; +using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneSkinEditorSceneLibrary : OsuGameTestScene + { + private SkinEditor skinEditor; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("open skin editor", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.S); + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType().FirstOrDefault()) != null); + } + + [Test] + public void TestEditComponentDuringGameplay() + { + switchToGameplayScene(); + + BarHitErrorMeter hitErrorMeter = null; + + AddUntilStep("select bar hit error blueprint", () => + { + var blueprint = skinEditor.ChildrenOfType().FirstOrDefault(b => b.Item is BarHitErrorMeter); + + if (blueprint == null) + return false; + + hitErrorMeter = (BarHitErrorMeter)blueprint.Item; + skinEditor.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Add(blueprint.Item); + return true; + }); + + AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault); + + AddStep("hover first slider", () => + { + InputManager.MoveMouseTo( + skinEditor.ChildrenOfType().First() + .ChildrenOfType>().First() + .ChildrenOfType>().First() + ); + }); + + AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left)); + + AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); + } + + [Test] + public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay() + { + AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + + switchToGameplayScene(); + + AddAssert("DT still selected", () => ((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Single() is OsuModDoubleTime); + } + + [Test] + public void TestAutoplayIncompatibleModsRemovedOnEnteringGameplay() + { + AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() }); + + switchToGameplayScene(); + + AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); + } + + [Test] + public void TestDuplicateAutoplayModRemovedOnEnteringGameplay() + { + AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); + + switchToGameplayScene(); + + AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); + } + + [Test] + public void TestCinemaModRemovedOnEnteringGameplay() + { + AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() }); + + switchToGameplayScene(); + + AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); + } + + private void switchToGameplayScene() + { + AddStep("Click gameplay scene button", () => skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay").TriggerClick()); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneBundledBeatmapDownloader.cs b/osu.Game.Tests/Visual/Online/TestSceneBundledBeatmapDownloader.cs new file mode 100644 index 0000000000..2af1c9a0f0 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneBundledBeatmapDownloader.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps.Drawables; + +namespace osu.Game.Tests.Visual.Online +{ + [Ignore("Only for visual testing")] + public class TestSceneBundledBeatmapDownloader : OsuTestScene + { + private BundledBeatmapDownloader downloader; + + [Test] + public void TestDownloader() + { + AddStep("Create downloader", () => + { + downloader?.Expire(); + Add(downloader = new BundledBeatmapDownloader(false)); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index cdeaafd828..377873f64a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -33,18 +33,21 @@ namespace osu.Game.Tests.Visual.Settings State = { Value = Visibility.Visible } }); }); + + AddStep("reset mouse", () => InputManager.MoveMouseTo(settings)); } [Test] - public void TestQuickFiltering() + public void TestFiltering([Values] bool beforeLoad) { - AddStep("set filter", () => - { - settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"; - }); + if (beforeLoad) + AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType().Any()); + if (!beforeLoad) + AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); + AddAssert("ensure all items match filter", () => settings.SectionsContainer .ChildrenOfType().Where(f => f.IsPresent) .All(section => @@ -56,6 +59,15 @@ namespace osu.Game.Tests.Visual.Settings )); AddAssert("ensure section is current", () => settings.CurrentSection.Value is GraphicsSection); + AddAssert("ensure section is placed first", () => settings.CurrentSection.Value.Y == 0); + } + + [Test] + public void TestFilterAfterLoad() + { + AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType().Any()); + + AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType().First().Current.Value = "scaling"); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index 1bb5cadc6a..1a879e2e70 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -10,11 +10,12 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Screens.Menu; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneButtonSystem : OsuTestScene + public class TestSceneButtonSystem : OsuManualInputManagerTestScene { private OsuLogo logo; private ButtonSystem buttons; @@ -64,6 +65,66 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Enter mode", performEnterMode); } + [TestCase(Key.P, true)] + [TestCase(Key.M, true)] + [TestCase(Key.L, true)] + [TestCase(Key.E, false)] + [TestCase(Key.D, false)] + [TestCase(Key.Q, false)] + [TestCase(Key.O, false)] + public void TestShortcutKeys(Key key, bool entersPlay) + { + int activationCount = -1; + AddStep("set up action", () => + { + activationCount = 0; + void action() => activationCount++; + + switch (key) + { + case Key.P: + buttons.OnSolo = action; + break; + + case Key.M: + buttons.OnMultiplayer = action; + break; + + case Key.L: + buttons.OnPlaylists = action; + break; + + case Key.E: + buttons.OnEdit = action; + break; + + case Key.D: + buttons.OnBeatmapListing = action; + break; + + case Key.Q: + buttons.OnExit = action; + break; + + case Key.O: + buttons.OnSettings = action; + break; + } + }); + + AddStep($"press {key}", () => InputManager.Key(key)); + AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); + + if (entersPlay) + { + AddStep("press P", () => InputManager.Key(Key.P)); + AddAssert("state is play", () => buttons.State == ButtonSystemState.Play); + } + + AddStep($"press {key}", () => InputManager.Key(key)); + AddAssert("action triggered", () => activationCount == 1); + } + private void performEnterMode() { buttons.State = ButtonSystemState.EnteringMode; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs new file mode 100644 index 0000000000..9747b5cc53 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Game.Overlays; +using osu.Game.Overlays.FirstRunSetup; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneFirstRunScreenBehaviour : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + public TestSceneFirstRunScreenBehaviour() + { + AddStep("load screen", () => + { + Child = new ScreenStack(new ScreenBehaviour()); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs new file mode 100644 index 0000000000..51065939f0 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBundledBeatmaps.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Game.Overlays; +using osu.Game.Overlays.FirstRunSetup; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneFirstRunScreenBundledBeatmaps : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + public TestSceneFirstRunScreenBundledBeatmaps() + { + AddStep("load screen", () => + { + Child = new ScreenStack(new ScreenBeatmaps()); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs index 5ca09b34aa..64ad4ff119 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs @@ -1,13 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Game.Overlays; using osu.Game.Overlays.FirstRunSetup; namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFirstRunScreenUIScale : OsuManualInputManagerTestScene { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + public TestSceneFirstRunScreenUIScale() { AddStep("load screen", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index efce4f350b..836cf6caad 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -66,6 +66,7 @@ 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)); @@ -165,7 +166,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click outside content", () => { - InputManager.MoveMouseTo(overlay.ScreenSpaceDrawQuad.TopLeft - new Vector2(1)); + InputManager.MoveMouseTo(new Vector2(overlay.ScreenSpaceDrawQuad.TopLeft.X, overlay.ScreenSpaceDrawQuad.Centre.Y)); InputManager.Click(MouseButton.Left); }); @@ -177,7 +178,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("step to next", () => overlay.NextButton.TriggerClick()); - AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale); + AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenBeatmaps); AddStep("hide", () => overlay.Hide()); AddAssert("overlay hidden", () => overlay.State.Value == Visibility.Hidden); @@ -187,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("run notification action", () => lastNotification.Activated()); AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible); - AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale); + AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs index 95323e5dfa..f56d9c8a91 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs @@ -47,12 +47,22 @@ namespace osu.Game.Tests.Visual.UserInterface { IncompatibilityDisplayingModPanel panel = null; - AddStep("create panel with DT", () => Child = panel = new IncompatibilityDisplayingModPanel(new OsuModDoubleTime()) + AddStep("create panel with DT", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.None, - Width = 300 + Child = panel = new IncompatibilityDisplayingModPanel(new OsuModDoubleTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = 300, + }; + + panel.Active.BindValueChanged(active => + { + SelectedMods.Value = active.NewValue + ? Array.Empty() + : new[] { panel.Mod }; + }); }); clickPanel(); @@ -63,11 +73,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set incompatible mod", () => SelectedMods.Value = new[] { new OsuModHalfTime() }); - clickPanel(); - AddAssert("panel not active", () => !panel.Active.Value); - - AddStep("reset mods", () => SelectedMods.Value = Array.Empty()); - clickPanel(); AddAssert("panel active", () => panel.Active.Value); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index 4a738cb29d..ec6e962c6a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -89,6 +90,54 @@ namespace osu.Game.Tests.Visual.UserInterface 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)); + + AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick()); + AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore)); + + 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))); + + 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))); + } + + [Test] + public void TestDimmedState() + { + createScreen(); + changeRuleset(0); + + AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value)); + + ModColumn lastColumn = null; + + AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value); + AddStep("request scroll to last column", () => + { + var lastDimContainer = this.ChildrenOfType().Last(); + lastColumn = lastDimContainer.Column; + lastDimContainer.RequestScroll?.Invoke(lastDimContainer); + }); + AddUntilStep("column undimmed", () => lastColumn.Active.Value); + + AddStep("click panel", () => + { + InputManager.MoveMouseTo(lastColumn.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("panel selected", () => lastColumn.ChildrenOfType().First().Active.Value); + } + [Test] public void TestCustomisationToggleState() { @@ -136,5 +185,8 @@ namespace osu.Game.Tests.Visual.UserInterface 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().Single(panel => panel.Mod.GetType() == modType); } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs index 2312c57af2..1f3736bd9b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs @@ -3,45 +3,79 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneSectionsContainer : OsuManualInputManagerTestScene { - private readonly SectionsContainer container; + private SectionsContainer container; private float custom; - private const float header_height = 100; - public TestSceneSectionsContainer() + private const float header_expandable_height = 300; + private const float header_fixed_height = 100; + + [SetUpSteps] + public void SetUpSteps() { - container = new SectionsContainer + AddStep("setup container", () => { - RelativeSizeAxes = Axes.Y, - Width = 300, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - FixedHeader = new Box + container = new SectionsContainer { - Alpha = 0.5f, + RelativeSizeAxes = Axes.Y, Width = 300, - Height = header_height, - Colour = Color4.Red - } - }; - container.SelectedSection.ValueChanged += section => - { - if (section.OldValue != null) - section.OldValue.Selected = false; - if (section.NewValue != null) - section.NewValue.Selected = true; - }; - Add(container); + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }; + + container.SelectedSection.ValueChanged += section => + { + if (section.OldValue != null) + section.OldValue.Selected = false; + if (section.NewValue != null) + section.NewValue.Selected = true; + }; + + Child = container; + }); + + AddToggleStep("disable expandable header", v => container.ExpandableHeader = v + ? null + : new TestBox(@"Expandable Header") + { + RelativeSizeAxes = Axes.X, + Height = header_expandable_height, + BackgroundColour = new OsuColour().GreySky, + }); + + AddToggleStep("disable fixed header", v => container.FixedHeader = v + ? null + : new TestBox(@"Fixed Header") + { + RelativeSizeAxes = Axes.X, + Height = header_fixed_height, + BackgroundColour = new OsuColour().Red.Opacity(0.5f), + }); + + AddToggleStep("disable footer", v => container.Footer = v + ? null + : new TestBox("Footer") + { + RelativeSizeAxes = Axes.X, + Height = 200, + BackgroundColour = new OsuColour().Green4, + }); } [Test] @@ -71,7 +105,6 @@ namespace osu.Game.Tests.Visual.UserInterface { const int sections_count = 11; float[] alternating = { 0.07f, 0.33f, 0.16f, 0.33f }; - AddStep("clear", () => container.Clear()); AddStep("fill with sections", () => { for (int i = 0; i < sections_count; i++) @@ -84,9 +117,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[scrollIndex]); AddUntilStep("section top is visible", () => { - float scrollPosition = container.ChildrenOfType().First().Current; - float sectionTop = container.Children[scrollIndex].BoundingBox.Top; - return scrollPosition < sectionTop; + var scrollContainer = container.ChildrenOfType().Single(); + float sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]); + return scrollContainer.Current < sectionPosition; }); } @@ -101,15 +134,56 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 1]); } - private static readonly ColourInfo selected_colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Gold); + [Test] + public void TestNavigation() + { + AddRepeatStep("add sections", () => append(1f), 3); + AddUntilStep("wait for load", () => container.Children.Any()); + + AddStep("hover sections container", () => InputManager.MoveMouseTo(container)); + AddStep("press page down", () => InputManager.Key(Key.PageDown)); + AddUntilStep("scrolled one page down", () => + { + var scroll = container.ChildrenOfType().First(); + return Precision.AlmostEquals(scroll.Current, Content.DrawHeight - header_fixed_height, 1f); + }); + + AddStep("press page down", () => InputManager.Key(Key.PageDown)); + AddUntilStep("scrolled two pages down", () => + { + var scroll = container.ChildrenOfType().First(); + return Precision.AlmostEquals(scroll.Current, (Content.DrawHeight - header_fixed_height) * 2, 1f); + }); + + AddStep("press page up", () => InputManager.Key(Key.PageUp)); + AddUntilStep("scrolled one page up", () => + { + var scroll = container.ChildrenOfType().First(); + return Precision.AlmostEquals(scroll.Current, Content.DrawHeight - header_fixed_height, 1f); + }); + } + + private static readonly ColourInfo selected_colour = ColourInfo.GradientVertical(new OsuColour().Orange2, new OsuColour().Orange3); private static readonly ColourInfo default_colour = ColourInfo.GradientVertical(Color4.White, Color4.DarkGray); private void append(float multiplier) { - container.Add(new TestSection + float fixedHeaderHeight = container.FixedHeader?.Height ?? 0; + float expandableHeaderHeight = container.ExpandableHeader?.Height ?? 0; + + float totalHeaderHeight = expandableHeaderHeight + fixedHeaderHeight; + float effectiveHeaderHeight = totalHeaderHeight; + + // if we're in the "next page" of the sections container, + // height of the expandable header should not be accounted. + var scrollContent = container.ChildrenOfType().Single().ScrollContent; + if (totalHeaderHeight + scrollContent.Height >= Content.DrawHeight) + effectiveHeaderHeight -= expandableHeaderHeight; + + container.Add(new TestSection($"Section #{container.Children.Count + 1}") { Width = 300, - Height = (container.ChildSize.Y - header_height) * multiplier, + Height = (Content.DrawHeight - effectiveHeaderHeight) * multiplier, Colour = default_colour }); } @@ -120,11 +194,50 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.ScrollVerticalBy(direction); } - private class TestSection : Box + private class TestSection : TestBox { public bool Selected { - set => Colour = value ? selected_colour : default_colour; + set => BackgroundColour = value ? selected_colour : default_colour; + } + + public TestSection(string label) + : base(label) + { + BackgroundColour = default_colour; + } + } + + private class TestBox : Container + { + private readonly Box background; + private readonly OsuSpriteText text; + + public ColourInfo BackgroundColour + { + set + { + background.Colour = value; + text.Colour = OsuColour.ForegroundTextColourFor(value.AverageColour); + } + } + + public TestBox(string label) + { + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = label, + Font = OsuFont.Default.With(size: 36), + } + }; } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs similarity index 63% rename from osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs index b5109aa58d..5a4eeef4d9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs @@ -11,11 +11,62 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneShearedToggleButton : OsuManualInputManagerTestScene + public class TestSceneShearedButtons : OsuManualInputManagerTestScene { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [TestCase(false)] + [TestCase(true)] + public void TestShearedButton(bool bigButton) + { + ShearedButton button = null; + bool actionFired = false; + + AddStep("create button", () => + { + actionFired = false; + + if (bigButton) + { + Child = button = new ShearedButton(400) + { + LighterColour = Colour4.FromHex("#FFFFFF"), + DarkerColour = Colour4.FromHex("#FFCC22"), + TextColour = Colour4.Black, + TextSize = 36, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Let's GO!", + Height = 80, + Action = () => actionFired = true, + }; + } + else + { + Child = button = new ShearedButton(200) + { + LighterColour = Colour4.FromHex("#FF86DD"), + DarkerColour = Colour4.FromHex("#DE31AE"), + TextColour = Colour4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Press me", + Height = 80, + Action = () => actionFired = true, + }; + } + }); + + AddStep("set disabled", () => button.Enabled.Value = false); + AddStep("press button", () => button.TriggerClick()); + AddAssert("action not fired", () => !actionFired); + + AddStep("set enabled", () => button.Enabled.Value = true); + AddStep("press button", () => button.TriggerClick()); + AddAssert("action fired", () => actionFired); + } + [Test] public void TestShearedToggleButton() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs new file mode 100644 index 0000000000..5a9cafde27 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneShearedOverlayContainer : OsuManualInputManagerTestScene + { + private TestShearedOverlayContainer overlay; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create overlay", () => + { + Child = overlay = new TestShearedOverlayContainer + { + State = { Value = Visibility.Visible } + }; + }); + } + + [Test] + public void TestClickAwayToExit() + { + AddStep("click inside header", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("overlay not dismissed", () => overlay.State.Value == Visibility.Visible); + + AddStep("click inside content", () => + { + InputManager.MoveMouseTo(overlay.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("overlay not dismissed", () => overlay.State.Value == Visibility.Visible); + + AddStep("click outside header", () => + { + InputManager.MoveMouseTo(new Vector2(overlay.ScreenSpaceDrawQuad.TopLeft.X, overlay.ScreenSpaceDrawQuad.Centre.Y)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden); + } + + public class TestShearedOverlayContainer : ShearedOverlayContainer + { + protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Green; + + [BackgroundDependencyLoader] + private void load() + { + Header.Title = "Sheared overlay header"; + Header.Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)); + + MainAreaContent.Child = new InputBlockingContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.9f), + Children = new Drawable[] + { + new Box + { + Colour = Color4.Blue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Font = OsuFont.Default.With(size: 24), + Text = "Content", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; + } + } + } +} diff --git a/osu.Game/.editorconfig b/osu.Game/.editorconfig index 4107d1bb35..539cd56dab 100644 --- a/osu.Game/.editorconfig +++ b/osu.Game/.editorconfig @@ -1,3 +1,4 @@ [*.cs] +dotnet_diagnostic.OLOC001.words_in_name = 5 dotnet_diagnostic.OLOC001.prefix_namespace = osu.Game.Resources.Localisation -dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. \ No newline at end of file +dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs new file mode 100644 index 0000000000..df0a69cb25 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -0,0 +1,339 @@ +// Copyright (c) ppy Pty Ltd . 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 System.Text.RegularExpressions; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Utils; + +namespace osu.Game.Beatmaps.Drawables +{ + public class BundledBeatmapDownloader : CompositeDrawable + { + private readonly bool shouldPostNotifications; + + public IEnumerable DownloadTrackers => downloadTrackers; + + private readonly List downloadTrackers = new List(); + + private readonly List downloadableFilenames = new List(); + + private BundledBeatmapModelDownloader beatmapDownloader; + + /// + /// Construct a new beatmap downloader. + /// + /// Whether only the tutorial should be downloaded, instead of bundled beatmaps. + /// Whether downloads should create tracking notifications. + public BundledBeatmapDownloader(bool onlyTutorial, bool shouldPostNotifications = false) + { + this.shouldPostNotifications = shouldPostNotifications; + + if (onlyTutorial) + { + queueDownloads(new[] { tutorial_filename }); + } + else + { + queueDownloads(always_bundled_beatmaps); + + queueDownloads(bundled_osu, 8); + queueDownloads(bundled_taiko, 3); + queueDownloads(bundled_catch, 3); + queueDownloads(bundled_mania, 3); + } + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var localDependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + localDependencies.CacheAs(beatmapDownloader = new BundledBeatmapModelDownloader(parent.Get(), parent.Get())); + + if (shouldPostNotifications && parent.Get() is INotificationOverlay notifications) + beatmapDownloader.PostNotification = notifications.Post; + + return localDependencies; + } + + [BackgroundDependencyLoader] + private void load() + { + foreach (string filename in downloadableFilenames) + { + var match = Regex.Match(filename, @"([0-9]*) (.*) - (.*)\.osz"); + + var beatmapSet = new APIBeatmapSet + { + OnlineID = int.Parse(match.Groups[1].Value), + Artist = match.Groups[2].Value, + Title = match.Groups[3].Value, + }; + + var beatmapDownloadTracker = new BeatmapDownloadTracker(beatmapSet); + downloadTrackers.Add(beatmapDownloadTracker); + AddInternal(beatmapDownloadTracker); + + beatmapDownloader.Download(beatmapSet); + } + } + + private void queueDownloads(string[] sourceFilenames, int? limit = null) + { + try + { + // Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps. + var random = new LegacyRandom(DateTime.UtcNow.Year * 1000 + (DateTime.UtcNow.DayOfYear / 7)); + + downloadableFilenames.AddRange(sourceFilenames.OrderBy(x => random.NextDouble()).Take(limit ?? int.MaxValue)); + } + catch { } + } + + private class BundledBeatmapModelDownloader : BeatmapModelDownloader + { + public BundledBeatmapModelDownloader(IModelImporter beatmapImporter, IAPIProvider api) + : base(beatmapImporter, api) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize) + => new BundledBeatmapDownloadRequest(set, minimiseDownloadSize); + + public class BundledBeatmapDownloadRequest : DownloadBeatmapSetRequest + { + protected override string Uri => $"https://assets.ppy.sh/client-resources/bundled/{Model.OnlineID}.osz"; + + public BundledBeatmapDownloadRequest(IBeatmapSetInfo beatmapSetInfo, bool minimiseDownloadSize) + : base(beatmapSetInfo, minimiseDownloadSize) + { + } + } + } + + private const string tutorial_filename = "1011011 nekodex - new beginnings.osz"; + + /// + /// Contest winners or other special cases. + /// + private static readonly string[] always_bundled_beatmaps = + { + // This thing is 40mb, I'm not sure we want it here... + @"1388906 Raphlesia & BilliumMoto - My Love.osz" + }; + + private static readonly string[] bundled_osu = + { + "682286 Yuyoyuppe - Emerald Galaxy.osz", + "682287 baker - For a Dead Girl+.osz", + "682289 Hige Driver - I Wanna Feel Your Love (feat. shully).osz", + "682290 Hige Driver - Miracle Sugite Yabai (feat. shully).osz", + "682416 Hige Driver - Palette.osz", + "682595 baker - Kimi ga Kimi ga -vocanico remix-.osz", + "716211 yuki. - Spring Signal.osz", + "716213 dark cat - BUBBLE TEA (feat. juu & cinders).osz", + "716215 LukHash - CLONED.osz", + "716219 IAHN - Snowdrop.osz", + "716249 *namirin - Senaka Awase no Kuukyo (with Kakichoco).osz", + "716390 sakuraburst - SHA.osz", + "716441 Fractal Dreamers - Paradigm Shift.osz", + "729808 Thaehan - Leprechaun.osz", + "751771 Cranky - Hanaarashi.osz", + "751772 Cranky - Ran.osz", + "751773 Cranky - Feline, the White....osz", + "751774 Function Phantom - Variable.osz", + "751779 Rin - Daishibyo set 14 ~ Sado no Futatsuiwa.osz", + "751782 Fractal Dreamers - Fata Morgana.osz", + "751785 Cranky - Chandelier - King.osz", + "751846 Fractal Dreamers - Celestial Horizon.osz", + "751866 Rin - Moriya set 08 ReEdit ~ Youkai no Yama.osz", + "751894 Fractal Dreamers - Blue Haven.osz", + "751896 Cranky - Rave 2 Rave.osz", + "751932 Cranky - La fuite des jours.osz", + "751972 Cranky - CHASER.osz", + "779173 Thaehan - Superpower.osz", + "780932 VINXIS - A Centralized View.osz", + "785572 S3RL - I'll See You Again (feat. Chi Chi).osz", + "785650 yuki. feat. setsunan - Hello! World.osz", + "785677 Dictate - Militant.osz", + "785731 S3RL - Catchit (Radio Edit).osz", + "785774 LukHash - GLITCH.osz", + "786498 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz", + "789374 Pulse - LP.osz", + "789528 James Portland - Sky.osz", + "789529 Lexurus - Gravity.osz", + "789544 Andromedik - Invasion.osz", + "789905 Gourski x Himmes - Silence.osz", + "791667 cYsmix - Babaroque (Short Ver.).osz", + "791798 cYsmix - Behind the Walls.osz", + "791845 cYsmix - Little Knight.osz", + "792241 cYsmix - Eden.osz", + "792396 cYsmix - The Ballad of a Mindless Girl.osz", + "795432 Phonetic - Journey.osz", + "831322 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz", + "847764 Cranky - Crocus.osz", + "847776 Culprate & Joe Ford - Gaucho.osz", + "847812 J. Pachelbel - Canon (Cranky Remix).osz", + "847900 Cranky - Time Alter.osz", + "847930 LukHash - 8BIT FAIRY TALE.osz", + "848003 Culprate - Aurora.osz", + "848068 nanobii - popsicle beach.osz", + "848090 Trial & Error - DAI*TAN SENSATION feat. Nanahira, Mii, Aitsuki Nakuru (Short Ver.).osz", + "848259 Culprate & Skorpion - Jester.osz", + "848976 Dictate - Treason.osz", + "851543 Culprate - Florn.osz", + "864748 Thaehan - Angry Birds Epic (Remix).osz", + "873667 OISHII - ONIGIRI FREEWAY.osz", + "876227 Culprate, Keota & Sophie Meiers - Mechanic Heartbeat.osz", + "880487 cYsmix - Peer Gynt.osz", + "883088 Wisp X - Somewhere I'd Rather Be.osz", + "891333 HyuN - White Aura.osz", + "891334 HyuN - Wild Card.osz", + "891337 HyuN feat. LyuU - Cross Over.osz", + "891338 HyuN & Ritoru - Apocalypse in Love.osz", + "891339 HyuN feat. Ato - Asu wa Ame ga Yamukara.osz", + "891345 HyuN - Infinity Heaven.osz", + "891348 HyuN - Guitian.osz", + "891356 HyuN - Legend of Genesis.osz", + "891366 HyuN - Illusion of Inflict.osz", + "891417 HyuN feat. Yu-A - My life is for you.osz", + "891441 HyuN - You'Re aRleAdY dEAd.osz", + "891632 HyuN feat. YURI - Disorder.osz", + "891712 HyuN - Tokyo's Starlight.osz", + "901091 *namirin - Ciel etoile.osz", + "916990 *namirin - Koishiteiku Planet.osz", + "929284 tieff - Sense of Nostalgia.osz", + "933940 Ben Briggs - Yes (Maybe).osz", + "934415 Ben Briggs - Fearless Living.osz", + "934627 Ben Briggs - New Game Plus.osz", + "934666 Ben Briggs - Wave Island.osz", + "936126 siromaru + cranky - conflict.osz", + "940377 onumi - ARROGANCE.osz", + "940597 tieff - Take Your Swimsuit.osz", + "941085 tieff - Our Story.osz", + "949297 tieff - Sunflower.osz", + "952380 Ben Briggs - Why Are We Yelling.osz", + "954272 *namirin - Kanzen Shouri*Esper Girl.osz", + "955866 KIRA & Heartbreaker - B.B.F (feat. Hatsune Miku & Kagamine Rin).osz", + "961320 Kuba Oms - All In All.osz", + "964553 The Flashbulb - You Take the World's Weight Away.osz", + "965651 Fractal Dreamers - Ad Astra.osz", + "966225 The Flashbulb - Passage D.osz", + "966324 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz", + "972810 James Landino & Kabuki - Birdsong.osz", + "972932 James Landino - Hide And Seek.osz", + "977276 The Flashbulb - Mellann.osz", + "981616 *namirin - Mizutamari Tobikoete (with Nanahira).osz", + "985788 Loki - Wizard's Tower.osz", + "996628 OISHII - ONIGIRI FREEWAY.osz", + "996898 HyuN - White Aura.osz", + "1003554 yuki. - Nadeshiko Sensation.osz", + "1014936 Thaehan - Bwa !.osz", + "1019827 UNDEAD CORPORATION - Sad Dream.osz", + "1020213 Creo - Idolize.osz", + "1021450 Thaehan - Chiptune & Baroque.osz", + }; + + private static readonly string[] bundled_taiko = + { + "707824 Fractal Dreamers - Fortuna Redux.osz", + "789553 Cranky - Ran.osz", + "827822 Function Phantom - Neuronecia.osz", + "847323 Nakanojojo - Bittersweet (feat. Kuishinboakachan a.k.a Kiato).osz", + "847433 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz", + "847576 dark cat - hot chocolate.osz", + "847957 Wisp X - Final Moments.osz", + "876282 VINXIS - Greetings.osz", + "876648 Thaehan - Angry Birds Epic (Remix).osz", + "877069 IAHN - Transform (Original Mix).osz", + "877496 Thaehan - Leprechaun.osz", + "877935 Thaehan - Overpowered.osz", + "878344 yuki. - Be Your Light.osz", + "918446 VINXIS - Facade.osz", + "918903 LukHash - Ghosts.osz", + "919251 *namirin - Hitokoto no Kyori.osz", + "919704 S3RL - I Will Pick You Up (feat. Tamika).osz", + "921535 SOOOO - Raven Haven.osz", + "927206 *namirin - Kanzen Shouri*Esper Girl.osz", + "927544 Camellia feat. Nanahira - Kansoku Eisei.osz", + "930806 Nakanojojo - Pararara (feat. Amekoya).osz", + "931741 Camellia - Quaoar.osz", + "935699 Rin - Mythic set ~ Heart-Stirring Urban Legends.osz", + "935732 Thaehan - Yuujou.osz", + "941145 Function Phantom - Euclid.osz", + "942334 Dictate - Cauldron.osz", + "946540 nanobii - astral blast.osz", + "948844 Rin - Kishinjou set 01 ~ Mist Lake.osz", + "949122 Wisp X - Petal.osz", + "951618 Rin - Kishinjou set 02 ~ Mermaid from the Uncharted Land.osz", + "957412 Rin - Lunatic set 16 ~ The Space Shrine Maiden Returns Home.osz", + "961335 Thaehan - Insert Coin.osz", + "965178 The Flashbulb - DIDJ PVC.osz", + "966087 The Flashbulb - Creep.osz", + "966277 The Flashbulb - Amen Iraq.osz", + "966407 LukHash - ROOM 12.osz", + "966451 The Flashbulb - Six Acid Strings.osz", + "972301 BilliumMoto - four veiled stars.osz", + "973173 nanobii - popsicle beach.osz", + "973954 BilliumMoto - Rocky Buinne (Short Ver.).osz", + "975435 BilliumMoto - life flashes before weeb eyes.osz", + "978759 L. V. Beethoven - Moonlight Sonata (Cranky Remix).osz", + "982559 BilliumMoto - HDHR.osz", + "984361 The Flashbulb - Ninedump.osz", + "1023681 Inferi - The Ruin of Mankind.osz", + "1034358 ALEPH - The Evil Spirit.osz", + "1037567 ALEPH - Scintillations.osz", + }; + + private static readonly string[] bundled_catch = + { + "554256 Helblinde - When Time Sleeps.osz", + "693123 yuki. - Nadeshiko Sensation.osz", + "767009 OISHII - PIZZA PLAZA.osz", + "767346 Thaehan - Bwa !.osz", + "815162 VINXIS - Greetings.osz", + "840964 cYsmix - Breeze.osz", + "932657 Wisp X - Eventide.osz", + "933700 onumi - CONFUSION PART ONE.osz", + "933984 onumi - PERSONALITY.osz", + "934785 onumi - FAKE.osz", + "936545 onumi - REGRET PART ONE.osz", + "943803 Fractal Dreamers - Everything for a Dream.osz", + "943876 S3RL - I Will Pick You Up (feat. Tamika).osz", + "946773 Trial & Error - DREAMING COLOR (Short Ver.).osz", + "955808 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI (Short Ver.).osz", + "957808 Fractal Dreamers - Module_410.osz", + "957842 antiPLUR - One Life Left to Live.osz", + "965730 The Flashbulb - Lawn Wake IV (Black).osz", + "966240 Creo - Challenger.osz", + "968232 Rin - Lunatic set 15 ~ The Moon as Seen from the Shrine.osz", + "972302 VINXIS - A Centralized View.osz", + "972887 HyuN - Illusion of Inflict.osz", + "1008600 LukHash - WHEN AN ANGEL DIES.osz", + "1032103 LukHash - H8 U.osz", + }; + + private static readonly string[] bundled_mania = + { + "943516 antiPLUR - Clockwork Spooks.osz", + "946394 VINXIS - Three Times The Original Charm.osz", + "966408 antiPLUR - One Life Left to Live.osz", + "971561 antiPLUR - Runengon.osz", + "983864 James Landino - Shiba Island.osz", + "989512 BilliumMoto - 1xMISS.osz", + "994104 James Landino - Reaction feat. Slyleaf.osz", + "1003217 nekodex - circles!.osz", + "1009907 James Landino & Kabuki - Birdsong.osz", + "1015169 Thaehan - Insert Coin.osz", + }; + } +} diff --git a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs index 54dcdc55e3..ad0ff876e8 100644 --- a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs @@ -28,11 +28,6 @@ namespace osu.Game.Beatmaps.Drawables }, downloadTracker = new BeatmapDownloadTracker(beatmapSet), }; - AddInternal(progressBar = new ProgressBar(false) - { - Height = 0, - Alpha = 0, - }); AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs index 54af6a5942..98633958ee 100644 --- a/osu.Game/Extensions/TimeDisplayExtensions.cs +++ b/osu.Game/Extensions/TimeDisplayExtensions.cs @@ -3,6 +3,7 @@ using System; using Humanizer; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; @@ -42,12 +43,12 @@ namespace osu.Game.Extensions public static LocalisableString ToFormattedDuration(this TimeSpan timeSpan) { if (timeSpan.TotalDays >= 1) - return new LocalisableFormattableString(timeSpan, @"dd\:hh\:mm\:ss"); + return timeSpan.ToLocalisableString(@"dd\:hh\:mm\:ss"); if (timeSpan.TotalHours >= 1) - return new LocalisableFormattableString(timeSpan, @"hh\:mm\:ss"); + return timeSpan.ToLocalisableString(@"hh\:mm\:ss"); - return new LocalisableFormattableString(timeSpan, @"mm\:ss"); + return timeSpan.ToLocalisableString(@"mm\:ss"); } /// diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index 017ea6ec32..817b8409e6 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -57,6 +58,26 @@ namespace osu.Game.Graphics.Containers { } + /// + /// Scrolls a into view. + /// + /// The to scroll into view. + /// Whether to animate the movement. + /// An added amount to scroll beyond the requirement to bring the target into view. + public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0) + { + float childPos0 = GetChildPosInContent(d); + float childPos1 = GetChildPosInContent(d, d.DrawSize); + + float minPos = Math.Min(childPos0, childPos1); + float maxPos = Math.Max(childPos0, childPos1); + + if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) + ScrollTo(minPos - extraScroll, animated); + else if (maxPos > Current + DisplayableContent) + ScrollTo(maxPos - DisplayableContent + extraScroll, animated); + } + protected override bool OnMouseDown(MouseDownEvent e) { if (shouldPerformRightMouseScroll(e)) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 540ca85809..6ad538959e 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -149,13 +149,11 @@ namespace osu.Game.Graphics.Containers { lastKnownScroll = null; - float fixedHeaderSize = FixedHeader?.BoundingBox.Height ?? 0; - // implementation similar to ScrollIntoView but a bit more nuanced. float top = scrollContainer.GetChildPosInContent(target); - float bottomScrollExtent = scrollContainer.ScrollableExtent - fixedHeaderSize; - float scrollTarget = top - fixedHeaderSize - scrollContainer.DisplayableContent * scroll_y_centre; + float bottomScrollExtent = scrollContainer.ScrollableExtent; + float scrollTarget = top - scrollContainer.DisplayableContent * scroll_y_centre; if (scrollTarget > bottomScrollExtent) scrollContainer.ScrollToEnd(); @@ -195,11 +193,8 @@ namespace osu.Game.Graphics.Containers protected void InvalidateScrollPosition() { - Schedule(() => - { - lastKnownScroll = null; - lastClickedSection = null; - }); + lastKnownScroll = null; + lastClickedSection = null; } protected override void UpdateAfterChildren() @@ -270,9 +265,13 @@ namespace osu.Game.Graphics.Containers { if (!Children.Any()) return; - var newMargin = originalSectionsMargin; + // if a fixed header is present, apply top padding for it + // to make the scroll container aware of its displayable area. + // (i.e. for page up/down to work properly) + scrollContainer.Padding = new MarginPadding { Top = FixedHeader?.LayoutSize.Y ?? 0 }; - newMargin.Top += (headerHeight ?? 0); + var newMargin = originalSectionsMargin; + newMargin.Top += (ExpandableHeader?.LayoutSize.Y ?? 0); newMargin.Bottom += (footerHeight ?? 0); scrollContentContainer.Margin = newMargin; diff --git a/osu.Game/Graphics/InputBlockingContainer.cs b/osu.Game/Graphics/InputBlockingContainer.cs new file mode 100644 index 0000000000..d8387b1401 --- /dev/null +++ b/osu.Game/Graphics/InputBlockingContainer.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; + +namespace osu.Game.Graphics +{ + /// + /// A simple container which blocks input events from travelling through it. + /// + public class InputBlockingContainer : Container + { + protected override bool OnHover(HoverEvent e) => true; + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 7c1e8d90a0..29a797bd78 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -108,7 +108,7 @@ namespace osu.Game.Graphics.UserInterface if (Enabled.Value) { Debug.Assert(backgroundColour != null); - Background.FlashColour(backgroundColour.Value, 200); + Background.FlashColour(backgroundColour.Value.Lighten(0.4f), 200); } return base.OnClick(e); diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs new file mode 100644 index 0000000000..c3c566782f --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -0,0 +1,197 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class ShearedButton : OsuClickableContainer + { + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + public float TextSize + { + get => text.Font.Size; + set => text.Font = OsuFont.TorusAlternate.With(size: value); + } + + public Colour4 DarkerColour + { + set + { + darkerColour = value; + Scheduler.AddOnce(updateState); + } + } + + public Colour4 LighterColour + { + set + { + lighterColour = value; + Scheduler.AddOnce(updateState); + } + } + + public Colour4 TextColour + { + set + { + textColour = value; + Scheduler.AddOnce(updateState); + } + } + + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } = null!; + + private readonly Box background; + private readonly OsuSpriteText text; + + private const float shear = 0.2f; + + private Colour4? darkerColour; + private Colour4? lighterColour; + private Colour4? textColour; + + private readonly Box flashLayer; + + /// + /// Creates a new + /// + /// + /// The width of the button. + /// + /// If a non- value is provided, this button will have a fixed width equal to the provided value. + /// If a value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text. + /// + /// + public ShearedButton(float? width = null) + { + Height = 50; + Padding = new MarginPadding { Horizontal = shear * 50 }; + + Content.CornerRadius = 7; + Content.Shear = new Vector2(shear, 0); + Content.Masking = true; + Content.BorderThickness = 2; + Content.Anchor = Content.Origin = Anchor.Centre; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.TorusAlternate.With(size: 17), + Shear = new Vector2(-shear, 0) + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White.Opacity(0.9f), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + }; + + if (width != null) + { + Width = width.Value; + } + else + { + AutoSizeAxes = Axes.X; + text.Margin = new MarginPadding { Horizontal = 15 }; + } + } + + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Enabled.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + + updateState(); + FinishTransforms(true); + } + + protected override bool OnClick(ClickEvent e) + { + if (Enabled.Value) + flashLayer.FadeOutFromOne(800, Easing.OutQuint); + + return base.OnClick(e); + } + + protected override bool OnHover(HoverEvent e) + { + Scheduler.AddOnce(updateState); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + Scheduler.AddOnce(updateState); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + Content.ScaleTo(0.8f, 2000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + Content.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + + private void updateState() + { + var colourDark = darkerColour ?? ColourProvider.Background3; + var colourLight = lighterColour ?? ColourProvider.Background1; + var colourText = textColour ?? ColourProvider.Content1; + + if (!Enabled.Value) + { + colourDark = colourDark.Darken(0.3f); + colourLight = colourLight.Darken(0.3f); + } + else if (IsHovered) + { + colourDark = colourDark.Lighten(0.2f); + colourLight = colourLight.Lighten(0.2f); + } + + background.FadeColour(colourDark, 150, Easing.OutQuint); + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(colourDark, colourLight), 150, Easing.OutQuint); + + if (!Enabled.Value) + colourText = colourText.Opacity(0.6f); + + text.FadeColour(colourText, 150, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/ShearedOverlayHeader.cs b/osu.Game/Graphics/UserInterface/ShearedOverlayHeader.cs index 9ed7bb35de..452a1dd394 100644 --- a/osu.Game/Graphics/UserInterface/ShearedOverlayHeader.cs +++ b/osu.Game/Graphics/UserInterface/ShearedOverlayHeader.cs @@ -66,7 +66,7 @@ namespace osu.Game.Graphics.UserInterface }, Children = new Drawable[] { - underlayContainer = new Container + underlayContainer = new InputBlockingContainer { RelativeSizeAxes = Axes.X, Height = HEIGHT, diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index aed3be20a0..4780270f66 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -8,39 +8,18 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; -using osuTK; namespace osu.Game.Graphics.UserInterface { - public class ShearedToggleButton : OsuClickableContainer + public class ShearedToggleButton : ShearedButton { - public BindableBool Active { get; } = new BindableBool(); - - public LocalisableString Text - { - get => text.Text; - set => text.Text = value; - } - - private readonly Box background; - private readonly OsuSpriteText text; - private Sample? sampleOff; private Sample? sampleOn; - private const float shear = 0.2f; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; + /// + /// Whether this button is currently toggled to an active state. + /// + public BindableBool Active { get; } = new BindableBool(); /// /// Creates a new @@ -53,40 +32,8 @@ namespace osu.Game.Graphics.UserInterface /// /// public ShearedToggleButton(float? width = null) + : base(width) { - Height = 50; - Padding = new MarginPadding { Horizontal = shear * 50 }; - - Content.CornerRadius = 7; - Content.Shear = new Vector2(shear, 0); - Content.Masking = true; - Content.BorderThickness = 2; - Content.Anchor = Content.Origin = Anchor.Centre; - - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - text = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.TorusAlternate.With(size: 17), - Shear = new Vector2(-shear, 0) - } - }; - - if (width != null) - { - Width = width.Value; - } - else - { - AutoSizeAxes = Axes.X; - text.Margin = new MarginPadding { Horizontal = 15 }; - } } [BackgroundDependencyLoader] @@ -100,70 +47,22 @@ namespace osu.Game.Graphics.UserInterface protected override void LoadComplete() { - base.LoadComplete(); - + Active.BindDisabledChanged(disabled => Action = disabled ? (Action?)null : Active.Toggle, true); Active.BindValueChanged(_ => { - updateState(); + updateActiveState(); playSample(); }); - Active.BindDisabledChanged(disabled => - { - updateState(); - Action = disabled ? (Action?)null : Active.Toggle; - }, true); - FinishTransforms(true); + updateActiveState(); + base.LoadComplete(); } - protected override bool OnHover(HoverEvent e) + private void updateActiveState() { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - Content.ScaleTo(0.8f, 2000, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - Content.ScaleTo(1, 1000, Easing.OutElastic); - base.OnMouseUp(e); - } - - private void updateState() - { - var darkerColour = Active.Value ? colourProvider.Highlight1 : colourProvider.Background3; - var lighterColour = Active.Value ? colourProvider.Colour0 : colourProvider.Background1; - - if (Active.Disabled) - { - darkerColour = darkerColour.Darken(0.3f); - lighterColour = lighterColour.Darken(0.3f); - } - else if (IsHovered) - { - darkerColour = darkerColour.Lighten(0.3f); - lighterColour = lighterColour.Lighten(0.3f); - } - - background.FadeColour(darkerColour, 150, Easing.OutQuint); - Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(darkerColour, lighterColour), 150, Easing.OutQuint); - - var textColour = Active.Value ? colourProvider.Background6 : colourProvider.Content1; - if (Active.Disabled) - textColour = textColour.Opacity(0.6f); - - text.FadeColour(textColour, 150, Easing.OutQuint); + DarkerColour = Active.Value ? ColourProvider.Highlight1 : ColourProvider.Background3; + LighterColour = Active.Value ? ColourProvider.Colour0 : ColourProvider.Background1; + TextColour = Active.Value ? ColourProvider.Background6 : ColourProvider.Content1; } private void playSample() diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 23ebc6e98d..f535a32b39 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -8,6 +8,7 @@ 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 { @@ -28,7 +29,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 [BackgroundDependencyLoader(true)] private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours) { - BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3; + if (BackgroundColour == Color4.White) + BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3; } protected override void LoadComplete() diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs index c6477d1781..f483e67b27 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs @@ -4,6 +4,8 @@ #nullable enable using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -31,6 +33,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 private Color4 enabledColour; private Color4 disabledColour; + private Sample? sampleChecked; + private Sample? sampleUnchecked; + public SwitchButton() { Size = new Vector2(45, 20); @@ -70,13 +75,16 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider? colourProvider, OsuColour colours) + private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark; disabledColour = colourProvider?.Background3 ?? colours.Gray3; switchContainer.Colour = enabledColour; fill.Colour = disabledColour; + + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); } protected override void LoadComplete() @@ -107,6 +115,16 @@ namespace osu.Game.Graphics.UserInterfaceV2 base.OnHoverLost(e); } + protected override void OnUserChange(bool value) + { + base.OnUserChange(value); + + if (value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + } + private void updateBorder() { circularContainer.TransformBorderTo((Current.Value ? enabledColour : disabledColour).Lighten(IsHovered ? 0.3f : 0)); diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 6da2e4d272..52e9811cf7 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -14,6 +14,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Back => new TranslatableString(getKey(@"back"), @"Back"); + /// + /// "Next" + /// + public static LocalisableString Next => new TranslatableString(getKey(@"next"), @"Next"); + /// /// "Finish" /// diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs new file mode 100644 index 0000000000..3a7fe4bb12 --- /dev/null +++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class FirstRunSetupBeatmapScreenStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.FirstRunSetupBeatmapScreen"; + + /// + /// "Obtaining Beatmaps" + /// + public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps"); + + /// + /// ""Beatmaps" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection." + /// + public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."); + + /// + /// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay." + /// + public static LocalisableString TutorialDescription => new TranslatableString(getKey(@"tutorial_description"), @"If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay."); + + /// + /// "Get the osu! tutorial" + /// + public static LocalisableString TutorialButton => new TranslatableString(getKey(@"tutorial_button"), @"Get the osu! tutorial"); + + /// + /// "To get you started, we have some recommended beatmaps." + /// + public static LocalisableString BundledDescription => new TranslatableString(getKey(@"bundled_description"), @"To get you started, we have some recommended beatmaps."); + + /// + /// "Get recommended beatmaps" + /// + public static LocalisableString BundledButton => new TranslatableString(getKey(@"bundled_button"), @"Get recommended beatmaps"); + + /// + /// "You can also obtain more beatmaps from the main menu "browse" button at any time." + /// + public static LocalisableString ObtainMoreBeatmaps => new TranslatableString(getKey(@"obtain_more_beatmaps"), @"You can also obtain more beatmaps from the main menu ""browse"" button at any time."); + + /// + /// "You currently have {0} beatmap(s) loaded!" + /// + public static LocalisableString CurrentlyLoadedBeatmaps(int beatmaps) => new TranslatableString(getKey(@"currently_loaded_beatmaps"), @"You currently have {0} beatmap(s) loaded!", beatmaps); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs index 001de93c16..91b427e2ca 100644 --- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs @@ -17,7 +17,8 @@ namespace osu.Game.Localisation /// /// "Click to resume first-run setup at any point" /// - public static LocalisableString ClickToResumeFirstRunSetupAtAnyPoint => new TranslatableString(getKey(@"click_to_resume_first_run_setup_at_any_point"), @"Click to resume first-run setup at any point"); + public static LocalisableString ClickToResumeFirstRunSetupAtAnyPoint => + new TranslatableString(getKey(@"click_to_resume_first_run_setup_at_any_point"), @"Click to resume first-run setup at any point"); /// /// "First-run setup" @@ -49,9 +50,29 @@ osu! is a very configurable game, and diving straight into the settings can some public static LocalisableString UIScaleDescription => new TranslatableString(getKey(@"ui_scale_description"), @"The size of the osu! user interface can be adjusted to your liking."); /// - /// "Next ({0})" + /// "Behaviour" /// - public static LocalisableString Next(LocalisableString nextStepDescription) => new TranslatableString(getKey(@"next"), @"Next ({0})", nextStepDescription); + public static LocalisableString Behaviour => new TranslatableString(getKey(@"behaviour"), @"Behaviour"); + + /// + /// "Some new defaults for game behaviours have been implemented, with the aim of improving the game experience and making it more accessible to everyone. + /// + /// We recommend you give the new defaults a try, but if you'd like to have things feel more like classic versions of osu!, you can easily apply some sane defaults below." + /// + public static LocalisableString BehaviourDescription => new TranslatableString(getKey(@"behaviour_description"), + @"Some new defaults for game behaviours have been implemented, with the aim of improving the game experience and making it more accessible to everyone. + +We recommend you give the new defaults a try, but if you'd like to have things feel more like classic versions of osu!, you can easily apply some sane defaults below."); + + /// + /// "New defaults" + /// + public static LocalisableString NewDefaults => new TranslatableString(getKey(@"new_defaults"), @"New defaults"); + + /// + /// "Classic defaults" + /// + public static LocalisableString ClassicDefaults => new TranslatableString(getKey(@"classic_defaults"), @"Classic defaults"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/JoystickSettingsStrings.cs b/osu.Game/Localisation/JoystickSettingsStrings.cs new file mode 100644 index 0000000000..410cd0a6f5 --- /dev/null +++ b/osu.Game/Localisation/JoystickSettingsStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class JoystickSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.JoystickSettings"; + + /// + /// "Joystick / Gamepad" + /// + public static LocalisableString JoystickGamepad => new TranslatableString(getKey(@"joystick_gamepad"), @"Joystick / Gamepad"); + + /// + /// "Deadzone Threshold" + /// + public static LocalisableString DeadzoneThreshold => new TranslatableString(getKey(@"deadzone_threshold"), @"Deadzone"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs index e4fda9d9c3..1f9a63e3b9 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs @@ -72,7 +72,8 @@ namespace osu.Game.Overlays.BeatmapListing Size = new Vector2(12), Icon = getIconForCardSize(Value) } - } + }, + new HoverClickSounds(HoverSampleSet.TabSelect) }; } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index ec795cf6b2..9ee002fd9d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; - ppColumn.Text = value.PP?.ToLocalisableString(@"N0"); + ppColumn.Text = value.PP?.ToLocalisableString(@"N0") ?? default(LocalisableString); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); modsColumn.Mods = value.Mods; diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs index eb4b97069c..1f18d181cb 100644 --- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs +++ b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osuTK; namespace osu.Game.Overlays.FirstRunSetup @@ -15,19 +19,37 @@ namespace osu.Game.Overlays.FirstRunSetup protected FillFlowContainer Content { get; private set; } - protected FirstRunSetupScreen() + [Resolved] + protected OverlayColourProvider OverlayColourProvider { get; private set; } + + [BackgroundDependencyLoader] + private void load() { + const float header_size = 40; + const float spacing = 20; + InternalChildren = new Drawable[] { new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, - Child = Content = new FillFlowContainer + ScrollbarOverlapsContent = false, + Children = new Drawable[] { - Spacing = new Vector2(20), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, + new OsuSpriteText + { + Text = this.GetLocalisableDescription(), + Font = OsuFont.Default.With(size: header_size), + Colour = OverlayColourProvider.Light1, + }, + Content = new FillFlowContainer + { + Y = header_size + spacing, + Spacing = new Vector2(spacing), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + } }, } }; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs new file mode 100644 index 0000000000..190a0badab --- /dev/null +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -0,0 +1,258 @@ +// Copyright (c) ppy Pty Ltd . 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; +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.Online; +using osuTK; +using Realms; + +namespace osu.Game.Overlays.FirstRunSetup +{ + [LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))] + public class ScreenBeatmaps : FirstRunSetupScreen + { + private ProgressRoundedButton downloadBundledButton = null!; + private ProgressRoundedButton importBeatmapsButton = null!; + private ProgressRoundedButton downloadTutorialButton = null!; + + private OsuTextFlowContainer currentlyLoadedBeatmaps = null!; + + private BundledBeatmapDownloader? tutorialDownloader; + private BundledBeatmapDownloader? bundledDownloader; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + private IDisposable? beatmapSubscription; + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(LegacyImportManager? legacyImportManager) + { + Vector2 buttonSize = new Vector2(500, 60); + + Content.Children = new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + Colour = OverlayColourProvider.Content1, + Text = FirstRunSetupBeatmapScreenStrings.Description, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 30, + Children = new Drawable[] + { + currentlyLoadedBeatmaps = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24, weight: FontWeight.SemiBold)) + { + Colour = OverlayColourProvider.Content2, + TextAnchor = Anchor.Centre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + }, + } + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + Colour = OverlayColourProvider.Content1, + Text = FirstRunSetupBeatmapScreenStrings.TutorialDescription, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + downloadTutorialButton = new ProgressRoundedButton + { + Size = buttonSize, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + BackgroundColour = colours.Pink3, + Text = FirstRunSetupBeatmapScreenStrings.TutorialButton, + Action = downloadTutorial + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + Colour = OverlayColourProvider.Content1, + Text = FirstRunSetupBeatmapScreenStrings.BundledDescription, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + downloadBundledButton = new ProgressRoundedButton + { + Size = buttonSize, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + BackgroundColour = colours.Blue3, + Text = FirstRunSetupBeatmapScreenStrings.BundledButton, + Action = downloadBundled + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + Colour = OverlayColourProvider.Content1, + Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + importBeatmapsButton = new ProgressRoundedButton + { + Size = buttonSize, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + BackgroundColour = colours.Blue3, + Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable, + Action = () => + { + importBeatmapsButton.Enabled.Value = false; + legacyImportManager?.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() => + { + if (t.IsCompletedSuccessfully) + importBeatmapsButton.Complete(); + else + importBeatmapsButton.Enabled.Value = true; + })); + } + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + Colour = OverlayColourProvider.Content1, + Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending && !s.Protected), beatmapsChanged); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmapSubscription?.Dispose(); + } + + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + { + currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count); + + if (sender.Count == 0) + { + currentlyLoadedBeatmaps.FadeColour(colours.Red1, 500, Easing.OutQuint); + } + else if (changes != null && (changes.DeletedIndices.Any() || changes.InsertedIndices.Any())) + { + currentlyLoadedBeatmaps.FadeColour(colours.Yellow) + .FadeColour(OverlayColourProvider.Content2, 1500, Easing.OutQuint); + + currentlyLoadedBeatmaps.ScaleTo(1.1f) + .ScaleTo(1, 1500, Easing.OutQuint); + } + } + + private void downloadTutorial() + { + if (tutorialDownloader != null) + return; + + tutorialDownloader = new BundledBeatmapDownloader(true); + + AddInternal(tutorialDownloader); + + var downloadTracker = tutorialDownloader.DownloadTrackers.First(); + + downloadTracker.Progress.BindValueChanged(progress => + { + downloadTutorialButton.SetProgress(progress.NewValue, false); + + if (progress.NewValue == 1) + downloadTutorialButton.Complete(); + }, true); + } + + private void downloadBundled() + { + if (bundledDownloader != null) + return; + + bundledDownloader = new BundledBeatmapDownloader(false); + + AddInternal(bundledDownloader); + + foreach (var tracker in bundledDownloader.DownloadTrackers) + tracker.State.BindValueChanged(_ => updateProgress(), true); + + void updateProgress() + { + double progress = (double)bundledDownloader.DownloadTrackers.Count(t => t.State.Value == DownloadState.LocallyAvailable) / bundledDownloader.DownloadTrackers.Count(); + + if (progress == 1) + downloadBundledButton.Complete(); + else + downloadBundledButton.SetProgress(progress, true); + } + } + + private class ProgressRoundedButton : RoundedButton + { + [Resolved] + private OsuColour colours { get; set; } = null!; + + private ProgressBar progressBar = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Add(progressBar = new ProgressBar(false) + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + FillColour = BackgroundColour, + Alpha = 0.5f, + Depth = float.MinValue + }); + } + + public void Complete() + { + Enabled.Value = false; + + Background.FadeColour(colours.Green, 500, Easing.OutQuint); + progressBar.FillColour = colours.Green; + + this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint); + } + + public void SetProgress(double progress, bool animated) + { + if (!Enabled.Value) + return; + + this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs new file mode 100644 index 0000000000..dc3d40ad95 --- /dev/null +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +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.Localisation; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; + +namespace osu.Game.Overlays.FirstRunSetup +{ + [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] + public class ScreenBehaviour : FirstRunSetupScreen + { + private SearchContainer searchContainer; + + [BackgroundDependencyLoader] + private void load() + { + Content.Children = new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24)) + { + Text = FirstRunSetupOverlayStrings.BehaviourDescription, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + new TriangleButton + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Text = FirstRunSetupOverlayStrings.NewDefaults, + RelativeSizeAxes = Axes.X, + Action = applyStandard, + }, + Empty(), + new DangerousTriangleButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = FirstRunSetupOverlayStrings.ClassicDefaults, + RelativeSizeAxes = Axes.X, + Action = applyClassic + } + }, + }, + }, + searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new SettingsSection[] + { + // This list should be kept in sync with SettingsOverlay. + new GeneralSection(), + new SkinSection(), + // InputSection is intentionally omitted for now due to its sub-panel being a pain to set up. + new UserInterfaceSection(), + new GameplaySection(), + new RulesetSection(), + new AudioSection(), + new GraphicsSection(), + new OnlineSection(), + new MaintenanceSection(), + new DebugSection(), + }, + SearchTerm = SettingsItem.CLASSIC_DEFAULT_SEARCH_TERM, + } + }; + } + + private void applyClassic() + { + foreach (var i in searchContainer.ChildrenOfType().Where(s => s.HasClassicDefault)) + i.ApplyClassicDefault(); + } + + private void applyStandard() + { + foreach (var i in searchContainer.ChildrenOfType().Where(s => s.HasClassicDefault)) + i.ApplyDefault(); + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index d9a612ea26..1bd82f6d99 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -27,6 +27,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { + [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] public class ScreenUIScale : FirstRunSetupScreen { [BackgroundDependencyLoader] @@ -62,7 +63,7 @@ namespace osu.Game.Overlays.FirstRunSetup new Drawable[] { new SampleScreenContainer(new PinnedMainMenu()), - new SampleScreenContainer(new PlaySongSelect()), + new SampleScreenContainer(new NestedSongSelect()), }, // TODO: add more screens here in the future (gameplay / results) // requires a bit more consideration to isolate their behaviour from the "parent" game. @@ -95,6 +96,11 @@ namespace osu.Game.Overlays.FirstRunSetup } } + private class NestedSongSelect : PlaySongSelect + { + protected override bool ControlGlobalMusic => false; + } + private class PinnedMainMenu : MainMenu { public override void OnEntering(ScreenTransitionEvent e) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 39da180f40..10e15a7555 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -1,16 +1,20 @@ // Copyright (c) ppy Pty Ltd . 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.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Localisation; namespace osu.Game.Overlays.FirstRunSetup { + [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))] public class ScreenWelcome : FirstRunSetupScreen { - public ScreenWelcome() + [BackgroundDependencyLoader] + private void load() { Content.Children = new Drawable[] { diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index a12fec4507..4277f0f2ba 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -7,10 +7,9 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -18,25 +17,22 @@ using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays.FirstRunSetup; +using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays { [Cached] - public class FirstRunSetupOverlay : OsuFocusedOverlayContainer + public class FirstRunSetupOverlay : ShearedOverlayContainer { - protected override bool StartHidden => true; + protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Purple; [Resolved] private IPerformFromScreenRunner performer { get; set; } = null!; @@ -52,173 +48,110 @@ namespace osu.Game.Overlays public PurpleTriangleButton NextButton = null!; public DangerousTriangleButton BackButton = null!; - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly Bindable showFirstRunSetup = new Bindable(); private int? currentStepIndex; - private const float scale_when_hidden = 0.9f; - /// /// The currently displayed screen, if any. /// public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen; - private readonly FirstRunStep[] steps = + private readonly Type[] steps = { - new FirstRunStep(typeof(ScreenWelcome), FirstRunSetupOverlayStrings.WelcomeTitle), - new FirstRunStep(typeof(ScreenUIScale), GraphicsSettingsStrings.UIScaling), + typeof(ScreenWelcome), + typeof(ScreenBeatmaps), + typeof(ScreenUIScale), + typeof(ScreenBehaviour), }; private Container stackContainer = null!; private Bindable? overlayActivationMode; - public FirstRunSetupOverlay() - { - RelativeSizeAxes = Axes.Both; - } + private Container content = null!; [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; + Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(0.95f); - - EdgeEffect = new EdgeEffectParameters + MainAreaContent.AddRange(new Drawable[] { - Type = EdgeEffectType.Shadow, - Radius = 5, - Colour = Color4.Black.Opacity(0.2f), - }; - - Masking = true; - CornerRadius = 10; - - Children = new Drawable[] - { - new Box + content = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Padding = new MarginPadding { Horizontal = 70 * 1.2f }, + Child = new InputBlockingContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + Masking = true, + CornerRadius = 14, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - Colour = colourProvider.Background5, - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding(10), - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = FirstRunSetupOverlayStrings.FirstRunSetupTitle, - Font = OsuFont.Default.With(size: 32), - Colour = colourProvider.Content1, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new OsuTextFlowContainer - { - Text = FirstRunSetupOverlayStrings.FirstRunSetupDescription, - Colour = colourProvider.Content2, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - }, - } - }, - } - }, - }, - new Drawable[] - { - stackContainer = new Container + new Box { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(20), + Colour = ColourProvider.Background6, }, - }, - new Drawable[] - { - new Container + stackContainer = new Container { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(20) + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - Top = 0 // provided by the stack container above. - }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new[] - { - BackButton = new DangerousTriangleButton - { - Width = 200, - Text = CommonStrings.Back, - Action = showPreviousStep, - Enabled = { Value = false }, - }, - Empty(), - NextButton = new PurpleTriangleButton - { - RelativeSizeAxes = Axes.X, - Width = 1, - Text = FirstRunSetupOverlayStrings.GetStarted, - Action = showNextStep - } - }, - } + Vertical = 20, + Horizontal = 70, }, } - } - } + }, + }, }, - }; + }); + + FooterContent.Add(new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.98f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + BackButton = new DangerousTriangleButton + { + Width = 300, + Text = CommonStrings.Back, + Action = showPreviousStep, + Enabled = { Value = false }, + }, + Empty(), + NextButton = new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Width = 1, + Text = FirstRunSetupOverlayStrings.GetStarted, + Action = showNextStep + } + }, + } + }); } protected override void LoadComplete() @@ -227,7 +160,8 @@ namespace osu.Game.Overlays config.BindWith(OsuSetting.ShowFirstRunSetup, showFirstRunSetup); - if (showFirstRunSetup.Value) Show(); + // TODO: uncomment when happy with the whole flow. + // if (showFirstRunSetup.Value) Show(); } public override bool OnPressed(KeyBindingPressEvent e) @@ -280,10 +214,8 @@ namespace osu.Game.Overlays { base.PopIn(); - this.ScaleTo(scale_when_hidden) - .ScaleTo(1, 400, Easing.OutElasticHalf); - - this.FadeIn(400, Easing.OutQuint); + content.ScaleTo(0.99f) + .ScaleTo(1, 400, Easing.OutQuint); if (currentStepIndex == null) showFirstStep(); @@ -291,6 +223,10 @@ namespace osu.Game.Overlays protected override void PopOut() { + base.PopOut(); + + content.ScaleTo(0.99f, 400, Easing.OutQuint); + if (overlayActivationMode != null) { // If this is non-null we are guaranteed to have come from the main menu. @@ -316,11 +252,6 @@ namespace osu.Game.Overlays stack?.FadeOut(100) .Expire(); } - - base.PopOut(); - - this.ScaleTo(0.96f, 400, Easing.OutQuint); - this.FadeOut(200, Easing.OutQuint); } private void showFirstStep() @@ -358,11 +289,12 @@ namespace osu.Game.Overlays if (currentStepIndex < steps.Length) { - stack.Push((Screen)Activator.CreateInstance(steps[currentStepIndex.Value].ScreenType)); + stack.Push((Screen)Activator.CreateInstance(steps[currentStepIndex.Value])); } else { - showFirstRunSetup.Value = false; + // TODO: uncomment when happy with the whole flow. + // showFirstRunSetup.Value = false; currentStepIndex = null; Hide(); } @@ -375,23 +307,24 @@ namespace osu.Game.Overlays BackButton.Enabled.Value = currentStepIndex > 0; NextButton.Enabled.Value = currentStepIndex != null; - if (currentStepIndex != null) + if (currentStepIndex == null) + return; + + bool isFirstStep = currentStepIndex == 0; + bool isLastStep = currentStepIndex == steps.Length - 1; + + if (isFirstStep) { - NextButton.Text = currentStepIndex + 1 < steps.Length - ? FirstRunSetupOverlayStrings.Next(steps[currentStepIndex.Value + 1].Description) - : CommonStrings.Finish; + BackButton.Text = CommonStrings.Back; + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; } - } - - private class FirstRunStep - { - public readonly Type ScreenType; - public readonly LocalisableString Description; - - public FirstRunStep(Type screenType, LocalisableString description) + else { - ScreenType = screenType; - Description = description; + BackButton.Text = LocalisableString.Interpolate($@"{CommonStrings.Back} ({steps[currentStepIndex.Value - 1].GetLocalisableDescription()})"); + + NextButton.Text = isLastStep + ? CommonStrings.Finish + : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStepIndex.Value + 1].GetLocalisableDescription()})"); } } } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index c31416e078..502f0cd22e 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -51,14 +52,14 @@ namespace osu.Game.Overlays.Login { username = new OsuTextBox { - PlaceholderText = UsersStrings.LoginUsername, + PlaceholderText = UsersStrings.LoginUsername.ToLower(), RelativeSizeAxes = Axes.X, Text = api?.ProvidedUsername ?? string.Empty, TabbableContentContainer = this }, password = new OsuPasswordTextBox { - PlaceholderText = UsersStrings.LoginPassword, + PlaceholderText = UsersStrings.LoginPassword.ToLower(), RelativeSizeAxes = Axes.X, TabbableContentContainer = this, }, diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 66fd6a202d..1d848fe456 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Mods Height = HEIGHT; AutoSizeAxes = Axes.X; - InternalChild = new Container + InternalChild = new InputBlockingContainer { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index 38781455fa..aeb983d352 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . 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.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Cursor; -using osu.Framework.Input.Events; using osu.Game.Rulesets.Mods; using osu.Game.Utils; @@ -42,41 +39,13 @@ namespace osu.Game.Overlays.Mods && !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(Mod)); } + protected override Colour4 BackgroundColour => incompatible.Value ? (Colour4)ColourProvider.Background6 : base.BackgroundColour; + protected override Colour4 ForegroundColour => incompatible.Value ? (Colour4)ColourProvider.Background5 : base.ForegroundColour; + protected override void UpdateState() { - Action = incompatible.Value ? () => { } : (Action)Active.Toggle; - - if (incompatible.Value) - { - Colour4 backgroundColour = ColourProvider.Background6; - Colour4 textBackgroundColour = ColourProvider.Background5; - - Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, textBackgroundColour), TRANSITION_DURATION, Easing.OutQuint); - Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); - - SwitchContainer.ResizeWidthTo(IDLE_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); - SwitchContainer.FadeColour(Colour4.Gray, TRANSITION_DURATION, Easing.OutQuint); - MainContentContainer.TransformTo(nameof(Padding), new MarginPadding - { - Left = IDLE_SWITCH_WIDTH, - Right = CORNER_RADIUS - }, TRANSITION_DURATION, Easing.OutQuint); - - TextBackground.FadeColour(textBackgroundColour, TRANSITION_DURATION, Easing.OutQuint); - TextFlow.FadeColour(Colour4.White.Opacity(0.5f), TRANSITION_DURATION, Easing.OutQuint); - return; - } - - SwitchContainer.FadeColour(Colour4.White, TRANSITION_DURATION, Easing.OutQuint); base.UpdateState(); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (incompatible.Value) - return true; // bypasses base call purposely in order to not play out the intermediate state animation. - - return base.OnMouseDown(e); + SwitchContainer.FadeColour(incompatible.Value ? Colour4.Gray : Colour4.White, TRANSITION_DURATION, Easing.OutQuint); } #region IHasCustomTooltip diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 1157c0c0c6..018922c074 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -53,6 +53,9 @@ namespace osu.Game.Overlays.Mods } public Bindable> SelectedMods = new Bindable>(Array.Empty()); + public Bindable Active = new BindableBool(true); + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod); @@ -441,7 +444,7 @@ namespace osu.Game.Overlays.Mods protected override bool OnKeyDown(KeyDownEvent e) { - if (e.ControlPressed || e.AltPressed) return false; + if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; if (toggleKeys == null) return false; int index = Array.IndexOf(toggleKeys, e.Key); diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 7ae325bde7..f2a97da3b2 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -203,20 +203,24 @@ namespace osu.Game.Overlays.Mods base.OnMouseUp(e); } + protected virtual Colour4 BackgroundColour => Active.Value ? activeColour.Darken(0.3f) : (Colour4)ColourProvider.Background3; + protected virtual Colour4 ForegroundColour => Active.Value ? activeColour : (Colour4)ColourProvider.Background2; + protected virtual Colour4 TextColour => Active.Value ? (Colour4)ColourProvider.Background6 : Colour4.White; + protected virtual void UpdateState() { float targetWidth = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH; double transitionDuration = TRANSITION_DURATION; - Colour4 textBackgroundColour = Active.Value ? activeColour : (Colour4)ColourProvider.Background2; - Colour4 mainBackgroundColour = Active.Value ? activeColour.Darken(0.3f) : (Colour4)ColourProvider.Background3; - Colour4 textColour = Active.Value ? (Colour4)ColourProvider.Background6 : Colour4.White; + Colour4 backgroundColour = BackgroundColour; + Colour4 foregroundColour = ForegroundColour; + Colour4 textColour = TextColour; // Hover affects colour of button background if (IsHovered) { - textBackgroundColour = textBackgroundColour.Lighten(0.1f); - mainBackgroundColour = mainBackgroundColour.Lighten(0.1f); + backgroundColour = backgroundColour.Lighten(0.1f); + foregroundColour = foregroundColour.Lighten(0.1f); } // Mouse down adds a halfway tween of the movement @@ -226,15 +230,15 @@ namespace osu.Game.Overlays.Mods transitionDuration *= 4; } - Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(mainBackgroundColour, textBackgroundColour), transitionDuration, Easing.OutQuint); - Background.FadeColour(mainBackgroundColour, transitionDuration, Easing.OutQuint); + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, foregroundColour), transitionDuration, Easing.OutQuint); + Background.FadeColour(backgroundColour, transitionDuration, Easing.OutQuint); SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint); MainContentContainer.TransformTo(nameof(Padding), new MarginPadding { Left = targetWidth, Right = CORNER_RADIUS }, transitionDuration, Easing.OutQuint); - TextBackground.FadeColour(textBackgroundColour, transitionDuration, Easing.OutQuint); + TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 693c85fafc..ffd6e9a52c 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -13,7 +13,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Layout; +using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; @@ -59,7 +61,8 @@ namespace osu.Game.Overlays.Mods private DifficultyMultiplierDisplay? multiplierDisplay; private ModSettingsArea modSettingsArea = null!; - private FillFlowContainer columnFlow = null!; + private ColumnScrollContainer columnScroll = null!; + private ColumnFlowContainer columnFlow = null!; [BackgroundDependencyLoader] private void load() @@ -95,27 +98,27 @@ namespace osu.Game.Overlays.Mods RelativePositionAxes = Axes.Both, Children = new Drawable[] { - new OsuScrollContainer(Direction.Horizontal) + columnScroll = new ColumnScrollContainer { RelativeSizeAxes = Axes.Both, Masking = false, ClampExtension = 100, ScrollbarOverlapsContent = false, - Child = columnFlow = new ModColumnContainer + Child = columnFlow = new ColumnFlowContainer { Direction = FillDirection.Horizontal, Shear = new Vector2(SHEAR, 0), RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Spacing = new Vector2(10, 0), - Margin = new MarginPadding { Right = 70 }, + Margin = new MarginPadding { Horizontal = 70 }, Children = new[] { - CreateModColumn(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }), - CreateModColumn(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }), - CreateModColumn(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }), - CreateModColumn(ModType.Conversion), - CreateModColumn(ModType.Fun) + 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) } } } @@ -153,6 +156,14 @@ namespace osu.Game.Overlays.Mods } } + private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) + => new ColumnDimContainer(CreateModColumn(modType, toggleKeys)) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -166,9 +177,9 @@ namespace osu.Game.Overlays.Mods updateSelectionFromBindable(); }, true); - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) { - column.SelectedMods.BindValueChanged(_ => updateBindableFromSelection()); + column.SelectedMods.BindValueChanged(updateBindableFromSelection); } customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); @@ -191,7 +202,7 @@ namespace osu.Game.Overlays.Mods private void updateAvailableMods() { - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) column.Filter = isValidMod; } @@ -237,33 +248,36 @@ namespace osu.Game.Overlays.Mods TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic); } - private bool selectionBindableSyncInProgress; - private void updateSelectionFromBindable() { - if (selectionBindableSyncInProgress) - return; - - selectionBindableSyncInProgress = true; - - foreach (var column in columnFlow) + // note that selectionBindableSyncInProgress is purposefully not checked here. + // this is because in the case of mod selection in solo gameplay, a user selection of a mod can actually lead to deselection of other incompatible mods. + // to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods, + // and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods. + // selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call. + foreach (var column in columnFlow.Columns) column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray(); - - selectionBindableSyncInProgress = false; } - private void updateBindableFromSelection() + private bool selectionBindableSyncInProgress; + + private void updateBindableFromSelection(ValueChangedEvent> modSelectionChange) { if (selectionBindableSyncInProgress) return; selectionBindableSyncInProgress = true; - SelectedMods.Value = columnFlow.SelectMany(column => column.SelectedMods.Value).ToArray(); + SelectedMods.Value = ComputeNewModsFromSelection( + modSelectionChange.NewValue.Except(modSelectionChange.OldValue), + modSelectionChange.OldValue.Except(modSelectionChange.NewValue)); selectionBindableSyncInProgress = false; } + protected virtual IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) + => columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray(); + protected override void PopIn() { const double fade_in_duration = 400; @@ -277,7 +291,8 @@ namespace osu.Game.Overlays.Mods for (int i = 0; i < columnFlow.Count; i++) { - columnFlow[i].TopLevelContent + columnFlow[i].Column + .TopLevelContent .Delay(i * 30) .MoveToY(0, fade_in_duration, Easing.OutQuint) .FadeIn(fade_in_duration, Easing.OutQuint); @@ -298,27 +313,68 @@ namespace osu.Game.Overlays.Mods { const float distance = 700; - columnFlow[i].TopLevelContent + columnFlow[i].Column + .TopLevelContent .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) .FadeOut(fade_out_duration, Easing.OutQuint); } } - private class ModColumnContainer : FillFlowContainer + internal class ColumnScrollContainer : OsuScrollContainer { + 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; + } + } + } + + internal class ColumnFlowContainer : FillFlowContainer + { + public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column); + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); - public ModColumnContainer() + public ColumnFlowContainer() { AddLayout(drawSizeLayout); } - public override void Add(ModColumn column) + public override void Add(ColumnDimContainer dimContainer) { - base.Add(column); + base.Add(dimContainer); - Debug.Assert(column != null); - column.Shear = Vector2.Zero; + Debug.Assert(dimContainer != null); + dimContainer.Column.Shear = Vector2.Zero; } protected override void Update() @@ -338,6 +394,63 @@ namespace osu.Game.Overlays.Mods } } + internal class ColumnDimContainer : Container + { + public ModColumn Column { get; } + + public readonly Bindable Active = new BindableBool(); + public Action? 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(_ => updateDim(), true); + FinishTransforms(); + } + + private void updateDim() + { + Colour4 targetColour; + + if (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); + updateDim(); + return Active.Value; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateDim(); + } + } + private class ClickToReturnContainer : Container { public BindableBool HandleMouse { get; } = new BindableBool(); diff --git a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs index aba47d5423..d27f97f3d2 100644 --- a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs +++ b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs @@ -14,13 +14,13 @@ namespace osu.Game.Overlays.Mods /// public class NestedVerticalScrollContainer : OsuScrollContainer { - private OsuScrollContainer? parentScrollContainer; + private ModSelectScreen.ColumnScrollContainer? parentScrollContainer; protected override void LoadComplete() { base.LoadComplete(); - parentScrollContainer = this.FindClosestParent(); + parentScrollContainer = this.FindClosestParent(); } protected override bool OnScroll(ScrollEvent e) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 62ed736dc2..eca192c8e5 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -5,6 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -88,7 +90,7 @@ namespace osu.Game.Overlays.Mods Bottom = footer_height + PADDING, } }, - Footer = new Container + Footer = new InputBlockingContainer { RelativeSizeAxes = Axes.X, Depth = float.MinValue, @@ -113,6 +115,17 @@ namespace osu.Game.Overlays.Mods }; } + protected override bool OnClick(ClickEvent e) + { + if (State.Value == Visibility.Visible) + { + Hide(); + return true; + } + + return base.OnClick(e); + } + protected override void PopIn() { const double fade_in_duration = 400; diff --git a/osu.Game/Overlays/Mods/UserModSelectScreen.cs b/osu.Game/Overlays/Mods/UserModSelectScreen.cs index 81943da514..ed0a07521b 100644 --- a/osu.Game/Overlays/Mods/UserModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/UserModSelectScreen.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . 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 @@ -11,6 +14,24 @@ namespace osu.Game.Overlays.Mods { protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); + protected override IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) + { + IEnumerable modsAfterRemoval = SelectedMods.Value.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) diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index d4dde0db3f..5f5cfce344 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -149,7 +149,7 @@ namespace osu.Game.Overlays } }); - AddInternal(new HoverClickSounds()); + AddInternal(new HoverClickSounds(HoverSampleSet.TabSelect)); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs index 8f6b935128..ec9cb55042 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateProgress(APIUser user) { levelProgressBar.Length = user?.Statistics?.Level.Progress / 100f ?? 0; - levelProgressText.Text = user?.Statistics?.Level.Progress.ToLocalisableString("0'%'"); + levelProgressText.Text = user?.Statistics?.Level.Progress.ToLocalisableString("0'%'") ?? default(LocalisableString); } } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index fab2487c0d..44e0d9c37f 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -31,7 +31,9 @@ namespace osu.Game.Overlays.Profile User.ValueChanged += e => updateDisplay(e.NewValue); TabControl.AddItem(LayoutStrings.HeaderUsersShow); - TabControl.AddItem(LayoutStrings.HeaderUsersModding); + + // todo: pending implementation. + // TabControl.AddItem(LayoutStrings.HeaderUsersModding); centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index dfa45cc543..48a4c31f30 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Rankings startDateColumn.Value = dateToString(response.Spotlight.StartDate); endDateColumn.Value = dateToString(response.Spotlight.EndDate); mapCountColumn.Value = response.BeatmapSets.Count.ToLocalisableString(@"N0"); - participantsColumn.Value = response.Spotlight.Participants?.ToLocalisableString(@"N0"); + participantsColumn.Value = response.Spotlight.Participants?.ToLocalisableString(@"N0") ?? default(LocalisableString); } private LocalisableString dateToString(DateTimeOffset date) => date.ToLocalisableString(@"yyyy-MM-dd"); diff --git a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs index 17c17b1f1a..bdbd2942d1 100644 --- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Users; @@ -24,7 +25,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected override Drawable[] CreateUniqueContent(UserStatistics item) => new Drawable[] { - new RowText { Text = item.PP?.ToLocalisableString(@"N0"), } + new RowText { Text = item.PP?.ToLocalisableString(@"N0") ?? default(LocalisableString), } }; } } diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index e7afa48502..61191dcacf 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -9,5 +9,20 @@ namespace osu.Game.Overlays.Settings public interface ISettingsItem : IDrawable, IDisposable { event Action SettingChanged; + + /// + /// Whether this setting has a classic default (ie. a different default which better aligns with osu-stable expectations). + /// + bool HasClassicDefault { get; } + + /// + /// Apply the classic default value of the associated setting. Will throw if is false. + /// + void ApplyClassicDefault(); + + /// + /// Apply the default value of the associated setting. + /// + void ApplyDefault(); } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs index 5029c6a617..e2e00813bd 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { + ClassicDefault = false, LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak, Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index d4e4fd571d..5231ce1211 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -21,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { new SettingsEnumDropdown { + ClassicDefault = ScoringMode.Classic, LabelText = GameplaySettingsStrings.ScoreDisplayMode, Current = config.GetBindable(OsuSetting.ScoreDisplayMode), Keywords = new[] { "scoring" } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index ba9779d650..829977e9b6 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { + ClassicDefault = false, LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), Keywords = new[] { "hp", "bar" } diff --git a/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs new file mode 100644 index 0000000000..60849cd6d4 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Handlers.Joystick; +using osu.Framework.Localisation; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public class JoystickSettings : SettingsSubsection + { + protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad; + + private readonly JoystickHandler joystickHandler; + + private readonly Bindable enabled = new BindableBool(true); + + private SettingsSlider deadzoneSlider; + + private Bindable handlerDeadzone; + + private Bindable localDeadzone; + + public JoystickSettings(JoystickHandler joystickHandler) + { + this.joystickHandler = joystickHandler; + } + + [BackgroundDependencyLoader] + private void load() + { + // use local bindable to avoid changing enabled state of game host's bindable. + handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy(); + localDeadzone = handlerDeadzone.GetUnboundCopy(); + + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = CommonStrings.Enabled, + Current = enabled + }, + deadzoneSlider = new SettingsSlider + { + LabelText = JoystickSettingsStrings.DeadzoneThreshold, + KeyboardStep = 0.01f, + DisplayAsPercentage = true, + Current = localDeadzone, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + enabled.BindTo(joystickHandler.Enabled); + enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true); + + handlerDeadzone.BindValueChanged(val => + { + bool disabled = localDeadzone.Disabled; + + localDeadzone.Disabled = false; + localDeadzone.Value = val.NewValue; + localDeadzone.Disabled = disabled; + }, true); + + localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index d282ba5318..d2c5d2fcf7 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -68,7 +68,10 @@ namespace osu.Game.Overlays.Settings.Sections break; // whitelist the handlers which should be displayed to avoid any weird cases of users touching settings they shouldn't. - case JoystickHandler _: + case JoystickHandler jh: + section = new JoystickSettings(jh); + break; + case MidiHandler _: section = new HandlerSection(handler); break; diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 6e1558f7d7..7fc049915e 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -37,6 +37,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface }, new SettingsSlider { + ClassicDefault = 0, LabelText = UserInterfaceStrings.HoldToConfirmActivationTime, Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), Keywords = new[] { @"delay" }, diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index 6290046987..b91b5c5243 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -32,6 +32,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { new SettingsCheckbox { + ClassicDefault = true, LabelText = UserInterfaceStrings.RightMouseScroll, Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), }, diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 1c5668479f..afcd41af22 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,6 +30,8 @@ namespace osu.Game.Overlays.Settings /// public object SettingSourceObject { get; internal set; } + public const string CLASSIC_DEFAULT_SEARCH_TERM = @"has-classic-default"; + private IHasCurrentValue controlWithCurrent => Control as IHasCurrentValue; protected override Container Content => FlowContent; @@ -96,18 +98,72 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } - public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); + public virtual IEnumerable FilterTerms + { + get + { + var keywords = new List(Keywords ?? Array.Empty()) + { + LabelText.ToString() + }; + + if (HasClassicDefault) + keywords.Add(CLASSIC_DEFAULT_SEARCH_TERM); + + return keywords; + } + } public IEnumerable Keywords { get; set; } - public override bool IsPresent => base.IsPresent && MatchingFilter; + private bool matchingFilter = true; - public bool MatchingFilter { get; set; } = true; + public bool MatchingFilter + { + get => matchingFilter; + set + { + bool wasPresent = IsPresent; + + matchingFilter = value; + + if (IsPresent != wasPresent) + Invalidate(Invalidation.Presence); + } + } + + public override bool IsPresent => base.IsPresent && MatchingFilter; public bool FilteringActive { get; set; } public event Action SettingChanged; + private T classicDefault; + + public bool HasClassicDefault { get; private set; } + + /// + /// A "classic" default value for this setting. + /// + public T ClassicDefault + { + set + { + classicDefault = value; + HasClassicDefault = true; + } + } + + public void ApplyClassicDefault() + { + if (!HasClassicDefault) + throw new InvalidOperationException($"Cannot apply a classic default to a setting which doesn't have one defined via {nameof(ClassicDefault)}."); + + Current.Value = classicDefault; + } + + public void ApplyDefault() => Current.SetDefault(); + protected SettingsItem() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 28c42a0e47..cfb0212b8c 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -21,8 +22,6 @@ namespace osu.Game.Overlays.Settings protected FillFlowContainer FlowContent; protected override Container Content => FlowContent; - public override bool IsPresent => base.IsPresent && MatchingFilter; - private IBindable selectedSection; private Box dim; @@ -40,11 +39,27 @@ namespace osu.Game.Overlays.Settings private const int header_size = 24; private const int border_size = 4; - public bool MatchingFilter { get; set; } = true; + private bool matchingFilter = true; + + public bool MatchingFilter + { + get => matchingFilter; + set + { + bool wasPresent = IsPresent; + + matchingFilter = value; + + if (IsPresent != wasPresent) + Invalidate(Invalidation.Presence); + } + } + + public override bool IsPresent => base.IsPresent && MatchingFilter; public bool FilteringActive { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private SettingsPanel settingsPanel { get; set; } protected SettingsSection() @@ -117,7 +132,7 @@ namespace osu.Game.Overlays.Settings }, }); - selectedSection = settingsPanel.CurrentSection.GetBoundCopy(); + selectedSection = settingsPanel?.CurrentSection.GetBoundCopy() ?? new Bindable(this); selectedSection.BindValueChanged(_ => updateContentFade(), true); } @@ -138,7 +153,10 @@ namespace osu.Game.Overlays.Settings protected override bool OnClick(ClickEvent e) { if (!isCurrentSection) + { + Debug.Assert(settingsPanel != null); settingsPanel.SectionsContainer.ScrollTo(this); + } return base.OnClick(e); } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index c84cba8189..7cd8fc6d66 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays protected override IEnumerable CreateSections() => new SettingsSection[] { + // This list should be kept in sync with ScreenBehaviour. new GeneralSection(), new SkinSection(), new InputSection(createSubPanel(new KeyBindingPanel())), diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index a92c30e593..0f51560476 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -43,11 +43,11 @@ namespace osu.Game.Rulesets.Scoring Health.Value += GetHealthIncreaseFor(result); - if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true) - return; - - if (Failed?.Invoke() != false) - HasFailed = true; + if (meetsAnyFailCondition(result)) + { + if (Failed?.Invoke() != false) + HasFailed = true; + } } protected override void RevertResultInternal(JudgementResult result) @@ -69,6 +69,28 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value); + /// + /// Whether the current state of or the provided meets any fail condition. + /// + /// The judgement result. + private bool meetsAnyFailCondition(JudgementResult result) + { + if (DefaultFailCondition) + return true; + + if (FailConditions != null) + { + foreach (var condition in FailConditions.GetInvocationList()) + { + bool conditionResult = (bool)condition.Method.Invoke(condition.Target, new object[] { this, result }); + if (conditionResult) + return true; + } + } + + return false; + } + protected override void Reset(bool storeResults) { base.Reset(storeResults); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1e268bb2eb..1dd1d1aeb6 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -375,13 +375,13 @@ namespace osu.Game.Rulesets.Scoring { if (acc == 1) return ScoreRank.X; - if (acc > 0.95) + if (acc >= 0.95) return ScoreRank.S; - if (acc > 0.9) + if (acc >= 0.9) return ScoreRank.A; - if (acc > 0.8) + if (acc >= 0.8) return ScoreRank.B; - if (acc > 0.7) + if (acc >= 0.7) return ScoreRank.C; return ScoreRank.D; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 885f4903b0..b48aef330a 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -196,11 +196,8 @@ namespace osu.Game.Screens.Menu if (State == ButtonSystemState.Initial) { - if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey)) - { - logo?.TriggerClick(); - return true; - } + logo?.TriggerClick(); + return true; } return base.OnKeyDown(e); diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 1d3aef0653..f5743c7d5a 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both, Children = new Drawable[] { - logoBounceContainer = new Container + logoBounceContainer = new DragContainer { AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -402,5 +402,28 @@ namespace osu.Game.Screens.Menu impactContainer.ScaleTo(0.96f); impactContainer.ScaleTo(1.12f, 250); } + + private class DragContainer : Container + { + public override bool DragBlocksClick => false; + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + Vector2 change = e.MousePosition - e.MouseDownPosition; + + // Diminish the drag distance as we go further to simulate "rubber band" feeling. + change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length; + + this.MoveTo(change); + } + + protected override void OnDragEnd(DragEndEvent e) + { + this.MoveTo(Vector2.Zero, 800, Easing.OutElastic); + base.OnDragEnd(e); + } + } } } diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index f95e949920..ead41a826a 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -43,8 +43,7 @@ namespace osu.Game.Screens.Play.Break Direction = FillDirection.Vertical, Children = new Drawable[] { - AccuracyDisplay = new PercentageBreakInfoLine(BeatmapsetsStrings.ShowStatsAccuracy), - + AccuracyDisplay = new PercentageBreakInfoLine(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy), // See https://github.com/ppy/osu/discussions/15185 // RankDisplay = new BreakInfoLine("Rank"), GradeDisplay = new BreakInfoLine("Grade"), diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 676a08b434..abfed1acd0 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Play private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; - internal readonly IBindable IsBreakTime = new Bindable(); + internal readonly IBindable IsPlaying = new Bindable(); private bool holdingForHUD; @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING))); - IsBreakTime.BindValueChanged(_ => updateVisibility()); + IsPlaying.BindValueChanged(_ => updateVisibility()); configVisibilityMode.BindValueChanged(_ => updateVisibility(), true); replayLoaded.BindValueChanged(replayLoadedValueChanged, true); @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Play case HUDVisibilityMode.HideDuringGameplay: // always show during replay as we want the seek bar to be visible. - ShowHud.Value = replayLoaded.Value || IsBreakTime.Value; + ShowHud.Value = replayLoaded.Value || !IsPlaying.Value; break; case HUDVisibilityMode.Always: diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2d5a67758a..ae3eb1ed8b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -457,7 +457,7 @@ namespace osu.Game.Screens.Play private void updateGameplayState() { - bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; + bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value && !GameplayState.HasFailed; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; localUserPlaying.Value = inGameplay; } @@ -812,6 +812,8 @@ namespace osu.Game.Screens.Play GameplayState.HasFailed = true; Score.ScoreInfo.Passed = false; + updateGameplayState(); + // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) // could process an extra frame after the GameplayClock is stopped. // In such cases we want the fail state to precede a user triggered pause. @@ -945,7 +947,7 @@ namespace osu.Game.Screens.Play failAnimationLayer.Background = b; }); - HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime); + HUDOverlay.IsPlaying.BindTo(localUserPlaying); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 42091c521f..1662ca399f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -99,8 +99,8 @@ namespace osu.Game.Screens.Play.PlayerSettings { public override LocalisableString TooltipText => Current.Value == 0 - ? new TranslatableString("_", @"{0} ms", base.TooltipText) - : new TranslatableString("_", @"{0} ms {1}", base.TooltipText, getEarlyLateText(Current.Value)); + ? LocalisableString.Interpolate($@"{base.TooltipText} ms") + : LocalisableString.Interpolate($@"{base.TooltipText} ms {getEarlyLateText(Current.Value)}"); private LocalisableString getEarlyLateText(double value) { diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index e50520e0ca..b9248bd67e 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -212,12 +212,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, Children = new[] { - new RankBadge(1f, getRank(ScoreRank.X)), - new RankBadge(0.95f, getRank(ScoreRank.S)), - new RankBadge(0.9f, getRank(ScoreRank.A)), - new RankBadge(0.8f, getRank(ScoreRank.B)), - new RankBadge(0.7f, getRank(ScoreRank.C)), - new RankBadge(0.35f, getRank(ScoreRank.D)), + new RankBadge(1, getRank(ScoreRank.X)), + new RankBadge(0.95, getRank(ScoreRank.S)), + new RankBadge(0.9, getRank(ScoreRank.A)), + new RankBadge(0.8, getRank(ScoreRank.B)), + new RankBadge(0.7, getRank(ScoreRank.C)), + new RankBadge(0.35, getRank(ScoreRank.D)), } }, rankText = new RankText(score.Rank) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs index 76cd408daa..d0b79aa4c7 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// /// The accuracy value corresponding to the displayed by this badge. /// - public readonly float Accuracy; + public readonly double Accuracy; private readonly ScoreRank rank; @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// /// The accuracy value corresponding to . /// The to be displayed in this . - public RankBadge(float accuracy, ScoreRank rank) + public RankBadge(double accuracy, ScoreRank rank) { Accuracy = accuracy; this.rank = rank; @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy base.Update(); // Starts at -90deg (top) and moves counter-clockwise by the accuracy - rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - Accuracy) * MathF.PI * 2); + rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)Accuracy) * MathF.PI * 2); } private Vector2 circlePosition(float t) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index c6037d1bd6..9772b1feb3 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -244,7 +244,7 @@ namespace osu.Game.Screens.Select.Carousel } if (hideRequested != null) - items.Add(new OsuMenuItem(CommonStrings.ButtonsHide, MenuItemType.Destructive, () => hideRequested(beatmapInfo))); + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmapInfo))); return items.ToArray(); } diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs index 1ab54fa069..5dfa2a2664 100644 --- a/osu.Game/Screens/Select/Filter/SortMode.cs +++ b/osu.Game/Screens/Select/Filter/SortMode.cs @@ -27,8 +27,9 @@ namespace osu.Game.Screens.Select.Filter [LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksLength))] Length, - [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))] - RankAchieved, + // todo: pending support (https://github.com/ppy/osu/issues/4917) + // [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))] + // RankAchieved, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))] Source, diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 928978cd08..2a1ed2a7a8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -50,6 +50,12 @@ namespace osu.Game.Screens.Select public FilterControl FilterControl { get; private set; } + /// + /// Whether this song select instance should take control of the global track, + /// applying looping and preview offsets. + /// + protected virtual bool ControlGlobalMusic => true; + protected virtual bool ShowFooter => true; protected virtual bool DisplayStableImportPrompt => legacyImportManager?.SupportsImportFromStable == true; @@ -604,15 +610,18 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); beginLooping(); - music.ResetTrackAdjustments(); if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { updateComponentFromBeatmap(Beatmap.Value); - // restart playback on returning to song select, regardless. - // not sure this should be a permanent thing (we may want to leave a user pause paused even on returning) - music.Play(requestedByUser: true); + if (ControlGlobalMusic) + { + // restart playback on returning to song select, regardless. + // not sure this should be a permanent thing (we may want to leave a user pause paused even on returning) + music.ResetTrackAdjustments(); + music.Play(requestedByUser: true); + } } this.FadeIn(250); @@ -663,6 +672,9 @@ namespace osu.Game.Screens.Select private void beginLooping() { + if (!ControlGlobalMusic) + return; + Debug.Assert(!isHandlingLooping); isHandlingLooping = true; @@ -733,6 +745,9 @@ namespace osu.Game.Screens.Select /// private void ensurePlayingSelected() { + if (!ControlGlobalMusic) + return; + ITrack track = music.CurrentTrack; bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 119b0ec9ad..fb24084659 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -155,7 +155,7 @@ namespace osu.Game.Skinning return skinnableTargetWrapper; } - break; + return null; } switch (component.LookupName) diff --git a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs index 4507526806..2124ba9b6d 100644 --- a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . 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.Framework.Allocation; using osu.Framework.Bindables; @@ -18,6 +20,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Select; +using osu.Game.Utils; using osuTK; namespace osu.Game.Skinning.Editor @@ -34,6 +37,9 @@ namespace osu.Game.Skinning.Editor [Resolved] private IBindable ruleset { get; set; } + [Resolved] + private Bindable> mods { get; set; } + public SkinEditorSceneLibrary() { Height = BUTTON_HEIGHT + padding * 2; @@ -95,6 +101,10 @@ namespace osu.Game.Skinning.Editor return; var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); + + if (!ModUtils.CheckCompatibleSet(mods.Value.Append(replayGeneratingMod), out var invalid)) + mods.Value = mods.Value.Except(invalid).ToArray(); + if (replayGeneratingMod != null) screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)))); }, new[] { typeof(Player), typeof(SongSelect) }) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index f7d5581621..b65ba8b04c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -390,10 +390,14 @@ namespace osu.Game.Skinning return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); } - break; - } + return null; - return this.GetAnimation(component.LookupName, false, false); + case SkinnableSprite.SpriteComponent sprite: + return this.GetAnimation(sprite.LookupName, false, false); + + default: + throw new UnsupportedSkinComponentException(component); + } } private Texture? getParticleTexture(HitResult result) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 4b4d7fe2c6..21b34fcd27 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -65,7 +65,7 @@ namespace osu.Game.Skinning public bool UsesFixedAnchor { get; set; } - private class SpriteComponent : ISkinComponent + internal class SpriteComponent : ISkinComponent { public string LookupName { get; set; } diff --git a/osu.Game/Skinning/UnsupportedSkinComponentException.cs b/osu.Game/Skinning/UnsupportedSkinComponentException.cs new file mode 100644 index 0000000000..7f0dd51d5b --- /dev/null +++ b/osu.Game/Skinning/UnsupportedSkinComponentException.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Skinning +{ + public class UnsupportedSkinComponentException : Exception + { + public UnsupportedSkinComponentException(ISkinComponent component) + : base($@"Unsupported component type: {component.GetType()} (lookup: ""{component.LookupName}"").") + { + } + } +} diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 7a6f8c8cfb..6e4adb4d4c 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; using osuTK.Graphics; using IntroSequence = osu.Game.Configuration.IntroSequence; @@ -106,6 +107,11 @@ namespace osu.Game.Tests.Visual protected void ConfirmAtMainMenu() => AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); + /// + /// Dismisses any notifications pushed which block from interacting with the game (or block screens from loading, e.g. ). + /// + protected void DismissAnyNotifications() => Game.Notifications.State.Value = Visibility.Hidden; + public class TestOsuGame : OsuGame { public new const float SIDE_OVERLAY_OFFSET_RATIO = OsuGame.SIDE_OVERLAY_OFFSET_RATIO; diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 2e1ca09fe4..296ed80e37 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -74,11 +74,15 @@ namespace osu.Game.Tests.Visual createdDrawables.Add(created); - SkinProvidingContainer mainProvider; Container childContainer; OutlineBox outlineBox; SkinProvidingContainer skinProvider; + ISkin provider = skin; + + if (provider is LegacySkin legacyProvider) + provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(legacyProvider, beatmap); + var children = new Container { RelativeSizeAxes = Axes.Both, @@ -107,12 +111,10 @@ namespace osu.Game.Tests.Visual Children = new Drawable[] { outlineBox = new OutlineBox(), - (mainProvider = new SkinProvidingContainer(skin)).WithChild( - skinProvider = new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) - { - Child = created, - } - ) + skinProvider = new SkinProvidingContainer(provider) + { + Child = created, + } } }, } @@ -130,7 +132,7 @@ namespace osu.Game.Tests.Visual { bool autoSize = created.RelativeSizeAxes == Axes.None; - foreach (var c in new[] { mainProvider, childContainer, skinProvider }) + foreach (var c in new[] { childContainer, skinProvider }) { c.RelativeSizeAxes = Axes.None; c.AutoSizeAxes = Axes.None; diff --git a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs b/osu.Game/Utils/LegacyRandom.cs similarity index 79% rename from osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs rename to osu.Game/Utils/LegacyRandom.cs index 46e427e1b7..cf731aa91f 100644 --- a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs +++ b/osu.Game/Utils/LegacyRandom.cs @@ -2,27 +2,36 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Utils; -namespace osu.Game.Rulesets.Catch.MathUtils +namespace osu.Game.Utils { /// /// A PRNG specified in http://heliosphan.org/fastrandom.html. + /// Should only be used to match legacy behaviour. See for a newer alternative. /// - public class FastRandom + /// + /// Known in osu-stable code as `FastRandom`. + /// + public class LegacyRandom { private const double int_to_real = 1.0 / (int.MaxValue + 1.0); private const uint int_mask = 0x7FFFFFFF; - private const uint y_initial = 842502087; - private const uint z_initial = 3579807591; - private const uint w_initial = 273326509; - private uint x, y = y_initial, z = z_initial, w = w_initial; + private const uint y = 842502087; + private const uint z = 3579807591; + private const uint w = 273326509; - public FastRandom(int seed) + public uint X { get; private set; } + public uint Y { get; private set; } = y; + public uint Z { get; private set; } = z; + public uint W { get; private set; } = w; + + public LegacyRandom(int seed) { - x = (uint)seed; + X = (uint)seed; } - public FastRandom() + public LegacyRandom() : this(Environment.TickCount) { } @@ -33,11 +42,11 @@ namespace osu.Game.Rulesets.Catch.MathUtils /// The random value. public uint NextUInt() { - uint t = x ^ (x << 11); - x = y; - y = z; - z = w; - return w = w ^ (w >> 19) ^ t ^ (t >> 8); + uint t = X ^ (X << 11); + X = Y; + Y = Z; + Z = W; + return W = W ^ (W >> 19) ^ t ^ (t >> 8); } /// diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c6c18f6061..26891ad978 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 64af0d70f3..d261e13ade 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - +