diff --git a/.editorconfig b/.editorconfig index be5652954b47..840fa9833441 100755 --- 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.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 4bbc17de75bd..9cf3abf02b49 100755 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -195,8 +195,6 @@ public override void SetHost(GameHost host) desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); - - desktopWindow.MinimumSize = new Size(800, 600); } private readonly List importableFiles = new List(); diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 737b4f7fe7e3..f1422e79cb8a 100755 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -128,7 +128,12 @@ private static void setupSquirrel() 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.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 4c1fb4f0f26b..18fa0eb36d4d 100755 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -9,7 +9,7 @@ osu!(lazer) lazer.ico app.manifest - 2022.426.0 + 2022.503.0 0.0.0 @@ -26,7 +26,6 @@ - diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 885d464215e9..d5811182aad6 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -5,10 +5,10 @@ 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 @@ public override void PostProcess() 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 @@ public void ApplyPositionOffsets(IBeatmap beatmap) 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 @@ private static void applyHardRockOffset(CatchHitObject hitObject, ref float? las /// 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/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 164f46543835..9ff6d10a4953 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Edit { - public class CatchHitObjectComposer : HitObjectComposer + public class CatchHitObjectComposer : DistancedHitObjectComposer { private const float distance_snap_radius = 50; @@ -42,6 +42,10 @@ public CatchHitObjectComposer(CatchRuleset ruleset) [BackgroundDependencyLoader] private void load() { + // todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation. + RightSideToolboxContainer.Alpha = 0; + DistanceSpacingMultiplier.Disabled = true; + LayerBelowRuleset.Add(new PlayfieldBorder { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 50be13c4e00e..2e55f86bb6aa 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -107,30 +106,5 @@ public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screen { throw new System.NotImplementedException(); } - - public override float GetBeatSnapDistanceAt(HitObject referenceObject) - { - throw new System.NotImplementedException(); - } - - public override float DurationToDistance(HitObject referenceObject, double duration) - { - throw new System.NotImplementedException(); - } - - public override double DistanceToDuration(HitObject referenceObject, float distance) - { - throw new System.NotImplementedException(); - } - - public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) - { - throw new System.NotImplementedException(); - } - - public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) - { - throw new System.NotImplementedException(); - } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 12d13d49cb90..082917375671 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -11,8 +11,8 @@ 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 @@ public class ManiaBeatmapConverter : BeatmapConverter 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 @@ protected override Beatmap ConvertBeatmap(IBeatmap original, Can 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 @@ private IEnumerable generateConverted(HitObject original, IBeatm /// 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 76ea1a7ab71a..352cc0987a90 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 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 @@ internal class DistanceObjectPatternGenerator : PatternGenerator 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 f8f9fd028e72..dd0407bb200b 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 @@ internal class EndTimeObjectPatternGenerator : PatternGenerator 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 53b059b4e214..41d4c9322be2 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 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 @@ internal class HitObjectPatternGenerator : PatternGenerator 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 eaf0ea0f2b88..d5689c047a26 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.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 @@ internal abstract class PatternGenerator : Patterns.PatternGenerator /// /// 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 ab6bd78ece33..31550a8105e9 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -14,6 +14,7 @@ public class Strain : StrainDecaySkill { 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 @@ protected override double StrainValueOf(DifficultyHitObject current) 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 a9cd7f247622..000000000000 --- 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.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index c770e2d96fe9..2ba30c5f7413 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -33,7 +34,7 @@ public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - [Cached(typeof(IPositionSnapProvider))] + [Cached(typeof(IDistanceSnapProvider))] private readonly SnapProvider snapProvider = new SnapProvider(); private TestOsuDistanceSnapGrid grid; @@ -179,13 +180,15 @@ public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObjec } } - private class SnapProvider : IPositionSnapProvider + private class SnapProvider : IDistanceSnapProvider { public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); + public IBindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); + public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length; public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs index de1f61a0bd00..5e46498aca81 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs @@ -16,31 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModAlternate : OsuModTestScene { - [Test] - public void TestInputAtIntro() => CreateModTest(new ModTestData - { - Mod = new OsuModAlternate(), - PassCondition = () => Player.ScoreProcessor.Combo.Value == 1, - Autoplay = false, - Beatmap = new Beatmap - { - HitObjects = new List - { - new HitCircle - { - StartTime = 1000, - Position = new Vector2(100), - }, - }, - }, - ReplayFrames = new List - { - new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton), - new OsuReplayFrame(501, new Vector2(200)), - new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton), - } - }); - [Test] public void TestInputAlternating() => CreateModTest(new ModTestData { @@ -116,17 +91,50 @@ public void TestInputSingular() => CreateModTest(new ModTestData } }); + /// + /// Ensures alternation is reset before the first hitobject after intro. + /// + [Test] + public void TestInputSingularAtIntro() => CreateModTest(new ModTestData + { + Mod = new OsuModAlternate(), + PassCondition = () => Player.ScoreProcessor.Combo.Value == 1, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle + { + StartTime = 1000, + Position = new Vector2(100), + }, + }, + }, + ReplayFrames = new List + { + // first press during intro. + new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(200)), + // press same key at hitobject and ensure it has been hit. + new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton), + } + }); + + /// + /// Ensures alternation is reset before the first hitobject after a break. + /// [Test] public void TestInputSingularWithBreak() => CreateModTest(new ModTestData { Mod = new OsuModAlternate(), - PassCondition = () => Player.ScoreProcessor.Combo.Value == 2, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2, Autoplay = false, Beatmap = new Beatmap { Breaks = new List { - new BreakPeriod(500, 2250), + new BreakPeriod(500, 2000), }, HitObjects = new List { @@ -138,16 +146,29 @@ public void TestInputSingularWithBreak() => CreateModTest(new ModTestData new HitCircle { StartTime = 2500, - Position = new Vector2(100), - } + Position = new Vector2(500, 100), + }, + new HitCircle + { + StartTime = 3000, + Position = new Vector2(500, 100), + }, } }, ReplayFrames = new List { + // first press to start alternate lock. new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton), new OsuReplayFrame(501, new Vector2(100)), - new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton), - new OsuReplayFrame(2501, new Vector2(100)), + // press same key after break but before hit object. + new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton), + new OsuReplayFrame(2251, new Vector2(300, 100)), + // press same key at second hitobject and ensure it has been hit. + new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton), + new OsuReplayFrame(2501, new Vector2(500, 100)), + // press same key at third hitobject and ensure it has been missed. + new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton), + new OsuReplayFrame(3001, new Vector2(500, 100)), } }); } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index e47edc37cca9..dcd2c7d321cf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -38,7 +38,11 @@ public override double DifficultyValue() double difficulty = 0; double weight = 1; - List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // These sections will not contribute to the difficulty. + var peaks = GetCurrentStrainPeaks().Where(p => p > 0); + + List strains = peaks.OrderByDescending(d => d).ToList(); // We are reducing the highest strains first to account for extreme difficulty spikes for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 7eb554ec818b..89285b282acd 100755 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -43,7 +43,7 @@ public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler< public Action> RemoveControlPointsRequested; [Resolved(CanBeNull = true)] - private IPositionSnapProvider snapProvider { get; set; } + private IDistanceSnapProvider snapProvider { get; set; } public PathControlPointVisualiser(Slider slider, bool allowSelection) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 51168349ccde..2ebc7f5d1424 100755 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -37,7 +37,7 @@ public class SliderPlacementBlueprint : PlacementBlueprint private int currentSegmentLength; [Resolved(CanBeNull = true)] - private HitObjectComposer composer { get; set; } + private IDistanceSnapProvider snapProvider { get; set; } public SliderPlacementBlueprint() : base(new Objects.Slider()) @@ -220,7 +220,7 @@ private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint) private void updateSlider() { - HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index b0bf59b732aa..18b0860b5ead 100755 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -38,7 +38,7 @@ public class SliderSelectionBlueprint : OsuSelectionBlueprint protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } [Resolved(CanBeNull = true)] - private HitObjectComposer composer { get; set; } + private IDistanceSnapProvider snapProvider { get; set; } [Resolved(CanBeNull = true)] private IPlacementHandler placementHandler { get; set; } @@ -208,7 +208,7 @@ private PathControlPoint addControlPoint(Vector2 position) // Move the control points from the insertion index onwards to make room for the insertion controlPoints.Insert(insertionIndex, pathControlPoint); - HitObject.SnapTo(composer); + HitObject.SnapTo(snapProvider); return pathControlPoint; } @@ -230,7 +230,7 @@ private void removeControlPoints(List toRemove) } // Snap the slider to the current beat divisor before checking length validity. - HitObject.SnapTo(composer); + HitObject.SnapTo(snapProvider); // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 1059a8a7860e..d8bc65176cf6 100755 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit { - public class OsuHitObjectComposer : HitObjectComposer + public class OsuHitObjectComposer : DistancedHitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) : base(ruleset) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index efbac5439c8e..70c60ab63535 100755 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit public class OsuSelectionHandler : EditorSelectionHandler { [Resolved(CanBeNull = true)] - private IPositionSnapProvider? positionSnapProvider { get; set; } + private IDistanceSnapProvider? snapProvider { get; set; } /// /// During a transform, the initial origin is stored so it can be used throughout the operation. @@ -206,7 +206,7 @@ private void scaleSlider(Slider slider, Vector2 scale) // Snap the slider's length to the current beat divisor // to calculate the final resulting duration / bounding box before the final checks. - slider.SnapTo(positionSnapProvider); + slider.SnapTo(snapProvider); //if sliderhead or sliderend end up outside playfield, revert scaling. Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); @@ -219,7 +219,7 @@ private void scaleSlider(Slider slider, Vector2 scale) point.Position = oldControlPoints.Dequeue(); // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. - slider.SnapTo(positionSnapProvider); + slider.SnapTo(snapProvider); } private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index b4aaff384cf7..b9b50de0d162 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -2,21 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, IApplicableToPlayer + public class OsuModAlternate : Mod, IApplicableToDrawableRuleset { public override string Name => @"双指"; public override string Acronym => @"AL"; @@ -26,9 +29,16 @@ public class OsuModAlternate : Mod, IApplicableToDrawableRuleset, public override ModType Type => ModType.Conversion; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; - private double firstObjectValidJudgementTime; - private IBindable isBreakTime; private const double flash_duration = 1000; + + /// + /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods). + /// + /// + /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time. + /// + private PeriodTracker nonGameplayPeriods; + private OsuAction? lastActionPressed; private DrawableRuleset ruleset; @@ -39,29 +49,30 @@ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset ruleset = drawableRuleset; drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); - var firstHitObject = ruleset.Objects.FirstOrDefault(); - firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0); - - gameplayClock = drawableRuleset.FrameStableClock; - } + var periods = new List(); - public void ApplyToPlayer(Player player) - { - isBreakTime = player.IsBreakTime.GetBoundCopy(); - isBreakTime.ValueChanged += e => + if (drawableRuleset.Objects.Any()) { - if (e.NewValue) - lastActionPressed = null; - }; + periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1)); + + foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks) + periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1)); + + static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh); + } + + nonGameplayPeriods = new PeriodTracker(periods); + + gameplayClock = drawableRuleset.FrameStableClock; } private bool checkCorrectAction(OsuAction action) { - if (isBreakTime.Value) - return true; - - if (gameplayClock.CurrentTime < firstObjectValidJudgementTime) + if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) + { + lastActionPressed = null; return true; + } switch (action) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 9030191c9507..4c89ac15a1a7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset { - public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray(); [SettingSource("去除滑条头的准确率要求", "滑条分数与其命中的滑条刻成比例。")] public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDance.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDance.cs index e3abac521019..cf8df862d44e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDance.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDance.cs @@ -1,30 +1,25 @@ // 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.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModDance : ModDance + public class OsuModDance : ModDance { public override string Name => "Cusordance"; public override string Acronym => "CD"; public override string Description => "观看 lazer!dance"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "lazer!dance", IsBot = true } }, + ScoreInfo = new ScoreInfo { User = new APIUser { Username = $"lazer!dance{ENDCHAR}", IsBot = true } }, Replay = new OsuDanceAutoGenerator(beatmap, mods).Generate() }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index dee067548a1d..ef18ef4e22dd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods { internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset { - public override string Name => "自瞄"; + public override string Name => "磁铁"; public override string Acronym => "MG"; public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override ModType Type => ModType.Fun; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 6dabf1da8ef3..15e480134aed 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -22,6 +22,8 @@ public class OsuModRandom : ModRandom, IApplicableToBeatmap { public override string Description => "打个措手不及!"; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray(); + private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; private Random? rng; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 686a903ecb15..c4a5dffb4a23 100755 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -20,7 +20,7 @@ public class OsuModSpunOut : Mod, IApplicableToDrawableHitObject public override ModType Type => ModType.Automation; public override string Description => @"转盘会自动完成"; public override double ScoreMultiplier => 0.9; - public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) }; public void ApplyToDrawableHitObject(DrawableHitObject hitObject) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 84de2602afe0..c63611b5ece5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -27,7 +27,7 @@ public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApp public override ModType Type => ModType.DifficultyIncrease; public override string Description => @"不要断滑条哦"; public override double ScoreMultiplier => 1.0; - public override Type[] IncompatibleMods => new[] { typeof(ModClassic) }; + public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) }; public void ApplyToDrawableHitObject(DrawableHitObject drawable) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index 0403e8122950..429fe30fc583 100755 --- 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 4e8550c97d0e..d32c30a7c28d 100755 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -42,7 +42,14 @@ public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRu public override string Description => @"练习跟上歌曲的节奏!"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] + { + typeof(IRequiresApproachCircles), + typeof(OsuModRandom), + typeof(OsuModSpunOut), + typeof(OsuModStrictTracking), + typeof(OsuModSuddenDeath) + }).ToArray(); [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/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index da63909bdedc..8732d1878f31 100755 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -37,6 +37,7 @@ private void load() }, new SettingsCheckbox { + ClassicDefault = false, LabelText = "渐出滑条", Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) }, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6afdef3f3cce..a9d512f07610 100755 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -141,7 +141,13 @@ private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina s double colourPeak = colourPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty; - peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); + + double peak = norm(2, colourPeak, rhythmPeak, staminaPeak); + + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // These sections will not contribute to the difficulty. + if (peak > 0) + peaks.Add(peak); } double difficulty = 0; diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 43f22e4e90e4..a2ee97210a80 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; @@ -69,7 +70,7 @@ public void Setup() => Schedule(() => [TestCase(2)] public void TestSliderMultiplier(float multiplier) { - AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); + AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); assertSnapDistance(100 * multiplier); } @@ -221,6 +222,8 @@ private class TestHitObjectComposer : OsuHitObjectComposer { public new EditorBeatmap EditorBeatmap => base.EditorBeatmap; + public new Bindable DistanceSpacingMultiplier => base.DistanceSpacingMultiplier; + public TestHitObjectComposer() : base(new OsuRuleset()) { diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index c5a58af603e8..cf7b4cf01dfc 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -160,6 +160,40 @@ public void TestHealthRemovedOnRevert() 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/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 0d9e06e47191..b9cfa84a5dc2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; @@ -25,7 +26,7 @@ public class TestSceneDistanceSnapGrid : EditorClockTestScene [Cached(typeof(EditorBeatmap))] private readonly EditorBeatmap editorBeatmap; - [Cached(typeof(IPositionSnapProvider))] + [Cached(typeof(IDistanceSnapProvider))] private readonly SnapProvider snapProvider = new SnapProvider(); public TestSceneDistanceSnapGrid() @@ -159,13 +160,15 @@ public override (Vector2 position, double time) GetSnappedPosition(Vector2 scree => (Vector2.Zero, 0); } - private class SnapProvider : IPositionSnapProvider + private class SnapProvider : IDistanceSnapProvider { public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); + public IBindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); + public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10; public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index 145d738f60e6..ae1b691767e5 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -21,6 +21,7 @@ using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { @@ -86,6 +87,23 @@ public void TestPlacementOnlyWorksWithTiming() AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType().First().CurrentTool is HitCircleCompositionTool); } + [Test] + public void TestDistanceSpacingHotkeys() + { + double originalSpacing = 0; + + AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.BeatmapInfo.DistanceSpacing); + + AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl)); + AddStep("hold alt", () => InputManager.PressKey(Key.LAlt)); + + AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5)); + AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); + + AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt)); + AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl)); + } + public class EditorBeatmapContainer : Container { private readonly IWorkingBeatmap working; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 79d7bb366de7..bf491db45a4e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -21,7 +21,9 @@ protected override Player CreatePlayer(Ruleset ruleset) 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 a224a785319d..ab5d766609b1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -85,7 +85,10 @@ public void TestPauseResume() { 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/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 312281ac1818..e05580fed689 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -35,7 +35,7 @@ public override void SetUpSteps() }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); - AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).MatchStarted()); + AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted()); } [Test] diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs index 63741451f302..c550c9afda71 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs @@ -4,11 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics.UserInterface; +using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Rulesets; namespace osu.Game.Tests.Visual.Online { @@ -17,79 +17,86 @@ public class TestSceneBeatmapRulesetSelector : OsuTestScene [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private readonly TestRulesetSelector selector; + private BeatmapRulesetSelector selector; - public TestSceneBeatmapRulesetSelector() + [SetUp] + public void SetUp() => Schedule(() => Child = selector = new BeatmapRulesetSelector { - Add(selector = new TestRulesetSelector()); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BeatmapSet = new APIBeatmapSet(), + }); - [Resolved] - private IRulesetStore rulesets { get; set; } + [Test] + public void TestDisplay() + { + AddSliderStep("osu", 0, 100, 0, v => updateBeatmaps(0, v)); + AddSliderStep("taiko", 0, 100, 0, v => updateBeatmaps(1, v)); + AddSliderStep("fruits", 0, 100, 0, v => updateBeatmaps(2, v)); + AddSliderStep("mania", 0, 100, 0, v => updateBeatmaps(3, v)); + + void updateBeatmaps(int ruleset, int count) + { + if (selector == null) + return; + + selector.BeatmapSet = new APIBeatmapSet + { + Beatmaps = selector.BeatmapSet.Beatmaps + .Where(b => b.Ruleset.OnlineID != ruleset) + .Concat(Enumerable.Range(0, count).Select(_ => new APIBeatmap { RulesetID = ruleset })) + .ToArray(), + }; + } + } [Test] public void TestMultipleRulesetsBeatmapSet() { - var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2); - AddStep("load multiple rulesets beatmapset", () => { selector.BeatmapSet = new APIBeatmapSet { - Beatmaps = enabledRulesets.Select(r => new APIBeatmap { RulesetID = r.OnlineID }).ToArray() + Beatmaps = new[] + { + new APIBeatmap { RulesetID = 1 }, + new APIBeatmap { RulesetID = 2 }, + } }; }); - var tabItems = selector.TabContainer.TabItems; - AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value)); - AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value); + AddAssert("osu disabled", () => !selector.ChildrenOfType().Single(t => t.Value.OnlineID == 0).Enabled.Value); + AddAssert("mania disabled", () => !selector.ChildrenOfType().Single(t => t.Value.OnlineID == 3).Enabled.Value); + + AddAssert("taiko selected", () => selector.ChildrenOfType().Single(t => t.Active.Value).Value.OnlineID == 1); } [Test] public void TestSingleRulesetBeatmapSet() { - var enabledRuleset = rulesets.AvailableRulesets.Last(); - AddStep("load single ruleset beatmapset", () => { selector.BeatmapSet = new APIBeatmapSet { - Beatmaps = new[] - { - new APIBeatmap - { - RulesetID = enabledRuleset.OnlineID - } - } + Beatmaps = new[] { new APIBeatmap { RulesetID = 3 } } }; }); - AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset)); + AddAssert("single ruleset selected", () => selector.ChildrenOfType().Single(t => t.Active.Value).Value.OnlineID == 3); } [Test] public void TestEmptyBeatmapSet() { AddStep("load empty beatmapset", () => selector.BeatmapSet = new APIBeatmapSet()); - - AddAssert("no ruleset selected", () => selector.SelectedTab == null); - AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value)); + AddAssert("all rulesets disabled", () => selector.ChildrenOfType().All(t => !t.Active.Value && !t.Enabled.Value)); } [Test] public void TestNullBeatmapSet() { AddStep("load null beatmapset", () => selector.BeatmapSet = null); - - AddAssert("no ruleset selected", () => selector.SelectedTab == null); - AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value)); - } - - private class TestRulesetSelector : BeatmapRulesetSelector - { - public new TabItem SelectedTab => base.SelectedTab; - - public new TabFillFlowContainer TabContainer => base.TabContainer; + AddAssert("all rulesets disabled", () => selector.ChildrenOfType().All(t => !t.Active.Value && !t.Enabled.Value)); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBundledBeatmapDownloader.cs b/osu.Game.Tests/Visual/Online/TestSceneBundledBeatmapDownloader.cs new file mode 100644 index 000000000000..2af1c9a0f0e4 --- /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/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs index 71f4b7bd67ea..278dfc50a33e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online public class TestSceneProfileRulesetSelector : OsuTestScene { [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); public TestSceneProfileRulesetSelector() { @@ -35,14 +35,14 @@ public TestSceneProfileRulesetSelector() }; AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo)); - AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo)); AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo)); AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo)); + AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo)); - AddStep("User with osu as default", () => user.Value = new APIUser { PlayMode = "osu" }); - AddStep("User with mania as default", () => user.Value = new APIUser { PlayMode = "mania" }); - AddStep("User with taiko as default", () => user.Value = new APIUser { PlayMode = "taiko" }); - AddStep("User with catch as default", () => user.Value = new APIUser { PlayMode = "fruits" }); + AddStep("User with osu as default", () => user.Value = new APIUser { Id = 0, PlayMode = "osu" }); + AddStep("User with taiko as default", () => user.Value = new APIUser { Id = 1, PlayMode = "taiko" }); + AddStep("User with catch as default", () => user.Value = new APIUser { Id = 2, PlayMode = "fruits" }); + AddStep("User with mania as default", () => user.Value = new APIUser { Id = 3, PlayMode = "mania" }); AddStep("null user", () => user.Value = null); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs index 0ac65b357c35..f27615eea5be 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs @@ -1,35 +1,99 @@ // 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.Framework.Graphics; using osu.Game.Screens.Select; +using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { public class TestSceneSongSelectFooter : OsuManualInputManagerTestScene { - public TestSceneSongSelectFooter() + private FooterButtonRandom randomButton; + + private bool nextRandomCalled; + private bool previousRandomCalled; + + [SetUp] + public void SetUp() => Schedule(() => + { + nextRandomCalled = false; + previousRandomCalled = false; + + Footer footer; + + Child = footer = new Footer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + footer.AddButton(new FooterButtonMods(), null); + footer.AddButton(randomButton = new FooterButtonRandom + { + NextRandom = () => nextRandomCalled = true, + PreviousRandom = () => previousRandomCalled = true, + }, null); + footer.AddButton(new FooterButtonOptions(), null); + + InputManager.MoveMouseTo(Vector2.Zero); + }); + + [Test] + public void TestFooterRandom() + { + AddStep("press F2", () => InputManager.Key(Key.F2)); + AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); + } + + [Test] + public void TestFooterRandomViaMouse() + { + AddStep("click button", () => + { + InputManager.MoveMouseTo(randomButton); + InputManager.Click(MouseButton.Left); + }); + AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); + } + + [Test] + public void TestFooterRewind() + { + AddStep("press Shift+F2", () => + { + InputManager.PressKey(Key.LShift); + InputManager.PressKey(Key.F2); + InputManager.ReleaseKey(Key.F2); + InputManager.ReleaseKey(Key.LShift); + }); + AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + } + + [Test] + public void TestFooterRewindViaShiftMouseLeft() + { + AddStep("shift + click button", () => + { + InputManager.PressKey(Key.LShift); + InputManager.MoveMouseTo(randomButton); + InputManager.Click(MouseButton.Left); + InputManager.ReleaseKey(Key.LShift); + }); + AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + } + + [Test] + public void TestFooterRewindViaMouseRight() { - AddStep("Create footer", () => + AddStep("right click button", () => { - Footer footer; - AddRange(new Drawable[] - { - footer = new Footer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }); - - footer.AddButton(new FooterButtonMods(), null); - footer.AddButton(new FooterButtonRandom - { - NextRandom = () => { }, - PreviousRandom = () => { }, - }, null); - footer.AddButton(new FooterButtonOptions(), null); + InputManager.MoveMouseTo(randomButton); + InputManager.Click(MouseButton.Right); }); + AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index f4920b4412c8..288c0cb1406a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -17,7 +17,7 @@ public class TestSceneExpandingContainer : OsuManualInputManagerTestScene private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; - private ExpandableSlider slider1; + private ExpandableSlider> slider1; private ExpandableSlider slider2; [SetUp] @@ -34,7 +34,7 @@ public void SetUp() => Schedule(() => Width = 1, Children = new Drawable[] { - slider1 = new ExpandableSlider + slider1 = new ExpandableSlider> { Current = new BindableFloat { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenBehaviour.cs new file mode 100644 index 000000000000..9747b5cc5302 --- /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 000000000000..51065939f079 --- /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 5ca09b34aae1..64ad4ff119ae 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 31c4d66784c0..39298f56ba5c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -65,6 +65,12 @@ public void SetUpSteps() }); } + [Test] + public void TestBasic() + { + AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible); + } + [Test] [Ignore("Enable when first run setup is being displayed on first run.")] public void TestDoesntOpenOnSecondRun() @@ -178,7 +184,7 @@ public void TestResumeViaNotification() { 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); @@ -188,7 +194,7 @@ public void TestResumeViaNotification() 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/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index 514538161e8f..ec6e962c6af0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -111,6 +111,33 @@ public void TestIncompatibilityToggling() && 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() { 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 b5109aa58d26..5a4eeef4d90d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs @@ -11,11 +11,62 @@ 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/.editorconfig b/osu.Game/.editorconfig index 4107d1bb35a8..539cd56dab7c 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/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index f90208d0c0e7..1a9703f47822 100755 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -109,7 +109,7 @@ public BeatmapOnlineStatus Status public bool SamplesMatchPlaybackRate { get; set; } = true; - public double DistanceSpacing { get; set; } + public double DistanceSpacing { get; set; } = 1.0; public int BeatDivisor { get; set; } diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs new file mode 100644 index 000000000000..df0a69cb25c4 --- /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 54dcdc55e31b..ad0ff876e856 100755 --- a/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/DownloadProgressBar.cs @@ -28,11 +28,6 @@ public DownloadProgressBar(IBeatmapSetInfo beatmapSet) }, downloadTracker = new BeatmapDownloadTracker(beatmapSet), }; - AddInternal(progressBar = new ProgressBar(false) - { - Height = 0, - Alpha = 0, - }); AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 5cf6a1421cd1..94ddf75823fc 100755 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -278,6 +278,8 @@ public enum OsuSetting AlwaysPlayFirstComboBreak, FloatingComments, HUDVisibilityMode, + + // This has been migrated to the component itself. can be removed 20221027. ShowProgressGraph, ShowHealthDisplayWhenCantFail, FadePlayfieldWhenHealthLow, diff --git a/osu.Game/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs index b386a576ceb6..34ed8fc2b32d 100644 --- a/osu.Game/Extensions/TimeDisplayExtensions.cs +++ b/osu.Game/Extensions/TimeDisplayExtensions.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Humanizer; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; namespace osu.Game.Extensions @@ -40,12 +42,12 @@ public static LocalisableString ToFormattedDuration(this double milliseconds) => 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 017ea6ec32ff..817b8409e620 100755 --- 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 @@ public OsuScrollContainer(Direction scrollDirection = Direction.Vertical) { } + /// + /// 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/UserInterface/BasicSearchTextBox.cs b/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs new file mode 100644 index 000000000000..aa101d7e08d7 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs @@ -0,0 +1,26 @@ +// 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.Graphics; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class BasicSearchTextBox : SearchTextBox + { + public BasicSearchTextBox() + { + Add(new SpriteIcon + { + Icon = FontAwesome.Solid.Search, + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + Size = new Vector2(20), + }); + + TextFlow.Padding = new MarginPadding { Right = 35 }; + } + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 60e83f9c8126..a05c0cfab0b5 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -70,6 +70,15 @@ public Bindable Current set => slider.Current = value; } + /// + /// A custom step value for each key press which actuates a change on this control. + /// + public float KeyboardStep + { + get => slider.KeyboardStep; + set => slider.KeyboardStep = value; + } + public BindableBool Expanded { get; } = new BindableBool(); public override bool HandlePositionalInput => true; diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 4c8eaa5d62a9..884c3ecdfc89 100755 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -114,7 +114,7 @@ protected override bool OnClick(ClickEvent e) 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/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 20fa7d51481e..b1d46919382e 100755 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -131,22 +130,7 @@ public Color4 SelectionColour BackgroundColourSelected = SelectionColour }; - protected override ScrollContainer CreateScrollContainer(Direction direction) => new DropdownScrollContainer(direction); - - // Hotfix for https://github.com/ppy/osu/issues/17961 - public class DropdownScrollContainer : OsuScrollContainer - { - public DropdownScrollContainer(Direction direction) - : base(direction) - { - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - base.OnMouseDown(e); - return true; - } - } + protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); #region DrawableOsuDropdownMenuItem diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index bfdfd32fb3a6..a16adcbd57cb 100755 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; @@ -82,22 +81,7 @@ protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) return new DrawableOsuMenuItem(item); } - protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuMenuScrollContainer(direction); - - // Hotfix for https://github.com/ppy/osu/issues/17961 - public class OsuMenuScrollContainer : OsuScrollContainer - { - public OsuMenuScrollContainer(Direction direction) - : base(direction) - { - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - base.OnMouseDown(e); - return true; - } - } + protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical) { diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index dd9ed7c9e95e..cc0bc2dc46be 100755 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -1,12 +1,9 @@ // 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.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Resources.Localisation.Web; -using osuTK; using osuTK.Input; namespace osu.Game.Graphics.UserInterface @@ -18,16 +15,6 @@ public class SearchTextBox : FocusedTextBox public SearchTextBox() { Height = 35; - Add(new SpriteIcon - { - Icon = FontAwesome.Solid.Search, - Origin = Anchor.CentreRight, - Anchor = Anchor.CentreRight, - Margin = new MarginPadding { Right = 10 }, - Size = new Vector2(20), - }); - - TextFlow.Padding = new MarginPadding { Right = 35 }; PlaceholderText = HomeStrings.SearchPlaceholder; } diff --git a/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs index 6a9e8a5b8c83..0b76ff658f4f 100755 --- a/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SeekLimitedSearchTextBox.cs @@ -6,7 +6,7 @@ namespace osu.Game.Graphics.UserInterface /// /// A which does not handle left/right arrow keys for seeking. /// - public class SeekLimitedSearchTextBox : SearchTextBox + public class SeekLimitedSearchTextBox : BasicSearchTextBox { public override bool HandleLeftRightArrows => false; } diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs new file mode 100644 index 000000000000..dea44e6d996b --- /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.9f, 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(1f); + colourLight = colourLight.Darken(1f); + } + 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/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index aed3be20a04d..4780270f6689 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -8,39 +8,18 @@ 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 @@ public LocalisableString Text /// /// 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 @@ private void load(AudioManager audio) 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); - } - - protected override bool OnHover(HoverEvent e) - { - 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); + updateActiveState(); + base.LoadComplete(); } - private void updateState() + private void updateActiveState() { - 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 23ebc6e98de4..f535a32b3999 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -8,6 +8,7 @@ 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 @@ public override float Height [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 c6477d178106..f483e67b27c4 100755 --- 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 @@ public class SwitchButton : Checkbox private Color4 enabledColour; private Color4 disabledColour; + private Sample? sampleChecked; + private Sample? sampleUnchecked; + public SwitchButton() { Size = new Vector2(45, 20); @@ -70,13 +75,16 @@ public SwitchButton() } [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 @@ protected override void OnHoverLost(HoverLostEvent e) 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 1ce6333565b5..471e062a42cb 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -14,6 +14,11 @@ public static class CommonStrings /// 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 000000000000..54eed7633000 --- /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(@"llin_header"), @"获取谱面"); + + /// + /// ""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(@"llin_description"), @"我们通常称呼那些可游玩的关卡为“谱面”。 osu!并不自带任何谱面,因此这一步将帮助您获取您的第一张图。"); + + /// + /// "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(@"llin_tutorial_description"), @"如果您是名新手玩家,我们建议先通过教程来掌握一些游玩的基础知识。"); + + /// + /// "Get the osu! tutorial" + /// + public static LocalisableString TutorialButton => new TranslatableString(getKey(@"llin_tutorial_button"), @"获取osu!教程"); + + /// + /// "To get you started, we have some recommended beatmaps." + /// + public static LocalisableString BundledDescription => new TranslatableString(getKey(@"llin_bundled_description"), @"在开始之前,我们这有一些推荐的谱面可以试试。"); + + /// + /// "Get recommended beatmaps" + /// + public static LocalisableString BundledButton => new TranslatableString(getKey(@"llin_bundled_button"), @"获取推荐谱面"); + + /// + /// "You can also obtain more beatmaps from the main menu "browse" button at any time." + /// + public static LocalisableString ObtainMoreBeatmaps => new TranslatableString(getKey(@"llin_obtain_more_beatmaps"), @"您也可以通过主菜单的“浏览谱面”按钮获取更多谱面"); + + /// + /// "You currently have {0} beatmap(s) loaded!" + /// + public static LocalisableString CurrentlyLoadedBeatmaps(int beatmaps) => new TranslatableString(getKey(@"llin_currently_loaded_beatmaps"), @"你目前共有{0}张图!", beatmaps); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs index d6f11cfcad7d..319cce6c9300 100644 --- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs @@ -12,46 +12,69 @@ public static class FirstRunSetupOverlayStrings /// /// "Get started" /// - public static LocalisableString GetStarted => new TranslatableString(getKey(@"get_started"), @"开始"); + public static LocalisableString GetStarted => new TranslatableString(getKey(@"llin_get_started"), @"开始"); /// /// "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"), @"点击这里即可继续设置"); + public static LocalisableString ClickToResumeFirstRunSetupAtAnyPoint => + new TranslatableString(getKey(@"llin_click_to_resume_first_run_setup_at_any_point"), @"点此继续设置"); /// /// "First-run setup" /// - public static LocalisableString FirstRunSetupTitle => new TranslatableString(getKey(@"first_run_setup_title"), @"设置向导"); + public static LocalisableString FirstRunSetupTitle => new TranslatableString(getKey(@"llin_first_run_setup_title"), @"设置向导"); /// /// "Set up osu! to suit you" /// - public static LocalisableString FirstRunSetupDescription => new TranslatableString(getKey(@"first_run_setup_description"), @"让osu!符合你的风格"); + public static LocalisableString FirstRunSetupDescription => new TranslatableString(getKey(@"llin_first_run_setup_description"), @"让osu!更符合你的风格"); /// /// "Welcome" /// - public static LocalisableString WelcomeTitle => new TranslatableString(getKey(@"welcome_title"), @"欢迎"); + public static LocalisableString WelcomeTitle => new TranslatableString(getKey(@"llin_welcome_title"), @"欢迎"); /// /// "Welcome to the first-run setup guide! /// /// osu! is a very configurable game, and diving straight into the settings can sometimes be overwhelming. This guide will help you get the important choices out of the way to ensure a great first experience!" /// - public static LocalisableString WelcomeDescription => new TranslatableString(getKey(@"welcome_description"), @"欢迎来到设置向导! + public static LocalisableString WelcomeDescription => new TranslatableString(getKey(@"llin_welcome_description"), @"欢迎来到设置向导! -osu!是一款高度可自定义的游戏,直接点开设置有时可能会让你不知所措, 因此此向导会帮助你优化你的初次体验!"); +osu!是一款高度可自定义的游戏,直接点开设置有时可能会让你不知所措, 因此此向导会帮助你优化你的初次体验! + +PS:此界面的一些翻译由mfosu单独汉化,并不代表官方品质。"); /// /// "The size of the osu! user interface can be adjusted to your liking." /// - public static LocalisableString UIScaleDescription => new TranslatableString(getKey(@"ui_scale_description"), @"osu!的界面大小可以根据你的喜好自由调整"); + public static LocalisableString UIScaleDescription => new TranslatableString(getKey(@"llin_ui_scale_description"), @"osu!的界面大小可以根据你的喜好自由调整"); + + /// + /// "Behaviour" + /// + public static LocalisableString Behaviour => new TranslatableString(getKey(@"llin_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(@"llin_behaviour_description"), + @"为了改进游戏体验和易用性,一些新的行为被添加到了游戏中。 + +我们建议您先尝试一下新的默认设置,但如果您更喜欢旧版osu!端的体验,您可以在下面轻松地更改一些设置。"); + + /// + /// "New defaults" + /// + public static LocalisableString NewDefaults => new TranslatableString(getKey(@"llin_new_defaults"), @"新版默认值"); /// - /// "Next ({0})" + /// "Classic defaults" /// - public static LocalisableString Next(LocalisableString nextStepDescription) => new TranslatableString(getKey(@"next"), @"下一步 ({0})", nextStepDescription); + public static LocalisableString ClassicDefaults => new TranslatableString(getKey(@"llin_classic_defaults"), @"旧版默认值"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 13ca2c7e827f..7fa5e04af606 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -64,11 +64,6 @@ public static class GameplaySettingsStrings /// public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode"); - /// - /// "Show difficulty graph on progress bar" - /// - public static LocalisableString ShowDifficultyGraph => new TranslatableString(getKey(@"show_difficulty_graph"), @"在进度条上显示难度分布图"); - /// /// "Show health display even when you can't fail" /// diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 165b9c8fdf03..bab504385a05 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -62,7 +62,7 @@ public static class GeneralSettingsStrings /// /// "Run setup wizard" /// - public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard"); + public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"llin_run_setup_wizard"), @"打开设置向导"); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs new file mode 100644 index 000000000000..4ec5019a07a8 --- /dev/null +++ b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MessagePack; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// A started by the server when clients being to load. + /// Indicates how long until gameplay will forcefully start, excluding any users which have not completed loading, + /// and forcing progression of any clients that are blocking load due to user interaction. + /// + [MessagePackObject] + public class ForceGameplayStartCountdown : MultiplayerCountdown + { + } +} diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 3e6821b1cdb5..2f454ea8350f 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -93,14 +93,20 @@ public interface IMultiplayerClient Task UserModsChanged(int userId, IEnumerable mods); /// - /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. + /// Signals that the match is starting and the loading of gameplay should be started. This will *only* be sent to clients which are to begin loading at this point. /// Task LoadRequested(); /// - /// Signals that a match has started. All users in the state should begin gameplay as soon as possible. + /// Signals that loading of gameplay is to be aborted. /// - Task MatchStarted(); + Task LoadAborted(); + + /// + /// Signals that gameplay has started. + /// All users in the or states should begin gameplay as soon as possible. + /// + Task GameplayStarted(); /// /// Signals that the match has ended, all players have finished and results are ready to be displayed. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 967220abbf77..cae675b406b7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -69,10 +69,15 @@ public abstract class MultiplayerClient : Component, IMultiplayerClient, IMultip /// public virtual event Action? LoadRequested; + /// + /// Invoked when the multiplayer server requests loading of play to be aborted. + /// + public event Action? LoadAborted; + /// /// Invoked when the multiplayer server requests gameplay to be started. /// - public event Action? MatchStarted; + public event Action? GameplayStarted; /// /// Invoked when the multiplayer server has finished collating results. @@ -604,14 +609,27 @@ Task IMultiplayerClient.LoadRequested() return Task.CompletedTask; } - Task IMultiplayerClient.MatchStarted() + Task IMultiplayerClient.LoadAborted() + { + Scheduler.Add(() => + { + if (Room == null) + return; + + LoadAborted?.Invoke(); + }, false); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.GameplayStarted() { Scheduler.Add(() => { if (Room == null) return; - MatchStarted?.Invoke(); + GameplayStarted?.Invoke(); }, false); return Task.CompletedTask; diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs index 81190e64c91f..dbf2ab667b30 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -14,6 +14,7 @@ namespace osu.Game.Online.Multiplayer /// [MessagePackObject] [Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(1, typeof(ForceGameplayStartCountdown))] public abstract class MultiplayerCountdown { /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index f0b7dcbff80b..50e539e8a6d1 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -65,5 +65,21 @@ public override bool Equals(object obj) } public override int GetHashCode() => UserID.GetHashCode(); + + /// + /// Whether this user has finished loading and can start gameplay. + /// + public bool CanStartGameplay() + { + switch (State) + { + case MultiplayerUserState.Loaded: + case MultiplayerUserState.ReadyForGameplay: + return true; + + default: + return false; + } + } } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs index c467ff84bbae..d1369a7970e8 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs @@ -29,10 +29,16 @@ public enum MultiplayerUserState WaitingForLoad, /// - /// The user's client has marked itself as loaded and ready to begin gameplay. + /// The user has marked itself as loaded, but may still be adjusting settings prior to being ready for gameplay. + /// Players remaining in this state for an extended period of time will be automatically transitioned to the state by the server. /// Loaded, + /// + /// The user has finished adjusting settings and is ready to start gameplay. + /// + ReadyForGameplay, + /// /// The user is currently playing in a game. This is a reserved state, and is set by the server. /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 7e62908ecdf5..4dc23d8b850e 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -54,7 +54,8 @@ private void load(IAPIProvider api) connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); - connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); + connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index c6ddc03564bc..e44dad1db510 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -10,8 +10,8 @@ public ProductionEndpointConfiguration() WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; - SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; - MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; + SpectatorEndpointUrl = "https://spectator2.ppy.sh/spectator"; + MultiplayerEndpointUrl = "https://spectator2.ppy.sh/multiplayer"; } } } diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 156f916cef6f..d1f0ba725f84 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -24,7 +24,8 @@ internal static class SignalRWorkaroundTypes (typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)), (typeof(TeamVersusUserState), typeof(MatchUserState)), - (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)) + (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)), + (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown)) }; } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs index e4fda9d9c3a5..1f9a63e3b90b 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs @@ -72,7 +72,8 @@ private void load() Size = new Vector2(12), Icon = getIconForCardSize(Value) } - } + }, + new HoverClickSounds(HoverSampleSet.TabSelect) }; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 2474515802d7..eeaa31a01301 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -163,7 +163,7 @@ private void load(OverlayColourProvider colourProvider, OsuConfigManager config) public void TakeFocus() => textBox.TakeFocus(); - private class BeatmapSearchTextBox : SearchTextBox + private class BeatmapSearchTextBox : BasicSearchTextBox { /// /// Any time the text box receives key events (even while masked). diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 94ea53c46954..f71808320830 100755 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -119,7 +119,7 @@ public ScoreInfo Score 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/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index b9d3854066a7..bd63c997df21 100755 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet; @@ -24,9 +23,6 @@ public class BeatmapSetOverlay : OnlineOverlay private readonly Bindable beatmapSet = new Bindable(); - // receive input outside our bounds so we can trigger a close event on ourselves. - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - public BeatmapSetOverlay() : base(OverlayColourScheme.Blue) { @@ -71,12 +67,6 @@ protected override void PopOutComplete() beatmapSet.Value = null; } - protected override bool OnClick(ClickEvent e) - { - Hide(); - return true; - } - public void FetchAndShowBeatmap(int beatmapId) { beatmapSet.Value = null; diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index f515ed1f465c..beb953917e75 100755 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -181,7 +181,7 @@ protected override void PopOut() base.PopOut(); } - private class HeaderSearchTextBox : SearchTextBox + private class HeaderSearchTextBox : BasicSearchTextBox { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs index eb4b97069cb0..1f18d181cb9f 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 @@ public abstract class FirstRunSetupScreen : Screen 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 000000000000..e6c506884fda --- /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 = "如果您的设备上装有稳定版,您也可以选择导入这些谱面。", + 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 000000000000..dc3d40ad9528 --- /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 ef48d9ced5de..862506add2ba 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -27,6 +27,7 @@ namespace osu.Game.Overlays.FirstRunSetup { + [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] public class ScreenUIScale : FirstRunSetupScreen { [BackgroundDependencyLoader] @@ -98,6 +99,8 @@ protected override void Update() private class NestedSongSelect : PlaySongSelect { protected override bool ControlGlobalMusic => false; + + public override bool? AllowTrackAdjustments => false; } private class PinnedMainMenu : MainMenu diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 39da180f4057..10e15a7555ff 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 dc1ae2be37c7..75778e6c4d9f 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,7 +25,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osu.Game.Screens.Menu; -using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Overlays { @@ -44,8 +44,8 @@ public class FirstRunSetupOverlay : ShearedOverlayContainer private ScreenStack? stack; - public PurpleTriangleButton NextButton = null!; - public DangerousTriangleButton BackButton = null!; + public ShearedButton NextButton = null!; + public ShearedButton BackButton = null!; private readonly Bindable showFirstRunSetup = new Bindable(); @@ -56,10 +56,12 @@ public class FirstRunSetupOverlay : ShearedOverlayContainer /// 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!; @@ -69,7 +71,7 @@ public class FirstRunSetupOverlay : ShearedOverlayContainer private Container content = null!; [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; @@ -81,7 +83,11 @@ private void load() Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 50 }, + Padding = new MarginPadding + { + Horizontal = 70 * 1.2f, + Bottom = 20, + }, Child = new InputBlockingContainer { Masking = true, @@ -102,7 +108,7 @@ private void load() Padding = new MarginPadding { Vertical = 20, - Horizontal = 20, + Horizontal = 70, }, } }, @@ -114,14 +120,15 @@ private void load() { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Width = 0.98f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Margin = new MarginPadding { Vertical = PADDING }, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, ColumnDimensions = new[] { - new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), }, RowDimensions = new[] { @@ -131,21 +138,25 @@ private void load() { new[] { - BackButton = new DangerousTriangleButton + Empty(), + BackButton = new ShearedButton(300) { - Width = 200, Text = CommonStrings.Back, Action = showPreviousStep, Enabled = { Value = false }, + DarkerColour = colours.Pink2, + LighterColour = colours.Pink1, }, - Empty(), - NextButton = new PurpleTriangleButton + NextButton = new ShearedButton(0) { RelativeSizeAxes = Axes.X, Width = 1, Text = FirstRunSetupOverlayStrings.GetStarted, + DarkerColour = ColourProvider.Colour2, + LighterColour = ColourProvider.Colour1, Action = showNextStep - } + }, + Empty(), }, } }); @@ -286,7 +297,7 @@ private void showNextStep() if (currentStepIndex < steps.Length) { - stack.Push((Screen)Activator.CreateInstance(steps[currentStepIndex.Value].ScreenType)); + stack.Push((Screen)Activator.CreateInstance(steps[currentStepIndex.Value])); } else { @@ -304,23 +315,24 @@ private void updateButtons() BackButton.Enabled.Value = currentStepIndex > 0; NextButton.Enabled.Value = currentStepIndex != null; - if (currentStepIndex != null) - { - NextButton.Text = currentStepIndex + 1 < steps.Length - ? FirstRunSetupOverlayStrings.Next(steps[currentStepIndex.Value + 1].Description) - : CommonStrings.Finish; - } - } + if (currentStepIndex == null) + return; - private class FirstRunStep - { - public readonly Type ScreenType; - public readonly LocalisableString Description; + bool isFirstStep = currentStepIndex == 0; + bool isLastStep = currentStepIndex == steps.Length - 1; - public FirstRunStep(Type screenType, LocalisableString description) + if (isFirstStep) { - ScreenType = screenType; - Description = description; + BackButton.Text = CommonStrings.Back; + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + } + else + { + 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/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 1157c0c0c6d0..018922c07468 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -53,6 +53,9 @@ public Func? Filter } 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 @@ private void updateFilter() 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/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 8a8307110946..ffd6e9a52cc5 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -13,7 +13,9 @@ 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 @@ public Func IsValidMod 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 @@ private void load() 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 @@ private void load() } } + 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,7 +177,7 @@ protected override void LoadComplete() updateSelectionFromBindable(); }, true); - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) { column.SelectedMods.BindValueChanged(updateBindableFromSelection); } @@ -191,7 +202,7 @@ private void updateMultiplier() private void updateAvailableMods() { - foreach (var column in columnFlow) + foreach (var column in columnFlow.Columns) column.Filter = isValidMod; } @@ -244,7 +255,7 @@ private void updateSelectionFromBindable() // 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) + foreach (var column in columnFlow.Columns) column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray(); } @@ -265,7 +276,7 @@ private void updateBindableFromSelection(ValueChangedEvent> m } protected virtual IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) - => columnFlow.SelectMany(column => column.SelectedMods.Value).ToArray(); + => columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray(); protected override void PopIn() { @@ -280,7 +291,8 @@ protected override void PopIn() 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); @@ -301,27 +313,68 @@ protected override void PopOut() { 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() @@ -341,6 +394,63 @@ protected override void Update() } } + 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 aba47d542351..d27f97f3d232 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/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index 66adbeebe8fd..46c66b4baef7 100755 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -56,7 +56,7 @@ protected override void LoadComplete() Collection = collectionDropdown.Current.Value?.Collection }; - public class FilterTextBox : SearchTextBox + public class FilterTextBox : BasicSearchTextBox { protected override bool AllowCommit => true; diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs index 77e54b18abcb..1f11b98881f2 100644 --- a/osu.Game/Overlays/OverlayRulesetTabItem.cs +++ b/osu.Game/Overlays/OverlayRulesetTabItem.cs @@ -5,18 +5,18 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osuTK.Graphics; using osuTK; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { - public class OverlayRulesetTabItem : TabItem + public class OverlayRulesetTabItem : TabItem, IHasTooltip { private Color4 accentColour; @@ -26,7 +26,7 @@ protected virtual Color4 AccentColour set { accentColour = value; - text.FadeColour(value, 120, Easing.OutQuint); + icon.FadeColour(value, 120, Easing.OutQuint); } } @@ -35,7 +35,9 @@ protected virtual Color4 AccentColour [Resolved] private OverlayColourProvider colourProvider { get; set; } - private readonly OsuSpriteText text; + private readonly Drawable icon; + + public LocalisableString TooltipText => Value.Name; public OverlayRulesetTabItem(RulesetInfo value) : base(value) @@ -48,15 +50,14 @@ public OverlayRulesetTabItem(RulesetInfo value) { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(3, 0), - Child = text = new OsuSpriteText + Spacing = new Vector2(4, 0), + Child = icon = new ConstrainedIconContainer { - Origin = Anchor.Centre, Anchor = Anchor.Centre, - Text = value.Name, - Font = OsuFont.GetFont(size: 18), - ShadowColour = Color4.Black.Opacity(0.75f) - } + Origin = Anchor.Centre, + Size = new Vector2(20f), + Icon = value.CreateInstance().CreateIcon(), + }, }, new HoverClickSounds() }); @@ -70,7 +71,7 @@ protected override void LoadComplete() Enabled.BindValueChanged(_ => updateState(), true); } - public override bool PropagatePositionalInputSubTree => Enabled.Value && !Active.Value && base.PropagatePositionalInputSubTree; + public override bool PropagatePositionalInputSubTree => Enabled.Value && base.PropagatePositionalInputSubTree; protected override bool OnHover(HoverEvent e) { @@ -91,7 +92,6 @@ protected override void OnHoverLost(HoverLostEvent e) private void updateState() { - text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Medium); AccentColour = Enabled.Value ? getActiveColour() : colourProvider.Foreground1; } diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index 39188c94b647..df84dc178311 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -149,7 +149,7 @@ public TabButton(T value) } }); - AddInternal(new HoverClickSounds()); + AddInternal(new HoverClickSounds(HoverSampleSet.TabSelect)); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs index 3d20fba54259..4a44e285bfb9 100755 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -23,7 +26,7 @@ public bool IsDefault isDefault = value; - icon.FadeTo(isDefault ? 1 : 0, 200, Easing.OutQuint); + icon.Alpha = isDefault ? 1 : 0; } } @@ -42,15 +45,20 @@ protected override Color4 AccentColour public ProfileRulesetTabItem(RulesetInfo value) : base(value) { - Add(icon = new SpriteIcon + Add(icon = new DefaultRulesetIcon { Alpha = 0 }); + } + + public class DefaultRulesetIcon : SpriteIcon, IHasTooltip + { + public LocalisableString TooltipText => UsersStrings.ShowEditDefaultPlaymodeIsDefaultTooltip; + + public DefaultRulesetIcon() { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Alpha = 0, - AlwaysPresent = true, - Icon = FontAwesome.Solid.Star, - Size = new Vector2(12), - }); + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + Icon = FontAwesome.Solid.Star; + Size = new Vector2(12); + } } } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index f67ad7fac80c..130901fd9e21 100755 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -32,7 +32,9 @@ public ProfileHeader() User.ValueChanged += e => updateDisplay(e.NewValue); TabControl.AddItem(LayoutStrings.HeaderUsersShow); - TabControl.AddItem(LayoutStrings.HeaderUsersModding); + + // todo: pending implementation. + // TabControl.AddItem(LayoutStrings.HeaderUsersModding); //topheaderContainer.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 380fda0285f0..6a0e643355e3 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -126,7 +126,7 @@ public void ShowInfo(GetSpotlightRankingsResponse response) 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 17c17b1f1a7d..bdbd2942d1df 100755 --- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs @@ -5,6 +5,7 @@ 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 @@ protected override RankingsTableColumn[] CreateUniqueHeaders() => new[] 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 e7afa4850273..61191dcacff1 100755 --- 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 5029c6a61716..e2e00813bd12 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs @@ -28,6 +28,7 @@ private void load(OsuConfigManager config, OsuConfigManager osuConfig) }, 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 d4e4fd571d82..5231ce121174 100755 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -21,6 +21,7 @@ private void load(OsuConfigManager config) { 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 ba9779d65096..0d31e708805f 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -25,11 +25,7 @@ private void load(OsuConfigManager config) }, new SettingsCheckbox { - LabelText = GameplaySettingsStrings.ShowDifficultyGraph, - Current = config.GetBindable(OsuSetting.ShowProgressGraph) - }, - new SettingsCheckbox - { + ClassicDefault = false, LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), Keywords = new[] { "hp", "bar" } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs index 962572ca6e7d..83ea655601fa 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs @@ -19,7 +19,7 @@ private void load(OsuConfigManager config) { Children = new Drawable[] { - new SettingsSlider + new SettingsSlider> { LabelText = SkinSettingsStrings.GameplayCursorSize, Current = config.GetBindable(OsuSetting.GameplayCursorSize), diff --git a/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs index c136ca6a196f..60849cd6d4d6 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/JoystickSettings.cs @@ -20,6 +20,10 @@ public class JoystickSettings : SettingsSubsection private SettingsSlider deadzoneSlider; + private Bindable handlerDeadzone; + + private Bindable localDeadzone; + public JoystickSettings(JoystickHandler joystickHandler) { this.joystickHandler = joystickHandler; @@ -28,6 +32,10 @@ public JoystickSettings(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 @@ -40,7 +48,7 @@ private void load() LabelText = JoystickSettingsStrings.DeadzoneThreshold, KeyboardStep = 0.01f, DisplayAsPercentage = true, - Current = joystickHandler.DeadzoneThreshold, + Current = localDeadzone, }, }; } @@ -51,6 +59,17 @@ protected override void 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/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs index 8aeb440be110..c8a46162afae 100644 --- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.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; +using System.Globalization; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; @@ -9,8 +11,9 @@ namespace osu.Game.Overlays.Settings.Sections /// /// A slider intended to show a "size" multiplier number, where 1x is 1.0. /// - internal class SizeSlider : OsuSliderBar + public class SizeSlider : OsuSliderBar + where T : struct, IEquatable, IComparable, IConvertible, IFormattable { - public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x", NumberFormatInfo.CurrentInfo); } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 6e1558f7d713..dd1b9cc2a0d4 100755 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -24,7 +24,7 @@ private void load(OsuConfigManager config) LabelText = UserInterfaceStrings.CursorRotation, Current = config.GetBindable(OsuSetting.CursorRotation) }, - new SettingsSlider + new SettingsSlider> { LabelText = UserInterfaceStrings.MenuCursorSize, Current = config.GetBindable(OsuSetting.MenuCursorSize), @@ -37,6 +37,7 @@ private void load(OsuConfigManager config) }, 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 57977da4cbca..608afe30dd06 100755 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -32,6 +32,7 @@ private void load(OsuConfigManager config) { 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 f7824d79e797..afcd41af222e 100755 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,6 +30,8 @@ public abstract class SettingsItem : Container, IFilterable, ISettingsItem, I /// 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,7 +98,21 @@ public virtual Bindable Current 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; } @@ -122,6 +138,32 @@ public bool MatchingFilter 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 b5f3d8e003c7..cfb0212b8c49 100755 --- 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; @@ -58,7 +59,7 @@ public bool MatchingFilter public bool FilteringActive { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private SettingsPanel settingsPanel { get; set; } protected SettingsSection() @@ -131,7 +132,7 @@ private void load(OverlayColourProvider colourProvider) }, }); - selectedSection = settingsPanel.CurrentSection.GetBoundCopy(); + selectedSection = settingsPanel?.CurrentSection.GetBoundCopy() ?? new Bindable(this); selectedSection.BindValueChanged(_ => updateContentFade(), true); } @@ -152,7 +153,10 @@ protected override void OnHoverLost(HoverLostEvent e) 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 42560c18d6ed..45fac10a3944 100755 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -23,6 +23,7 @@ public class SettingsOverlay : SettingsPanel, INamedOverlayComponent protected override IEnumerable CreateSections() => new SettingsSection[] { + // This list should be kept in sync with ScreenBehaviour. new MfSection(createSubPanel(new MfSettingsPanel())), new GeneralSection(), new SkinSection(), diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index bbd2f079aaae..97266562e46f 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -100,9 +100,13 @@ public override double DifficultyValue() double difficulty = 0; double weight = 1; + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // These sections will not contribute to the difficulty. + var peaks = GetCurrentStrainPeaks().Where(p => p > 0); + // Difficulty is the weighted sum of the highest strains from every section. // We're sorting from highest to lowest strain. - foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d)) + foreach (double strain in peaks.OrderByDescending(d => d)) { difficulty += strain * weight; weight *= DecayWeight; diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs new file mode 100644 index 000000000000..0505f9ab0e80 --- /dev/null +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -0,0 +1,162 @@ +// 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.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings.Sections; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + /// + /// Represents a for rulesets with the concept of distances between objects. + /// + /// The base type of supported objects. + [Cached(typeof(IDistanceSnapProvider))] + public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider + where TObject : HitObject + { + protected Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) + { + MinValue = 0.1, + MaxValue = 6.0, + Precision = 0.01, + }; + + IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + + protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; } + + private ExpandableSlider> distanceSpacingSlider; + private bool distanceSpacingScrollActive; + + protected DistancedHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer + { + Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Child = new EditorToolboxGroup("snapping") + { + Child = distanceSpacingSlider = new ExpandableSlider> + { + Current = { BindTarget = DistanceSpacingMultiplier }, + KeyboardStep = 0.1f, + } + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (!DistanceSpacingMultiplier.Disabled) + { + DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; + DistanceSpacingMultiplier.BindValueChanged(v => + { + distanceSpacingSlider.ContractedLabelText = $"D. S. ({v.NewValue:0.##x})"; + distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})"; + EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue; + }, true); + } + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (!DistanceSpacingMultiplier.Disabled && e.ControlPressed && e.AltPressed && !e.Repeat) + { + RightSideToolboxContainer.Expanded.Value = true; + distanceSpacingScrollActive = true; + return true; + } + + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + if (!DistanceSpacingMultiplier.Disabled && distanceSpacingScrollActive && (!e.AltPressed || !e.ControlPressed)) + { + RightSideToolboxContainer.Expanded.Value = false; + distanceSpacingScrollActive = false; + } + } + + protected override bool OnScroll(ScrollEvent e) + { + if (distanceSpacingScrollActive) + { + DistanceSpacingMultiplier.Value += e.ScrollDelta.Y * (e.IsPrecise ? 0.01f : 0.1f); + return true; + } + + return base.OnScroll(e); + } + + public virtual float GetBeatSnapDistanceAt(HitObject referenceObject) + { + return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor); + } + + public virtual float DurationToDistance(HitObject referenceObject, double duration) + { + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); + } + + public virtual double DistanceToDuration(HitObject referenceObject, float distance) + { + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; + } + + public virtual double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) + => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; + + public virtual float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) + { + double startTime = referenceObject.StartTime; + + double actualDuration = startTime + DistanceToDuration(referenceObject, distance); + + double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); + + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); + + // we don't want to exceed the actual duration and snap to a point in the future. + // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. + if (snappedEndTime > actualDuration + 1) + snappedEndTime -= beatLength; + + return DurationToDistance(referenceObject, snappedEndTime - startTime); + } + + protected class ExpandingToolboxContainer : ExpandingContainer + { + protected override double HoverExpansionDelay => 250; + + public ExpandingToolboxContainer() + : base(130, 250) + { + RelativeSizeAxes = Axes.Y; + Padding = new MarginPadding { Left = 10 }; + + FillFlow.Spacing = new Vector2(10); + } + } + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 599811206dec..c0ab60d7fc3f 100755 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -381,44 +381,6 @@ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpac return new SnapResult(screenSpacePosition, targetTime, playfield); } - public override float GetBeatSnapDistanceAt(HitObject referenceObject) - { - return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor); - } - - public override float DurationToDistance(HitObject referenceObject, double duration) - { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); - } - - public override double DistanceToDuration(HitObject referenceObject, float distance) - { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; - } - - public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) - => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; - - public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) - { - double startTime = referenceObject.StartTime; - - double actualDuration = startTime + DistanceToDuration(referenceObject, distance); - - double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); - - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); - - // we don't want to exceed the actual duration and snap to a point in the future. - // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. - if (snappedEndTime > actualDuration + 1) - snappedEndTime -= beatLength; - - return DurationToDistance(referenceObject, snappedEndTime - startTime); - } - #endregion private class LeftToolboxFlow : ExpandingButtonContainer @@ -471,16 +433,6 @@ protected HitObjectComposer() public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); - public abstract float GetBeatSnapDistanceAt(HitObject referenceObject); - - public abstract float DurationToDistance(HitObject referenceObject, double duration); - - public abstract double DistanceToDuration(HitObject referenceObject, float distance); - - public abstract double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); - - public abstract float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); - #endregion } } \ No newline at end of file diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs new file mode 100644 index 000000000000..c6e866561e97 --- /dev/null +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -0,0 +1,58 @@ +// 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.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit +{ + public interface IDistanceSnapProvider : IPositionSnapProvider + { + /// + /// The spacing multiplier applied to beat snap distances. + /// + /// + IBindable DistanceSpacingMultiplier { get; } + + /// + /// Retrieves the distance between two points within a timing point that are one beat length apart. + /// + /// An object to be used as a reference point for this operation. + /// The distance between two points residing in the timing point that are one beat length apart. + float GetBeatSnapDistanceAt(HitObject referenceObject); + + /// + /// Converts a duration to a distance. + /// + /// An object to be used as a reference point for this operation. + /// The duration to convert. + /// A value that represents as a distance in the timing point. + float DurationToDistance(HitObject referenceObject, double duration); + + /// + /// Converts a distance to a duration. + /// + /// An object to be used as a reference point for this operation. + /// The distance to convert. + /// A value that represents as a duration in the timing point. + double DistanceToDuration(HitObject referenceObject, float distance); + + /// + /// Converts a distance to a snapped duration. + /// + /// An object to be used as a reference point for this operation. + /// The distance to convert. + /// A value that represents as a duration snapped to the closest beat of the timing point. + double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); + + /// + /// Converts an unsnapped distance to a snapped distance. + /// The returned distance will always be floored (as to never exceed the provided . + /// + /// An object to be used as a reference point for this operation. + /// The distance to convert. + /// A value that represents snapped to the closest beat of the timing point. + float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); + } +} diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index 743a2f41fcd2..8a179ed4249e 100755 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Edit @@ -24,45 +23,5 @@ public interface IPositionSnapProvider /// The screen-space position to be snapped. /// The position post-snapping. Time will always be null. SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition); - - /// - /// Retrieves the distance between two points within a timing point that are one beat length apart. - /// - /// An object to be used as a reference point for this operation. - /// The distance between two points residing in the timing point that are one beat length apart. - float GetBeatSnapDistanceAt(HitObject referenceObject); - - /// - /// Converts a duration to a distance. - /// - /// An object to be used as a reference point for this operation. - /// The duration to convert. - /// A value that represents as a distance in the timing point. - float DurationToDistance(HitObject referenceObject, double duration); - - /// - /// Converts a distance to a duration. - /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration in the timing point. - double DistanceToDuration(HitObject referenceObject, float distance); - - /// - /// Converts a distance to a snapped duration. - /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration snapped to the closest beat of the timing point. - double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); - - /// - /// Converts an unsnapped distance to a snapped distance. - /// The returned distance will always be floored (as to never exceed the provided . - /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents snapped to the closest beat of the timing point. - float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); } } diff --git a/osu.Game/Rulesets/Mods/ModDance.cs b/osu.Game/Rulesets/Mods/ModDance.cs index 05abcaf671dc..a934036fb7bf 100644 --- a/osu.Game/Rulesets/Mods/ModDance.cs +++ b/osu.Game/Rulesets/Mods/ModDance.cs @@ -4,24 +4,11 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Replays; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI; using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModDance : ModDance, IApplicableToDrawableRuleset - where T : HitObject - { - //Copied from ModAutoplay.cs - public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); - } - //Copy end - } - - public abstract class ModDance : Mod + public abstract class ModDance : Mod, ICreateReplayData, IApplicableFailOverride { public override Type[] IncompatibleMods => new[] { @@ -33,6 +20,8 @@ public abstract class ModDance : Mod [SettingSource("保存Dance回放")] public Bindable SaveScore { get; } = new BindableBool(); + public override bool UserPlayable => false; + public override ModType Type => ModType.Automation; public override bool RequiresConfiguration => true; @@ -45,5 +34,24 @@ public abstract class ModDance : Mod public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => CreateReplayScore(beatmap); #pragma warning restore 618 //Copy end + + public ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + { + var score = CreateReplayScore(beatmap, mods); + return new ModReplayData(score.Replay, new ModCreatedUser{ Username = score.ScoreInfo.User.Username }); + } + + public bool PerformFail() => false; + + public bool RestartOnFail => false; + + /// + /// 表示是否是事实生成的回放
+ /// 如果是,去除ENDCHAR并记录
+ /// 如果不是,则不要记录 + ///
+ public readonly string ENDCHAR = " "; + + public readonly string ENDCHARREPLACE = ""; } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index ba614900c0f9..3100d26a55b9 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -15,7 +15,7 @@ public static class SliderPathExtensions /// /// Snaps the provided 's duration using the . /// - public static void SnapTo(this THitObject hitObject, IPositionSnapProvider? snapProvider) + public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider) where THitObject : HitObject, IHasPath { hitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance; diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index a92c30e593d3..0f51560476d0 100755 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -43,11 +43,11 @@ protected override void ApplyResultInternal(JudgementResult result) 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 @@ protected override void RevertResultInternal(JudgementResult result) /// 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 898c252f6114..94d239cb9378 100755 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -376,13 +376,13 @@ private ScoreRank rankFrom(double acc) { 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/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 05bf405f3cf2..5568c1551457 100755 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -44,7 +45,7 @@ public abstract class DistanceSnapGrid : CompositeDrawable protected OsuColour Colours { get; private set; } [Resolved] - protected IPositionSnapProvider SnapProvider { get; private set; } + protected IDistanceSnapProvider SnapProvider { get; private set; } [Resolved] private EditorBeatmap beatmap { get; set; } @@ -52,6 +53,8 @@ public abstract class DistanceSnapGrid : CompositeDrawable [Resolved] private BindableBeatDivisor beatDivisor { get; set; } + private IBindable distanceSpacingMultiplier; + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); private readonly double? endTime; @@ -81,12 +84,15 @@ protected override void LoadComplete() { base.LoadComplete(); - beatDivisor.BindValueChanged(_ => updateSpacing(), true); + beatDivisor.BindValueChanged(_ => updateSpacing()); + + distanceSpacingMultiplier = SnapProvider.DistanceSpacingMultiplier.GetBoundCopy(); + distanceSpacingMultiplier.BindValueChanged(_ => updateSpacing(), true); } private void updateSpacing() { - DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject); + DistanceSpacing = (float)(SnapProvider.GetBeatSnapDistanceAt(ReferenceObject) * distanceSpacingMultiplier.Value); if (endTime == null) MaxIntervals = int.MaxValue; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 8729c8cf7eba..746e85bf7ce0 100755 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -15,7 +15,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -316,15 +315,5 @@ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition private double getTimeFromPosition(Vector2 localPosition) => (localPosition.X / Content.DrawWidth) * track.Length; - - public float GetBeatSnapDistanceAt(HitObject referenceObject) => throw new NotImplementedException(); - - public float DurationToDistance(HitObject referenceObject, double duration) => throw new NotImplementedException(); - - public double DistanceToDuration(HitObject referenceObject, float distance) => throw new NotImplementedException(); - - public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException(); - - public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 109f6432e4e3..a96b5d1c5aba 100755 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -109,7 +109,7 @@ public OsuLogo() AutoSizeAxes = Axes.Both, Children = new Drawable[] { - logoBounceContainer = new Container + logoBounceContainer = new DragContainer { AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -402,5 +402,28 @@ public void Impact() 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/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 8a45af39f3e0..8b08df4bb011 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -129,7 +129,7 @@ private void load([CanBeNull] IdleTracker idleTracker) { RelativeSizeAxes = Axes.X, Height = Header.HEIGHT, - Child = searchTextBox = new SearchTextBox + Child = searchTextBox = new BasicSearchTextBox { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index c84fcff11ec6..1a51aebb7673 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -77,7 +77,7 @@ protected override void Dispose(bool isDisposing) private void onRoomUpdated() => Scheduler.AddOnce(() => { - bool countdownActive = multiplayerClient.Room?.Countdown != null; + bool countdownActive = multiplayerClient.Room?.Countdown is MatchStartCountdown; if (countdownActive) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 51beb44452e9..eaea70b7aa2b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -55,7 +55,21 @@ protected override void LoadComplete() private void onRoomUpdated() => Scheduler.AddOnce(() => { - if (countdown != room?.Countdown) + MultiplayerCountdown newCountdown; + + switch (room?.Countdown) + { + case MatchStartCountdown _: + newCountdown = room.Countdown; + break; + + // Clear the countdown with any other (including non-null) countdown values. + default: + newCountdown = null; + break; + } + + if (newCountdown != countdown) { countdown = room?.Countdown; countdownChangeTime = Time.Current; @@ -92,7 +106,7 @@ void onCountdownTick() { updateButtonText(); - int secondsRemaining = countdownTimeRemaining.Seconds; + int secondsRemaining = (int)countdownTimeRemaining.TotalSeconds; playTickSound(secondsRemaining); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 46b000c9e5ac..8a72485dc293 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; @@ -20,6 +21,7 @@ protected override void LoadComplete() base.LoadComplete(); client.RoomUpdated += onRoomUpdated; + client.LoadAborted += onLoadAborted; onRoomUpdated(); } @@ -35,6 +37,16 @@ private void onRoomUpdated() transitionFromResults(); } + private void onLoadAborted() + { + // If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens. + if (!this.IsCurrentScreen()) + { + Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important); + this.MakeCurrent(); + } + } + public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); @@ -42,9 +54,15 @@ public override void OnResuming(ScreenTransitionEvent e) if (client.Room == null) return; + Debug.Assert(client.LocalUser != null); + if (!(e.Last is MultiplayerPlayerLoader playerLoader)) return; + // Nothing needs to be done if already in the idle state (e.g. via load being aborted by the server). + if (client.LocalUser.State == MultiplayerUserState.Idle) + return; + // If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay. if (!playerLoader.GameplayPassed) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 70f8f1b7525e..02ff040a9440 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -115,7 +115,7 @@ protected override void LoadAsyncComplete() if (!ValidForResume) return; // token retrieval may have failed. - client.MatchStarted += onMatchStarted; + client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; ScoreProcessor.HasCompleted.BindValueChanged(completed => @@ -144,10 +144,12 @@ protected override void LoadComplete() protected override void StartGameplay() { - // block base call, but let the server know we are ready to start. - loadingDisplay.Show(); - - client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); + if (client.LocalUser?.State == MultiplayerUserState.Loaded) + { + // block base call, but let the server know we are ready to start. + loadingDisplay.Show(); + client.ChangeState(MultiplayerUserState.ReadyForGameplay); + } } private void failAndBail(string message = null) @@ -175,7 +177,7 @@ private void adjustLeaderboardPosition() leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); } - private void onMatchStarted() => Scheduler.Add(() => + private void onGameplayStarted() => Scheduler.Add(() => { if (!this.IsCurrentScreen()) return; @@ -223,7 +225,7 @@ protected override void Dispose(bool isDisposing) if (client != null) { - client.MatchStarted -= onMatchStarted; + client.GameplayStarted -= onGameplayStarted; client.ResultsReady -= onResultsReady; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 53dea83f18ec..7f01bd64ab86 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -11,6 +15,9 @@ public class MultiplayerPlayerLoader : PlayerLoader { public bool GameplayPassed => player?.GameplayState.HasPassed == true; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + private Player player; public MultiplayerPlayerLoader(Func createPlayer) @@ -18,6 +25,31 @@ public MultiplayerPlayerLoader(Func createPlayer) { } + protected override bool ReadyForGameplay => + base.ReadyForGameplay + // The server is forcefully starting gameplay. + || multiplayerClient.LocalUser?.State == MultiplayerUserState.Playing; + + protected override void OnPlayerLoaded() + { + base.OnPlayerLoaded(); + + multiplayerClient.ChangeState(MultiplayerUserState.Loaded) + .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); + } + + private void failAndBail(string message = null) + { + if (!string.IsNullOrEmpty(message)) + Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important); + + Schedule(() => + { + if (this.IsCurrentScreen()) + this.Exit(); + }); + } + public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index 2616b07c1f5b..658fc43e8d61 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -112,6 +112,7 @@ public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availab break; case MultiplayerUserState.Loaded: + case MultiplayerUserState.ReadyForGameplay: text.Text = "loaded"; icon.Icon = FontAwesome.Solid.DotCircle; icon.Colour = colours.YellowLight; diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index 63c583aeafee..6a600cd2d75d 100755 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -43,8 +43,7 @@ public BreakInfo() 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("评分"), diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 589ee1b674ef..1cfe1eb7b389 100755 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -66,7 +66,7 @@ public class HUDOverlay : Container, IKeyBindingHandler 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 @@ protected override void LoadComplete() 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 @@ private void updateVisibility() 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 0b50458ae967..6b7417fc78ac 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -457,7 +457,7 @@ private void onBreakTimeChanged(ValueChangedEvent isBreakTime) 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; } @@ -813,6 +813,8 @@ private bool onFail() 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. @@ -946,7 +948,7 @@ public override void OnEntering(ScreenTransitionEvent e) failAnimationLayer.Background = b; }); - HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime); + HUDOverlay.IsPlaying.BindTo(localUserPlaying); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); @@ -1042,12 +1044,12 @@ public override bool OnExiting(ScreenExitEvent e) /// Imports the player's to the local database. /// /// The to import. - /// 绕过分数检查,用于DanceMod + /// 绕过分数检查,用于DanceMod /// The imported score. - protected virtual Task ImportScore(Score score, bool bypassCheck = false) + protected virtual Task ImportScore(Score score, bool haveDanceMod = false) { // Replays are already populated and present in the game's database, so should not be re-imported. - if (DrawableRuleset.ReplayScore != null || bypassCheck) + if (DrawableRuleset.ReplayScore != null && !haveDanceMod) return Task.CompletedTask; LegacyByteArrayReader replayReader; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index ce01aa90b71a..1bf4c6b52350 100755 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -104,11 +104,15 @@ protected bool BackgroundBrightnessReduction !playerConsumed // don't push unless the player is completely loaded && CurrentPlayer?.LoadState == LoadState.Ready - // don't push if the user is hovering one of the panes, unless they are idle. - && (IsHovered || idleTracker.IsIdle.Value) - // don't push if the user is dragging a slider or otherwise. + // don't push unless the player is ready to start gameplay + && ReadyForGameplay; + + protected virtual bool ReadyForGameplay => + // not ready if the user is hovering one of the panes, unless they are idle. + (IsHovered || idleTracker.IsIdle.Value) + // not ready if the user is dragging a slider or otherwise. && inputManager.DraggedDrawable == null - // don't push if a focused overlay is visible, like settings. + // not ready if a focused overlay is visible, like settings. && inputManager.FocusedDrawable == null; private readonly Func createPlayer; @@ -395,7 +399,15 @@ private void prepareNewPlayer() CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; - LoadTask = LoadComponentAsync(CurrentPlayer, _ => MetadataInfo.Loading = false); + LoadTask = LoadComponentAsync(CurrentPlayer, _ => + { + MetadataInfo.Loading = false; + OnPlayerLoaded(); + }); + } + + protected virtual void OnPlayerLoaded() + { } private void restartRequested() diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 42091c521fc2..1662ca399f95 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -99,8 +99,8 @@ protected class CustomSliderBar : SliderBar { 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/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index e034097bdbe2..24f1fa0bc0d3 100755 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -55,7 +55,21 @@ protected override void PrepareReplay() protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. - protected override Task ImportScore(Score score, bool bypassChesk = false) => Task.CompletedTask; + protected override Task ImportScore(Score score, bool bypassChesk = false) + { + //目前的DanceMod仍然需要手动导入 + var danceMod = (ModDance)Mods.Value.FirstOrDefault(m => m is ModDance); + + if (danceMod?.SaveScore.Value ?? false) + { + if (!score.ScoreInfo.User.Username.EndsWith(danceMod.ENDCHAR, StringComparison.Ordinal)) return Task.CompletedTask; + + score.ScoreInfo.User.Username = score.ScoreInfo.User.Username.Replace(danceMod.ENDCHAR, danceMod.ENDCHARREPLACE); + base.ImportScore(score, bypassChesk); + } + + return Task.CompletedTask; + } protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index e620abb90f2c..b38dcb937d9e 100755 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -1,20 +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 osuTK; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; -using osu.Game.Graphics; -using osu.Framework.Allocation; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play { @@ -42,7 +42,8 @@ public class SongProgress : OverlayContainer, ISkinnableDrawable /// public readonly Bindable AllowSeeking = new Bindable(); - public readonly Bindable ShowGraph = new Bindable(); + [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")] + public Bindable ShowGraph { get; } = new BindableBool(true); public override bool HandleNonPositionalInput => AllowSeeking.Value; public override bool HandlePositionalInput => AllowSeeking.Value; @@ -116,7 +117,7 @@ public SongProgress() } [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, OsuConfigManager config) + private void load(OsuColour colours) { base.LoadComplete(); @@ -129,8 +130,6 @@ private void load(OsuColour colours, OsuConfigManager config) Objects = drawableRuleset.Objects; } - config.BindWith(OsuSetting.ShowProgressGraph, ShowGraph); - graph.FillColour = bar.FillColour = colours.BlueLighter; } @@ -140,6 +139,56 @@ protected override void LoadComplete() AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); + + migrateSettingFromConfig(); + } + + [Resolved] + private OsuConfigManager config { get; set; } + + [Resolved] + private SkinManager skinManager { get; set; } + + /// + /// This setting has been migrated to a per-component level. + /// Only take the value from the config if it is in a non-default state (then reset it to default so it only applies once). + /// + /// Can be removed 20221027. + /// + private void migrateSettingFromConfig() + { + Bindable configShowGraph = config.GetBindable(OsuSetting.ShowProgressGraph); + + if (!configShowGraph.IsDefault) + { + ShowGraph.Value = configShowGraph.Value; + + // This is pretty ugly, but the only way to make this stick... + if (skinManager != null) + { + var skinnableTarget = this.FindClosestParent(); + + if (skinnableTarget != null) + { + // If the skin is not mutable, a mutable instance will be created, causing this migration logic to run again on the correct skin. + // Therefore we want to avoid resetting the config value on this invocation. + if (skinManager.EnsureMutableSkin()) + return; + + // If `EnsureMutableSkin` actually changed the skin, default layout may take a frame to apply. + // See `SkinnableTargetComponentsContainer`'s use of ScheduleAfterChildren. + ScheduleAfterChildren(() => + { + var skin = skinManager.CurrentSkin.Value; + skin.UpdateDrawableTarget(skinnableTarget); + + skinManager.Save(skin); + }); + + configShowGraph.SetDefault(); + } + } + } } protected override void PopIn() diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index e50520e0cafc..b9248bd67eb1 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -212,12 +212,12 @@ private void load() 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 76cd408daa68..d0b79aa4c78d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -23,7 +23,7 @@ public class RankBadge : CompositeDrawable /// /// 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 @@ public class RankBadge : CompositeDrawable /// /// 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 @@ protected override void Update() 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/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs index 3b16961fdf97..22edc2a39e22 100755 --- a/osu.Game/Screens/Select/Filter/SortMode.cs +++ b/osu.Game/Screens/Select/Filter/SortMode.cs @@ -27,8 +27,9 @@ public enum SortMode [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/FooterButtonRandom.cs b/osu.Game/Screens/Select/FooterButtonRandom.cs index 2a01983a5abf..6712084c9a7c 100755 --- a/osu.Game/Screens/Select/FooterButtonRandom.cs +++ b/osu.Game/Screens/Select/FooterButtonRandom.cs @@ -5,11 +5,13 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Select { @@ -18,6 +20,9 @@ public class FooterButtonRandom : FooterButton public Action NextRandom { get; set; } public Action PreviousRandom { get; set; } + private Container persistentText; + private OsuSpriteText randomSpriteText; + private OsuSpriteText rewindSpriteText; private bool rewindSearch; [BackgroundDependencyLoader] @@ -25,7 +30,32 @@ private void load(OsuColour colours) { SelectedColour = colours.Green; DeselectedColour = SelectedColour.Opacity(0.5f); - Text = @"随机选择"; + + TextContainer.Add(persistentText = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AlwaysPresent = true, + AutoSizeAxes = Axes.Both, + Children = new[] + { + randomSpriteText = new OsuSpriteText + { + AlwaysPresent = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "随机选择", + }, + rewindSpriteText = new OsuSpriteText + { + AlwaysPresent = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "撤销随机", + Alpha = 0f, + } + } + }); Action = () => { @@ -33,22 +63,22 @@ private void load(OsuColour colours) { const double fade_time = 500; - OsuSpriteText rewindSpriteText; + OsuSpriteText fallingRewind; - TextContainer.Add(rewindSpriteText = new OsuSpriteText + TextContainer.Add(fallingRewind = new OsuSpriteText { Alpha = 0, - Text = @"撤销随机", + Text = rewindSpriteText.Text, AlwaysPresent = true, // make sure the button is sized large enough to always show this Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - rewindSpriteText.FadeOutFromOne(fade_time, Easing.In); - rewindSpriteText.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In); - rewindSpriteText.Expire(); + fallingRewind.FadeOutFromOne(fade_time, Easing.In); + fallingRewind.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In); + fallingRewind.Expire(); - SpriteText.FadeInFromZero(fade_time, Easing.In); + persistentText.FadeInFromZero(fade_time, Easing.In); PreviousRandom.Invoke(); } @@ -59,6 +89,44 @@ private void load(OsuColour colours) }; } + protected override bool OnKeyDown(KeyDownEvent e) + { + updateText(e.ShiftPressed); + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + updateText(e.ShiftPressed); + base.OnKeyUp(e); + } + + protected override bool OnClick(ClickEvent e) + { + try + { + // this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp). + rewindSearch |= e.ShiftPressed; + return base.OnClick(e); + } + finally + { + rewindSearch = false; + } + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + { + rewindSearch = true; + TriggerClick(); + return; + } + + base.OnMouseUp(e); + } + public override bool OnPressed(KeyBindingPressEvent e) { rewindSearch = e.Action == GlobalAction.SelectPreviousRandom; @@ -79,5 +147,11 @@ public override void OnReleased(KeyBindingReleaseEvent e) rewindSearch = false; } } + + private void updateText(bool rewind = false) + { + randomSpriteText.Alpha = rewind ? 0 : 1; + rewindSpriteText.Alpha = rewind ? 1 : 0; + } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 01e7646644c1..5e1902f52036 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -143,12 +143,15 @@ public void SelectRandomSkin() /// Ensure that the current skin is in a state it can accept user modifications. /// This will create a copy of any internal skin and being tracking in the database if not already. /// - public void EnsureMutableSkin() + /// + /// Whether a new skin was created to allow for mutation. + /// + public bool EnsureMutableSkin() { - CurrentSkinInfo.Value.PerformRead(s => + return CurrentSkinInfo.Value.PerformRead(s => { if (!s.Protected) - return; + return false; string[] existingSkinNames = realm.Run(r => r.All() .Where(skin => !skin.DeletePending) @@ -160,7 +163,7 @@ public void EnsureMutableSkin() { Creator = s.Creator, InstantiationInfo = s.InstantiationInfo, - Name = NamingUtils.GetNextBestName(existingSkinNames, $"{s.Name} (modified)") + Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)") }; var result = skinModelManager.Import(skinInfo); @@ -171,7 +174,10 @@ public void EnsureMutableSkin() // currently this only happens on save. result.PerformRead(skin => Save(skin.CreateInstance(this))); CurrentSkinInfo.Value = result; + return true; } + + return false; }); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 21774b73a0dc..725499d0e56d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -155,7 +155,7 @@ private void updateRoomStateIfRequired() foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) ChangeUserState(u.UserID, MultiplayerUserState.Playing); - ((IMultiplayerClient)this).MatchStarted(); + ((IMultiplayerClient)this).GameplayStarted(); ChangeRoomState(MultiplayerRoomState.Playing); } 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 46e427e1b788..cf731aa91f45 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 @@ public FastRandom() /// 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.iOS.props b/osu.iOS.props index 8775442be2e2..e472b5f1a819 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - +