diff --git a/global.json b/global.json index 233a040d18..a9a531f59c 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.52" + "Microsoft.Build.Traversal": "2.1.1" } } \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index 1a76a24496..62397ca028 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 8c48158acd..466cbdaf8d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] + [Timeout(10000)] public class CatchBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index f9d56dfa78..dfe3bf8af4 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 18cc300ff9..f009c10a9c 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; @@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { Name = @"Fruit Count", Content = fruits.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { Name = @"Juice Stream Count", Content = juiceStreams.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { Name = @"Banana Shower Count", Content = bananaShowers.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } }; } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9437023c70..ca75a816f1 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -21,13 +21,11 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using System; -using osu.Framework.Testing; using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch { - [ExcludeFromDynamicCompile] public class CatchRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods); diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index d929da1a29..ea2f031d65 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { @@ -61,7 +62,12 @@ namespace osu.Game.Rulesets.Catch.Skinning switch (lookup) { case CatchSkinColour colour: - return Source.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour)); + if (result == null) + return null; + + result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value); + return (IBindable)result; } return Source.GetConfig(lookup); diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 5be54d3882..381d066750 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning colouredSprite = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index d0ff1fab43..0c57267970 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -14,11 +14,13 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] + [Timeout(10000)] public class ManiaBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; [TestCase("basic")] + [TestCase("zero-length-slider")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 957743c5f1..b22687a0a7 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] + [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new ManiaRuleset(); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index ff4865c71d..8ba58e3af3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private readonly Column column; - public ColumnTestContainer(int column, ManiaAction action) + public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false) { - this.column = new Column(column) + InternalChildren = new[] { - Action = { Value = action }, - AccentColour = Color4.Orange, - ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd - }; - - InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) - { - RelativeSizeAxes = Axes.Both + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange, + ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd, + Alpha = showColumn ? 1 : 0 + }, + content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }, + this.column.TopLevelContainer.CreateProxy() }; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 18eebada00..d24c81dac6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ColumnTestContainer(0, ManiaAction.Key1) + new ColumnTestContainer(0, ManiaAction.Key1, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning })); }) }, - new ColumnTestContainer(1, ManiaAction.Key2) + new ColumnTestContainer(1, ManiaAction.Key2, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs new file mode 100644 index 0000000000..ab840e1c46 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -0,0 +1,117 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene + { + [Test] + public void TestPreviousHitWindowDoesNotExtendPastNextObject() + { + var objects = new List(); + var frames = new List(); + + for (int i = 0; i < 7; i++) + { + double time = 1000 + i * 100; + + objects.Add(new Note { StartTime = time }); + + if (i > 0) + { + frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1)); + frames.Add(new ManiaReplayFrame(time + 11)); + } + } + + performTest(objects, frames); + + addJudgementAssert(objects[0], HitResult.Miss); + + for (int i = 1; i < 7; i++) + { + addJudgementAssert(objects[i], HitResult.Perfect); + addJudgementOffsetAssert(objects[i], 10); + } + } + + private void addJudgementAssert(ManiaHitObject hitObject, HitResult result) + { + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); + } + + private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset) + { + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", + () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); + } + + private ScoreAccessibleReplayPlayer currentPlayer; + private List judgementResults; + + private void performTest(List hitObjects, List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) + { + HitObjects = hitObjects, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index ed00ed0b4c..892f27d27f 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index dc24a344e9..d1d5adea75 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -41,14 +40,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps new BeatmapStatistic { Name = @"Note Count", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), - Icon = FontAwesome.Regular.Circle }, new BeatmapStatistic { Name = @"Hold Note Count", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdnotes.ToString(), - Icon = FontAwesome.Regular.Circle }, }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index b025ac7992..211905835c 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -5,7 +5,6 @@ using osu.Game.Rulesets.Mania.Objects; using System; using System.Linq; using System.Collections.Generic; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; @@ -167,8 +166,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps var positionData = original as IHasPosition; - for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration) + for (int i = 0; i <= generator.SpanCount; i++) { + double time = original.StartTime + generator.SegmentDuration * i; + recordNote(time, positionData?.Position ?? Vector2.Zero); computeDensity(time); } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index d03eb0b3c9..fe146c5324 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy public readonly double EndTime; public readonly double SegmentDuration; - - private readonly int spanCount; + public readonly int SpanCount; private PatternType convertType; @@ -42,20 +41,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var distanceData = hitObject as IHasDistance; var repeatsData = hitObject as IHasRepeats; - spanCount = repeatsData?.SpanCount() ?? 1; + SpanCount = repeatsData?.SpanCount() ?? 1; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); // The true distance, accounting for any repeats - double distance = (distanceData?.Distance ?? 0) * spanCount; + double distance = (distanceData?.Distance ?? 0) * SpanCount; // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; EndTime = hitObject.StartTime + osuDuration; - SegmentDuration = (EndTime - HitObject.StartTime) / spanCount; + SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount; } public override IEnumerable Generate() @@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (spanCount > 1) + if (SpanCount > 1) { if (SegmentDuration <= 90) return generateRandomHoldNotes(HitObject.StartTime, 1); @@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (SegmentDuration <= 120) { convertType |= PatternType.ForceNotStack; - return generateRandomNotes(HitObject.StartTime, spanCount + 1); + return generateRandomNotes(HitObject.StartTime, SpanCount + 1); } if (SegmentDuration <= 160) @@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (duration >= 4000) return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); - if (SegmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart) + if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart) return generateTiledHoldNotes(HitObject.StartTime); return generateHoldAndNormalNotes(HitObject.StartTime); @@ -251,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); bool increasing = Random.NextDouble() > 0.5; - for (int i = 0; i <= spanCount; i++) + for (int i = 0; i <= SpanCount; i++) { addToPattern(pattern, column, startTime, startTime); startTime += SegmentDuration; @@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - for (int i = 0; i <= spanCount; i++) + for (int i = 0; i <= SpanCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); @@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); - int columnRepeat = Math.Min(spanCount, TotalColumns); + int columnRepeat = Math.Min(SpanCount, TotalColumns); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) @@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var rowPattern = new Pattern(); - for (int i = 0; i <= spanCount; i++) + for (int i = 0; i <= SpanCount; i++) { if (!(ignoreHead && startTime == HitObject.StartTime)) { diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2795868c97..71ac85dd1b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -12,7 +12,6 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; @@ -35,7 +34,6 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Mania { - [ExcludeFromDynamicCompile] public class ManiaRuleset : Ruleset, ILegacyRuleset { /// @@ -126,6 +124,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlag(LegacyMods.Random)) yield return new ManiaModRandom(); + + if (mods.HasFlag(LegacyMods.Mirror)) + yield return new ManiaModMirror(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -175,6 +176,10 @@ namespace osu.Game.Rulesets.Mania case ManiaModFadeIn _: value |= LegacyMods.FadeIn; break; + + case ManiaModMirror _: + value |= LegacyMods.Mirror; + break; } } @@ -326,6 +331,16 @@ namespace osu.Game.Rulesets.Mania Height = 250 }), } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) + } } }; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 0712026ca6..2ebcc5451a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -238,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) + { ApplyResult(r => r.Type = HitResult.Perfect); + endHold(); + } if (Tail.Result.Type == HitResult.Miss) HasBroken = true; @@ -252,6 +255,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; + if (CheckHittable?.Invoke(this, Time.Current) == false) + return false; + // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed). // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time. // Note: Unlike below, we use the tail's start time to determine the time offset. diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index ab76a5b8f8..08c41b0d75 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,6 +9,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -34,6 +36,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } + /// + /// Whether this can be hit, given a time value. + /// If non-null, judgements will be ignored whilst the function returns false. + /// + public Func CheckHittable; + protected DrawableManiaHitObject(ManiaHitObject hitObject) : base(hitObject) { @@ -124,6 +132,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables break; } } + + /// + /// Causes this to get missed, disregarding all conditions in implementations of . + /// + public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); } public abstract class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 9451bc4430..973dc06e05 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -64,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; + if (CheckHittable?.Invoke(this, Time.Current) == false) + return false; + return UpdateResult(true); } diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json new file mode 100644 index 0000000000..229760cd1c --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json @@ -0,0 +1,14 @@ +{ + "Mappings": [{ + "RandomW": 3083084786, + "RandomX": 273326509, + "RandomY": 273553282, + "RandomZ": 2659838971, + "StartTime": 4836, + "Objects": [{ + "StartTime": 4836, + "EndTime": 4836, + "Column": 0 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu new file mode 100644 index 0000000000..9b8ac1f9db --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu @@ -0,0 +1,20 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:1 +CircleSize:4 +OverallDifficulty:1 +ApproachRate:9 +SliderMultiplier:2.5 +SliderTickRate:0.5 + +[TimingPoints] +34,431.654676258993,4,1,0,50,1,0 +4782,-66.6666666666667,4,1,0,20,0,0 + +[HitObjects] +15,199,4836,22,0,L,1,46.8750017881394 \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs new file mode 100644 index 0000000000..c8b05ed2f8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9f716428c0..c0f0fcb4af 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); - private Drawable sprite; + [CanBeNull] + private Drawable bodySprite; + + [CanBeNull] + private Drawable lightContainer; + + [CanBeNull] + private Drawable light; public LegacyBodyPiece() { @@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; - sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => + string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value + ?? "lightingL"; + + float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value + ?? 1; + + // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. + // This animation is discarded and re-queried with the appropriate frame length afterwards. + var tmp = skin.GetAnimation(lightImage, true, false); + double frameLength = 0; + if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); + + light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d => + { + if (d == null) + return; + + d.Origin = Anchor.Centre; + d.Blending = BlendingParameters.Additive; + d.Scale = new Vector2(lightScale); + }); + + if (light != null) + { + lightContainer = new HitTargetInsetContainer + { + Alpha = 0, + Child = light + }; + } + + bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { if (d == null) return; @@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning // Todo: Wrap }); - if (sprite != null) - InternalChild = sprite; + if (bodySprite != null) + InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); @@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning private void onIsHittingChanged(ValueChangedEvent isHitting) { - if (!(sprite is TextureAnimation animation)) + if (bodySprite is TextureAnimation bodyAnimation) + { + bodyAnimation.GotoFrame(0); + bodyAnimation.IsPlaying = isHitting.NewValue; + } + + if (lightContainer == null) return; - animation.GotoFrame(0); - animation.IsPlaying = isHitting.NewValue; + if (isHitting.NewValue) + { + // Clear the fade out and, more importantly, the removal. + lightContainer.ClearTransforms(); + + // Only add the container if the removal has taken place. + if (lightContainer.Parent == null) + Column.TopLevelContainer.Add(lightContainer); + + // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). + if (light is TextureAnimation lightAnimation) + lightAnimation.GotoFrame(0); + + lightContainer.FadeIn(80); + } + else + { + lightContainer.FadeOut(120) + .OnComplete(d => Column.TopLevelContainer.Remove(d)); + } } private void onDirectionChanged(ValueChangedEvent direction) { - if (sprite == null) - return; - if (direction.NewValue == ScrollingDirection.Up) { - sprite.Origin = Anchor.BottomCentre; - sprite.Scale = new Vector2(1, -1); + if (bodySprite != null) + { + bodySprite.Origin = Anchor.BottomCentre; + bodySprite.Scale = new Vector2(1, -1); + } + + if (light != null) + light.Anchor = Anchor.TopCentre; } else { - sprite.Origin = Anchor.TopCentre; - sprite.Scale = Vector2.One; + if (bodySprite != null) + { + bodySprite.Origin = Anchor.TopCentre; + bodySprite.Scale = Vector2.One; + } + + if (light != null) + light.Anchor = Anchor.BottomCentre; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + lightContainer?.Expire(); + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index acae4cd6fb..3bf51b3073 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = lightColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 1e1a9c2237..6eced571d2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, - Colour = lineColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour), Alpha = showJudgementLine ? 0.9f : 0 } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 44f3e7d7b3..b269ea25d4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false) + Column.UnderlayElements.Add(CreateProxy()); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 19ec86b1ed..b0bab8e760 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -2,14 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -108,75 +106,43 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChildren = new Drawable[] { - new Container + LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box { - RelativeSizeAxes = Axes.Both, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - }, + RelativeSizeAxes = Axes.Both + }, backgroundColour), new HitTargetInsetContainer { RelativeSizeAxes = Axes.Both, Children = new[] { - new Box + new Container { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 + Alpha = hasLeftLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, - new Box + new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 + Alpha = hasRightLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, } } }; } } - - private class HitTargetInsetContainer : Container - { - private readonly IBindable direction = new Bindable(); - - protected override Container Content => content; - private readonly Container content; - - private float hitPosition; - - public HitTargetInsetContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin, IScrollingInfo scrollingInfo) - { - hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; - - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - } - - private void onDirectionChanged(ValueChangedEvent direction) - { - content.Padding = direction.NewValue == ScrollingDirection.Up - ? new MarginPadding { Top = hitPosition } - : new MarginPadding { Bottom = hitPosition }; - } - } } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index de4648e4fa..9aabcc6699 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { @@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly ColumnHitObjectArea HitObjectArea; internal readonly Container TopLevelContainer; private readonly DrawablePool hitExplosionPool; + private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; @@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; + hitPolicy = new OrderedHitPolicy(HitObjectContainer); + TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); } @@ -90,6 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI hitObject.AccentColour.Value = AccentColour; hitObject.OnNewResult += OnNewResult; + DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; + maniaObject.CheckHittable = hitPolicy.IsHittable; + HitObjectContainer.Add(hitObject); } @@ -104,6 +111,9 @@ namespace osu.Game.Rulesets.Mania.UI internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { + if (result.IsHit) + hitPolicy.HandleHit(judgedObject); + if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs new file mode 100644 index 0000000000..0f9cd48dd8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -0,0 +1,78 @@ +// 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 osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// Ensures that only the most recent is hittable, affectionately known as "note lock". + /// + public class OrderedHitPolicy + { + private readonly HitObjectContainer hitObjectContainer; + + public OrderedHitPolicy(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + + /// + /// Determines whether a can be hit at a point in time. + /// + /// + /// Only the most recent can be hit, a previous hitobject's window cannot extend past the next one. + /// + /// The to check. + /// The time to check. + /// Whether can be hit at the given . + public bool IsHittable(DrawableHitObject hitObject, double time) + { + var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject); + return nextObject == null || time < nextObject.HitObject.StartTime; + } + + /// + /// Handles a being hit to potentially miss all earlier s. + /// + /// The that was hit. + public void HandleHit(DrawableHitObject hitObject) + { + if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); + + foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) + { + if (obj.Judged) + continue; + + ((DrawableManiaHitObject)obj).MissForcefully(); + } + } + + private IEnumerable enumerateHitObjectsUpTo(double targetTime) + { + foreach (var obj in hitObjectContainer.AliveObjects) + { + if (obj.HitObject.GetEndTime() >= targetTime) + yield break; + + yield return obj; + + foreach (var nestedObj in obj.NestedHitObjects) + { + if (nestedObj.HitObject.GetEndTime() >= targetTime) + break; + + yield return nestedObj; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index cd3daf18a9..7d32895083 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] + [Timeout(10000)] public class OsuBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png new file mode 100644 index 0000000000..4f50f638c5 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png new file mode 100644 index 0000000000..daf28e09cb Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png new file mode 100644 index 0000000000..6ef1068420 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index a69646507a..3d100e4b1c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -22,7 +22,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Storyboards; using osuTK; -using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Rulesets.Osu.Tests { @@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audioManager { get; set; } - private TrackVirtualManual track; - protected override bool Autoplay => autoplay; private bool autoplay; @@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests private const double fade_in_modifier = -1200; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); - track = (TrackVirtualManual)working.Track; - return working; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] private void load(RulesetConfigCache configCache) @@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable autoplay", () => autoplay = true); base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; retrieveDrawableSlider(sliderIndex); @@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("have autoplay", () => autoplay = true); base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; retrieveDrawableSlider(sliderIndex); @@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void addSeekStep(double time) { - AddStep($"seek to {time}", () => track.Seek(time)); + AddStep($"seek to {time}", () => MusicController.SeekTo(time)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 47b3926ceb..94d1cb8864 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests drawableSpinner = new TestDrawableSpinner(spinner, auto) { Anchor = Anchor.Centre, - Depth = depthIndex++ + Depth = depthIndex++, + Scale = new Vector2(0.75f) }; foreach (var mod in SelectedMods.Value.OfType()) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 69857f8ef9..f7909071ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -25,7 +25,6 @@ using osu.Game.Scoring; using osu.Game.Storyboards; using osu.Game.Tests.Visual; using osuTK; -using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Rulesets.Osu.Tests { @@ -34,18 +33,12 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audioManager { get; set; } - private TrackVirtualManual track; - protected override bool Autoplay => true; protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); - track = (TrackVirtualManual)working.Track; - return working; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); private DrawableSpinner drawableSpinner; private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single(); @@ -55,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests { base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); } @@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(0); - AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); + AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); // autoplay replay frames use track time; // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, @@ -230,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void addSeekStep(double time) { - AddStep($"seek to {time}", () => track.Seek(time)); + AddStep($"seek to {time}", () => MusicController.SeekTo(time)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index f3837ea6b1..3639c3616f 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 491d82b89e..2d3cc3c103 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { Name = @"Circle Count", Content = circles.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { Name = @"Slider Count", Content = sliders.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { Name = @"Spinner Count", Content = spinners.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 8308c0c576..2946331bc6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X; /// - /// Whether this can be hit. + /// Whether this can be hit, given a time value. /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// public Func CheckHittable; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index eaa5d8937a..7f4a0dcbbb 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -30,14 +30,12 @@ using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; -using osu.Framework.Testing; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Osu { - [ExcludeFromDynamicCompile] public class OsuRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods); @@ -193,30 +191,46 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), - } - }, - new StatisticRow - { - Columns = new[] + new StatisticItem("Timing Distribution", + new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 4cb2cd6539..76b2631894 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -154,8 +154,12 @@ namespace osu.Game.Rulesets.Osu.Replays // The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position // We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition. // TODO: Shouldn't the spinner always spin in the same direction? - if (h is Spinner) + if (h is Spinner spinner) { + // spinners with 0 spins required will auto-complete - don't bother + if (spinner.SpinsRequired == 0) + return; + calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0ab3e8825b..d15a0a3203 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning hitCircleSprite = new Sprite { Texture = getTextureWithFallback(string.Empty), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 81a0df5ea5..e157842fd1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Skinning { private DrawableSpinner drawableSpinner; private Sprite disc; + private Sprite metreSprite; private Container metre; - private const float background_y_offset = 20; - private const float sprite_scale = 1 / 1.6f; + private const float final_metre_height = 692 * sprite_scale; [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) @@ -35,50 +35,58 @@ namespace osu.Game.Rulesets.Osu.Skinning RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChild = new Container { - new Sprite + // the old-style spinner relied heavily on absolute screen-space coordinate values. + // wrap everything in a container simulating absolute coords to preserve alignment + // as there are skins that depend on it. + Width = 640, + Height = 480, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Texture = source.GetTexture("spinner-background"), - Y = background_y_offset, - Scale = new Vector2(sprite_scale) - }, - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(sprite_scale) - }, - metre = new Container - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Y = background_y_offset, - Masking = true, - Child = new Sprite + new Sprite { - Texture = source.GetTexture("spinner-metre"), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-background"), + Scale = new Vector2(sprite_scale) }, - Scale = new Vector2(0.625f) + disc = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-circle"), + Scale = new Vector2(sprite_scale) + }, + metre = new Container + { + AutoSizeAxes = Axes.Both, + // this anchor makes no sense, but that's what stable uses. + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + // adjustment for stable (metre has additional offset) + Margin = new MarginPadding { Top = 20 }, + Masking = true, + Child = metreSprite = new Sprite + { + Texture = source.GetTexture("spinner-metre"), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Scale = new Vector2(0.625f) + } + } } }; } - private Vector2 metreFinalSize; - protected override void LoadComplete() { base.LoadComplete(); this.FadeOut(); drawableSpinner.State.BindValueChanged(updateStateTransforms, true); - - metreFinalSize = metre.Size = metre.Child.Size; } private void updateStateTransforms(ValueChangedEvent state) @@ -93,7 +101,16 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Update(); disc.Rotation = drawableSpinner.RotationTracker.Rotation; - metre.Height = getMetreHeight(drawableSpinner.Progress); + + // careful: need to call this exactly once for all calculations in a frame + // as the function has a random factor in it + var metreHeight = getMetreHeight(drawableSpinner.Progress); + + // hack to make the metre blink up from below than down from above. + // move down the container to be able to apply masking for the metre, + // and then move the sprite back up the same amount to keep its position absolute. + metre.Y = final_metre_height - metreHeight; + metreSprite.Y = -metre.Y; } private const int total_bars = 10; @@ -108,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning if (RNG.NextBool(((int)progress % 10) / 10f)) barCount++; - return (float)barCount / total_bars * metreFinalSize.Y; + return (float)barCount / total_bars * final_metre_height; } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 0f586034d5..25ab96445a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { - animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] { @@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.With(d => + LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; - }), + }), ballColour), layerSpec = new Sprite { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 21df49d80b..aad8b189d9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); + protected new float CalculatedBorderPortion + // Roughly matches osu!stable's slider border portions. + => base.CalculatedBorderPortion * 0.77f; + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index d0c57b20c0..5e550a5d03 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] + [Timeout(10000)] public class TaikoBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index e896606ee8..b59f3a4344 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index b595f43fbb..16a0726c8c 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; @@ -22,20 +21,20 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps new BeatmapStatistic { Name = @"Hit Count", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), - Icon = FontAwesome.Regular.Circle }, new BeatmapStatistic { Name = @"Drumroll Count", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumrolls.ToString(), - Icon = FontAwesome.Regular.Circle }, new BeatmapStatistic { Name = @"Swell Count", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), - Icon = FontAwesome.Regular.Circle } }; } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs index bfcf268c3d..9b73ccd248 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - backgroundLayer.Colour = accentColour; + backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 8223e3bc01..5ab8e3a8c8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = accentColour; - body.Colour = accentColour; - end.Colour = accentColour; + var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + + headCircle.AccentColour = colour; + body.Colour = colour; + end.Colour = colour; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 656728f6e4..b11b64c22c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load() { - AccentColour = component == TaikoSkinComponents.CentreHit - ? new Color4(235, 69, 44, 255) - : new Color4(67, 142, 172, 255); + AccentColour = LegacyColourCompatibility.DisallowZeroAlpha( + component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255)); } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2011842591..9d485e3f20 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -22,7 +22,6 @@ using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; using System.Linq; -using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -32,7 +31,6 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko { - [ExcludeFromDynamicCompile] public class TaikoRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); @@ -161,19 +159,34 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs new file mode 100644 index 0000000000..0f6d956b3c --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs @@ -0,0 +1,32 @@ +// 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; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class BeatmapDifficultyManagerTest + { + [Test] + public void TestKeyEqualsWithDifferentModInstances() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + + [Test] + public void TestKeyEqualsWithDifferentModOrder() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + } +} diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 30331e98d2..8b22309033 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -10,6 +10,7 @@ using System.Text; using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +using osu.Game.Skinning; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Beatmaps.Formats @@ -26,18 +28,33 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyBeatmapEncoderTest { - private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); + private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore(); + + private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) { - var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name)); - var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded)); + var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name); + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); - sort(decoded); - sort(decodedAfterEncode); + sort(decoded.beatmap); + sort(decodedAfterEncode.beatmap); - Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize())); + Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); + Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); + } + + private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b) + { + // equal to null, no need to SequenceEqual + if (a.ComboColours == null && b.ComboColours == null) + return true; + + if (a.ComboColours == null || b.ComboColours == null) + return false; + + return a.ComboColours.SequenceEqual(b.ComboColours); } private void sort(IBeatmap beatmap) @@ -50,18 +67,31 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private IBeatmap decodeFromLegacy(Stream stream) + private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name) { using (var reader = new LineBufferedReader(stream)) - return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader)); + { + var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); + var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name); + return (convert(beatmap), beatmapSkin); + } } - private Stream encodeToLegacy(IBeatmap beatmap) + private class TestLegacySkin : LegacySkin { + public TestLegacySkin(IResourceStore storage, string fileName) + : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) + { + } + } + + private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap) + { + var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmap).Encode(writer); + new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer); stream.Position = 0; @@ -106,7 +136,7 @@ namespace osu.Game.Tests.Beatmaps.Formats protected override Texture GetBackground() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs new file mode 100644 index 0000000000..9c71466489 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyScoreDecoderTest + { + [Test] + public void TestDecodeManiaReplay() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID); + + Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); + Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); + + Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(3, score.ScoreInfo.MaxCombo); + Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); + + Assert.That(score.Replay.Frames, Is.Not.Empty); + } + } + + private class TestLegacyScoreDecoder : LegacyScoreDecoder + { + private static readonly Dictionary rulesets = new Ruleset[] + { + new OsuRuleset(), + new TaikoRuleset(), + new CatchRuleset(), + new ManiaRuleset() + }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; + + protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + MD5Hash = md5Hash, + Ruleset = new OsuRuleset().RulesetInfo, + BaseDifficulty = new BeatmapDifficulty() + } + }); + } + } +} diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0151678db3..dd3dba1274 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -15,8 +15,10 @@ using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.IO; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Resources; +using osu.Game.Users; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -32,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportWhenClosed() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -49,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDelete() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -70,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -96,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithReZip() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -154,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithChangedFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -205,7 +207,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithDifferentFilename() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -257,7 +259,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportCorruptThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -299,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestRollbackOnFailure() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -376,7 +378,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDeleteThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -404,7 +406,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(TestImportThenDeleteThenImportWithOnlineIDMismatch)}-{set}")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(set.ToString())) { try { @@ -438,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportWithDuplicateBeatmapIDs() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -524,7 +526,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWhenFileOpen() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenFileOpen))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -546,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithDuplicateHashes() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -588,7 +590,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportNestedStructure() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -633,7 +635,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithIgnoredDirectoryInArchive() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithIgnoredDirectoryInArchive))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -687,7 +689,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestUpdateBeatmapInfo() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapInfo))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -717,7 +719,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestUpdateBeatmapFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -756,6 +758,63 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public void TestCreateNewEmptyBeatmap() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER); + + manager.Save(working.BeatmapInfo, working.Beatmap); + + var retrievedSet = manager.GetAllUsableBeatmapSets()[0]; + + // Check that the new file is referenced correctly by attempting a retrieval + Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap; + Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(0)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestCreateNewBeatmapWithObject() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER); + + ((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 }); + + manager.Save(working.BeatmapInfo, working.Beatmap); + + var retrievedSet = manager.GetAllUsableBeatmapSets()[0]; + + // Check that the new file is referenced correctly by attempting a retrieval + Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap; + Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1)); + Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000)); + } + finally + { + host.Exit(); + } + } + } + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) { var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index ff17f23d50..b491157627 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -351,7 +351,7 @@ namespace osu.Game.Tests.Editing using (var encoded = new MemoryStream()) { using (var sw = new StreamWriter(encoded)) - new LegacyBeatmapEncoder(beatmap).Encode(sw); + new LegacyBeatmapEncoder(beatmap, null).Encode(sw); return encoded.ToArray(); } diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index cd3669f160..891537c4ad 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -1,10 +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 NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; @@ -19,7 +17,14 @@ namespace osu.Game.Tests.Gameplay { GameplayClockContainer gcc = null; - AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0))); + AddStep("create container", () => + { + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.LoadTrack(); + + Add(gcc = new GameplayClockContainer(working, 0)); + }); + AddStep("start track", () => gcc.Start()); AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index b30870d057..a690eb3b59 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -59,7 +59,10 @@ namespace osu.Game.Tests.Gameplay AddStep("create container", () => { - Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0)); + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.LoadTrack(); + + Add(gameplayContainer = new GameplayClockContainer(working, 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { @@ -103,7 +106,7 @@ namespace osu.Game.Tests.Gameplay Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); SelectedMods.Value = new[] { testedMod }; - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); + Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)); gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs new file mode 100644 index 0000000000..ad6f01881b --- /dev/null +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.NonVisual.Ranking +{ + [TestFixture] + public class UnstableRateTest + { + [Test] + public void TestDistributedHits() + { + var events = Enumerable.Range(-5, 11) + .Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null)); + + var unstableRate = new UnstableRate(events); + + Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value, 10 * Math.Sqrt(10))); + } + + [Test] + public void TestMissesAndEmptyWindows() + { + var events = new[] + { + new HitEvent(-100, HitResult.Miss, new HitObject(), null, null), + new HitEvent(0, HitResult.Great, new HitObject(), null, null), + new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), + }; + + var unstableRate = new UnstableRate(events); + + Assert.AreEqual(0, unstableRate.Value); + } + } +} diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr new file mode 100644 index 0000000000..da1a7bdd28 Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/mania-replay.osr differ diff --git a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini deleted file mode 100644 index 3c0dae6b13..0000000000 --- a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini +++ /dev/null @@ -1,5 +0,0 @@ -[General] -Version: latest - -[Colours] -Combo1: 255,255,255,0 \ No newline at end of file diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 57f0d7e957..a4d20714fa 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestBasicImport() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestImportMods() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestImportStatistics() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestImportWithDeletedBeatmapSet() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestOnlineScoreIsAvailableLocally() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestOnlineScoreIsAvailableLocally")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index c408d2f182..aedf26ee75 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -108,15 +108,5 @@ namespace osu.Game.Tests.Skins using (var stream = new LineBufferedReader(resStream)) Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m)); } - - [Test] - public void TestDecodeColourWithZeroAlpha() - { - var decoder = new LegacySkinDecoder(); - - using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini")) - using (var stream = new LineBufferedReader(resStream)) - Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f)); - } } } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 4d3b73fb32..eff430ac25 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -26,6 +26,7 @@ namespace osu.Game.Tests.Skins { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + beatmap.LoadTrack(); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs new file mode 100644 index 0000000000..62e12158ab --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs @@ -0,0 +1,32 @@ +// 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.Allocation; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Tests.Visual.Editing +{ + [TestFixture] + public class TestSceneSetupScreen : EditorClockTestScene + { + [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] + private readonly EditorBeatmap editorBeatmap; + + public TestSceneSetupScreen() + { + editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + } + + [BackgroundDependencyLoader] + private void load() + { + Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + Child = new SetupScreen(); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 79275d70a7..6fd5511e5a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Timing; @@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneCompletionCancellation : OsuPlayerTestScene { - private Track track; - [Resolved] private AudioManager audio { get; set; } @@ -34,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay base.SetUpSteps(); // Ensure track has actually running before attempting to seek - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); } [Test] @@ -73,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void complete() { - AddStep("seek to completion", () => track.Seek(5000)); + AddStep("seek to completion", () => Beatmap.Value.Track.Seek(5000)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); } private void cancel() { - AddStep("rewind to cancel", () => track.Seek(4000)); + AddStep("rewind to cancel", () => Beatmap.Value.Track.Seek(4000)); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); } @@ -91,11 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio); - track = working.Track; - return working; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 2a119f5199..73c6970482 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -21,19 +20,13 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private AudioManager audioManager { get; set; } - private Track track; - - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); - track = working.Track; - return working; - } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [Test] public void TestNoJudgementsOnRewind() { - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); @@ -46,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void addSeekStep(double time) { - AddStep($"seek to {time}", () => track.Seek(time)); + AddStep($"seek to {time}", () => Beatmap.Value.Track.Seek(time)); // Allow a few frames of lenience AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs new file mode 100644 index 0000000000..3ee0f4e720 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.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 System.Linq; +using NUnit.Framework; +using osu.Game.Overlays; +using osu.Game.Rulesets; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneOverlayActivation : OsuPlayerTestScene + { + protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; + + [Test] + public void TestGameplayOverlayActivation() + { + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + } + + [Test] + public void TestGameplayOverlayActivationPaused() + { + AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("pause gameplay", () => Player.Pause()); + AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); + } + + [Test] + public void TestGameplayOverlayActivationReplayLoaded() + { + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true); + AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); + } + + [Test] + public void TestGameplayOverlayActivationBreaks() + { + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); + AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); + AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime)); + AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + } + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer(); + + protected class OverlayTestPlayer : TestPlayer + { + public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value; + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 7ed7a116b4..841722a8f1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -1,11 +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 System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK; @@ -32,7 +30,10 @@ namespace osu.Game.Tests.Visual.Gameplay requestCount = 0; increment = skip_time; - Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), Array.Empty(), 0) + var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + working.LoadTrack(); + + Child = gameplayClockContainer = new GameplayClockContainer(working, 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 9f1492a25f..5a2b8d22fd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -22,19 +22,32 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneStoryboard : OsuTestScene { - private readonly Container storyboardContainer; + private Container storyboardContainer; private DrawableStoryboard storyboard; - [Cached] - private MusicController musicController = new MusicController(); + [Test] + public void TestStoryboard() + { + AddStep("Restart", restart); + AddToggleStep("Passing", passing => + { + if (storyboard != null) storyboard.Passing = passing; + }); + } - public TestSceneStoryboard() + [Test] + public void TestStoryboardMissingVideo() + { + AddStep("Load storyboard with missing video", loadStoryboardNoVideo); + } + + [BackgroundDependencyLoader] + private void load() { Clock = new FramedClock(); AddRange(new Drawable[] { - musicController, new Container { RelativeSizeAxes = Axes.Both, @@ -58,32 +71,11 @@ namespace osu.Game.Tests.Visual.Gameplay State = { Value = Visibility.Visible }, } }); + + Beatmap.BindValueChanged(beatmapChanged, true); } - [Test] - public void TestStoryboard() - { - AddStep("Restart", restart); - AddToggleStep("Passing", passing => - { - if (storyboard != null) storyboard.Passing = passing; - }); - } - - [Test] - public void TestStoryboardMissingVideo() - { - AddStep("Load storyboard with missing video", loadStoryboardNoVideo); - } - - [BackgroundDependencyLoader] - private void load() - { - Beatmap.ValueChanged += beatmapChanged; - } - - private void beatmapChanged(ValueChangedEvent e) - => loadStoryboard(e.NewValue); + private void beatmapChanged(ValueChangedEvent e) => loadStoryboard(e.NewValue); private void restart() { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 8f20e38494..5f135febf4 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Audio.Track; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -15,11 +15,9 @@ namespace osu.Game.Tests.Visual.Menus public TestSceneIntroWelcome() { - AddUntilStep("wait for load", () => getTrack() != null); - - AddAssert("check if menu music loops", () => getTrack().Looping); + AddUntilStep("wait for load", () => MusicController.TrackLoaded); + AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1)); + AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping); } - - private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track; } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs new file mode 100644 index 0000000000..4cad2b19d5 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.Navigation; + +namespace osu.Game.Tests.Visual.Menus +{ + public class TestSceneMusicActionHandling : OsuGameTestScene + { + private GlobalActionContainer globalActionContainer => Game.ChildrenOfType().First(); + + [Test] + public void TestMusicPlayAction() + { + AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething()); + AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused); + AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused); + } + + [Test] + public void TestMusicNavigationActions() + { + int importId = 0; + Queue<(WorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null; + + // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. + AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(new BeatmapSetInfo + { + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + } + }, + Metadata = new BeatmapMetadata + { + Artist = $"a test map {importId++}", + Title = "title", + } + }).Wait(), 5); + + AddStep("import beatmap with track", () => + { + var setWithTrack = Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result; + Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); + }); + + AddStep("bind to track change", () => + { + trackChangeQueue = new Queue<(WorkingBeatmap, TrackChangeDirection)>(); + Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection)); + }); + + AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000)); + AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000); + + AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); + AddAssert("no track change", () => trackChangeQueue.Count == 0); + AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000); + + AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); + AddAssert("track changed to previous", () => + trackChangeQueue.Count == 1 && + trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev); + + AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext)); + AddAssert("track changed to next", () => + trackChangeQueue.Count == 1 && + trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next); + } + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs index d7f23f5cc0..4b22af38c5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; @@ -11,14 +10,10 @@ namespace osu.Game.Tests.Visual.Menus { public class TestSceneSongTicker : OsuTestScene { - [Cached] - private MusicController musicController = new MusicController(); - public TestSceneSongTicker() { AddRange(new Drawable[] { - musicController, new SongTicker { Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index f819ae4682..2a4486812c 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Menus public class TestToolbar : Toolbar { - public new Bindable OverlayActivationMode => base.OverlayActivationMode; + public new Bindable OverlayActivationMode => base.OverlayActivationMode as Bindable; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 3d225aa0a9..faea32f90f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -162,6 +162,28 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); } + /// + /// Tests that the global mod instances are not retained by the rooms, as global mod instances are retained and re-used by the mod select overlay. + /// + [Test] + public void TestGlobalModInstancesNotRetained() + { + OsuModDoubleTime mod = null; + + AddStep("set dt mod and store", () => + { + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + + // Mod select overlay replaces our mod. + mod = (OsuModDoubleTime)SelectedMods.Value[0]; + }); + + AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); + + AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); + AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + } + private class TestMatchSongSelect : MatchSongSelect { public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index b2e18849c9..a899d072ac 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; +using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8ccaca8630..73a833c15d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -46,7 +45,6 @@ namespace osu.Game.Tests.Visual.Navigation Player player = null; WorkingBeatmap beatmap() => Game.Beatmap.Value; - Track track() => beatmap().Track; PushAndConfirm(() => new TestSongSelect()); @@ -62,30 +60,27 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for fail", () => player.HasFailed); - AddUntilStep("wait for track stop", () => !track().IsRunning); - AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); + AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); + AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); pushEscape(); - AddUntilStep("wait for track playing", () => track().IsRunning); - AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); + AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying); + AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); } [Test] public void TestMenuMakesMusic() { - WorkingBeatmap beatmap() => Game.Beatmap.Value; - Track track() => beatmap().Track; - TestSongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestSongSelect()); - AddUntilStep("wait for no track", () => track() is TrackVirtual); + AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice); AddStep("return to menu", () => songSelect.Exit()); - AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning); + AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying); } [Test] @@ -140,12 +135,12 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddStep("Seek close to end", () => { - Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000); - Game.Beatmap.Value.Track.Completed += () => trackCompleted = true; + Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000); + Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true; }); AddUntilStep("Track was completed", () => trackCompleted); - AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning); + AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying); } private void pushEscape() => diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs index e60adcee34..8f20bcdcc1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneFullscreenOverlay : OsuTestScene { - private FullscreenOverlay overlay; + private FullscreenOverlay overlay; protected override void LoadComplete() { @@ -38,10 +38,10 @@ namespace osu.Game.Tests.Visual.Online AddAssert("fire count 3", () => fireCount == 3); } - private class TestFullscreenOverlay : FullscreenOverlay + private class TestFullscreenOverlay : FullscreenOverlay { public TestFullscreenOverlay() - : base(OverlayColourScheme.Pink) + : base(OverlayColourScheme.Pink, null) { Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 103308d34d..9662bd65b4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); - AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null) + AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null } }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 7ca1fc842f..144f8da2fa 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -35,6 +35,18 @@ namespace osu.Game.Tests.Visual.Ranking createTest(new List()); } + [Test] + public void TestMissesDontShow() + { + createTest(Enumerable.Range(0, 100).Select(i => + { + if (i % 2 == 0) + return new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null); + + return new HitEvent(30, HitResult.Miss, new HitCircle(), new HitCircle(), null); + }).ToList()); + } + private void createTest(List events) => AddStep("create test", () => { Children = new Drawable[] diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 74808bc2f5..03cb5fa3db 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; @@ -212,6 +213,25 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); } + [Test] + public void TestDownloadButtonInitiallyDisabled() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + + AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value); + + AddStep("click contracted panel", () => + { + var contractedPanel = this.ChildrenOfType().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X); + InputManager.MoveMouseTo(contractedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value); + } + private class TestResultsContainer : Container { [Cached(typeof(Player))] @@ -255,6 +275,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); score.TotalScore += 10 - i; + score.Hash = $"test{i}"; scores.Add(score); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs similarity index 88% rename from osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs index aa569e47b1..07a0bcc8d8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneSimpleStatisticRow : OsuTestScene + public class TestSceneSimpleStatisticTable : OsuTestScene { private Container container; @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestEmpty() { AddStep("create with no items", - () => container.Add(new SimpleStatisticRow(2, Enumerable.Empty()))); + () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty()))); } [Test] @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking Value = RNG.Next(100) }); - container.Add(new SimpleStatisticRow(columnCount, items)); + container.Add(new SimpleStatisticTable(columnCount, items)); }); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 48b718c04d..67cd720260 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -5,9 +5,9 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; @@ -53,53 +53,46 @@ namespace osu.Game.Tests.Visual.SongSelect private void showPersonalBestWithNullPosition() { - leaderboard.TopScore = new APILegacyUserTopScoreInfo + leaderboard.TopScore = new ScoreInfo { - Position = null, - Score = new APILegacyScoreInfo + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + User = new User { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, - User = new User + Id = 6602580, + Username = @"waaiiru", + Country = new Country { - Id = 6602580, - Username = @"waaiiru", - Country = new Country - { - FullName = @"Spain", - FlagName = @"ES", - }, + FullName = @"Spain", + FlagName = @"ES", }, - } + }, }; } private void showPersonalBest() { - leaderboard.TopScore = new APILegacyUserTopScoreInfo + leaderboard.TopScore = new ScoreInfo { Position = 999, - Score = new APILegacyScoreInfo + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, - User = new User + Id = 6602580, + Username = @"waaiiru", + Country = new Country { - Id = 6602580, - Username = @"waaiiru", - Country = new Country - { - FullName = @"Spain", - FlagName = @"ES", - }, + FullName = @"Spain", + FlagName = @"ES", }, - } + }, }; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs index 0598324110..b8b8792b9b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs @@ -6,11 +6,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Tests.Visual.SongSelect @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.SongSelect public TestSceneUserTopScoreContainer() { - UserTopScoreContainer topScoreContainer; + UserTopScoreContainer topScoreContainer; Add(dialogOverlay = new DialogOverlay { @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.Both, Colour = Color4.DarkGreen, }, - topScoreContainer = new UserTopScoreContainer + topScoreContainer = new UserTopScoreContainer(s => new LeaderboardScore(s, s.Position, false)) { Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre, @@ -52,69 +52,60 @@ namespace osu.Game.Tests.Visual.SongSelect var scores = new[] { - new APILegacyUserTopScoreInfo + new ScoreInfo { Position = 999, - Score = new APILegacyScoreInfo + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, - User = new User + Id = 6602580, + Username = @"waaiiru", + Country = new Country { - Id = 6602580, - Username = @"waaiiru", - Country = new Country - { - FullName = @"Spain", - FlagName = @"ES", - }, + FullName = @"Spain", + FlagName = @"ES", }, - } + }, }, - new APILegacyUserTopScoreInfo + new ScoreInfo { Position = 110000, - Score = new APILegacyScoreInfo + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User { - Rank = ScoreRank.X, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - User = new User + Id = 4608074, + Username = @"Skycries", + Country = new Country { - Id = 4608074, - Username = @"Skycries", - Country = new Country - { - FullName = @"Brazil", - FlagName = @"BR", - }, + FullName = @"Brazil", + FlagName = @"BR", }, - } + }, }, - new APILegacyUserTopScoreInfo + new ScoreInfo { Position = 22333, - Score = new APILegacyScoreInfo + Rank = ScoreRank.S, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User { - Rank = ScoreRank.S, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - User = new User + Id = 1541390, + Username = @"Toukai", + Country = new Country { - Id = 1541390, - Username = @"Toukai", - Country = new Country - { - FullName = @"Canada", - FlagName = @"CA", - }, + FullName = @"Canada", + FlagName = @"CA", }, - } + }, } }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index dd5ceec739..82b7e65c4f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface { private readonly NowPlayingOverlay np; - [Cached] - private MusicController musicController = new MusicController(); - public TestSceneBeatSyncedContainer() { Clock = new FramedClock(); @@ -36,7 +33,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddRange(new Drawable[] { - musicController, new BeatContainer { Anchor = Anchor.BottomCentre, @@ -71,6 +67,9 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly Box flashLayer; + [Resolved] + private MusicController musicController { get; set; } + public BeatContainer() { RelativeSizeAxes = Axes.X; @@ -165,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); + return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index eb4750a597..e54292f7cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; @@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, Audio, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0]; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 532744a0fc..475ab0c414 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -1,15 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Graphics; -using osu.Framework.Platform; -using osu.Game.Beatmaps; using osu.Game.Overlays; -using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; namespace osu.Game.Tests.Visual.UserInterface @@ -20,18 +15,11 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private MusicController musicController = new MusicController(); - private WorkingBeatmap currentBeatmap; - private NowPlayingOverlay nowPlayingOverlay; - private RulesetStore rulesets; - [BackgroundDependencyLoader] - private void load(AudioManager audio, GameHost host) + private void load() { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); nowPlayingOverlay = new NowPlayingOverlay @@ -51,44 +39,5 @@ namespace osu.Game.Tests.Visual.UserInterface AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); AddStep(@"hide", () => nowPlayingOverlay.Hide()); } - - private BeatmapManager manager { get; set; } - - private int importId; - - [Test] - public void TestPrevTrackBehavior() - { - // ensure we have at least two beatmaps available. - AddRepeatStep("import beatmap", () => manager.Import(new BeatmapSetInfo - { - Beatmaps = new List - { - new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - } - }, - Metadata = new BeatmapMetadata - { - Artist = $"a test map {importId++}", - Title = "title", - } - }).Wait(), 5); - - AddStep(@"Next track", () => musicController.NextTrack()); - AddStep("Store track", () => currentBeatmap = Beatmap.Value); - - AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); - AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000); - - AddStep(@"Set previous", () => musicController.PreviousTrack()); - - AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); - AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000); - - AddStep(@"Set previous", () => musicController.PreviousTrack()); - AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); - } } } diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 90c91eb007..7dc5ce1d7f 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); - protected override Track GetTrack() => trackStore.Get(firstAudioFile); + protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile); private string firstAudioFile { diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index d767973528..c692bcd5e4 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs new file mode 100644 index 0000000000..33165d385a --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs @@ -0,0 +1,41 @@ +// 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.Tests.Visual; +using osu.Game.Tournament.Components; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneDateTextBox : OsuManualInputManagerTestScene + { + private DateTextBox textBox; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = textBox = new DateTextBox + { + Width = 0.3f + }; + }); + + [Test] + public void TestCommitWithoutSettingBindable() + { + AddStep("click textbox", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + }); + + AddStep("unfocus", () => + { + InputManager.MoveMouseTo(Vector2.Zero); + InputManager.Click(MouseButton.Left); + }); + } + } +} diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 95f5deb2cc..5d55196dcf 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index ee7e350970..aee5241e35 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -22,11 +22,12 @@ namespace osu.Game.Tournament.Components } // hold a reference to the provided bindable so we don't have to in every settings section. - private Bindable bindable; + private Bindable bindable = new Bindable(); public DateTextBox() { base.Bindable = new Bindable(); + ((OsuTextBox)Control).OnCommit = (sender, newText) => { try diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index f3eecf8afe..efec4cffdd 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -26,6 +26,8 @@ namespace osu.Game.Tournament.Screens.Editors [Cached] private LadderEditorInfo editorInfo = new LadderEditorInfo(); + private WarningBox rightClickMessage; + protected override bool DrawLoserPaths => true; [BackgroundDependencyLoader] @@ -37,6 +39,16 @@ namespace osu.Game.Tournament.Screens.Editors Origin = Anchor.TopRight, Margin = new MarginPadding(5) }); + + AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); + + LadderInfo.Matches.CollectionChanged += (_, __) => updateMessage(); + updateMessage(); + } + + private void updateMessage() + { + rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1; } public void BeginJoin(TournamentMatch match, bool losers) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 307ee1c773..bbe4a53d8f 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -87,30 +87,7 @@ namespace osu.Game.Tournament }, } }, - heightWarning = new Container - { - Masking = true, - CornerRadius = 5, - Depth = float.MinValue, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.Red, - RelativeSizeAxes = Axes.Both, - }, - new TournamentSpriteText - { - Text = "Please make the window wider", - Font = OsuFont.Torus.With(weight: FontWeight.Bold), - Colour = Color4.White, - Padding = new MarginPadding(20) - } - } - }, + heightWarning = new WarningBox("Please make the window wider"), new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/WarningBox.cs b/osu.Game.Tournament/WarningBox.cs new file mode 100644 index 0000000000..814482aea4 --- /dev/null +++ b/osu.Game.Tournament/WarningBox.cs @@ -0,0 +1,40 @@ +// 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.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Tournament +{ + internal class WarningBox : Container + { + public WarningBox(string text) + { + Masking = true; + CornerRadius = 5; + Depth = float.MinValue; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new Box + { + Colour = Color4.Red, + RelativeSizeAxes = Axes.Both, + }, + new TournamentSpriteText + { + Text = text, + Font = OsuFont.Torus.With(weight: FontWeight.Bold), + Colour = Color4.White, + Padding = new MarginPadding(20) + } + }; + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index c02b6002d9..e9d26683c3 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => + { + // Computation may have finished in a previous task. + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _)) + return existing; + + return computeDifficulty(key, beatmapInfo, rulesetInfo); + }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -245,7 +251,7 @@ namespace osu.Game.Beatmaps updateScheduler?.Dispose(); } - private readonly struct DifficultyCacheLookup : IEquatable + public readonly struct DifficultyCacheLookup : IEquatable { public readonly int BeatmapId; public readonly int RulesetId; @@ -261,7 +267,7 @@ namespace osu.Game.Beatmaps public bool Equals(DifficultyCacheLookup other) => BeatmapId == other.BeatmapId && RulesetId == other.RulesetId - && Mods.SequenceEqual(other.Mods); + && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym)); public override int GetHashCode() { diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3860f12baa..c5be5810e9 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.IO.Serialization; using osu.Game.Rulesets; @@ -14,6 +15,7 @@ using osu.Game.Scoring; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] [Serializable] public class BeatmapInfo : IEquatable, IJsonSerializable, IHasPrimaryKey { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b4b341634c..34bb578b2a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,6 +9,7 @@ using System.Linq.Expressions; using System.Text; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -26,6 +27,8 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Users; +using osu.Game.Skinning; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -63,16 +66,16 @@ namespace osu.Game.Beatmaps private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; private readonly AudioManager audioManager; - private readonly GameHost host; private readonly BeatmapOnlineLookupQueue onlineLookupQueue; + private readonly TextureStore textureStore; + private readonly ITrackStore trackStore; - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; this.audioManager = audioManager; - this.host = host; DefaultBeatmap = defaultBeatmap; @@ -83,6 +86,9 @@ namespace osu.Game.Beatmaps beatmaps.ItemUpdated += removeWorkingCache; onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + + textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); + trackStore = audioManager.GetTrackStore(Files.Store); } protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => @@ -90,6 +96,34 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) + { + var metadata = new BeatmapMetadata + { + Artist = "artist", + Title = "title", + Author = user, + }; + + var set = new BeatmapSetInfo + { + Metadata = metadata, + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + Metadata = metadata, + Version = "difficulty" + } + } + }; + + var working = Import(set).Result; + return GetWorkingBeatmap(working.Beatmaps.First()); + } + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) @@ -194,31 +228,42 @@ namespace osu.Game.Beatmaps /// /// The to save the content against. The file referenced by will be replaced. /// The content to write. - public void Save(BeatmapInfo info, IBeatmap beatmapContent) + /// The beatmap content to write, null if to be omitted. + public void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) { var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent).Encode(sw); + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); stream.Seek(0, SeekOrigin.Begin); using (ContextFactory.GetForWrite()) { var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; + + // grab the original file (or create a new one if not found). + var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); + + // metadata may have changed; update the path with the standard format. + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + // update existing or populate new file's filename. + fileInfo.Filename = beatmapInfo.Path; + stream.Seek(0, SeekOrigin.Begin); - UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); + UpdateFile(setInfo, fileInfo, stream); } } removeWorkingCache(info); } - private readonly WeakList workingCache = new WeakList(); + private readonly WeakList workingCache = new WeakList(); /// /// Retrieve a instance for the provided @@ -246,16 +291,13 @@ namespace osu.Game.Beatmaps lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; - if (working == null) - { - beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; + beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, - new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager)); - } + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager)); - previous?.TransferTo(working); return working; } } @@ -459,7 +501,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; - protected override Track GetTrack() => null; + protected override Track GetBeatmapTrack() => null; } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 39c5ccab27..362c99ea3f 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -17,19 +17,25 @@ namespace osu.Game.Beatmaps { public partial class BeatmapManager { - protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap + private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { private readonly IResourceStore store; + private readonly TextureStore textureStore; + private readonly ITrackStore trackStore; - public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, BeatmapInfo beatmapInfo, AudioManager audioManager) + public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager) : base(beatmapInfo, audioManager) { this.store = store; this.textureStore = textureStore; + this.trackStore = trackStore; } protected override IBeatmap GetBeatmap() { + if (BeatmapInfo.Path == null) + return new Beatmap { BeatmapInfo = BeatmapInfo }; + try { using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) @@ -44,10 +50,6 @@ namespace osu.Game.Beatmaps private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - private TextureStore textureStore; - - private ITrackStore trackStore; - protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. protected override Texture GetBackground() @@ -66,11 +68,14 @@ namespace osu.Game.Beatmaps } } - protected override Track GetTrack() + protected override Track GetBeatmapTrack() { + if (Metadata?.AudioFile == null) + return null; + try { - return (trackStore ??= AudioManager.GetTrackStore(store)).Get(getPathForFile(Metadata.AudioFile)); + return trackStore.Get(getPathForFile(Metadata.AudioFile)); } catch (Exception e) { @@ -79,24 +84,11 @@ namespace osu.Game.Beatmaps } } - public override void RecycleTrack() - { - base.RecycleTrack(); - - trackStore?.Dispose(); - trackStore = null; - } - - public override void TransferTo(WorkingBeatmap other) - { - base.TransferTo(other); - - if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) - owb.textureStore = textureStore; - } - protected override Waveform GetWaveform() { + if (Metadata?.AudioFile == null) + return null; + try { var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 775d78f1fb..39b3c23ddd 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -6,11 +6,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Users; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] [Serializable] public class BeatmapMetadata : IEquatable, IHasPrimaryKey { diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index a8b83dca38..b76d780860 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using osu.Framework.Testing; using osu.Game.Database; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable { public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 0745ec5222..9d87a20d60 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -1,14 +1,31 @@ // 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 osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osuTK; namespace osu.Game.Beatmaps { public class BeatmapStatistic { - public IconUsage Icon; + [Obsolete("Use CreateIcon instead")] // can be removed 20210203 + public IconUsage Icon = FontAwesome.Regular.QuestionCircle; + + /// + /// A function to create the icon for display purposes. Use default icons available via whenever possible for conformity. + /// + public Func CreateIcon; + public string Content; public string Name; + + public BeatmapStatistic() + { +#pragma warning disable 618 + CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) }; +#pragma warning restore 618 + } } } diff --git a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs new file mode 100644 index 0000000000..181fb540df --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Beatmaps +{ + /// + /// A default implementation of an icon used to represent beatmap statistics. + /// + public class BeatmapStatisticIcon : Sprite + { + private readonly BeatmapStatisticsIconType iconType; + + public BeatmapStatisticIcon(BeatmapStatisticsIconType iconType) + { + this.iconType = iconType; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().Kebaberize()}"); + } + } + + public enum BeatmapStatisticsIconType + { + Accuracy, + ApproachRate, + Bpm, + Circles, + HpDrain, + Length, + OverallDifficulty, + Size, + Sliders, + Spinners, + } +} diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 8080e94075..af2a2ac250 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; @@ -19,7 +20,7 @@ namespace osu.Game.Beatmaps { private readonly TextureStore textures; - public DummyWorkingBeatmap(AudioManager audio, TextureStore textures) + public DummyWorkingBeatmap([NotNull] AudioManager audio, TextureStore textures) : base(new BeatmapInfo { Metadata = new BeatmapMetadata @@ -44,7 +45,7 @@ namespace osu.Game.Beatmaps protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); - protected override Track GetTrack() => GetVirtualTrack(); + protected override Track GetBeatmapTrack() => GetVirtualTrack(); private class DummyRulesetInfo : RulesetInfo { diff --git a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs index 8f6c7dc328..dba3a37545 100644 --- a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs @@ -8,6 +8,6 @@ namespace osu.Game.Beatmaps.Formats { public interface IHasCustomColours { - Dictionary CustomColours { get; set; } + Dictionary CustomColours { get; } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 57555cce90..80a4d6dea4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -7,13 +7,16 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Beatmaps.Formats { @@ -23,9 +26,18 @@ namespace osu.Game.Beatmaps.Formats private readonly IBeatmap beatmap; - public LegacyBeatmapEncoder(IBeatmap beatmap) + [CanBeNull] + private readonly ISkin skin; + + /// + /// Creates a new . + /// + /// The beatmap to encode. + /// The beatmap's skin, used for encoding combo colours. + public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) { this.beatmap = beatmap; + this.skin = skin; if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3) throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap)); @@ -53,6 +65,9 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(); handleControlPoints(writer); + writer.WriteLine(); + handleColours(writer); + writer.WriteLine(); handleHitObjects(writer); } @@ -196,6 +211,28 @@ namespace osu.Game.Beatmaps.Formats } } + private void handleColours(TextWriter writer) + { + var colours = skin?.GetConfig>(GlobalSkinColours.ComboColours)?.Value; + + if (colours == null || colours.Count == 0) + return; + + writer.WriteLine("[Colours]"); + + for (var i = 0; i < colours.Count; i++) + { + var comboColour = colours[i]; + + writer.Write(FormattableString.Invariant($"Combo{i}: ")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.A * byte.MaxValue)}")); + writer.WriteLine(); + } + } + private void handleHitObjects(TextWriter writer) { if (beatmap.HitObjects.Count == 0) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 44ef9bcacc..c15240a4f6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats try { byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255; - - if (alpha == 0) - alpha = 255; - colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha); } catch diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 31975157a0..bcd94d76fd 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -26,11 +26,6 @@ namespace osu.Game.Beatmaps /// Texture Background { get; } - /// - /// Retrieves the audio track for this . - /// - Track Track { get; } - /// /// Retrieves the for the of this . /// @@ -59,5 +54,18 @@ namespace osu.Game.Beatmaps /// The converted . /// If could not be converted to . IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); + + /// + /// Load a new audio track instance for this beatmap. This should be called once before accessing . + /// The caller of this method is responsible for the lifetime of the track. + /// + /// + /// In a standard game context, the loading of the track is managed solely by MusicController, which will + /// automatically load the track of the current global IBindable WorkingBeatmap. + /// As such, this method should only be called in very special scenarios, such as external tests or apps which are + /// outside of the game context. + /// + /// A fresh track instance, which will also be available via . + Track LoadTrack(); } } diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 583e950e49..0e517ea3df 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -38,5 +38,6 @@ namespace osu.Game.Beatmaps.Legacy Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + Mirror = 1 << 30, } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index b4bcf285b9..d9780233d1 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -7,11 +7,13 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Statistics; +using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; @@ -21,6 +23,7 @@ using osu.Game.Storyboards; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] public abstract class WorkingBeatmap : IWorkingBeatmap { public readonly BeatmapInfo BeatmapInfo; @@ -40,7 +43,6 @@ namespace osu.Game.Beatmaps BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); - track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack(1000)); background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); @@ -53,7 +55,7 @@ namespace osu.Game.Beatmaps { const double excess_length = 1000; - var lastObject = Beatmap.HitObjects.LastOrDefault(); + var lastObject = Beatmap?.HitObjects.LastOrDefault(); double length; @@ -259,10 +261,39 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; - public virtual bool TrackLoaded => track.IsResultAvailable; - public Track Track => track.Value; - protected abstract Track GetTrack(); - private RecyclableLazy track; + private Track loadedTrack; + + [NotNull] + public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000); + + /// + /// Transfer a valid audio track into this working beatmap. Used as an optimisation to avoid reload / track swap + /// across difficulties in the same beatmap set. + /// + /// The track to transfer. + public void TransferTrack([NotNull] Track track) => loadedTrack = track ?? throw new ArgumentNullException(nameof(track)); + + /// + /// Whether this beatmap's track has been loaded via . + /// + public virtual bool TrackLoaded => loadedTrack != null; + + /// + /// Get the loaded audio track instance. must have first been called. + /// This generally happens via MusicController when changing the global beatmap. + /// + public Track Track + { + get + { + if (!TrackLoaded) + throw new InvalidOperationException($"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}."); + + return loadedTrack; + } + } + + protected abstract Track GetBeatmapTrack(); public bool WaveformLoaded => waveform.IsResultAvailable; public Waveform Waveform => waveform.Value; @@ -280,22 +311,6 @@ namespace osu.Game.Beatmaps protected virtual ISkin GetSkin() => new DefaultSkin(); private readonly RecyclableLazy skin; - /// - /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. - /// - /// The new beatmap which is being switched to. - public virtual void TransferTo(WorkingBeatmap other) - { - if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) - other.track = track; - } - - /// - /// Eagerly dispose of the audio track associated with this (if any). - /// Accessing track again will load a fresh instance. - /// - public virtual void RecycleTrack() => track.Recycle(); - ~WorkingBeatmap() { total_count.Value--; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a8a8794320..71820dea55 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -6,6 +6,7 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; @@ -13,6 +14,7 @@ using osu.Game.Screens.Select.Filter; namespace osu.Game.Configuration { + [ExcludeFromDynamicCompile] public class OsuConfigManager : IniConfigManager { protected override void InitialiseDefaults() @@ -231,6 +233,6 @@ namespace osu.Game.Configuration UIHoldActivationDelay, HitLighting, MenuBackgroundSource, - GameplayDisableWinKey + GameplayDisableWinKey, } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 915d980d24..49d7edd56c 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -397,15 +397,24 @@ namespace osu.Game.Database } } + /// + /// Update an existing file, or create a new entry if not already part of the 's files. + /// + /// The item to operate on. + /// The file model to be updated or added. + /// The new file contents. public void UpdateFile(TModel model, TFileModel file, Stream contents) { using (var usage = ContextFactory.GetForWrite()) { // Dereference the existing file info, since the file model will be removed. - Files.Dereference(file.FileInfo); + if (file.FileInfo != null) + { + Files.Dereference(file.FileInfo); - // Remove the file model. - usage.Context.Set().Remove(file); + // Remove the file model. + usage.Context.Set().Remove(file); + } // Add the new file info and containing file model. model.Files.Remove(file); diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index df063f57d5..1c9cdc174a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -47,7 +47,7 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - Track track = null; + ITrack track = null; IBeatmap beatmap = null; double currentTrackTime = 0; diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 93ac69bdbf..751ccc8f15 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers [Resolved] private PreviewTrackManager previewTrackManager { get; set; } - protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); + protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); [BackgroundDependencyLoader(true)] private void load(AudioManager audio) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index d739f56828..f32f8e0c67 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -26,8 +26,11 @@ namespace osu.Game.Graphics.Containers { if (value == expandableHeader) return; - expandableHeader?.Expire(); + if (expandableHeader != null) + RemoveInternal(expandableHeader); + expandableHeader = value; + if (value == null) return; AddInternal(expandableHeader); diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 3015c44613..4c7f7957e9 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; +using osu.Framework.Input.StateChanges; namespace osu.Game.Graphics.Cursor { @@ -47,7 +48,10 @@ namespace osu.Game.Graphics.Cursor { base.Update(); - if (!CanShowCursor) + var lastMouseSource = inputManager.CurrentState.Mouse.LastSource; + bool hasValidInput = lastMouseSource != null && !(lastMouseSource is ISourcedFromTouch); + + if (!hasValidInput || !CanShowCursor) { currentTarget?.Cursor?.Hide(); currentTarget = null; diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index fc3a7229fa..cc76c12975 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -17,6 +17,8 @@ namespace osu.Game.Graphics.UserInterface { public class OsuDropdown : Dropdown, IHasAccentColour { + private const float corner_radius = 4; + private Color4 accentColour; public Color4 AccentColour @@ -57,9 +59,11 @@ namespace osu.Game.Graphics.UserInterface // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring public OsuDropdownMenu() { - CornerRadius = 4; + CornerRadius = corner_radius; BackgroundColour = Color4.Black.Opacity(0.5f); + MaskingContainer.CornerRadius = corner_radius; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring ItemsContainer.Padding = new MarginPadding(5); } @@ -138,7 +142,7 @@ namespace osu.Game.Graphics.UserInterface Foreground.Padding = new MarginPadding(2); Masking = true; - CornerRadius = 6; + CornerRadius = corner_radius; } [BackgroundDependencyLoader] @@ -237,7 +241,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.None; Margin = new MarginPadding { Bottom = 4 }; - CornerRadius = 4; + CornerRadius = corner_radius; Height = 40; Foreground.Children = new Drawable[] diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index 3ea9c1053c..1ccf7798e5 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -40,10 +40,5 @@ namespace osu.Game.Graphics.UserInterface protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true)); - - public override void Increment(double amount) - { - Current.Value += amount; - } } } diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index a469927595..91a557094d 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -61,20 +61,12 @@ namespace osu.Game.Graphics.UserInterface } } - public abstract void Increment(T amount); - /// /// Skeleton of a numeric counter which value rolls over time. /// protected RollingCounter() { AutoSizeAxes = Axes.Both; - - Current.ValueChanged += val => - { - if (IsLoaded) - TransformCount(DisplayedCount, val.NewValue); - }; } [BackgroundDependencyLoader] @@ -85,6 +77,13 @@ namespace osu.Game.Graphics.UserInterface Child = displayedCountSpriteText; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(val => TransformCount(DisplayedCount, val.NewValue), true); + } + /// /// Sets count value, bypassing rollover animation. /// diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index faabe69f87..73bbe5f03e 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -51,10 +51,5 @@ namespace osu.Game.Graphics.UserInterface protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); - - public override void Increment(double amount) - { - Current.Value += amount; - } } } diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs index aac0166774..c9790aed46 100644 --- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs +++ b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs @@ -33,11 +33,6 @@ namespace osu.Game.Graphics.UserInterface return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f; } - public override void Increment(int amount) - { - Current.Value += amount; - } - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 2cbe095d0b..290aba3468 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -32,6 +33,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => Component.Text = value; } + public Container TabbableContentContainer + { + set => Component.TabbableContentContainer = value; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 1dd3afbfae..5b2549d2ee 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -25,7 +25,13 @@ namespace osu.Game.IO this.subPath = subPath; } - protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path; + protected virtual string MutatePath(string path) + { + if (path == null) + return null; + + return !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path; + } protected virtual void ChangeTargetStorage(Storage newStorage) { diff --git a/osu.Game/Input/GameIdleTracker.cs b/osu.Game/Input/GameIdleTracker.cs new file mode 100644 index 0000000000..260be7e5c9 --- /dev/null +++ b/osu.Game/Input/GameIdleTracker.cs @@ -0,0 +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 osu.Framework.Input; + +namespace osu.Game.Input +{ + public class GameIdleTracker : IdleTracker + { + private InputManager inputManager; + + public GameIdleTracker(int time) + : base(time) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); + } + + protected override bool AllowIdle => inputManager.FocusedDrawable == null; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs index 75be9171b0..009639c1dc 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Rulesets; +using osu.Game.Scoring; namespace osu.Game.Online.API.Requests.Responses { @@ -22,5 +24,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"score")] public APILegacyScoreInfo Score; + + public ScoreInfo CreateScoreInfo(RulesetStore rulesets) + { + var score = Score.CreateScoreInfo(rulesets); + score.Position = Position; + return score; + } } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs index 0bba6a93bd..bcc8721400 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs @@ -33,6 +33,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("user")] public User User { get; set; } + [JsonProperty("position")] + public int? Position { get; set; } + public ScoreInfo CreateScoreInfo() => new ScoreInfo { @@ -40,6 +43,7 @@ namespace osu.Game.Online.API.Requests.Responses PP = PP, TotalScore = TotalScore, User = User, + Position = Position }; } } diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6af2561c89..648e4a762b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) + if (args.Length > 3 && args[1] == "osu.ppy.sh") { switch (args[2]) { diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 800029ceb9..db0f835c67 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -27,6 +27,7 @@ namespace osu.Game.Online.Leaderboards private readonly OsuScrollContainer scrollContainer; private readonly Container placeholderContainer; + private readonly UserTopScoreContainer topScoreContainer; private FillFlowContainer scrollFlow; @@ -87,6 +88,20 @@ namespace osu.Game.Online.Leaderboards } } + public TScoreInfo TopScore + { + get => topScoreContainer.Score.Value; + set + { + topScoreContainer.Score.Value = value; + + if (value == null) + topScoreContainer.Hide(); + else + topScoreContainer.Show(); + } + } + protected virtual FillFlowContainer CreateScoreFlow() => new FillFlowContainer { @@ -198,8 +213,9 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, + Child = topScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) }, - } + }, }, }, }, @@ -367,5 +383,7 @@ namespace osu.Game.Online.Leaderboards } protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); + + protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); } } diff --git a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs similarity index 77% rename from osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs rename to osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index 8e10734454..ab4210251e 100644 --- a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -9,31 +9,29 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; -using osu.Game.Scoring; using osuTK; -namespace osu.Game.Screens.Select.Leaderboards +namespace osu.Game.Online.Leaderboards { - public class UserTopScoreContainer : VisibilityContainer + public class UserTopScoreContainer : VisibilityContainer { private const int duration = 500; + public Bindable Score = new Bindable(); + private readonly Container scoreContainer; - - public Bindable Score = new Bindable(); - - public Action ScoreSelected; + private readonly Func createScoreDelegate; protected override bool StartHidden => true; [Resolved] private RulesetStore rulesets { get; set; } - public UserTopScoreContainer() + public UserTopScoreContainer(Func createScoreDelegate) { + this.createScoreDelegate = createScoreDelegate; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -72,7 +70,7 @@ namespace osu.Game.Screens.Select.Leaderboards private CancellationTokenSource loadScoreCancellation; - private void onScoreChanged(ValueChangedEvent score) + private void onScoreChanged(ValueChangedEvent score) { var newScore = score.NewValue; @@ -82,12 +80,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (newScore == null) return; - var scoreInfo = newScore.Score.CreateScoreInfo(rulesets); - - LoadComponentAsync(new LeaderboardScore(scoreInfo, newScore.Position, false) - { - Action = () => ScoreSelected?.Invoke(scoreInfo) - }, drawableScore => + LoadComponentAsync(createScoreDelegate(newScore), drawableScore => { scoreContainer.Child = drawableScore; drawableScore.FadeInFromZero(duration, Easing.OutQuint); diff --git a/osu.Game/Online/Multiplayer/APILeaderboard.cs b/osu.Game/Online/Multiplayer/APILeaderboard.cs new file mode 100644 index 0000000000..65863d6e0e --- /dev/null +++ b/osu.Game/Online/Multiplayer/APILeaderboard.cs @@ -0,0 +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 System.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.Multiplayer +{ + public class APILeaderboard + { + [JsonProperty("leaderboard")] + public List Leaderboard; + + [JsonProperty("user_score")] + public APIUserScoreAggregate UserScore; + } +} diff --git a/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs similarity index 65% rename from osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs index bc913030dd..37c21457bc 100644 --- a/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs @@ -1,17 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.Multiplayer { - public class GetRoomScoresRequest : APIRequest> + public class GetRoomLeaderboardRequest : APIRequest { private readonly int roomId; - public GetRoomScoresRequest(int roomId) + public GetRoomLeaderboardRequest(int roomId) { this.roomId = roomId; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 053eb01dcd..fec6640f2f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -38,6 +38,7 @@ using osu.Game.Input; using osu.Game.Overlays.Notifications; using osu.Game.Input.Bindings; using osu.Game.Online.Chat; +using osu.Game.Overlays.Music; using osu.Game.Skinning; using osuTK.Graphics; using osu.Game.Overlays.Volume; @@ -88,7 +89,10 @@ namespace osu.Game private IdleTracker idleTracker; - public readonly Bindable OverlayActivationMode = new Bindable(); + /// + /// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen. + /// + public readonly IBindable OverlayActivationMode = new Bindable(); protected OsuScreenStack ScreenStack; @@ -425,23 +429,7 @@ namespace osu.Game updateModDefaults(); - var newBeatmap = beatmap.NewValue; - - if (newBeatmap != null) - { - newBeatmap.Track.Completed += () => Scheduler.AddOnce(() => trackCompleted(newBeatmap)); - newBeatmap.BeginAsyncLoad(); - } - - void trackCompleted(WorkingBeatmap b) - { - // the source of track completion is the audio thread, so the beatmap may have changed before firing. - if (Beatmap.Value != b) - return; - - if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) - MusicController.NextTrack(); - } + beatmap.NewValue?.BeginAsyncLoad(); } private void modsChanged(ValueChangedEvent> mods) @@ -615,8 +603,6 @@ namespace osu.Game loadComponentSingleFile(new OnScreenDisplay(), Add, true); - loadComponentSingleFile(MusicController = new MusicController(), Add, true); - loadComponentSingleFile(notifications.With(d => { d.GetToolbarHeight = () => ToolbarOffset; @@ -662,6 +648,7 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); + Add(new MusicKeyBindingHandler()); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; @@ -713,9 +700,9 @@ namespace osu.Game float offset = 0; if (Settings.State.Value == Visibility.Visible) - offset += ToolbarButton.WIDTH / 2; + offset += Toolbar.HEIGHT / 2; if (notifications.State.Value == Visibility.Visible) - offset -= ToolbarButton.WIDTH / 2; + offset -= Toolbar.HEIGHT / 2; screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint); } @@ -733,24 +720,6 @@ namespace osu.Game overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime); } - public class GameIdleTracker : IdleTracker - { - private InputManager inputManager; - - public GameIdleTracker(int time) - : base(time) - { - } - - protected override void LoadComplete() - { - base.LoadComplete(); - inputManager = GetContainingInputManager(); - } - - protected override bool AllowIdle => inputManager.FocusedDrawable == null; - } - private void forwardLoggedErrorsToNotifications() { int recentLogCount = 0; @@ -921,8 +890,6 @@ namespace osu.Game private ScalingContainer screenContainer; - protected MusicController MusicController { get; private set; } - protected override bool OnExiting() { if (ScreenStack.CurrentScreen is Loader) @@ -972,9 +939,12 @@ namespace osu.Game break; } + if (current is IOsuScreen currentOsuScreen) + OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); + if (newScreen is IOsuScreen newOsuScreen) { - OverlayActivationMode.Value = newOsuScreen.InitialOverlayActivationMode; + OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; @@ -1005,10 +975,4 @@ namespace osu.Game Exit(); } } - - public enum ScorePresentType - { - Results, - Gameplay - } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3839d4e734..95b730261b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Overlays; using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -75,6 +76,8 @@ namespace osu.Game protected MenuCursorContainer MenuCursorContainer; + protected MusicController MusicController; + private Container content; protected override Container Content => content; @@ -239,16 +242,6 @@ namespace osu.Game Beatmap = new NonNullableBindable(defaultBeatmap); - // ScheduleAfterChildren is safety against something in the current frame accessing the previous beatmap's track - // and potentially causing a reload of it after just unloading. - // Note that the reason for this being added *has* been resolved, so it may be feasible to removed this if required. - Beatmap.BindValueChanged(b => ScheduleAfterChildren(() => - { - // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) - if (b.OldValue?.TrackLoaded == true && b.OldValue?.Track != b.NewValue?.Track) - b.OldValue.RecycleTrack(); - })); - dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); @@ -258,10 +251,11 @@ namespace osu.Game AddInternal(apiAccess); AddInternal(RulesetConfigCache); - GlobalActionContainer globalBinding; - MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; - MenuCursorContainer.Child = globalBinding = new GlobalActionContainer(this) + + GlobalActionContainer globalBindings; + + MenuCursorContainer.Child = globalBindings = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both } @@ -269,13 +263,16 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer)); - KeyBindingStore.Register(globalBinding); - dependencies.Cache(globalBinding); + KeyBindingStore.Register(globalBindings); + dependencies.Cache(globalBindings); PreviewTrackManager previewTrackManager; dependencies.Cache(previewTrackManager = new PreviewTrackManager()); Add(previewTrackManager); + AddInternal(MusicController = new MusicController()); + dependencies.CacheAs(MusicController); + Ruleset.BindValueChanged(onRulesetChanged); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 1bab200fec..6a9a71210a 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -12,7 +12,8 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapListingTitle() { Title = "beatmap listing"; - IconTexture = "Icons/changelog"; + Description = "browse for new beatmaps"; + IconTexture = "Icons/Hexacons/beatmap"; } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 225a8a0578..144af91145 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -24,7 +24,7 @@ using osuTK; namespace osu.Game.Overlays { - public class BeatmapListingOverlay : FullscreenOverlay + public class BeatmapListingOverlay : FullscreenOverlay { [Resolved] private PreviewTrackManager previewTrackManager { get; set; } @@ -38,7 +38,7 @@ namespace osu.Game.Overlays private OverlayScrollContainer resultScrollContainer; public BeatmapListingOverlay() - : base(OverlayColourScheme.Blue) + : base(OverlayColourScheme.Blue, new BeatmapListingHeader()) { } @@ -65,7 +65,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - new BeatmapListingHeader(), + Header, filterControl = new BeatmapListingFilterControl { SearchStarted = onSearchStarted, diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 4626589d81..6511b15fc8 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = "beatmap info"; - IconTexture = "Icons/changelog"; + IconTexture = "Icons/Hexacons/beatmap"; } } } diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 3e23442023..bbec62a85a 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -19,12 +19,14 @@ using osuTK; namespace osu.Game.Overlays { - public class BeatmapSetOverlay : FullscreenOverlay + public class BeatmapSetOverlay : FullscreenOverlay // we don't provide a standard header for now. { public const float X_PADDING = 40; public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; - protected readonly Header Header; + + //todo: should be an OverlayHeader? or maybe not? + protected new readonly Header Header; [Resolved] private RulesetStore rulesets { get; set; } @@ -37,7 +39,7 @@ namespace osu.Game.Overlays private readonly Box background; public BeatmapSetOverlay() - : base(OverlayColourScheme.Blue) + : base(OverlayColourScheme.Blue, null) { OverlayScrollContainer scroll; Info info; diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 050bdea03a..f4be4328e7 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -115,7 +115,8 @@ namespace osu.Game.Overlays.Changelog public ChangelogHeaderTitle() { Title = "changelog"; - IconTexture = "Icons/changelog"; + Description = "track recent dev updates in the osu! ecosystem"; + IconTexture = "Icons/Hexacons/devtools"; } } } diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 726be9e194..c7e9a86fa4 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -21,12 +21,10 @@ using osu.Game.Overlays.Changelog; namespace osu.Game.Overlays { - public class ChangelogOverlay : FullscreenOverlay + public class ChangelogOverlay : FullscreenOverlay { public readonly Bindable Current = new Bindable(); - protected ChangelogHeader Header; - private Container content; private SampleChannel sampleBack; @@ -36,7 +34,7 @@ namespace osu.Game.Overlays protected List Streams; public ChangelogOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, new ChangelogHeader()) { } @@ -61,10 +59,11 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new ChangelogHeader + Header.With(h => { - ListingSelected = ShowListing, - }, + h.ListingSelected = ShowListing; + h.Build.BindTarget = Current; + }), content = new Container { RelativeSizeAxes = Axes.X, @@ -77,8 +76,6 @@ namespace osu.Game.Overlays sampleBack = audio.Samples.Get(@"UI/generic-select-soft"); - Header.Build.BindTo(Current); - Current.BindValueChanged(e => { if (e.NewValue != null) diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index b46ca6b040..be9ecc6746 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Chat.Selection { public class ChannelSelectionOverlay : WaveOverlayContainer { - public const float WIDTH_PADDING = 170; + public new const float WIDTH_PADDING = 170; private const float transition_duration = 500; diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 5ba55f6d45..25a59e9b25 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osuTK; using osuTK.Graphics; @@ -25,8 +26,12 @@ using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays { - public class ChatOverlay : OsuFocusedOverlayContainer + public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/messaging"; + public string Title => "chat"; + public string Description => "join the real-time discussion"; + private const float textbox_height = 60; private const float channel_selection_min_height = 0.3f; @@ -218,14 +223,13 @@ namespace osu.Game.Overlays Schedule(() => { // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. - channelManager.JoinedChannels.ItemsAdded += onChannelAddedToJoinedChannels; - channelManager.JoinedChannels.ItemsRemoved += onChannelRemovedFromJoinedChannels; + channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; + foreach (Channel channel in channelManager.JoinedChannels) ChannelTabControl.AddChannel(channel); - channelManager.AvailableChannels.ItemsAdded += availableChannelsChanged; - channelManager.AvailableChannels.ItemsRemoved += availableChannelsChanged; - ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels); + channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; + availableChannelsChanged(null, null); currentChannel = channelManager.CurrentChannel.GetBoundCopy(); currentChannel.BindValueChanged(currentChannelChanged, true); @@ -384,34 +388,41 @@ namespace osu.Game.Overlays base.PopOut(); } - private void onChannelAddedToJoinedChannels(IEnumerable channels) + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { - foreach (Channel channel in channels) - ChannelTabControl.AddChannel(channel); - } - - private void onChannelRemovedFromJoinedChannels(IEnumerable channels) - { - foreach (Channel channel in channels) + switch (args.Action) { - ChannelTabControl.RemoveChannel(channel); + case NotifyCollectionChangedAction.Add: + foreach (Channel channel in args.NewItems.Cast()) + ChannelTabControl.AddChannel(channel); + break; - var loaded = loadedChannels.Find(c => c.Channel == channel); + case NotifyCollectionChangedAction.Remove: + foreach (Channel channel in args.OldItems.Cast()) + { + ChannelTabControl.RemoveChannel(channel); - if (loaded != null) - { - loadedChannels.Remove(loaded); + var loaded = loadedChannels.Find(c => c.Channel == channel); - // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared - // to ensure that the previous channel doesn't get updated after it's disposed - currentChannelContainer.Remove(loaded); - loaded.Dispose(); - } + if (loaded != null) + { + loadedChannels.Remove(loaded); + + // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared + // to ensure that the previous channel doesn't get updated after it's disposed + currentChannelContainer.Remove(loaded); + loaded.Dispose(); + } + } + + break; } } - private void availableChannelsChanged(IEnumerable channels) - => ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels); + private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels); + } protected override void Dispose(bool isDisposing) { @@ -420,10 +431,8 @@ namespace osu.Game.Overlays if (channelManager != null) { channelManager.CurrentChannel.ValueChanged -= currentChannelChanged; - channelManager.JoinedChannels.ItemsAdded -= onChannelAddedToJoinedChannels; - channelManager.JoinedChannels.ItemsRemoved -= onChannelRemovedFromJoinedChannels; - channelManager.AvailableChannels.ItemsAdded -= availableChannelsChanged; - channelManager.AvailableChannels.ItemsRemoved -= availableChannelsChanged; + channelManager.JoinedChannels.CollectionChanged -= joinedChannelsChanged; + channelManager.AvailableChannels.CollectionChanged -= availableChannelsChanged; } } diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 9ee679a866..36bf589877 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -12,7 +12,8 @@ namespace osu.Game.Overlays.Dashboard public DashboardTitle() { Title = "dashboard"; - IconTexture = "Icons/changelog"; + Description = "view your friends and other information"; + IconTexture = "Icons/Hexacons/social"; } } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index e3a4b0e152..8135b83a03 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -15,17 +15,21 @@ using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Overlays { - public class DashboardOverlay : FullscreenOverlay + public class DashboardOverlay : FullscreenOverlay { private CancellationTokenSource cancellationToken; private Container content; - private DashboardOverlayHeader header; private LoadingLayer loading; private OverlayScrollContainer scrollFlow; public DashboardOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, new DashboardOverlayHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Depth = -float.MaxValue + }) { } @@ -50,12 +54,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new DashboardOverlayHeader - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Depth = -float.MaxValue - }, + Header, content = new Container { RelativeSizeAxes = Axes.X, @@ -72,7 +71,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - header.Current.BindValueChanged(onTabChanged); + Header.Current.BindValueChanged(onTabChanged); } private bool displayUpdateRequired = true; @@ -84,7 +83,7 @@ namespace osu.Game.Overlays // We don't want to create a new display on every call, only when exiting from fully closed state. if (displayUpdateRequired) { - header.Current.TriggerChange(); + Header.Current.TriggerChange(); displayUpdateRequired = false; } } @@ -136,7 +135,7 @@ namespace osu.Game.Overlays if (State.Value == Visibility.Hidden) return; - header.Current.TriggerChange(); + Header.Current.TriggerChange(); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 3464ce6086..bd6b07c65f 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -12,16 +12,25 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent + public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent, INamedOverlayComponent + where T : OverlayHeader { + public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty; + public virtual string Title => Header?.Title.Title ?? string.Empty; + public virtual string Description => Header?.Title.Description ?? string.Empty; + + public T Header { get; } + [Resolved] protected IAPIProvider API { get; private set; } [Cached] protected readonly OverlayColourProvider ColourProvider; - protected FullscreenOverlay(OverlayColourScheme colourScheme) + protected FullscreenOverlay(OverlayColourScheme colourScheme, T header) { + Header = header; + ColourProvider = new OverlayColourProvider(colourScheme); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs new file mode 100644 index 0000000000..38fb8679a0 --- /dev/null +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays +{ + public interface INamedOverlayComponent + { + string IconTexture { get; } + + string Title { get; } + + string Description { get; } + } +} diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs new file mode 100644 index 0000000000..e6edfb1e3e --- /dev/null +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -0,0 +1,81 @@ +// 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.Bindings; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Overlays.OSD; + +namespace osu.Game.Overlays.Music +{ + /// + /// Handles s related to music playback, and displays s via the global accordingly. + /// + public class MusicKeyBindingHandler : Component, IKeyBindingHandler + { + [Resolved] + private IBindable beatmap { get; set; } + + [Resolved] + private MusicController musicController { get; set; } + + [Resolved(canBeNull: true)] + private OnScreenDisplay onScreenDisplay { get; set; } + + public bool OnPressed(GlobalAction action) + { + if (beatmap.Disabled) + return false; + + switch (action) + { + case GlobalAction.MusicPlay: + // use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842) + bool wasPlaying = musicController.IsPlaying; + + if (musicController.TogglePause()) + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track")); + return true; + + case GlobalAction.MusicNext: + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track"))); + + return true; + + case GlobalAction.MusicPrev: + musicController.PreviousTrack(res => + { + switch (res) + { + case PreviousTrackResult.Restart: + onScreenDisplay?.Display(new MusicActionToast("Restart track")); + break; + + case PreviousTrackResult.Previous: + onScreenDisplay?.Display(new MusicActionToast("Previous track")); + break; + } + }); + + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + + private class MusicActionToast : Toast + { + public MusicActionToast(string action) + : base("Music Playback", action, string.Empty) + { + } + } + } +} diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index b878aba489..b45d84049f 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Music { if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) { - beatmap.Value?.Track?.Seek(0); + beatmap.Value?.Track.Seek(0); return; } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a990f9a6ab..b568e4d02b 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -4,16 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Input.Bindings; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Input.Bindings; -using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays @@ -21,7 +22,7 @@ namespace osu.Game.Overlays /// /// Handles playback of the global music track. /// - public class MusicController : Component, IKeyBindingHandler + public class MusicController : CompositeDrawable { [Resolved] private BeatmapManager beatmaps { get; set; } @@ -58,8 +59,8 @@ namespace osu.Game.Overlays [Resolved] private IBindable> mods { get; set; } - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } + [NotNull] + public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); private IBindable> managerUpdated; private IBindable> managerRemoved; @@ -73,12 +74,9 @@ namespace osu.Game.Overlays managerRemoved.BindValueChanged(beatmapRemoved); beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); - } - - protected override void LoadComplete() - { - base.LoadComplete(); + // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now. + // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load(). beatmap.BindValueChanged(beatmapChanged, true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } @@ -95,9 +93,14 @@ namespace osu.Game.Overlays } /// - /// Returns whether the current beatmap track is playing. + /// Returns whether the beatmap track is playing. /// - public bool IsPlaying => current?.Track.IsRunning ?? false; + public bool IsPlaying => CurrentTrack.IsRunning; + + /// + /// Returns whether the beatmap track is loaded. + /// + public bool TrackLoaded => CurrentTrack.TrackLoaded; private void beatmapUpdated(ValueChangedEvent> weakSet) { @@ -130,7 +133,7 @@ namespace osu.Game.Overlays seekDelegate = Schedule(() => { if (!beatmap.Disabled) - current?.Track.Seek(position); + CurrentTrack.Seek(position); }); } @@ -142,9 +145,7 @@ namespace osu.Game.Overlays { if (IsUserPaused) return; - var track = current?.Track; - - if (track == null || track is TrackVirtual) + if (CurrentTrack.IsDummyDevice) { if (beatmap.Disabled) return; @@ -163,17 +164,12 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool Play(bool restart = false) { - var track = current?.Track; - IsUserPaused = false; - if (track == null) - return false; - if (restart) - track.Restart(); + CurrentTrack.Restart(); else if (!IsPlaying) - track.Start(); + CurrentTrack.Start(); return true; } @@ -183,11 +179,9 @@ namespace osu.Game.Overlays /// public void Stop() { - var track = current?.Track; - IsUserPaused = true; - if (track?.IsRunning == true) - track.Stop(); + if (CurrentTrack.IsRunning) + CurrentTrack.Stop(); } /// @@ -196,9 +190,7 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool TogglePause() { - var track = current?.Track; - - if (track?.IsRunning == true) + if (CurrentTrack.IsRunning) Stop(); else Play(); @@ -209,7 +201,13 @@ namespace osu.Game.Overlays /// /// Play the previous track or restart the current track if it's current time below . /// - public void PreviousTrack() => Schedule(() => prev()); + /// Invoked when the operation has been performed successfully. + public void PreviousTrack(Action onSuccess = null) => Schedule(() => + { + PreviousTrackResult res = prev(); + if (res != PreviousTrackResult.None) + onSuccess?.Invoke(res); + }); /// /// Play the previous track or restart the current track if it's current time below . @@ -220,7 +218,7 @@ namespace osu.Game.Overlays if (beatmap.Disabled) return PreviousTrackResult.None; - var currentTrackPosition = current?.Track.CurrentTime; + var currentTrackPosition = CurrentTrack.CurrentTime; if (currentTrackPosition >= restart_cutoff_point) { @@ -234,9 +232,7 @@ namespace osu.Game.Overlays if (playable != null) { - if (beatmap is Bindable working) - working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - + changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value)); restartTrack(); return PreviousTrackResult.Previous; } @@ -247,7 +243,14 @@ namespace osu.Game.Overlays /// /// Play the next random or playlist track. /// - public void NextTrack() => Schedule(() => next()); + /// Invoked when the operation has been performed successfully. + /// A of the operation. + public void NextTrack(Action onSuccess = null) => Schedule(() => + { + bool res = next(); + if (res) + onSuccess?.Invoke(); + }); private bool next() { @@ -260,9 +263,7 @@ namespace osu.Game.Overlays if (playable != null) { - if (beatmap is Bindable working) - working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - + changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value)); restartTrack(); return true; } @@ -274,21 +275,30 @@ namespace osu.Game.Overlays { // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). // we probably want to move this to a central method for switching to a new working beatmap in the future. - Schedule(() => beatmap.Value.Track.Restart()); + Schedule(() => CurrentTrack.Restart()); } private WorkingBeatmap current; private TrackChangeDirection? queuedDirection; - private void beatmapChanged(ValueChangedEvent beatmap) + private void beatmapChanged(ValueChangedEvent beatmap) => changeBeatmap(beatmap.NewValue); + + private void changeBeatmap(WorkingBeatmap newWorking) { + // This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order + // (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when the local beatmap bindable is updated directly). + if (newWorking == current) + return; + + var lastWorking = current; + TrackChangeDirection direction = TrackChangeDirection.None; + bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false; + if (current != null) { - bool audioEquals = beatmap.NewValue?.BeatmapInfo?.AudioEquals(current.BeatmapInfo) ?? false; - if (audioEquals) direction = TrackChangeDirection.None; else if (queuedDirection.HasValue) @@ -300,18 +310,74 @@ namespace osu.Game.Overlays { // figure out the best direction based on order in playlist. var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); - var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); + var next = newWorking == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != newWorking.BeatmapSetInfo?.ID).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } } - current = beatmap.NewValue; + current = newWorking; + + if (!audioEquals || CurrentTrack.IsDummyDevice) + { + changeTrack(); + } + else + { + // transfer still valid track to new working beatmap + current.TransferTrack(lastWorking.Track); + } + TrackChanged?.Invoke(current, direction); ResetTrackAdjustments(); queuedDirection = null; + + // this will be a noop if coming from the beatmapChanged event. + // the exception is local operations like next/prev, where we want to complete loading the track before sending out a change. + if (beatmap.Value != current && beatmap is Bindable working) + working.Value = current; + } + + private void changeTrack() + { + var lastTrack = CurrentTrack; + + var queuedTrack = new DrawableTrack(current.LoadTrack()); + queuedTrack.Completed += () => onTrackCompleted(current); + + CurrentTrack = queuedTrack; + + // At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now. + // CurrentTrack is immediately updated above for situations where a immediate knowledge about the new track is required, + // but the mutation of the hierarchy is scheduled to avoid exceptions. + Schedule(() => + { + lastTrack.VolumeTo(0, 500, Easing.Out).Expire(); + + if (queuedTrack == CurrentTrack) + { + AddInternal(queuedTrack); + queuedTrack.VolumeTo(0).Then().VolumeTo(1, 300, Easing.Out); + } + else + { + // If the track has changed since the call to changeTrack, it is safe to dispose the + // queued track rather than consume it. + queuedTrack.Dispose(); + } + }); + } + + private void onTrackCompleted(WorkingBeatmap workingBeatmap) + { + // the source of track completion is the audio thread, so the beatmap may have changed before firing. + if (current != workingBeatmap) + return; + + if (!CurrentTrack.Looping && !beatmap.Disabled) + NextTrack(); } private bool allowRateAdjustments; @@ -332,66 +398,20 @@ namespace osu.Game.Overlays } } + /// + /// Resets the speed adjustments currently applied on and applies the mod adjustments if is true. + /// + /// + /// Does not reset speed adjustments applied directly to the beatmap track. + /// public void ResetTrackAdjustments() { - var track = current?.Track; - if (track == null) - return; - - track.ResetSpeedAdjustments(); + CurrentTrack.ResetSpeedAdjustments(); if (allowRateAdjustments) { foreach (var mod in mods.Value.OfType()) - mod.ApplyToTrack(track); - } - } - - public bool OnPressed(GlobalAction action) - { - if (beatmap.Disabled) - return false; - - switch (action) - { - case GlobalAction.MusicPlay: - if (TogglePause()) - onScreenDisplay?.Display(new MusicControllerToast(IsPlaying ? "Play track" : "Pause track")); - return true; - - case GlobalAction.MusicNext: - if (next()) - onScreenDisplay?.Display(new MusicControllerToast("Next track")); - - return true; - - case GlobalAction.MusicPrev: - switch (prev()) - { - case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicControllerToast("Restart track")); - break; - - case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicControllerToast("Previous track")); - break; - } - - return true; - } - - return false; - } - - public void OnReleased(GlobalAction action) - { - } - - public class MusicControllerToast : Toast - { - public MusicControllerToast(string action) - : base("Music Playback", action, string.Empty) - { + mod.ApplyToTrack(CurrentTrack); } } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index ddada2bdaf..63174128e7 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -57,7 +57,8 @@ namespace osu.Game.Overlays.News public NewsHeaderTitle() { Title = "news"; - IconTexture = "Icons/news"; + Description = "get up-to-date on community happenings"; + IconTexture = "Icons/Hexacons/news"; } } } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 09fb445b1f..c8c1db012f 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -13,17 +13,16 @@ using osu.Game.Overlays.News.Displays; namespace osu.Game.Overlays { - public class NewsOverlay : FullscreenOverlay + public class NewsOverlay : FullscreenOverlay { private readonly Bindable article = new Bindable(null); private Container content; private LoadingLayer loading; - private NewsHeader header; private OverlayScrollContainer scrollFlow; public NewsOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, new NewsHeader()) { } @@ -48,10 +47,10 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new NewsHeader + Header.With(h => { - ShowFrontPage = ShowFrontPage - }, + h.ShowFrontPage = ShowFrontPage; + }), content = new Container { RelativeSizeAxes = Axes.X, @@ -112,12 +111,12 @@ namespace osu.Game.Overlays if (e.NewValue == null) { - header.SetFrontPage(); + Header.SetFrontPage(); LoadDisplay(new FrontPageDisplay()); return; } - header.SetArticle(e.NewValue); + Header.SetArticle(e.NewValue); LoadDisplay(Empty()); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 41160d10ec..b5714fbcae 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -16,8 +16,12 @@ using osu.Framework.Threading; namespace osu.Game.Overlays { - public class NotificationOverlay : OsuFocusedOverlayContainer + public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/notification"; + public string Title => "notifications"; + public string Description => "waiting for 'ya"; + private const float width = 320; public const float TRANSITION_LENGTH = 600; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index ebb4a96d14..55adf02a45 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -25,8 +25,12 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class NowPlayingOverlay : OsuFocusedOverlayContainer + public class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/music"; + public string Title => "now playing"; + public string Description => "manage the currently playing track"; + private const float player_height = 130; private const float transition_length = 800; private const float progress_height = 10; @@ -234,9 +238,9 @@ namespace osu.Game.Overlays pendingBeatmapSwitch = null; } - var track = beatmap.Value?.TrackLoaded ?? false ? beatmap.Value.Track : null; + var track = musicController.CurrentTrack; - if (track?.IsDummyDevice == false) + if (!track.IsDummyDevice) { progressBar.EndTime = track.Length; progressBar.CurrentTime = track.CurrentTime; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index cc7f798c4a..fed1e57686 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays { public abstract class OverlayHeader : Container { + public OverlayTitle Title { get; } + private float contentSidePadding; /// @@ -73,7 +75,7 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Children = new[] { - CreateTitle().With(title => + Title = CreateTitle().With(title => { title.Anchor = Anchor.CentreLeft; title.Origin = Anchor.CentreLeft; diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index e7415e6f74..b67d5db1a4 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { /// - /// which provides . Mostly used in . + /// which provides . Mostly used in . /// public class OverlayScrollContainer : OsuScrollContainer { diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 1c9567428c..17eeece1f8 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -12,19 +12,27 @@ using osuTK; namespace osu.Game.Overlays { - public abstract class OverlayTitle : CompositeDrawable + public abstract class OverlayTitle : CompositeDrawable, INamedOverlayComponent { - private readonly OsuSpriteText title; + private readonly OsuSpriteText titleText; private readonly Container icon; - protected string Title + private string title; + + public string Title { - set => title.Text = value; + get => title; + protected set => titleText.Text = title = value; } - protected string IconTexture + public string Description { get; protected set; } + + private string iconTexture; + + public string IconTexture { - set => icon.Child = new OverlayTitleIcon(value); + get => iconTexture; + protected set => icon.Child = new OverlayTitleIcon(iconTexture = value); } protected OverlayTitle() @@ -45,7 +53,7 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Horizontal = 5 }, // compensates for osu-web sprites having around 5px of whitespace on each side Size = new Vector2(30) }, - title = new OsuSpriteText + titleText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index 3e2c54c726..312271316a 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -9,7 +9,7 @@ using osu.Game.Online.API; namespace osu.Game.Overlays { /// - /// A subview containing online content, to be displayed inside a . + /// A subview containing online content, to be displayed inside a . /// /// /// Automatically performs a data fetch on load. diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 55474c9d3e..c947ef0781 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeaderTitle() { Title = "player info"; - IconTexture = "Icons/profile"; + IconTexture = "Icons/Hexacons/profile"; } } diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index e30c6f07a8..92e22f5873 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -30,7 +30,8 @@ namespace osu.Game.Overlays.Rankings public RankingsTitle() { Title = "ranking"; - IconTexture = "Icons/rankings"; + Description = "find out who's the best right now"; + IconTexture = "Icons/Hexacons/rankings"; } } } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 7b200d4226..ae6d49960a 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -17,17 +17,16 @@ using osu.Game.Overlays.Rankings.Tables; namespace osu.Game.Overlays { - public class RankingsOverlay : FullscreenOverlay + public class RankingsOverlay : FullscreenOverlay { - protected Bindable Country => header.Country; + protected Bindable Country => Header.Country; - protected Bindable Scope => header.Current; + protected Bindable Scope => Header.Current; private readonly OverlayScrollContainer scrollFlow; private readonly Container contentContainer; private readonly LoadingLayer loading; private readonly Box background; - private readonly RankingsOverlayHeader header; private APIRequest lastRequest; private CancellationTokenSource cancellationToken; @@ -36,7 +35,12 @@ namespace osu.Game.Overlays private IAPIProvider api { get; set; } public RankingsOverlay() - : base(OverlayColourScheme.Green) + : base(OverlayColourScheme.Green, new RankingsOverlayHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Depth = -float.MaxValue + }) { Children = new Drawable[] { @@ -55,12 +59,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new RankingsOverlayHeader - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Depth = -float.MaxValue - }, + Header, new Container { RelativeSizeAxes = Axes.X, @@ -97,7 +96,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - header.Ruleset.BindTo(ruleset); + Header.Ruleset.BindTo(ruleset); Country.BindValueChanged(_ => { diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index e0163b5b0c..1990674aa9 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.SearchableList /// /// The amount of padding added to content (does not affect background or tab control strip). /// - protected virtual float ContentHorizontalPadding => SearchableListOverlay.WIDTH_PADDING; + protected virtual float ContentHorizontalPadding => WaveOverlayContainer.WIDTH_PADDING; protected SearchableListFilterControl() { diff --git a/osu.Game/Overlays/SearchableList/SearchableListHeader.cs b/osu.Game/Overlays/SearchableList/SearchableListHeader.cs deleted file mode 100644 index 66fedf0a56..0000000000 --- a/osu.Game/Overlays/SearchableList/SearchableListHeader.cs +++ /dev/null @@ -1,82 +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; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; - -namespace osu.Game.Overlays.SearchableList -{ - public abstract class SearchableListHeader : Container - where T : struct, Enum - { - public readonly HeaderTabControl Tabs; - - protected abstract Color4 BackgroundColour { get; } - protected abstract T DefaultTab { get; } - protected abstract Drawable CreateHeaderText(); - protected abstract IconUsage Icon { get; } - - protected SearchableListHeader() - { - RelativeSizeAxes = Axes.X; - Height = 90; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = SearchableListOverlay.WIDTH_PADDING, Right = SearchableListOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(-35f, 5f), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0f), - Children = new[] - { - new SpriteIcon - { - Size = new Vector2(25), - Icon = Icon, - }, - CreateHeaderText(), - }, - }, - Tabs = new HeaderTabControl - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - }, - }, - }; - - Tabs.Current.Value = DefaultTab; - Tabs.Current.TriggerChange(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Tabs.StripColour = colours.Green; - } - } -} diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs deleted file mode 100644 index 4ab2de06b6..0000000000 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ /dev/null @@ -1,128 +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; -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Cursor; - -namespace osu.Game.Overlays.SearchableList -{ - public abstract class SearchableListOverlay : FullscreenOverlay - { - public const float WIDTH_PADDING = 80; - - protected SearchableListOverlay(OverlayColourScheme colourScheme) - : base(colourScheme) - { - } - } - - public abstract class SearchableListOverlay : SearchableListOverlay - where THeader : struct, Enum - where TTab : struct, Enum - where TCategory : struct, Enum - { - private readonly Container scrollContainer; - - protected readonly SearchableListHeader Header; - protected readonly SearchableListFilterControl Filter; - protected readonly FillFlowContainer ScrollFlow; - - protected abstract Color4 BackgroundColour { get; } - protected abstract Color4 TrianglesColourLight { get; } - protected abstract Color4 TrianglesColourDark { get; } - protected abstract SearchableListHeader CreateHeader(); - protected abstract SearchableListFilterControl CreateFilterControl(); - - protected SearchableListOverlay(OverlayColourScheme colourScheme) - : base(colourScheme) - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] - { - new Triangles - { - RelativeSizeAxes = Axes.Both, - TriangleScale = 5, - ColourLight = TrianglesColourLight, - ColourDark = TrianglesColourDark, - }, - }, - }, - scrollContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Child = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = ScrollFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Bottom = 50 }, - Direction = FillDirection.Vertical, - }, - }, - }, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header = CreateHeader(), - Filter = CreateFilterControl(), - }, - }, - }; - } - - protected override void Update() - { - base.Update(); - - scrollContainer.Padding = new MarginPadding { Top = Header.Height + Filter.Height }; - } - - protected override void OnFocus(FocusEvent e) - { - Filter.Search.TakeFocus(); - } - - protected override void PopIn() - { - base.PopIn(); - - Filter.Search.HoldFocus = true; - } - - protected override void PopOut() - { - base.PopOut(); - - Filter.Search.HoldFocus = false; - } - } -} diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index aca507f20a..e5cebd28e2 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -1,12 +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 System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays.Settings.Sections.Gameplay; using osu.Game.Rulesets; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; namespace osu.Game.Overlays.Settings.Sections { @@ -34,9 +36,17 @@ namespace osu.Game.Overlays.Settings.Sections { foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) { - SettingsSubsection section = ruleset.CreateSettings(); - if (section != null) - Add(section); + try + { + SettingsSubsection section = ruleset.CreateSettings(); + + if (section != null) + Add(section); + } + catch (Exception e) + { + Logger.Error(e, "Failed to load ruleset settings"); + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 00b7643332..4312b319c0 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -163,8 +163,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); }, true); - windowModes.ItemsAdded += _ => windowModesChanged(); - windowModes.ItemsRemoved += _ => windowModesChanged(); + windowModes.CollectionChanged += (sender, args) => windowModesChanged(); windowModesChanged(); } diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 358f94b659..031ecaae46 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -4,22 +4,21 @@ using System; using System.Linq; using osu.Framework; -using osuTK; -using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Toolbar; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Settings { public class Sidebar : Container, IStateful { private readonly FillFlowContainer content; - public const float DEFAULT_WIDTH = ToolbarButton.WIDTH; + public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f; public const int EXPANDED_WIDTH = 200; public event Action StateChanged; diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index bb84de5d3a..e1bcdbbaf0 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -13,8 +13,12 @@ using osu.Framework.Bindables; namespace osu.Game.Overlays { - public class SettingsOverlay : SettingsPanel + public class SettingsOverlay : SettingsPanel, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/settings"; + public string Title => "settings"; + public string Description => "change the way osu! behaves"; + protected override IEnumerable CreateSections() => new SettingsSection[] { new GeneralSection(), @@ -30,7 +34,7 @@ namespace osu.Game.Overlays private readonly List subPanels = new List(); - protected override Drawable CreateHeader() => new SettingsHeader("settings", "Change the way osu! behaves"); + protected override Drawable CreateHeader() => new SettingsHeader(Title, Description); protected override Drawable CreateFooter() => new SettingsFooter(); public SettingsOverlay() diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 3bf9e85428..393e349bd0 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar private const double transition_time = 500; - protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); + protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. public override bool PropagateNonPositionalInputSubTree => true; diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index cde305fffd..0363873326 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarBeatmapListingButton() { - SetIcon(OsuIcon.ChevronDownCircle); - TooltipMain = "Beatmap listing"; - TooltipSub = "Browse for new beatmaps"; - Hotkey = GlobalAction.ToggleDirect; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 0afc6642b2..49b9c62d85 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -25,8 +26,6 @@ namespace osu.Game.Overlays.Toolbar { public abstract class ToolbarButton : OsuClickableContainer, IKeyBindingHandler { - public const float WIDTH = Toolbar.HEIGHT * 1.4f; - protected GlobalAction? Hotkey { get; set; } public void SetIcon(Drawable icon) @@ -35,16 +34,14 @@ namespace osu.Game.Overlays.Toolbar IconContainer.Show(); } - public void SetIcon(IconUsage icon) => SetIcon(new SpriteIcon - { - Size = new Vector2(20), - Icon = icon - }); + [Resolved] + private TextureStore textures { get; set; } - public IconUsage Icon - { - set => SetIcon(value); - } + public void SetIcon(string texture) => + SetIcon(new Sprite + { + Texture = textures.Get(texture), + }); public string Text { @@ -82,7 +79,7 @@ namespace osu.Game.Overlays.Toolbar protected ToolbarButton() : base(HoverSampleSet.Loud) { - Width = WIDTH; + Width = Toolbar.HEIGHT; RelativeSizeAxes = Axes.Y; Children = new Drawable[] @@ -116,7 +113,7 @@ namespace osu.Game.Overlays.Toolbar { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(20), + Size = new Vector2(26), Alpha = 0, }, DrawableText = new OsuSpriteText diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index c88b418853..23f8b141b2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -2,19 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Toolbar { public class ToolbarChangelogButton : ToolbarOverlayToggleButton { - public ToolbarChangelogButton() - { - SetIcon(FontAwesome.Solid.Bullhorn); - TooltipMain = "Changelog"; - TooltipSub = "Track recent dev updates in the osu! ecosystem"; - } - [BackgroundDependencyLoader(true)] private void load(ChangelogOverlay changelog) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index dee4be0c1f..f9a66ae7bb 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarChatButton() { - SetIcon(FontAwesome.Solid.Comments); - TooltipMain = "Chat"; - TooltipSub = "Join the real-time discussion"; - Hotkey = GlobalAction.ToggleChat; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 4845c9a99f..76fbd40d66 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Sprites; +using osu.Framework.Allocation; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -10,11 +10,16 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarHomeButton() { - Icon = FontAwesome.Solid.Home; - TooltipMain = "Home"; - TooltipSub = "Return to the main menu"; - + Width *= 1.4f; Hotkey = GlobalAction.Home; } + + [BackgroundDependencyLoader] + private void load() + { + TooltipMain = "home"; + TooltipSub = "return to the main menu"; + SetIcon("Icons/Hexacons/home"); + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index f9aa2de497..0f5e8e5456 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -14,10 +13,6 @@ namespace osu.Game.Overlays.Toolbar public ToolbarMusicButton() { - Icon = FontAwesome.Solid.Music; - TooltipMain = "Now playing"; - TooltipSub = "Manage the currently playing track"; - Hotkey = GlobalAction.ToggleNowPlaying; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs index 106c67a041..0ba2935c80 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs @@ -2,19 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Toolbar { public class ToolbarNewsButton : ToolbarOverlayToggleButton { - public ToolbarNewsButton() - { - Icon = FontAwesome.Solid.Newspaper; - TooltipMain = "News"; - TooltipSub = "Get up-to-date on community happenings"; - } - [BackgroundDependencyLoader(true)] private void load(NewsOverlay news) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index a699fd907f..79d0fc74c1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; @@ -25,10 +24,6 @@ namespace osu.Game.Overlays.Toolbar public ToolbarNotificationButton() { - Icon = FontAwesome.Solid.Bars; - TooltipMain = "Notifications"; - TooltipSub = "Waiting for 'ya"; - Hotkey = GlobalAction.ToggleNotifications; Add(countDisplay = new CountCircle diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 36387bb00d..0dea71cc08 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -32,6 +32,13 @@ namespace osu.Game.Overlays.Toolbar Action = stateContainer.ToggleVisibility; overlayState.BindTo(stateContainer.State); } + + if (stateContainer is INamedOverlayComponent named) + { + TooltipMain = named.Title; + TooltipSub = named.Description; + SetIcon(named.IconTexture); + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs index c026ce99fe..22a01bcdb5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs @@ -2,19 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Toolbar { public class ToolbarRankingsButton : ToolbarOverlayToggleButton { - public ToolbarRankingsButton() - { - SetIcon(FontAwesome.Regular.ChartBar); - TooltipMain = "Ranking"; - TooltipSub = "Find out who's the best right now"; - } - [BackgroundDependencyLoader(true)] private void load(RankingsOverlay rankings) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 422bf00c6d..905d5b44c6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Toolbar }, ModeButtonLine = new Container { - Size = new Vector2(ToolbarButton.WIDTH, 3), + Size = new Vector2(Toolbar.HEIGHT, 3), Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, Masking = true, diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index a5194ea752..564fd65719 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Toolbar var rInstance = value.CreateInstance(); ruleset.TooltipMain = rInstance.Description; - ruleset.TooltipSub = $"Play some {rInstance.Description}"; + ruleset.TooltipSub = $"play some {rInstance.Description}"; ruleset.SetIcon(rInstance.CreateIcon()); } @@ -65,12 +65,6 @@ namespace osu.Game.Overlays.Toolbar Parent.Click(); return base.OnClick(e); } - - protected override void LoadComplete() - { - base.LoadComplete(); - IconContainer.Scale *= 1.4f; - } } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index ed2a23ec2a..c53f4a55d9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSettingsButton() { - Icon = FontAwesome.Solid.Cog; - TooltipMain = "Settings"; - TooltipSub = "Change your settings"; - + Width *= 1.4f; Hotkey = GlobalAction.ToggleSettings; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index 6faa58c559..e62c7bc807 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSocialButton() { - Icon = FontAwesome.Solid.Users; - TooltipMain = "Friends"; - TooltipSub = "Interact with those close to you"; - Hotkey = GlobalAction.ToggleSocial; } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index b4c8a2d3ca..2b316c0e34 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -17,19 +17,18 @@ using osuTK; namespace osu.Game.Overlays { - public class UserProfileOverlay : FullscreenOverlay + public class UserProfileOverlay : FullscreenOverlay { private ProfileSection lastSection; private ProfileSection[] sections; private GetUserRequest userReq; - protected ProfileHeader Header; private ProfileSectionsContainer sectionsContainer; private ProfileSectionTabControl tabs; public const float CONTENT_X_MARGIN = 70; public UserProfileOverlay() - : base(OverlayColourScheme.Pink) + : base(OverlayColourScheme.Pink, new ProfileHeader()) { } @@ -45,6 +44,9 @@ namespace osu.Game.Overlays if (user.Id == Header?.User.Value?.Id) return; + if (sectionsContainer != null) + sectionsContainer.ExpandableHeader = null; + userReq?.Cancel(); Clear(); lastSection = null; @@ -77,7 +79,7 @@ namespace osu.Game.Overlays Add(sectionsContainer = new ProfileSectionsContainer { - ExpandableHeader = Header = new ProfileHeader(), + ExpandableHeader = Header, FixedHeader = tabs, HeaderBackground = new Box { diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 5c87096dd4..d0fa9987d5 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -14,6 +14,8 @@ namespace osu.Game.Overlays protected override bool BlockNonPositionalInput => true; protected override Container Content => Waves; + public const float WIDTH_PADDING = 80; + protected override bool StartHidden => true; protected WaveOverlayContainer() diff --git a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs index 4d6d958e82..9b840cea08 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToTrack : IApplicableMod { - void ApplyToTrack(Track track); + void ApplyToTrack(ITrack track); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 52ffa0ad2a..b8dc7a2661 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -9,6 +9,7 @@ using System.Reflection; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -18,6 +19,7 @@ namespace osu.Game.Rulesets.Mods /// /// The base class for gameplay modifiers. /// + [ExcludeFromDynamicCompile] public abstract class Mod : IMod, IJsonSerializable { /// diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index bd98e735e5..61ad7db706 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(Track track) + public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index ed8eb2fb66..4004953cd1 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(Track track) + public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 874384686f..fec21764b0 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods { public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(Track track) + public virtual void ApplyToTrack(ITrack track) { track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 839d97f04e..20c8d0f3e7 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - private Track track; + private ITrack track; protected ModTimeRamp() { @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.BindValueChanged(applyPitchAdjustment); } - public void ApplyToTrack(Track track) + public void ApplyToTrack(ITrack track) { this.track = track; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 3a7f433a37..915544d010 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -23,10 +23,12 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Users; using JetBrains.Annotations; +using osu.Framework.Testing; using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets { + [ExcludeFromDynamicCompile] public abstract class Ruleset { public RulesetInfo RulesetInfo { get; internal set; } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 2e32b96084..d5aca8c650 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -5,9 +5,11 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Testing; namespace osu.Game.Rulesets { + [ExcludeFromDynamicCompile] public class RulesetInfo : IEquatable { public int? ID { get; set; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a4a560c8e4..97cb5ca7ab 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; using osu.Game.Users; using SharpCompress.Compressors.LZMA; @@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy protected void CalculateAccuracy(ScoreInfo score) { - score.Statistics.TryGetValue(HitResult.Miss, out int countMiss); - score.Statistics.TryGetValue(HitResult.Meh, out int count50); - score.Statistics.TryGetValue(HitResult.Good, out int count100); - score.Statistics.TryGetValue(HitResult.Great, out int count300); - score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki); - score.Statistics.TryGetValue(HitResult.Ok, out int countKatu); + int countMiss = score.GetCountMiss() ?? 0; + int count50 = score.GetCount50() ?? 0; + int count100 = score.GetCount100() ?? 0; + int count300 = score.GetCount300() ?? 0; + int countGeki = score.GetCountGeki() ?? 0; + int countKatu = score.GetCountKatu() ?? 0; switch (score.Ruleset.ID) { @@ -241,12 +240,15 @@ namespace osu.Game.Scoring.Legacy } var diff = Parsing.ParseFloat(split[0]); + var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE); + var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); lastTime += diff; - if (i == 0 && diff == 0) - // osu-stable adds a zero-time frame before potentially valid negative user frames. - // we need to ignore this. + if (i < 2 && mouseX == 256 && mouseY == -500) + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; // Todo: At some point we probably want to rewind and play back the negative-time frames @@ -255,8 +257,8 @@ namespace osu.Game.Scoring.Legacy continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, - Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), - Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), + mouseX, + mouseY, (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); replay.Frames.Add(currentFrame); diff --git a/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs b/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs index 089da4f222..b8bc5cdf36 100644 --- a/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs +++ b/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs @@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Components.Menus Height = 1, Colour = Color4.White.Opacity(0.2f), }); - - Current.Value = EditorScreenMode.Compose; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index fcff672045..865e225645 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AddRangeInternal(new[] { - DragBox = CreateDragBox(select), + DragBox = CreateDragBox(selectBlueprintsFromDragRectangle), selectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), selectionHandler.CreateProxy(), @@ -326,7 +326,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Select all masks in a given rectangle selection area. /// /// The rectangle to perform a selection on in screen-space coordinates. - private void select(RectangleF rect) + private void selectBlueprintsFromDragRectangle(RectangleF rect) { foreach (var blueprint in SelectionBlueprints) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 717d60b4f3..ed3d328330 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -26,6 +26,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorClock editorClock { get; set; } + /// + /// The timeline's scroll position in the last frame. + /// + private float lastScrollPosition; + + /// + /// The track time in the last frame. + /// + private double lastTrackTime; + + /// + /// Whether the user is currently dragging the timeline. + /// + private bool handlingDragInput; + + /// + /// Whether the track was playing before a user drag event. + /// + private bool trackWasPlaying; + + private Track track; + public Timeline() { ZoomDuration = 200; @@ -59,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveform.Waveform = b.NewValue.Waveform; track = b.NewValue.Track; + // todo: i don't think this is safe, the track may not be loaded yet. if (track.Length > 0) { MaxZoom = getZoomLevelForVisibleMilliseconds(500); @@ -68,29 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } - private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds); - - /// - /// The timeline's scroll position in the last frame. - /// - private float lastScrollPosition; - - /// - /// The track time in the last frame. - /// - private double lastTrackTime; - - /// - /// Whether the user is currently dragging the timeline. - /// - private bool handlingDragInput; - - /// - /// Whether the track was playing before a user drag event. - /// - private bool trackWasPlaying; - - private Track track; + private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); protected override void Update() { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d92f3922c3..ac1f61c4fd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -28,6 +28,7 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; +using osu.Game.Online.API; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Setup; @@ -72,6 +73,9 @@ namespace osu.Game.Screens.Edit protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + [Resolved] + private IAPIProvider api { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host) { @@ -89,6 +93,14 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); + bool isNewBeatmap = false; + + if (Beatmap.Value is DummyWorkingBeatmap) + { + isNewBeatmap = true; + Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + } + try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); @@ -101,9 +113,8 @@ namespace osu.Game.Screens.Edit return; } - AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, Beatmap.Value.Skin)); dependencies.CacheAs(editorBeatmap); - changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); @@ -148,6 +159,7 @@ namespace osu.Game.Screens.Edit Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose }, Items = new[] { new MenuItem("File") @@ -284,7 +296,7 @@ namespace osu.Game.Screens.Edit // this is a special case to handle the "pivot" scenario. // if we are precise scrolling in one direction then change our mind and scroll backwards, // the existing accumulation should be applied in the inverse direction to maintain responsiveness. - if (Math.Sign(scrollAccumulation) != scrollDirection) + if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection) scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation)); scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1); @@ -386,7 +398,11 @@ namespace osu.Game.Screens.Edit break; } - LoadComponentAsync(currentScreen, screenContainer.Add); + LoadComponentAsync(currentScreen, newScreen => + { + if (newScreen == currentScreen) + screenContainer.Add(newScreen); + }); } private void seek(UIEvent e, int direction) @@ -399,7 +415,14 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap); + private void saveBeatmap() + { + // apply any set-level metadata changes. + beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); + + // save the loaded beatmap's data stream. + beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + } private void exportBeatmap() { diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 23c8c9f605..061009e519 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; namespace osu.Game.Screens.Edit { @@ -47,6 +48,8 @@ namespace osu.Game.Screens.Edit public readonly IBeatmap PlayableBeatmap; + public readonly ISkin BeatmapSkin; + [Resolved] private BindableBeatDivisor beatDivisor { get; set; } @@ -54,9 +57,10 @@ namespace osu.Game.Screens.Edit private readonly Dictionary> startTimeBindables = new Dictionary>(); - public EditorBeatmap(IBeatmap playableBeatmap) + public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null) { PlayableBeatmap = playableBeatmap; + BeatmapSkin = beatmapSkin; beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 1553c2d2ef..927c823c64 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Edit using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); savedStates.Add(stream.ToArray()); } diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index d42447ac4b..8b5f0aaa71 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit public void Exit() { - this.FadeOut(250).Expire(); + Expire(); } } } diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index fc3dd4c105..57b7ce6940 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit protected override Texture GetBackground() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 758dbc6e16..a2c8f19016 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,13 +1,123 @@ // 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.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + namespace osu.Game.Screens.Edit.Setup { public class SetupScreen : EditorScreen { - public SetupScreen() + private FillFlowContainer flow; + private LabelledTextBox artistTextBox; + private LabelledTextBox titleTextBox; + private LabelledTextBox creatorTextBox; + private LabelledTextBox difficultyTextBox; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) { - Child = new ScreenWhiteBox.UnderConstructionMessage("Setup mode"); + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(50), + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Child = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = 250, + Masking = true, + CornerRadius = 10, + Child = new BeatmapBackgroundSprite(Beatmap.Value) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, + }, + new OsuSpriteText + { + Text = "Beatmap metadata" + }, + artistTextBox = new LabelledTextBox + { + Label = "Artist", + Current = { Value = Beatmap.Value.Metadata.Artist }, + TabbableContentContainer = this + }, + titleTextBox = new LabelledTextBox + { + Label = "Title", + Current = { Value = Beatmap.Value.Metadata.Title }, + TabbableContentContainer = this + }, + creatorTextBox = new LabelledTextBox + { + Label = "Creator", + Current = { Value = Beatmap.Value.Metadata.AuthorString }, + TabbableContentContainer = this + }, + difficultyTextBox = new LabelledTextBox + { + Label = "Difficulty Name", + Current = { Value = Beatmap.Value.BeatmapInfo.Version }, + TabbableContentContainer = this + }, + } + }, + }, + } + } + }; + + foreach (var item in flow.OfType()) + item.OnCommit += onCommit; + } + + private void onCommit(TextBox sender, bool newText) + { + if (!newText) return; + + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; + Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; + Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; + Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; } } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 5c59cfbfe8..c0c0bcead2 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -112,8 +112,7 @@ namespace osu.Game.Screens.Edit.Timing }; controlPoints = group.ControlPoints.GetBoundCopy(); - controlPoints.ItemsAdded += _ => createChildren(); - controlPoints.ItemsRemoved += _ => createChildren(); + controlPoints.CollectionChanged += (_, __) => createChildren(); createChildren(); } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index a08a660e7e..8c40c8e721 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -124,8 +124,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - controlGroups.ItemsAdded += _ => createContent(); - controlGroups.ItemsRemoved += _ => createContent(); + controlGroups.CollectionChanged += (sender, args) => createContent(); createContent(); } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 906644ce14..879363ba08 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -1,30 +1,30 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { internal class TimingSection : Section { - private SettingsSlider bpm; + private SettingsSlider bpmSlider; private SettingsEnumDropdown timeSignature; + private BPMTextBox bpmTextEntry; [BackgroundDependencyLoader] private void load() { Flow.AddRange(new Drawable[] { - bpm = new BPMSlider - { - Bindable = new TimingControlPoint().BeatLengthBindable, - LabelText = "BPM", - }, + bpmTextEntry = new BPMTextBox(), + bpmSlider = new BPMSlider(), timeSignature = new SettingsEnumDropdown { LabelText = "Time Signature" @@ -36,7 +36,8 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - bpm.Bindable = point.NewValue.BeatLengthBindable; + bpmSlider.Bindable = point.NewValue.BeatLengthBindable; + bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; timeSignature.Bindable = point.NewValue.TimeSignatureBindable; } } @@ -52,34 +53,88 @@ namespace osu.Game.Screens.Edit.Timing }; } + private class BPMTextBox : LabelledTextBox + { + private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; + + public BPMTextBox() + { + Label = "BPM"; + + OnCommit += (val, isNew) => + { + if (!isNew) return; + + if (double.TryParse(Current.Value, out double doubleVal)) + { + try + { + beatLengthBindable.Value = beatLengthToBpm(doubleVal); + } + catch + { + // will restore the previous text value on failure. + beatLengthBindable.TriggerChange(); + } + } + }; + + beatLengthBindable.BindValueChanged(val => + { + Current.Value = beatLengthToBpm(val.NewValue).ToString("N2"); + }, true); + } + + public Bindable Bindable + { + get => beatLengthBindable; + set + { + // incoming will be beat length, not bpm + beatLengthBindable.UnbindBindings(); + beatLengthBindable.BindTo(value); + } + } + } + private class BPMSlider : SettingsSlider { - private readonly BindableDouble beatLengthBindable = new BindableDouble(); + private const double sane_minimum = 60; + private const double sane_maximum = 240; - private BindableDouble bpmBindable; + private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; + private readonly BindableDouble bpmBindable = new BindableDouble(); + + public BPMSlider() + { + beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); + bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); + + base.Bindable = bpmBindable; + } public override Bindable Bindable { get => base.Bindable; set { - // incoming will be beatlength - + // incoming will be beat length, not bpm beatLengthBindable.UnbindBindings(); beatLengthBindable.BindTo(value); - - base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value)) - { - MinValue = beatLengthToBpm(beatLengthBindable.MaxValue), - MaxValue = beatLengthToBpm(beatLengthBindable.MinValue), - Default = beatLengthToBpm(beatLengthBindable.Default), - }; - - bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); } } - private double beatLengthToBpm(double beatLength) => 60000 / beatLength; + private void updateCurrent(double newValue) + { + // we use a more sane range for the slider display unless overridden by the user. + // if a value comes in outside our range, we should expand temporarily. + bpmBindable.MinValue = Math.Min(newValue, sane_minimum); + bpmBindable.MaxValue = Math.Max(newValue, sane_maximum); + + bpmBindable.Value = newValue; + } } + + private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; } } diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 761f842c22..e19037c2c4 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -39,9 +39,9 @@ namespace osu.Game.Screens bool HideOverlaysOnEnter { get; } /// - /// Whether overlays should be able to be opened once this screen is entered or resumed. + /// Whether overlays should be able to be opened when this screen is current. /// - OverlayActivation InitialOverlayActivationMode { get; } + IBindable OverlayActivationMode { get; } /// /// The amount of parallax to be applied while this screen is displayed. diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 5ba7a8ddc3..4becdd58cd 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -270,9 +270,6 @@ namespace osu.Game.Screens.Menu ButtonSystemState lastState = state; state = value; - if (game != null) - game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All; - updateLogoState(lastState); Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 986de1edf0..fcb9aacd76 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -190,7 +190,7 @@ namespace osu.Game.Screens.Menu { "You can press Ctrl-T anywhere in the game to toggle the toolbar!", "You can press Ctrl-O anywhere in the game to access options!", - "All settings are dynamic and take effect in real-time. Try changing the skin while playing!", + "All settings are dynamic and take effect in real-time. Try pausing and changing the skin while playing!", "New features are coming online every update. Make sure to stay up-to-date!", "If you find the UI too large or small, try adjusting UI scale in settings!", "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 5f91aaad15..1df5c503d6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; +using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; using osuTK; @@ -43,9 +44,7 @@ namespace osu.Game.Screens.Menu private WorkingBeatmap initialBeatmap; - protected Track Track => initialBeatmap?.Track; - - private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); + protected ITrack Track { get; private set; } private const int exit_delay = 3000; @@ -60,8 +59,12 @@ namespace osu.Game.Screens.Menu [Resolved] private AudioManager audio { get; set; } + [Resolved] + private MusicController musicController { get; set; } + /// /// Whether the is provided by osu! resources, rather than a user beatmap. + /// Only valid during or after . /// protected bool UsingThemedIntro { get; private set; } @@ -111,7 +114,6 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = !(Track is TrackVirtual); } return UsingThemedIntro; @@ -123,17 +125,35 @@ namespace osu.Game.Screens.Menu this.FadeIn(300); double fadeOutTime = exit_delay; + + var track = musicController.CurrentTrack; + + // ensure the track doesn't change or loop as we are exiting. + track.Looping = false; + Beatmap.Disabled = true; + // we also handle the exit transition. if (MenuVoice.Value) + { seeya.Play(); + + // if playing the outro voice, we have more time to have fun with the background track. + // initially fade to almost silent then ramp out over the remaining time. + const double initial_fade = 200; + track + .VolumeTo(0.03f, initial_fade).Then() + .VolumeTo(0, fadeOutTime - initial_fade, Easing.In); + } else + { fadeOutTime = 500; - audio.AddAdjustment(AdjustableProperty.Volume, exitingVolumeFade); - this.TransformBindableTo(exitingVolumeFade, 0, fadeOutTime).OnComplete(_ => this.Exit()); + // if outro voice is turned off, just do a simple fade out. + track.VolumeTo(0, fadeOutTime, Easing.Out); + } //don't want to fade out completely else we will stop running updates. - Game.FadeTo(0.01f, fadeOutTime); + Game.FadeTo(0.01f, fadeOutTime).OnComplete(_ => this.Exit()); base.OnResuming(last); } @@ -164,6 +184,11 @@ namespace osu.Game.Screens.Menu if (!resuming) { beatmap.Value = initialBeatmap; + Track = initialBeatmap.Track; + UsingThemedIntro = !initialBeatmap.Track.IsDummyDevice; + + // ensure the track starts at maximum volume + musicController.CurrentTrack.FinishTransforms(); logo.MoveTo(new Vector2(0.5f)); logo.ScaleTo(Vector2.One); diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index 6731fef6f7..d92d38da45 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu const int line_end_offset = 120; smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); + smallRing.Delay(400).FadeColour(Color4.Black, 300); lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index a9ef20436f..a96fddb5ad 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load() { - if (MenuVoice.Value && !UsingThemedIntro) + if (MenuVoice.Value) welcome = audio.Samples.Get(@"Intro/welcome"); } @@ -64,7 +64,8 @@ namespace osu.Game.Screens.Menu }, t => { AddInternal(t); - welcome?.Play(); + if (!UsingThemedIntro) + welcome?.Play(); StartTrack(); }); diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index bf42e36e8c..e81646456f 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -39,8 +39,6 @@ namespace osu.Game.Screens.Menu welcome = audio.Samples.Get(@"Intro/Welcome/welcome"); pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); - - Track.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -49,6 +47,8 @@ namespace osu.Game.Screens.Menu if (!resuming) { + Track.Looping = true; + LoadComponentAsync(new WelcomeIntroSequence { RelativeSizeAxes = Axes.Both diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 57252d557e..c3ecd75963 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -5,7 +5,6 @@ using System.Linq; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; @@ -46,8 +45,8 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } - [Resolved(canBeNull: true)] - private MusicController music { get; set; } + [Resolved] + private MusicController musicController { get; set; } [Resolved(canBeNull: true)] private LoginOverlay login { get; set; } @@ -62,8 +61,6 @@ namespace osu.Game.Screens.Menu protected override BackgroundScreen CreateBackground() => background; - internal Track Track { get; private set; } - private Bindable holdDelay; private Bindable loginDisplayed; @@ -101,7 +98,11 @@ namespace osu.Game.Screens.Menu { buttons = new ButtonSystem { - OnEdit = delegate { this.Push(new Editor()); }, + OnEdit = delegate + { + Beatmap.SetDefault(); + this.Push(new Editor()); + }, OnSolo = onSolo, OnMulti = delegate { this.Push(new Multiplayer()); }, OnExit = confirmAndExit, @@ -177,15 +178,14 @@ namespace osu.Game.Screens.Menu base.OnEntering(last); buttons.FadeInFromZero(500); - Track = Beatmap.Value.Track; var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && Track != null) + if (last is IntroScreen && musicController.TrackLoaded) { - if (!Track.IsRunning) + if (!musicController.CurrentTrack.IsRunning) { - Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length); - Track.Start(); + musicController.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.CurrentTrack.Length); + musicController.CurrentTrack.Start(); } } @@ -260,7 +260,7 @@ namespace osu.Game.Screens.Menu // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); - music.EnsurePlayingSomething(); + musicController.EnsurePlayingSomething(); } public override bool OnExiting(IScreen next) @@ -280,6 +280,7 @@ namespace osu.Game.Screens.Menu } buttons.State = ButtonSystemState.Exit; + OverlayActivationMode.Value = OverlayActivation.Disabled; songTicker.Hide(); diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f5e4b078da..4515ee8ed0 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -17,6 +17,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -46,7 +47,6 @@ namespace osu.Game.Screens.Menu private SampleChannel sampleBeat; private readonly Container colourAndTriangles; - private readonly Triangles triangles; /// @@ -319,6 +319,9 @@ namespace osu.Game.Screens.Menu intro.Delay(length + fade).FadeOut(); } + [Resolved] + private MusicController musicController { get; set; } + protected override void Update() { base.Update(); @@ -327,9 +330,9 @@ namespace osu.Game.Screens.Menu const float velocity_adjust_cutoff = 0.98f; const float paused_velocity = 0.5f; - if (Beatmap.Value.Track.IsRunning) + if (musicController.CurrentTrack.IsRunning) { - var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0; + var maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed)); if (maxAmplitude > velocity_adjust_cutoff) diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 653cb3791a..cd8695286b 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -11,8 +11,8 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.SearchableList; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Multi Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = SearchableListOverlay.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, Children = new Drawable[] { title = new MultiHeaderTitle diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 447c99039a..321d7b0a19 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -53,8 +54,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components protected override void LoadComplete() { - rooms.ItemsAdded += addRooms; - rooms.ItemsRemoved += removeRooms; + rooms.CollectionChanged += roomsChanged; roomManager.RoomsUpdated += updateSorting; rooms.BindTo(roomManager.Rooms); @@ -82,6 +82,20 @@ namespace osu.Game.Screens.Multi.Lounge.Components }); } + private void roomsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + addRooms(args.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Remove: + removeRooms(args.OldItems.Cast()); + break; + } + } + private void addRooms(IEnumerable rooms) { foreach (var room in rooms) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index ff7d56a95b..a1e99c83b2 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; -using osu.Game.Overlays.SearchableList; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; @@ -32,7 +31,7 @@ namespace osu.Game.Screens.Multi.Lounge [Resolved] private Bindable selectedRoom { get; set; } - [Resolved(canBeNull: true)] + [Resolved] private MusicController music { get; set; } private bool joiningRoom; @@ -102,8 +101,8 @@ namespace osu.Game.Screens.Multi.Lounge content.Padding = new MarginPadding { Top = Filter.DrawHeight, - Left = SearchableListOverlay.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, - Right = SearchableListOverlay.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, + Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, + Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, }; } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 1afbf5c32a..f2409d64e7 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -14,8 +14,6 @@ namespace osu.Game.Screens.Multi.Match.Components { public class MatchLeaderboard : Leaderboard { - public Action> ScoresLoaded; - [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } @@ -39,18 +37,20 @@ namespace osu.Game.Screens.Multi.Match.Components if (roomId.Value == null) return null; - var req = new GetRoomScoresRequest(roomId.Value ?? 0); + var req = new GetRoomLeaderboardRequest(roomId.Value ?? 0); req.Success += r => { - scoresCallback?.Invoke(r); - ScoresLoaded?.Invoke(r); + scoresCallback?.Invoke(r.Leaderboard); + TopScore = r.UserScore; }; return req; } protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index); + + protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position, false); } public enum MatchLeaderboardScope diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs index 73a40d9579..1fabdbb86a 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs @@ -14,8 +14,8 @@ namespace osu.Game.Screens.Multi.Match.Components { private readonly APIUserScoreAggregate score; - public MatchLeaderboardScore(APIUserScoreAggregate score, int rank) - : base(score.CreateScoreInfo(), rank) + public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool allowHighlight = true) + : base(score.CreateScoreInfo(), rank, allowHighlight) { this.score = score; } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 49a0fc434b..caefc194b1 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.SearchableList; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new Container { - Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 7c2d5cf85d..0d2adeb27c 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Multi.Match new Dimension(GridSizeMode.AutoSize), new Dimension(), new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 240), + new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120), } }, null diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 872a1cd39a..a44d14fb5c 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -44,9 +44,13 @@ namespace osu.Game.Screens public virtual bool HideOverlaysOnEnter => false; /// - /// Whether overlays should be able to be opened once this screen is entered or resumed. + /// The initial overlay activation mode to use when this screen is entered for the first time. /// - public virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; + protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; + + protected readonly Bindable OverlayActivationMode; + + IBindable IOsuScreen.OverlayActivationMode => OverlayActivationMode; public virtual bool CursorVisible => true; @@ -138,6 +142,8 @@ namespace osu.Game.Screens { Anchor = Anchor.Centre; Origin = Anchor.Centre; + + OverlayActivationMode = new Bindable(InitialOverlayActivationMode); } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 0653373c91..7a9cb3dddd 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using osu.Framework; @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play { @@ -25,12 +24,9 @@ namespace osu.Game.Screens.Play public class GameplayClockContainer : Container { private readonly WorkingBeatmap beatmap; - private readonly IReadOnlyList mods; - /// - /// The 's track. - /// - private Track track; + [NotNull] + private ITrack track; public readonly BindableBool IsPaused = new BindableBool(); @@ -63,17 +59,16 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; - public GameplayClockContainer(WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) + public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) { this.beatmap = beatmap; - this.mods = mods; this.gameplayStartTime = gameplayStartTime; + track = beatmap.Track; + firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; RelativeSizeAxes = Axes.Both; - track = beatmap.Track; - adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. @@ -123,13 +118,10 @@ namespace osu.Game.Screens.Play public void Restart() { - // The Reset() call below causes speed adjustments to be reset in an async context, leading to deadlocks. - // The deadlock can be prevented by resetting the track synchronously before entering the async context. - track.ResetSpeedAdjustments(); - Task.Run(() => { - track.Reset(); + track.Seek(0); + track.Stop(); Schedule(() => { @@ -195,16 +187,13 @@ namespace osu.Game.Screens.Play } /// - /// Changes the backing clock to avoid using the originally provided beatmap's track. + /// Changes the backing clock to avoid using the originally provided track. /// public void StopUsingBeatmapClock() { - if (track != beatmap.Track) - return; - removeSourceClockAdjustments(); - track = new TrackVirtual(beatmap.Track.Length); + track = new TrackVirtual(track.Length); adjustableClock.ChangeSource(track); } @@ -220,33 +209,29 @@ namespace osu.Game.Screens.Play private void updateRate() { - if (track == null) return; - - speedAdjustmentsApplied = true; - track.ResetSpeedAdjustments(); + if (speedAdjustmentsApplied) + return; track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - foreach (var mod in mods.OfType()) - mod.ApplyToTrack(track); + speedAdjustmentsApplied = true; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - removeSourceClockAdjustments(); - track = null; } private void removeSourceClockAdjustments() { - if (speedAdjustmentsApplied) - { - track.ResetSpeedAdjustments(); - speedAdjustmentsApplied = false; - } + if (!speedAdjustmentsApplied) return; + + track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + speedAdjustmentsApplied = false; } private class HardwareCorrectionOffsetClock : FramedOffsetClock diff --git a/osu.Game/Screens/Play/HUD/ComboResultCounter.cs b/osu.Game/Screens/Play/HUD/ComboResultCounter.cs deleted file mode 100644 index 7ae8bc0ddf..0000000000 --- a/osu.Game/Screens/Play/HUD/ComboResultCounter.cs +++ /dev/null @@ -1,32 +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 osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// Used to display combo with a roll-up animation in results screen. - /// - public class ComboResultCounter : RollingCounter - { - protected override double RollingDuration => 500; - protected override Easing RollingEasing => Easing.Out; - - protected override double GetProportionalDuration(long currentValue, long newValue) - { - return currentValue > newValue ? currentValue - newValue : newValue - currentValue; - } - - protected override string FormatCount(long count) - { - return $@"{count}x"; - } - - public override void Increment(long amount) - { - Current.Value += amount; - } - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..539f9227a3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -50,7 +50,10 @@ namespace osu.Game.Screens.Play public override bool HideOverlaysOnEnter => true; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; + + // We are managing our own adjustments (see OnEntering/OnExiting). + public override bool AllowRateAdjustments => false; /// /// Whether gameplay should pause when the game window focus is lost. @@ -77,6 +80,9 @@ namespace osu.Game.Screens.Play [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private MusicController musicController { get; set; } + private SampleChannel sampleRestart; public BreakOverlay BreakOverlay; @@ -178,7 +184,7 @@ namespace osu.Game.Screens.Play if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); + InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -197,6 +203,10 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } + DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); + breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it @@ -341,6 +351,16 @@ namespace osu.Game.Screens.Play HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } + private void updateOverlayActivationMode() + { + bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value; + + if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) + OverlayActivationMode.Value = OverlayActivation.UserTriggered; + else + OverlayActivationMode.Value = OverlayActivation.Disabled; + } + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value @@ -627,6 +647,15 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); + + // Our mods are local copies of the global mods so they need to be re-applied to the track. + // This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack. + // Todo: In the future, player will receive in a track and will probably not have to worry about this... + musicController.ResetTrackAdjustments(); + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToTrack(musicController.CurrentTrack); + + updateOverlayActivationMode(); } public override void OnSuspending(IScreen next) @@ -660,6 +689,8 @@ namespace osu.Game.Screens.Play // as we are no longer the current screen, we cannot guarantee the track is still usable. GameplayClockContainer?.StopUsingBeatmapClock(); + musicController.ResetTrackAdjustments(); + fadeOut(); return base.OnExiting(next); } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 6933456e7e..288a107874 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -46,9 +46,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override string FormatCount(double count) => count.FormatAccuracy(); - public override void Increment(double amount) - => Current.Value += amount; - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => { s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 043a560d12..e820831809 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -49,9 +49,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); s.Spacing = new Vector2(-2, 0); }); - - public override void Increment(int amount) - => Current.Value += amount; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs index 7f6fd1eabe..65082d3fae 100644 --- a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -36,8 +36,5 @@ namespace osu.Game.Screens.Ranking.Expanded s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); s.Spacing = new Vector2(-5, 0); }); - - public override void Increment(long amount) - => Current.Value += amount; } } diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index d0142e57fe..b76842f405 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -74,23 +74,33 @@ namespace osu.Game.Screens.Ranking { button.State.Value = state.NewValue; - switch (replayAvailability) - { - case ReplayAvailability.Local: - button.TooltipText = @"watch replay"; - break; - - case ReplayAvailability.Online: - button.TooltipText = @"download replay"; - break; - - default: - button.TooltipText = @"replay unavailable"; - break; - } + updateTooltip(); }, true); - button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; + Model.BindValueChanged(_ => + { + button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; + + updateTooltip(); + }, true); + } + + private void updateTooltip() + { + switch (replayAvailability) + { + case ReplayAvailability.Local: + button.TooltipText = @"watch replay"; + break; + + case ReplayAvailability.Online: + button.TooltipText = @"download replay"; + break; + + default: + button.TooltipText = @"replay unavailable"; + break; + } } private enum ReplayAvailability diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 527da429ed..45fdc3ff33 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows)).ToList(); + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss).ToList(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index e6c4ab1c4e..6fe7e4eda8 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -41,13 +41,14 @@ namespace osu.Game.Screens.Ranking.Statistics { Text = Name, Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14) }, value = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Torus.With(weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) } }); } @@ -58,12 +59,19 @@ namespace osu.Game.Screens.Ranking.Statistics /// public class SimpleStatisticItem : SimpleStatisticItem { + private TValue value; + /// /// The statistic's value to be displayed. /// public new TValue Value { - set => base.Value = DisplayValue(value); + get => value; + set + { + this.value = value; + base.Value = DisplayValue(value); + } } /// diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs similarity index 93% rename from osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs rename to osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index 16501aae54..8b503cc04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,10 +14,10 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Represents a statistic row with simple statistics (ones that only need textual display). + /// Represents a table with simple statistics (ones that only need textual display). /// Richer visualisations should be done with s and s. /// - public class SimpleStatisticRow : CompositeDrawable + public class SimpleStatisticTable : CompositeDrawable { private readonly SimpleStatisticItem[] items; private readonly int columnCount; @@ -28,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The number of columns to layout the into. /// The s to display in this row. - public SimpleStatisticRow(int columnCount, IEnumerable items) + public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) { if (columnCount < 1) throw new ArgumentOutOfRangeException(nameof(columnCount)); diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index ed98698411..485d24d024 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - new Drawable[] + new[] { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Children = new Drawable[] - { - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 9, - Width = 4, - Colour = Color4Extensions.FromHex("#00FFAA") - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - } - } - } + createHeader(item) }, new Drawable[] { @@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } + + private static Drawable createHeader(StatisticItem item) + { + if (string.IsNullOrEmpty(item.Name)) + return Empty(); + + return new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 9, + Width = 4, + Colour = Color4Extensions.FromHex("#00FFAA") + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = item.Name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + }; + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index e959ed24fc..4903983759 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of the item. + /// The name of the item. Can be to hide the item header. /// The content to be displayed. /// The of this item. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 7f406331cd..c2ace6a04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -94,14 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), + Alpha = 0 }; foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) { rows.Add(new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] @@ -125,6 +126,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); + d.FadeIn(250, Easing.OutQuint); }, localCancellationSource.Token); }), localCancellationSource.Token); } diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs new file mode 100644 index 0000000000..18a2238784 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -0,0 +1,40 @@ +// 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.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Displays the unstable rate statistic for a given play. + /// + public class UnstableRate : SimpleStatisticItem + { + /// + /// Creates and computes an statistic. + /// + /// Sequence of s to calculate the unstable rate based on. + public UnstableRate(IEnumerable hitEvents) + : base("Unstable Rate") + { + var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss) + .Select(ev => ev.TimeOffset).ToArray(); + Value = 10 * standardDeviation(timeOffsets); + } + + private static double standardDeviation(double[] timeOffsets) + { + if (timeOffsets.Length == 0) + return double.NaN; + + var mean = timeOffsets.Average(); + var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); + return Math.Sqrt(squares / timeOffsets.Length); + } + + protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2"); + } +} diff --git a/osu.Game/Screens/ScorePresentType.cs b/osu.Game/Screens/ScorePresentType.cs new file mode 100644 index 0000000000..3216f92091 --- /dev/null +++ b/osu.Game/Screens/ScorePresentType.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens +{ + public enum ScorePresentType + { + Results, + Gameplay + } +} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 27ce9e82dd..2a3eb8c67a 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -27,6 +27,7 @@ using osu.Framework.Logging; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Ranking.Expanded; namespace osu.Game.Screens.Select { @@ -222,10 +223,20 @@ namespace osu.Game.Screens.Select Direction = FillDirection.Vertical, Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, AutoSizeAxes = Axes.Both, - Children = new Drawable[] + Shear = wedged_container_shear, + Children = new[] { + createStarRatingDisplay(beatmapInfo).With(display => + { + display.Anchor = Anchor.TopRight; + display.Origin = Anchor.TopRight; + display.Shear = -wedged_container_shear; + }), StatusPill = new BeatmapSetOnlineStatusPill { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -wedged_container_shear, TextSize = 11, TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, Status = beatmapInfo.Status, @@ -282,6 +293,13 @@ namespace osu.Game.Screens.Select StatusPill.Hide(); } + private static Drawable createStarRatingDisplay(BeatmapInfo beatmapInfo) => beatmapInfo.StarDifficulty > 0 + ? new StarRatingDisplay(beatmapInfo) + { + Margin = new MarginPadding { Bottom = 5 } + } + : Empty(); + private void setMetadata(string source) { ArtistLabel.Text = artistBinding.Value; @@ -300,14 +318,14 @@ namespace osu.Game.Screens.Select labels.Add(new InfoLabel(new BeatmapStatistic { Name = "Length", - Icon = FontAwesome.Regular.Clock, + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"), })); labels.Add(new InfoLabel(new BeatmapStatistic { Name = "BPM", - Icon = FontAwesome.Regular.Circle, + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), Content = getBPMRange(b), })); @@ -400,10 +418,18 @@ namespace osu.Game.Screens.Select Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.8f), Colour = Color4Extensions.FromHex(@"f7dd55"), - Icon = statistic.Icon, + Icon = FontAwesome.Regular.Circle, + Size = new Vector2(0.8f) }, + statistic.CreateIcon().With(i => + { + i.Anchor = Anchor.Centre; + i.Origin = Anchor.Centre; + i.RelativeSizeAxes = Axes.Both; + i.Colour = Color4Extensions.FromHex(@"f7dd55"); + i.Size = new Vector2(0.64f); + }), } }, new OsuSpriteText diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 8e85eb4eb2..8ddae67dba 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -41,25 +40,8 @@ namespace osu.Game.Screens.Select.Leaderboards } } - public APILegacyUserTopScoreInfo TopScore - { - get => topScoreContainer.Score.Value; - set - { - if (value == null) - topScoreContainer.Hide(); - else - { - topScoreContainer.Show(); - topScoreContainer.Score.Value = value; - } - } - } - private bool filterMods; - private UserTopScoreContainer topScoreContainer; - private IBindable> itemRemoved; /// @@ -101,11 +83,6 @@ namespace osu.Game.Screens.Select.Leaderboards UpdateScores(); }; - Content.Add(topScoreContainer = new UserTopScoreContainer - { - ScoreSelected = s => ScoreSelected?.Invoke(s) - }); - itemRemoved = scoreManager.ItemRemoved.GetBoundCopy(); itemRemoved.BindValueChanged(onScoreRemoved); } @@ -183,7 +160,7 @@ namespace osu.Game.Screens.Select.Leaderboards req.Success += r => { scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets))); - TopScore = r.UserScore; + TopScore = r.UserScore?.CreateScoreInfo(rulesets); }; return req; @@ -193,5 +170,10 @@ namespace osu.Game.Screens.Select.Leaderboards { Action = () => ScoreSelected?.Invoke(model) }; + + protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false) + { + Action = () => ScoreSelected?.Invoke(model) + }; } } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 96a48fa3ac..8692833a21 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -76,9 +76,7 @@ namespace osu.Game.Screens.Select item.Ruleset.Value = Ruleset.Value; item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value); - - Mods.Value = Mods.Value.Select(m => m.CreateCopy()).ToArray(); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 74a5ee8309..ddbb021054 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -32,6 +31,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Audio.Track; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Game.Graphics.UserInterface; @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Select private readonly Bindable decoupledRuleset = new Bindable(); - [Resolved(canBeNull: true)] + [Resolved] private MusicController music { get; set; } [BackgroundDependencyLoader(true)] @@ -561,15 +561,15 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); - Beatmap.Value.Track.Looping = true; - music?.ResetTrackAdjustments(); + music.CurrentTrack.Looping = true; + music.ResetTrackAdjustments(); if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { updateComponentFromBeatmap(Beatmap.Value); // restart playback on returning to song select, regardless. - music?.Play(); + music.Play(); } this.FadeIn(250); @@ -586,8 +586,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); - if (Beatmap.Value.Track != null) - Beatmap.Value.Track.Looping = false; + music.CurrentTrack.Looping = false; this.ScaleTo(1.1f, 250, Easing.InSine); @@ -608,8 +607,7 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); - if (Beatmap.Value.Track != null) - Beatmap.Value.Track.Looping = false; + music.CurrentTrack.Looping = false; return false; } @@ -650,11 +648,10 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; - if (beatmap.Track != null) - beatmap.Track.Looping = true; + music.CurrentTrack.Looping = true; } - private readonly WeakReference lastTrack = new WeakReference(null); + private readonly WeakReference lastTrack = new WeakReference(null); /// /// Ensures some music is playing for the current track. @@ -662,14 +659,14 @@ namespace osu.Game.Screens.Select /// private void ensurePlayingSelected() { - Track track = Beatmap.Value.Track; + ITrack track = music.CurrentTrack; bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - if (!track.IsRunning && (music?.IsUserPaused != true || isNewTrack)) - music?.Play(true); + if (!track.IsRunning && (music.IsUserPaused != true || isNewTrack)) + music.Play(true); lastTrack.SetTarget(track); } diff --git a/osu.Game/Screens/StartupScreen.cs b/osu.Game/Screens/StartupScreen.cs index c3e36c8e9d..e5e134fd39 100644 --- a/osu.Game/Screens/StartupScreen.cs +++ b/osu.Game/Screens/StartupScreen.cs @@ -18,6 +18,6 @@ namespace osu.Game.Screens public override bool AllowRateAdjustments => false; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; } } diff --git a/osu.Game/Skinning/LegacyColourCompatibility.cs b/osu.Game/Skinning/LegacyColourCompatibility.cs new file mode 100644 index 0000000000..b842b50426 --- /dev/null +++ b/osu.Game/Skinning/LegacyColourCompatibility.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Skinning +{ + /// + /// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only. + /// + public static class LegacyColourCompatibility + { + /// + /// Forces an alpha of 1 if a given is fully transparent. + /// + /// + /// This is equivalent to setting colour post-constructor in osu!stable. + /// + /// The to disallow zero alpha on. + /// The resultant . + public static Color4 DisallowZeroAlpha(Color4 colour) + { + if (colour.A == 0) + colour.A = 1; + return colour; + } + + /// + /// Applies a to a , doubling the alpha value into the property. + /// + /// + /// This is equivalent to setting colour in the constructor in osu!stable. + /// + /// The to apply the colour to. + /// The to apply. + /// The given . + public static T ApplyWithDoubledAlpha(T drawable, Color4 colour) + where T : Drawable + { + drawable.Alpha = colour.A; + drawable.Colour = DisallowZeroAlpha(colour); + return drawable; + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..35a6140cbc 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning public readonly int Keys; - public Dictionary CustomColours { get; set; } = new Dictionary(); + public Dictionary CustomColours { get; } = new Dictionary(); public Dictionary ImageLookups = new Dictionary(); @@ -31,10 +31,12 @@ namespace osu.Game.Skinning public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; public readonly float[] ExplosionWidth; + public readonly float[] HoldNoteLightWidth; public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; + public bool KeysUnderNotes; public LegacyManiaSkinConfiguration(int keys) { @@ -44,6 +46,7 @@ namespace osu.Game.Skinning ColumnSpacing = new float[keys - 1]; ColumnWidth = new float[keys]; ExplosionWidth = new float[keys]; + HoldNoteLightWidth = new float[keys]; ColumnLineWidth.AsSpan().Fill(2); ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 4990ca8e60..a99710ea96 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,6 +34,8 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + HoldNoteLightImage, + HoldNoteLightScale, ExplosionImage, ExplosionScale, ColumnLineColour, @@ -50,5 +52,6 @@ namespace osu.Game.Skinning Hit100, Hit50, Hit0, + KeysUnderNotes, } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 1e6102eaa4..a9d88e77ad 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -97,10 +97,18 @@ namespace osu.Game.Skinning currentConfig.ShowJudgementLine = pair.Value == "1"; break; + case "KeysUnderNotes": + currentConfig.KeysUnderNotes = pair.Value == "1"; + break; + case "LightingNWidth": parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + case "LightingLWidth": + parseArrayValue(pair.Value, currentConfig.HoldNoteLightWidth); + break; + case "WidthForNoteHeightScale": float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; if (minWidth > 0) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 10fb476728..5caf07b554 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -220,6 +220,20 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: + return SkinUtils.As(getManiaImage(existing, "LightingL")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: + Debug.Assert(maniaLookup.TargetColumn != null); + + if (GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + case LegacyManiaSkinConfigurationLookups.KeyImage: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); @@ -258,6 +272,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.Hit300: case LegacyManiaSkinConfigurationLookups.Hit300g: return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); + + case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: + return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); } return null; diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index a55870aa6d..25a924c929 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -45,7 +45,7 @@ namespace osu.Game.Skinning public void AddComboColours(params Color4[] colours) => comboColours.AddRange(colours); - public Dictionary CustomColours { get; set; } = new Dictionary(); + public Dictionary CustomColours { get; } = new Dictionary(); public readonly Dictionary ConfigDictionary = new Dictionary(); } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 6ada632850..e492069c5e 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) { diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index cdf9170701..bfcb2403c1 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -37,6 +37,6 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => null; - protected override Track GetTrack() => null; + protected override Track GetBeatmapTrack() => null; } } diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index bfbf7bb9da..baa7b27d28 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Runtime.CompilerServices; using osu.Framework.Platform; namespace osu.Game.Tests @@ -10,8 +11,15 @@ namespace osu.Game.Tests /// public class CleanRunHeadlessGameHost : HeadlessGameHost { - public CleanRunHeadlessGameHost(string gameName = @"", bool bindIPC = false, bool realtime = true) - : base(gameName, bindIPC, realtime) + /// + /// Create a new instance. + /// + /// An optional suffix which will isolate this host from others called from the same method source. + /// Whether to bind IPC channels. + /// Whether the host should be forced to run in realtime, rather than accelerated test time. + /// The name of the calling method, used for test file isolation and clean-up. + public CleanRunHeadlessGameHost(string gameSuffix = @"", bool bindIPC = false, bool realtime = true, [CallerMemberName] string callingMethodName = @"") + : base(callingMethodName + gameSuffix, bindIPC, realtime) { } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 866fc215d6..4db5139813 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -20,6 +20,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -29,6 +30,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [ExcludeFromDynamicCompile] public abstract class OsuTestScene : TestScene { protected Bindable Beatmap { get; private set; } @@ -135,6 +137,9 @@ namespace osu.Game.Tests.Visual [Resolved] protected AudioManager Audio { get; private set; } + [Resolved] + protected MusicController MusicController { get; private set; } + /// /// Creates the ruleset to be used for this test scene. /// @@ -164,8 +169,8 @@ namespace osu.Game.Tests.Visual rulesetDependencies?.Dispose(); - if (Beatmap?.Value.TrackLoaded == true) - Beatmap.Value.Track.Stop(); + if (MusicController?.TrackLoaded == true) + MusicController.CurrentTrack.Stop(); if (contextFactory.IsValueCreated) contextFactory.Value.ResetDatabase(); @@ -219,7 +224,7 @@ namespace osu.Game.Tests.Visual store?.Dispose(); } - protected override Track GetTrack() => track; + protected override Track GetBeatmapTrack() => track; public class TrackVirtualStore : AudioCollectionManager, ITrackStore { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d1e2033596..1de0633d1f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,9 +24,9 @@ - - - + + + diff --git a/osu.iOS.props b/osu.iOS.props index 9b25eaab41..7187b48907 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -80,7 +80,7 @@ - +