diff --git a/README.md b/README.md index f3f025fa10..eb2fe6d0eb 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web). -For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. +We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so. ## Licence diff --git a/osu.Android.props b/osu.Android.props index 927d66d93f..3ede0b85da 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -11,7 +11,7 @@ manifestmerger.jar - + diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index ea5f54a775..cd8894753f 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit private void load() { // todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation. - RightSideToolboxContainer.Alpha = 0; DistanceSpacingMultiplier.Disabled = true; LayerBelowRuleset.Add(new PlayfieldBorder diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs index cae19e9468..180cb98ed7 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index e59a0a0431..6efb415880 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods public DifficultyBindable CircleSize { get; } = new DifficultyBindable { Precision = 0.1f, - MinValue = 1, + MinValue = 0, MaxValue = 10, ExtendedMaxValue = 11, ReadCurrentFromDifficulty = diff => diff.CircleSize, @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Mods public DifficultyBindable ApproachRate { get; } = new DifficultyBindable { Precision = 0.1f, - MinValue = 1, + MinValue = 0, MaxValue = 10, ExtendedMaxValue = 11, ReadCurrentFromDifficulty = diff => diff.ApproachRate, diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs index 57c06e1cd1..83db9f665b 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs index ce06b841aa..3afb8c3d89 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs index 9e38913be7..c537897439 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModNightcore : ModNightcore { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 1c52c092ec..f77dab56c8 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopCentre; Size = new Vector2(BASE_SIZE); + if (difficulty != null) Scale = calculateScale(difficulty); @@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI base.Update(); var scaleFromDirection = new Vector2((int)VisualDirection, 1); + body.Scale = scaleFromDirection; - caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One; + // Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit. + caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One); + hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One; // Correct overshooting. if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || @@ -414,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI private void clearPlate(DroppedObjectAnimation animation) { - var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray(); + var caughtObjects = caughtObjectContainer.Children.ToArray(); caughtObjectContainer.Clear(false); + // Use the already returned PoolableDrawables for new objects + var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); + droppedObjectTarget.AddRange(droppedObjects); foreach (var droppedObject in droppedObjects) @@ -426,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) { - var droppedObject = getDroppedObject(caughtObject); - caughtObjectContainer.Remove(caughtObject, false); + var droppedObject = getDroppedObject(caughtObject); + droppedObjectTarget.Add(droppedObject); applyDropAnimation(droppedObject, animation); @@ -452,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI break; } + // Define lifetime start for dropped objects to be disposed correctly when rewinding replay + d.LifetimeStart = Clock.CurrentTime; d.Expire(); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs index bec0a6a1d3..309393b664 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.5; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs index a302f95966..f4b9cf3b88 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs index 014954dd60..8d48e3acde 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.5; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index 4cc712060c..748725af9f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModNightcore : ModNightcore { - public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png new file mode 100644 index 0000000000..258162c486 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 907422858e..c84a6ab70f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -18,6 +19,7 @@ using osu.Framework.Testing.Input; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; @@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable background; + private readonly Bindable ripples = new Bindable(); + public TestSceneGameplayCursor() { var ruleset = new OsuRuleset(); @@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests }); }); + AddToggleStep("ripples", v => ripples.Value = v); + AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); @@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("test cursor container", () => loadContent(false)); } + [BackgroundDependencyLoader] + private void load() + { + var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); + rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples); + } + [TestCase(1, 1)] [TestCase(5, 1)] [TestCase(10, 1)] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index b2e4e07526..bb424eb587 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -21,7 +21,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI.Cursor; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private OsuConfigManager config { get; set; } = null!; - private TestActionKeyCounter leftKeyCounter = null!; + private DefaultKeyCounter leftKeyCounter = null!; - private TestActionKeyCounter rightKeyCounter = null!; + private DefaultKeyCounter rightKeyCounter = null!; private OsuInputManager osuInputManager = null!; @@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests Origin = Anchor.Centre, Children = new Drawable[] { - leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton) + leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton)) { Anchor = Anchor.Centre, Origin = Anchor.CentreRight, Depth = float.MinValue, X = -100, }, - rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) + rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton)) { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, @@ -598,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests private void assertKeyCounter(int left, int right) { - AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left)); - AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right)); + AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left)); + AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right)); } private void releaseAllTouches() @@ -615,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action)); private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action)); - public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler + public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler { public OsuAction Action { get; } - public TestActionKeyCounter(OsuAction action) + public TestActionKeyCounterTrigger(OsuAction action) : base(action.ToString()) { Action = action; @@ -629,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests { if (e.Action == Action) { - IsLit = true; - Increment(); + Activate(); } return false; @@ -638,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void OnReleased(KeyBindingReleaseEvent e) { - if (e.Action == Action) IsLit = false; + if (e.Action == Action) + Deactivate(); } } diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index b8ad61e6dd..2056a50eda 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.SnakingInSliders, true); SetDefault(OsuRulesetSetting.SnakingOutSliders, true); SetDefault(OsuRulesetSetting.ShowCursorTrail, true); + SetDefault(OsuRulesetSetting.ShowCursorRipples, false); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); } } @@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration SnakingInSliders, SnakingOutSliders, ShowCursorTrail, + ShowCursorRipples, PlayfieldBorderStyle, } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index 3e161089cd..d6409279a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints protected override bool AlwaysShowWhenSelected => true; protected override bool ShouldBeAlive => base.ShouldBeAlive - || (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); protected OsuSelectionBlueprint(T hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs index 371dfe6a1a..1de6b9ce55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs index 700a3f44bc..5569df8d95 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs index 4769e7660b..bf65a6c9d3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs index b7838ebaa7..661cc948c5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModNightcore : ModNightcore { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 8fdf3821fa..52fdfea95f 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu Cursor, CursorTrail, CursorParticles, + CursorRipple, SliderScorePoint, ReverseArrow, HitCircleText, diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 620540b8ef..f049aa088f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; + case OsuSkinComponents.CursorRipple: + if (GetTexture("cursor-ripple") != null) + { + var ripple = this.GetAnimation("cursor-ripple", false, false); + + // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible. + // If anyone complains about these not being applied, this can be uncommented. + // + // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size, + // so we might be okay. + // + // if (ripple != null) + // { + // ripple.Scale = new Vector2(0.5f); + // ripple.Alpha = 0.2f; + // } + + return ripple; + } + + return null; + case OsuSkinComponents.CursorParticles: if (GetTexture("star2") != null) return new LegacyCursorParticles(); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs new file mode 100644 index 0000000000..076d97d06a --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -0,0 +1,105 @@ +// 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.Framework.Graphics.Pooling; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI.Cursor +{ + public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler + { + private readonly Bindable showRipples = new Bindable(true); + + private readonly DrawablePool ripplePool = new DrawablePool(20); + + public CursorRippleVisualiser() + { + RelativeSizeAxes = Axes.Both; + } + + public Vector2 CursorScale { get; set; } = Vector2.One; + + [BackgroundDependencyLoader(true)] + private void load(OsuRulesetConfigManager? rulesetConfig) + { + rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (showRipples.Value) + { + AddInternal(ripplePool.Get(r => + { + r.Position = e.MousePosition; + r.Scale = CursorScale; + })); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private partial class CursorRipple : PoolableDrawable + { + private Drawable ripple = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple()) + { + Blending = BlendingParameters.Additive, + }; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + ClearTransforms(true); + + ripple.ScaleTo(0.1f) + .ScaleTo(1, 700, Easing.Out); + + this + .FadeOutFromOne(700) + .Expire(true); + } + } + + public partial class DefaultCursorRipple : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new RingPiece(3) + { + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2), + Alpha = 0.1f, + } + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 5d7648b073..bf1ff872dd 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private Bindable userCursorScale; private Bindable autoCursorScale; + private readonly CursorRippleVisualiser rippleVisualiser; + public OsuCursorContainer() { InternalChild = fadeContainer = new Container @@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Children = new[] { cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling), + rippleVisualiser = new CursorRippleVisualiser(), new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling), } }; @@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor var newScale = new Vector2(e.NewValue); ActiveCursor.Scale = newScale; + rippleVisualiser.CursorScale = newScale; cursorTrail.Scale = newScale; }, true); diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 64c4e7eef6..0e410dbf57 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI LabelText = RulesetSettingsStrings.CursorTrail, Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, + new SettingsCheckbox + { + LabelText = RulesetSettingsStrings.CursorRipples, + Current = config.GetBindable(OsuRulesetSetting.ShowCursorRipples) + }, new SettingsEnumDropdown { LabelText = RulesetSettingsStrings.PlayfieldBorderStyle, diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 0a1f5380b5..8b1a4f688c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints protected override bool OnMouseDown(MouseDownEvent e) { - switch (e.Button) - { - case MouseButton.Left: - HitObject.Type = HitType.Centre; - EndPlacement(true); - return true; + if (e.Button != MouseButton.Left) + return false; - case MouseButton.Right: - HitObject.Type = HitType.Rim; - EndPlacement(true); - return true; - } - - return false; + EndPlacement(true); + return true; } public override void UpdateTimeAndPosition(SnapResult result) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index d0361b1c8d..cdeaafde10 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; - drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; + drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false; var playfield = (TaikoPlayfield)drawableRuleset.Playfield; playfield.ClassicHitTargetPosition.Value = true; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs index 84aa5e6bba..f442435d9c 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs index 89581c57bd..e517439ba4 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs index 68d6305fbf..9ef6fe8649 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs index 7cb14635ff..ad5da3d601 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModNightcore : ModNightcore { - public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index a08877e2dd..64d406a308 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI { public new BindableDouble TimeRange => base.TimeRange; - public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true); + public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true); public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; @@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI const float scroll_rate = 10; // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. - float ratio = DrawHeight / 480; + // Width is used because it defines how many notes fit on the playfield. + // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default. + float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT); TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; } @@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer { - LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect } + LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange } }; protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 42732d90e4..3587783104 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; - private const float default_aspect = 16f / 9f; - public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true); + public const float MAXIMUM_ASPECT = 16f / 9f; + public const float MINIMUM_ASPECT = 5f / 4f; + + public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); protected override void Update() { @@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI // // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit. // This is still a bit weird, because readability changes with window size, but it is what it is. - if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect) - height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; + if (LockPlayfieldAspectRange.Value) + { + float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y; + if (currentAspect > MAXIMUM_ASPECT) + height *= currentAspect / MAXIMUM_ASPECT; + else if (currentAspect < MINIMUM_ASPECT) + height *= currentAspect / MINIMUM_ASPECT; + } + + // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. + height = Math.Min(height, 1f / 3f); Height = height; - // Position the taiko playfield exactly one playfield from the top of the screen. + // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it. + // Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered. RelativePositionAxes = Axes.Y; Y = height; } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 85d304da9c..d898650b66 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using System.Diagnostics; using System.IO; using System.Linq; using NUnit.Framework; @@ -161,6 +160,51 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeVideoWithLowercaseExtension() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + var metadata = beatmap.Metadata; + + Assert.AreEqual("BG.jpg", metadata.BackgroundFile); + } + } + + [Test] + public void TestDecodeVideoWithUppercaseExtension() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + var metadata = beatmap.Metadata; + + Assert.AreEqual("BG.jpg", metadata.BackgroundFile); + } + } + + [Test] + public void TestDecodeImageSpecifiedAsVideo() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("image-specified-as-video.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + var metadata = beatmap.Metadata; + + Assert.AreEqual("BG.jpg", metadata.BackgroundFile); + } + } + [Test] public void TestDecodeBeatmapTimingPoints() { @@ -320,6 +364,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var comboColors = decoder.Decode(stream).ComboColours; + Debug.Assert(comboColors != null); + Color4[] expectedColors = { new Color4(142, 199, 255, 255), @@ -330,7 +376,7 @@ namespace osu.Game.Tests.Beatmaps.Formats new Color4(255, 177, 140, 255), new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored. }; - Assert.AreEqual(expectedColors.Length, comboColors?.Count); + Assert.AreEqual(expectedColors.Length, comboColors.Count); for (int i = 0; i < expectedColors.Length; i++) Assert.AreEqual(expectedColors[i], comboColors[i]); } @@ -415,14 +461,14 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsNotNull(positionData); Assert.IsNotNull(curveData); - Assert.AreEqual(new Vector2(192, 168), positionData.Position); + Assert.AreEqual(new Vector2(192, 168), positionData!.Position); Assert.AreEqual(956, hitObjects[0].StartTime); Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); positionData = hitObjects[1] as IHasPosition; Assert.IsNotNull(positionData); - Assert.AreEqual(new Vector2(304, 56), positionData.Position); + Assert.AreEqual(new Vector2(304, 56), positionData!.Position); Assert.AreEqual(1285, hitObjects[1].StartTime); Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); } @@ -578,8 +624,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestFallbackDecoderForCorruptedHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -596,8 +642,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestFallbackDecoderForMissingHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("missing-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -614,8 +660,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeFileWithEmptyLinesAtStart() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -632,8 +678,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeFileWithEmptyLinesAndNoHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -650,8 +696,8 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeFileWithContentImmediatelyAfterHeader() { - Decoder decoder = null; - Beatmap beatmap = null; + Decoder decoder = null!; + Beatmap beatmap = null!; using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu")) using (var stream = new LineBufferedReader(resStream)) @@ -678,7 +724,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestAllowFallbackDecoderOverwrite() { - Decoder decoder = null; + Decoder decoder = null!; using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var stream = new LineBufferedReader(resStream)) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 281ea4e4ff..34ff8bfd84 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osuTK; @@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(storyboard.HasDrawable); Assert.AreEqual(6, storyboard.Layers.Count()); - StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); Assert.IsNotNull(background); Assert.AreEqual(16, background.Elements.Count); Assert.IsTrue(background.VisibleWhenFailing); Assert.IsTrue(background.VisibleWhenPassing); Assert.AreEqual("Background", background.Name); - StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); + StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2); Assert.IsNotNull(fail); Assert.AreEqual(0, fail.Elements.Count); Assert.IsTrue(fail.VisibleWhenFailing); Assert.IsFalse(fail.VisibleWhenPassing); Assert.AreEqual("Fail", fail.Name); - StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); + StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1); Assert.IsNotNull(pass); Assert.AreEqual(0, pass.Elements.Count); Assert.IsFalse(pass.VisibleWhenFailing); Assert.IsTrue(pass.VisibleWhenPassing); Assert.AreEqual("Pass", pass.Name); - StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); + StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0); Assert.IsNotNull(foreground); Assert.AreEqual(151, foreground.Elements.Count); Assert.IsTrue(foreground.VisibleWhenFailing); Assert.IsTrue(foreground.VisibleWhenPassing); Assert.AreEqual("Foreground", foreground.Name); - StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue); + StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue); Assert.IsNotNull(overlay); Assert.IsEmpty(overlay.Elements); Assert.IsTrue(overlay.VisibleWhenFailing); @@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var sprite = background.Elements.ElementAt(0) as StoryboardSprite; Assert.NotNull(sprite); - Assert.IsTrue(sprite.HasCommands); + Assert.IsTrue(sprite!.HasCommands); Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.IsTrue(sprite.IsDrawable); Assert.AreEqual(Anchor.Centre, sprite.Origin); @@ -97,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestLoopWithoutExplicitFadeOut() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(1, background.Elements.Count); + + Assert.AreEqual(2000, background.Elements[0].StartTime); + Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime); + + Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime()); + Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay); + } + } + [Test] public void TestCorrectAnimationStartTime() { @@ -171,6 +190,55 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeVideoWithLowercaseExtension() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.EqualTo(1)); + + Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path); + } + } + + [Test] + public void TestDecodeVideoWithUppercaseExtension() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.EqualTo(1)); + + Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path); + } + } + + [Test] + public void TestDecodeImageSpecifiedAsVideo() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("image-specified-as-video.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.Zero); + } + } + [Test] public void TestDecodeOutOfRangeLoopAnimationType() { diff --git a/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb new file mode 100644 index 0000000000..7afaa445df --- /dev/null +++ b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb @@ -0,0 +1,6 @@ +[Events] +//Storyboard Layer 0 (Background) +Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever + F,0,2000,,0,1 + L,2000,10 + F,18,0,1000,1,0 diff --git a/osu.Game.Tests/Resources/image-specified-as-video.osb b/osu.Game.Tests/Resources/image-specified-as-video.osb new file mode 100644 index 0000000000..9cea7dd4e7 --- /dev/null +++ b/osu.Game.Tests/Resources/image-specified-as-video.osb @@ -0,0 +1,4 @@ +osu file format v14 + +[Events] +Video,0,"BG.jpg",0,0 diff --git a/osu.Game.Tests/Resources/video-with-lowercase-extension.osb b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb new file mode 100644 index 0000000000..eec09722ed --- /dev/null +++ b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,0,"BG.jpg",0,0 +Video,0,"Video.avi",0,0 diff --git a/osu.Game.Tests/Resources/video-with-uppercase-extension.osb b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb new file mode 100644 index 0000000000..3834a547f2 --- /dev/null +++ b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,0,"BG.jpg",0,0 +Video,0,"Video.AVI",0,0 diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index f8248e88bb..6639b6dd68 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -164,7 +164,7 @@ namespace osu.Game.Tests.Rulesets this.parentManager = parentManager; } - public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name); + public override byte[] GetRawData(string fileName) => parentManager.GetRawData(fileName); public bool IsDisposed { get; private set; } diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs index f1533a32b9..585a3f95e7 100644 --- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs +++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using NUnit.Framework; @@ -51,9 +49,11 @@ namespace osu.Game.Tests.Testing [Test] public void TestRetrieveShader() { - AddAssert("ruleset shaders retrieved", () => - Dependencies.Get().LoadRaw(@"sh_TestVertex.vs") != null && - Dependencies.Get().LoadRaw(@"sh_TestFragment.fs") != null); + AddStep("ruleset shaders retrieved without error", () => + { + Dependencies.Get().GetRawData(@"sh_TestVertex.vs"); + Dependencies.Get().GetRawData(@"sh_TestFragment.fs"); + }); } [Test] @@ -76,12 +76,12 @@ namespace osu.Game.Tests.Testing } public override IResourceStore CreateResourceStore() => new NamespacedResourceStore(TestResources.GetStore(), @"Resources"); - public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager(); + public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TestRulesetConfigManager(); public override IEnumerable GetModsFor(ModType type) => Array.Empty(); - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null; - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => null!; + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!; + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!; } private class TestRulesetConfigManager : IRulesetConfigManager diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index fbdaad1cd8..8f4250799e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsDrawable => true; public double StartTime => double.MinValue; public double EndTime => double.MaxValue; + public double EndTimeForDisplay => double.MaxValue; public Drawable CreateDrawable() => new DrawableTestStoryboardElement(); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs new file mode 100644 index 0000000000..7f9a69833c --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Game.Database; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene + { + public override void SetUpSteps() + { + CreateInitialBeatmap = () => + { + var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely(); + return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First()); + }; + + base.SetUpSteps(); + } + + [Test] + public void TestLocallyModifyingOnlineBeatmap() + { + AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); + + AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); + SaveEditor(); + + ReloadEditorToSameBeatmap(); + AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 5442b3bfef..f3f942b74b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2)); seekTo(referenceBeatmap.Breaks[0].StartTime); - AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); + AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); - AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 1dffeed01b..751aeb4e13 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay 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.Select(kc => kc.CountPresses).Sum() == 15); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0)); AddAssert("no results triggered", () => Player.Results.Count == 0); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index b918c5e64a..ae46dda750 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; - private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); + private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); [BackgroundDependencyLoader] private void load() @@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 890ac21b40..22f7111f68 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -8,6 +8,8 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -17,43 +19,63 @@ namespace osu.Game.Tests.Visual.Gameplay { public TestSceneKeyCounter() { - KeyCounterKeyboard testCounter; - - KeyCounterDisplay kc = new KeyCounterDisplay + KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Children = new KeyCounter[] - { - testCounter = new KeyCounterKeyboard(Key.X), - new KeyCounterKeyboard(Key.X), - new KeyCounterMouse(MouseButton.Left), - new KeyCounterMouse(MouseButton.Right), - }, + Position = new Vector2(0, 72.7f) }; + KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Position = new Vector2(0, -72.7f) + }; + + defaultDisplay.AddRange(new InputTrigger[] + { + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterMouseTrigger(MouseButton.Left), + new KeyCounterMouseTrigger(MouseButton.Right), + }); + + argonDisplay.AddRange(new InputTrigger[] + { + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterMouseTrigger(MouseButton.Left), + new KeyCounterMouseTrigger(MouseButton.Right), + }); + + var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First(); + AddStep("Add random", () => { Key key = (Key)((int)Key.A + RNG.Next(26)); - kc.Add(new KeyCounterKeyboard(key)); + defaultDisplay.Add(new KeyCounterKeyboardTrigger(key)); + argonDisplay.Add(new KeyCounterKeyboardTrigger(key)); }); - Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; + Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key; - void addPressKeyStep() + addPressKeyStep(); + AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1); + addPressKeyStep(); + AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2); + AddStep("Disable counting", () => { - AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); - } + argonDisplay.IsCounting.Value = false; + defaultDisplay.IsCounting.Value = false; + }); + addPressKeyStep(); + AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2); - addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); - addPressKeyStep(); - AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); - AddStep("Disable counting", () => testCounter.IsCounting = false); - addPressKeyStep(); - AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); + Add(defaultDisplay); + Add(argonDisplay); - Add(kc); + void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 2ea27c2fef..dbd1ce1f6e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SessionStatics sessionStatics { get; set; } + [Resolved] + private OsuConfigManager config { get; set; } + [Cached(typeof(INotificationOverlay))] private readonly NotificationOverlay notificationOverlay; @@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay saveVolumes(); setFullVolume(); + AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay restoreVolumes(); } + [Test] + public void TestEpilepsyWarningWithDisabledStoryboard() + { + saveVolumes(); + setFullVolume(); + + AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false)); + AddStep("change epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("epilepsy warning absent", () => getWarning() == null); + + restoreVolumes(); + } + [Test] public void TestEpilepsyWarningEarlyExit() { saveVolumes(); setFullVolume(); + AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("click notification", () => notification.TriggerClick()); } - private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(w => w.IsAlive); private partial class TestPlayerLoader : PlayerLoader { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 0469df1de3..d16f51f36e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -164,6 +164,36 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0)); } + [Test] + public void TestRevertNestedObjects() + { + ManualClock clock = null; + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 }); + + createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); + + AddStep("skip to middle of object", () => clock.CurrentTime = (beatmap.HitObjects[0].StartTime + beatmap.HitObjects[0].GetEndTime()) / 2); + AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2)); + + AddStep("skip to before end of object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() - 1); + AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3)); + + DrawableHitObject drawableHitObject = null; + HashSet revertedHitObjects = new HashSet(); + + AddStep("retrieve drawable hit object", () => drawableHitObject = playfield.ChildrenOfType().Single()); + AddStep("set up revert tracking", () => + { + revertedHitObjects.Clear(); + drawableHitObject.OnRevertResult += (ho, _) => revertedHitObjects.Add(ho.HitObject); + }); + AddStep("skip back to object start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime); + AddAssert("3 reverts fired", () => revertedHitObjects, () => Has.Count.EqualTo(3)); + AddAssert("no objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0)); + } + [Test] public void TestApplyHitResultOnKilled() { @@ -258,6 +288,8 @@ namespace osu.Game.Tests.Visual.Gameplay { RegisterPool(poolSize); RegisterPool(poolSize); + RegisterPool(poolSize); + RegisterPool(poolSize); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); @@ -388,6 +420,120 @@ namespace osu.Game.Tests.Visual.Gameplay } } + private class TestHitObjectWithNested : TestHitObject + { + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + for (int i = 0; i < 3; ++i) + AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 }); + } + } + + private class NestedHitObject : ConvertHitObject + { + } + + private partial class DrawableTestHitObjectWithNested : DrawableHitObject + { + private Container nestedContainer; + + public DrawableTestHitObjectWithNested() + : base(null) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Red + }, + nestedContainer = new Container + { + RelativeSizeAxes = Axes.Both + } + }); + } + + protected override void OnApply() + { + base.OnApply(); + + Size = new Vector2(200, 50); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + protected override void AddNestedHitObject(DrawableHitObject hitObject) + { + base.AddNestedHitObject(hitObject); + nestedContainer.Add(hitObject); + } + + protected override void ClearNestedHitObjects() + { + base.ClearNestedHitObjects(); + nestedContainer.Clear(false); + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + base.CheckForResult(userTriggered, timeOffset); + if (timeOffset >= 0) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + } + + private partial class DrawableNestedHitObject : DrawableHitObject + { + public DrawableNestedHitObject() + : this(null) + { + } + + public DrawableNestedHitObject(NestedHitObject hitObject) + : base(hitObject) + { + Size = new Vector2(15); + Colour = Colour4.White; + RelativePositionAxes = Axes.Both; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new Circle + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override void OnApply() + { + base.OnApply(); + + X = (float)((HitObject.StartTime - ParentHitObject!.HitObject.StartTime) / (ParentHitObject.HitObject.GetEndTime() - ParentHitObject.HitObject.StartTime)); + Y = 0.5f; + + LifetimeStart = ParentHitObject.LifetimeStart; + LifetimeEnd = ParentHitObject.LifetimeEnd; + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + base.CheckForResult(userTriggered, timeOffset); + if (timeOffset >= 0) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + } + #endregion } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index c476aae202..bf9b13b320 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); + AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0)); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs index 3e415af86e..ae10207de0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs @@ -8,6 +8,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps; using osuTK.Input; @@ -45,6 +46,18 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime); } + [Test] + public void TestDoesNotFailOnExit() + { + loadPlayerWithBeatmap(); + + AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0); + AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F)); + AddStep("exit player", () => Player.Exit()); + AddUntilStep("wait for exit", () => Player.Parent == null); + AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F)); + } + [Test] public void TestPauseViaSpaceWithSkip() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index a7da8f9832..93fec60de4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); scoreProcessor.Combo.Value = 1; return new Container diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 1f2329af4a..0439656aae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Gameplay; using osuTK.Input; @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; - private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); + private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); [Test] public void TestComboCounterIncrementing() @@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay hudOverlay = new HUDOverlay(null, Array.Empty()); // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space)); action?.Invoke(hudOverlay); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 5c69062e67..3f78dbfd96 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -22,8 +20,10 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSkinnableSound : OsuTestScene { - private TestSkinSourceContainer skinSource; - private PausableSkinnableSound skinnableSound; + private TestSkinSourceContainer skinSource = null!; + private PausableSkinnableSound skinnableSound = null!; + + private const string sample_lookup = "Gameplay/normal-sliderslide"; [SetUpSteps] public void SetUpSteps() @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; // has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached. - skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide"))); + skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo(sample_lookup))); }); } @@ -99,10 +99,28 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample not playing", () => !skinnableSound.IsPlaying); } + [Test] + public void TestSampleUpdatedBeforePlaybackWhenNotPresent() + { + AddStep("make sample non-present", () => skinnableSound.Hide()); + AddUntilStep("ensure not present", () => skinnableSound.IsPresent, () => Is.False); + + AddUntilStep("ensure sample loaded", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo(sample_lookup)); + + AddStep("change source", () => + { + skinSource.OverridingSample = new SampleVirtual("new skin"); + skinSource.TriggerSourceChanged(); + }); + + AddStep("start sample", () => skinnableSound.Play()); + AddUntilStep("sample updated", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo("new skin")); + } + [Test] public void TestSkinChangeDoesntPlayOnPause() { - DrawableSample sample = null; + DrawableSample? sample = null; AddStep("start sample", () => { skinnableSound.Play(); @@ -118,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("retrieve and ensure current sample is different", () => { - DrawableSample oldSample = sample; + DrawableSample? oldSample = sample; sample = skinnableSound.ChildrenOfType().Single(); return sample != oldSample; }); @@ -134,20 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler { [Resolved] - private ISkinSource source { get; set; } + private ISkinSource source { get; set; } = null!; - public event Action SourceChanged; + public event Action? SourceChanged; public Bindable SamplePlaybackDisabled { get; } = new Bindable(); + public ISample? OverridingSample; + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled; - public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => source?.GetDrawableComponent(lookup); - public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); - public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); - public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup); - public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction); - public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty()); + public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => source.GetDrawableComponent(lookup); + public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source.GetTexture(componentName, wrapModeS, wrapModeT); + public ISample? GetSample(ISampleInfo sampleInfo) => OverridingSample ?? source.GetSample(sampleInfo); + + public IBindable? GetConfig(TLookup lookup) + where TLookup : notnull + where TValue : notnull + { + return source.GetConfig(lookup); + } + + public ISkin? FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source.FindProvider(lookupFunction); + public IEnumerable AllSources => new[] { this }.Concat(source.AllSources); public void TriggerSourceChanged() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 0d88fb01a8..283866bef2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; @@ -106,6 +107,26 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } + [Test] + public void TestSaveFailedReplayWithStoryboardEndedDoesNotProgress() + { + CreateTest(() => + { + AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true); + AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0); + }); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + + AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); + AddUntilStep("wait for button clickable", () => Player.ChildrenOfType().First().ChildrenOfType().First().Enabled.Value); + AddStep("click save button", () => Player.ChildrenOfType().First().ChildrenOfType().First().TriggerClick()); + + // Test a regression where importing the fail replay would cause progression to results screen in a failed state. + AddWaitStep("wait some", 10); + AddAssert("player is still current screen", () => Player.IsCurrentScreen()); + } + [Test] public void TestShowResultsFalse() { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index aef6f9ade0..22c7bb64b2 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -114,6 +114,19 @@ namespace osu.Game.Tests.Visual.Menus } } + [TestCase(OverlayActivation.All)] + [TestCase(OverlayActivation.Disabled)] + public void TestButtonKeyboardInputRespectsOverlayActivation(OverlayActivation mode) + { + AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode); + AddStep("hide toolbar", () => toolbar.Hide()); + + if (mode == OverlayActivation.Disabled) + AddAssert("check buttons not accepting input", () => InputManager.NonPositionalInputQueue.OfType().Count(), () => Is.Zero); + else + AddAssert("check buttons accepting input", () => InputManager.NonPositionalInputQueue.OfType().Count(), () => Is.Not.Zero); + } + [TestCase(OverlayActivation.All)] [TestCase(OverlayActivation.Disabled)] public void TestRespectsOverlayActivation(OverlayActivation mode) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs new file mode 100644 index 0000000000..603573058e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Navigation +{ + public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene + { + /// + /// When entering the editor, a new beatmap is created as part of the asynchronous load process. + /// This test ensures that in the case of an early exit from the editor (ie. while it's still loading) + /// doesn't leave a dangling beatmap behind. + /// + /// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough + /// as a test. + /// + [Test] + public void TestCancelNavigationToEditor() + { + BeatmapSetInfo[] beatmapSets = null!; + + AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets()); + + AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault()); + + AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader); + AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit()); + + AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets)); + + BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All().Where(x => !x.DeletePending).ToArray()); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index d937b9e6d7..224e7e411e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -69,10 +70,10 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false); AddStep("press 'z'", () => InputManager.Key(Key.Z)); - AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0); + AddAssert("key counter didn't increase", () => keyCounter.CountPresses.Value == 0); AddStep("press 's'", () => InputManager.Key(Key.S)); - AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); + AddAssert("key counter did increase", () => keyCounter.CountPresses.Value == 1); } private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 5d13421195..a27c4ddad2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Testing; using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -26,7 +27,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Tests.Visual.Online { - public partial class TestSceneBeatmapSetOverlay : OsuTestScene + public partial class TestSceneBeatmapSetOverlay : OsuManualInputManagerTestScene { private readonly TestBeatmapSetOverlay overlay; @@ -281,6 +282,22 @@ namespace osu.Game.Tests.Visual.Online AddAssert(@"type is correct", () => type == lookupType.ToString()); } + [Test] + public void TestBeatmapSetWithGuestDifficulty() + { + AddStep("show map", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty())); + AddStep("move mouse to host difficulty", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(0)); + }); + AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().All(s => s.Text != "BanchoBot")); + AddStep("move mouse to guest difficulty", () => + { + InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1)); + }); + AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot")); + } + private APIBeatmapSet createManyDifficultiesBeatmapSet() { var set = getBeatmapSet(); @@ -320,6 +337,60 @@ namespace osu.Game.Tests.Visual.Online return beatmapSet; } + private APIBeatmapSet createBeatmapSetWithGuestDifficulty() + { + var set = getBeatmapSet(); + + var beatmaps = new List(); + + var guestUser = new APIUser + { + Username = @"BanchoBot", + Id = 3, + }; + + set.RelatedUsers = new[] + { + set.Author, guestUser + }; + + beatmaps.Add(new APIBeatmap + { + OnlineID = 1145, + DifficultyName = "Host Diff", + RulesetID = Ruleset.Value.OnlineID, + StarRating = 1.4, + OverallDifficulty = 3.5f, + AuthorID = set.AuthorID, + FailTimes = new APIFailTimes + { + Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(), + Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(), + }, + Status = BeatmapOnlineStatus.Graveyard + }); + + beatmaps.Add(new APIBeatmap + { + OnlineID = 1919, + DifficultyName = "Guest Diff", + RulesetID = Ruleset.Value.OnlineID, + StarRating = 8.1, + OverallDifficulty = 3.5f, + AuthorID = 3, + FailTimes = new APIFailTimes + { + Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(), + Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(), + }, + Status = BeatmapOnlineStatus.Graveyard + }); + + set.Beatmaps = beatmaps.ToArray(); + + return set; + } + private void downloadAssert(bool shown) { AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index feab86d3ee..f094d40caa 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1068,6 +1068,21 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value); } + [Test] + public void TestTextBoxBeatmapDifficultyCount() + { + createSongSelect(); + + AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); + + addRulesetImportStep(0); + + AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches"); + AddStep("delete all beatmaps", () => manager.Delete()); + AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault); + AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs index 316035275f..dd7bf48791 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs @@ -14,10 +14,11 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneBeatmapListingSortTabControl : OsuTestScene + public partial class TestSceneBeatmapListingSortTabControl : OsuManualInputManagerTestScene { private readonly BeatmapListingSortTabControl control; @@ -111,6 +112,29 @@ namespace osu.Game.Tests.Visual.UserInterface resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Mine); } + [Test] + public void TestSortDirectionOnCriteriaChange() + { + AddStep("set category to leaderboard", () => control.Reset(SearchCategory.Leaderboard, false)); + AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending); + + AddStep("click ranked sort button", () => + { + InputManager.MoveMouseTo(control.TabControl.ChildrenOfType().Single(s => s.Active.Value)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("sort direction is ascending", () => control.SortDirection.Value == SortDirection.Ascending); + + AddStep("click first inactive sort button", () => + { + InputManager.MoveMouseTo(control.TabControl.ChildrenOfType().First(s => !s.Active.Value)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending); + } + private void criteriaShowsOnCategory(bool expected, SortCriteria criteria, SearchCategory category) { AddAssert($"{criteria.ToString().ToLowerInvariant()} {(expected ? "shown" : "not shown")} on {category.ToString().ToLowerInvariant()}", () => diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 4752a88199..4731a70753 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; } + public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } public BeatmapImporter(Storage storage, RealmAccess realm) : base(storage, realm) @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps first.PerformRead(s => { // Re-run processing even in this case. We might have outdated metadata. - ProcessBeatmap?.Invoke((s, false)); + ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst); }); return first; } @@ -206,7 +206,7 @@ namespace osu.Game.Beatmaps protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) { base.PostImport(model, realm, parameters); - ProcessBeatmap?.Invoke((model, parameters.Batch)); + ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); } private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3208598f56..63e878b80d 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -167,7 +167,7 @@ namespace osu.Game.Beatmaps /// public double DistanceSpacing { get; set; } = 1.0; - public int BeatDivisor { get; set; } + public int BeatDivisor { get; set; } = 4; public int GridSize { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ad56bbbc3a..ae62564b0d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; - public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; } + public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } public override bool PauseImports { @@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps BeatmapTrackStore = audioManager.GetTrackStore(userResources); beatmapImporter = CreateBeatmapImporter(storage, realm); - beatmapImporter.ProcessBeatmap = args => ProcessBeatmap?.Invoke(args); + beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope); beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); @@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps // user requested abort return; - var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal))); + var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); if (video != null) { @@ -415,6 +415,13 @@ namespace osu.Game.Beatmaps // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. beatmapContent.BeatmapInfo = beatmapInfo; + // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. + // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file, + // which influences the beatmap checksums. + beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; + beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + beatmapInfo.ResetOnlineInfo(); + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) @@ -438,9 +445,6 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); - beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; - beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; - AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); updateHashAndMarkDirty(setInfo); @@ -454,7 +458,9 @@ namespace osu.Game.Beatmaps if (transferCollections) beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); - ProcessBeatmap?.Invoke((liveBeatmapSet, false)); + // do not look up metadata. + // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst. + ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None); }); } @@ -542,4 +548,11 @@ namespace osu.Game.Beatmaps public override string HumanisedModelName => "beatmap"; } + + /// + /// Delegate type for beatmap processing callbacks. + /// + /// The beatmap set to be processed. + /// The scope to use when looking up metadata. + public delegate void ProcessBeatmapDelegate(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope); } diff --git a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs index 98aefd75d3..b160043820 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs @@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps var matchingSet = r.All().FirstOrDefault(s => s.OnlineID == id); if (matchingSet != null) - beatmapUpdater.Queue(matchingSet.ToLive(realm), true); + beatmapUpdater.Queue(matchingSet.ToLive(realm), MetadataLookupScope.OnlineFirst); } }); } diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index d7b1fac7b3..af9f32f834 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -42,24 +42,25 @@ namespace osu.Game.Beatmaps /// Queue a beatmap for background processing. /// /// The managed beatmap set to update. A transaction will be opened to apply changes. - /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible. - public void Queue(Live beatmapSet, bool preferOnlineFetch = false) + /// The preferred scope to use for metadata lookup. + public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) { Logger.Log($"Queueing change for local beatmap {beatmapSet}"); - Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, preferOnlineFetch)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// /// Run all processing on a beatmap immediately. /// /// The managed beatmap set to update. A transaction will be opened to apply changes. - /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible. - public void Process(BeatmapSetInfo beatmapSet, bool preferOnlineFetch = false) => beatmapSet.Realm.Write(r => + /// The preferred scope to use for metadata lookup. + public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r => { // Before we use below, we want to invalidate. workingBeatmapCache.Invalidate(beatmapSet); - metadataLookup.Update(beatmapSet, preferOnlineFetch); + if (lookupScope != MetadataLookupScope.None) + metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst); foreach (var beatmap in beatmapSet.Beatmaps) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index eabc63b341..ef1dbc0488 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -363,6 +363,19 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); break; + case LegacyEventType.Video: + string filename = CleanFilename(split[2]); + + // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO + // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported + // video extensions and handle similar to a background if it doesn't match. + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) + { + beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + } + + break; + case LegacyEventType.Background: beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); break; diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 44dbb3cc9f..df5d3edb55 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -109,6 +109,14 @@ namespace osu.Game.Beatmaps.Formats int offset = Parsing.ParseInt(split[1]); string path = CleanFilename(split[2]); + // See handling in LegacyBeatmapDecoder for the special case where a video type is used but + // the file extension is not a valid video. + // + // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video + // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) + break; + storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); break; } @@ -276,7 +284,8 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case "A": - timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); + timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, + startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); break; case "H": diff --git a/osu.Game/Beatmaps/MetadataLookupScope.cs b/osu.Game/Beatmaps/MetadataLookupScope.cs new file mode 100644 index 0000000000..e1fbedc26a --- /dev/null +++ b/osu.Game/Beatmaps/MetadataLookupScope.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + /// + /// Determines which sources (if any at all) should be queried in which order for a beatmap's metadata. + /// + public enum MetadataLookupScope + { + /// + /// Do not attempt to look up the beatmap metadata either in the local cache or online. + /// + None, + + /// + /// Try the local metadata cache first before querying online sources. + /// + LocalCacheFirst, + + /// + /// Query online sources immediately. + /// + OnlineFirst + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 37e15c6127..7097102335 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { get { - if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension)) + if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant())) return FontAwesome.Regular.FileVideo; switch (File.Extension) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index f4e23ae7cb..20258b9c35 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -99,6 +99,16 @@ namespace osu.Game.Localisation /// public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks"); + /// + /// "{0:0}°" + /// + public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0}°", newRotation); + + /// + /// "{0:0}° (snapped)" + /// + public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs index 3a7fe4bb12..a77ee066e4 100644 --- a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs @@ -15,9 +15,9 @@ namespace osu.Game.Localisation public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps"); /// - /// ""Beatmaps" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection." + /// ""Beatmaps" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection." /// - public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."); + public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call sets of playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."); /// /// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay." diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 6a9793b20c..5e2600bc50 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -50,16 +50,18 @@ namespace osu.Game.Localisation public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!"); /// - /// "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting." + /// "osu! doesn't seem to be able to play audio correctly. + /// + /// Please try changing your audio device to a working setting." /// - public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), - @"osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."); + public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), @"osu! doesn't seem to be able to play audio correctly. + +Please try changing your audio device to a working setting."); /// /// "The score overlay is currently disabled. You can toggle this by pressing {0}." /// - public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), - @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0); + public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs index d2ff783413..3fa86c188c 100644 --- a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs +++ b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs @@ -65,6 +65,11 @@ namespace osu.Game.Localisation if (manager == null) return null; + // When using the English culture, prefer the fallbacks rather than osu-resources baked strings. + // They are guaranteed to be up-to-date, and is also what a developer expects to see when making changes to `xxxStrings.cs` files. + if (EffectiveCulture.Name == @"en") + return null; + try { return manager.GetString(key, EffectiveCulture); diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 1b0df6ecf6..52e6a5eaac 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Localisation /// public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail"); + /// + /// "Cursor ripples" + /// + public static LocalisableString CursorRipples => new TranslatableString(getKey(@"cursor_ripples"), @"Cursor ripples"); + /// /// "Playfield border style" /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index cf58d07b9e..34e31b0d61 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -71,7 +71,7 @@ namespace osu.Game [Cached(typeof(OsuGameBase))] public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider { - public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; + public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; public const string OSU_PROTOCOL = "osu://"; @@ -310,7 +310,7 @@ namespace osu.Game base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); - BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch); + BeatmapManager.ProcessBeatmap = (beatmapSet, scope) => beatmapUpdater.Process(beatmapSet, scope); dependencies.Cache(userCache = new UserLookupCache()); base.Content.Add(userCache); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 2e20f83e9e..219cbe7eef 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -209,7 +209,7 @@ namespace osu.Game.Overlays.AccountCreation if (!string.IsNullOrEmpty(errors.Message)) passwordDescription.AddErrors(new[] { errors.Message }); - game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}"); + game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true); } } else diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 76b6dec65b..3336c383ff 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -3,6 +3,7 @@ #nullable disable +using osu.Framework.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -10,8 +11,12 @@ namespace osu.Game.Overlays.BeatmapListing { public partial class BeatmapListingHeader : OverlayHeader { + public BeatmapListingFilterControl FilterControl { get; private set; } + protected override OverlayTitle CreateTitle() => new BeatmapListingTitle(); + protected override Drawable CreateContent() => FilterControl = new BeatmapListingFilterControl(); + private partial class BeatmapListingTitle : OverlayTitle { public BeatmapListingTitle() diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 23de1cf76d..3fa0fc7a77 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapListing Padding = new MarginPadding { Vertical = 20, - Horizontal = 40, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, }, Child = new FillFlowContainer { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index 025738710f..2f290d05e9 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.BeatmapListing if (currentParameters == null) Reset(SearchCategory.Leaderboard, false); + + Current.BindValueChanged(_ => SortDirection.Value = Overlays.SortDirection.Descending); } public void Reset(SearchCategory category, bool hasQuery) @@ -102,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapListing }; } - private partial class BeatmapTabButton : TabButton + public partial class BeatmapTabButton : TabButton { public readonly Bindable SortDirection = new Bindable(); @@ -136,7 +138,7 @@ namespace osu.Game.Overlays.BeatmapListing SortDirection.BindValueChanged(direction => { - icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown; + icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown; }, true); } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 73961487ed..f8784504b8 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -43,7 +43,8 @@ namespace osu.Game.Overlays private Container panelTarget; private FillFlowContainer foundContent; - private BeatmapListingFilterControl filterControl; + + private BeatmapListingFilterControl filterControl => Header.FilterControl; public BeatmapListingOverlay() : base(OverlayColourScheme.Blue) @@ -60,12 +61,6 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - filterControl = new BeatmapListingFilterControl - { - TypingStarted = onTypingStarted, - SearchStarted = onSearchStarted, - SearchFinished = onSearchFinished, - }, new Container { AutoSizeAxes = Axes.Y, @@ -88,6 +83,10 @@ namespace osu.Game.Overlays }, } }; + + filterControl.TypingStarted = onTypingStarted; + filterControl.SearchStarted = onSearchStarted; + filterControl.SearchFinished = onSearchFinished; } protected override void LoadComplete() diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 585e0dd1a2..104f861df7 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -31,6 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet private const float tile_spacing = 2; private readonly OsuSpriteText version, starRating, starRatingText; + private readonly LinkFlowContainer guestMapperContainer; private readonly FillFlowContainer starRatingContainer; private readonly Statistic plays, favourites; @@ -88,6 +89,14 @@ namespace osu.Game.Overlays.BeatmapSet Origin = Anchor.BottomLeft, Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold) }, + guestMapperContainer = new LinkFlowContainer(s => + s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11)) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Bottom = 1 }, + }, starRatingContainer = new FillFlowContainer { Anchor = Anchor.BottomLeft, @@ -198,8 +207,21 @@ namespace osu.Game.Overlays.BeatmapSet updateDifficultyButtons(); } - private void showBeatmap(IBeatmapInfo? beatmapInfo) + private void showBeatmap(APIBeatmap? beatmapInfo) { + guestMapperContainer.Clear(); + + if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID) + { + APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID); + + if (user != null) + { + guestMapperContainer.AddText("mapped by "); + guestMapperContainer.AddUserLink(user); + } + } + version.Text = beatmapInfo?.DifficultyName ?? string.Empty; } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 26e6b1f158..7ff8352054 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -97,8 +97,8 @@ namespace osu.Game.Overlays.BeatmapSet Padding = new MarginPadding { Vertical = BeatmapSetOverlay.Y_PADDING, - Left = BeatmapSetOverlay.X_PADDING, - Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, + Left = WaveOverlayContainer.HORIZONTAL_PADDING, + Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, }, Children = new Drawable[] { @@ -170,7 +170,7 @@ namespace osu.Game.Overlays.BeatmapSet Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING }, + Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = WaveOverlayContainer.HORIZONTAL_PADDING }, Direction = FillDirection.Vertical, Spacing = new Vector2(10), Children = new Drawable[] diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index 58739eb471..8758b9c5cf 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 15, Horizontal = BeatmapSetOverlay.X_PADDING }, + Padding = new MarginPadding { Top = 15, Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Children = new Drawable[] { new Container diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 9eb04d9cc5..6d89313979 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = 50 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Margin = new MarginPadding { Vertical = 20 }, Children = new Drawable[] { diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 237ce22767..873336bb6e 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -25,7 +25,6 @@ namespace osu.Game.Overlays { public partial class BeatmapSetOverlay : OnlineOverlay { - public const float X_PADDING = 40; public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 96d5203d14..08978ac2ab 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -18,8 +18,6 @@ namespace osu.Game.Overlays.Changelog { public partial class ChangelogBuild : FillFlowContainer { - public const float HORIZONTAL_PADDING = 70; - public Action SelectBuild; protected readonly APIChangelogBuild Build; @@ -33,7 +31,7 @@ namespace osu.Game.Overlays.Changelog RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; - Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING }; + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }; Children = new Drawable[] { diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 54ada24987..e9be67e977 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Changelog AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Horizontal = 65, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - ChangelogUpdateStreamItem.PADDING, Vertical = 20 }, Child = Streams = new ChangelogUpdateStreamControl { Current = currentStream }, diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs index d7c9ff67fe..4b784c7a28 100644 --- a/osu.Game/Overlays/Changelog/ChangelogListing.cs +++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Changelog { RelativeSizeAxes = Axes.X, Height = 1, - Padding = new MarginPadding { Horizontal = ChangelogBuild.HORIZONTAL_PADDING }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Margin = new MarginPadding { Top = 30 }, Child = new Box { diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs index 04526eb7ba..4aded1dd59 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Changelog Padding = new MarginPadding { Vertical = 20, - Horizontal = 50, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, }; } @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Changelog Direction = FillDirection.Vertical, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Right = 50 + image_container_width }, + Padding = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING + image_container_width }, Children = new Drawable[] { new OsuSpriteText diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index c4e4700674..24536fe460 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Comments { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 50, Vertical = 20 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 }, Children = new Drawable[] { avatar = new UpdateableAvatar(api.LocalUser.Value) @@ -152,7 +152,7 @@ namespace osu.Game.Overlays.Comments ShowDeleted = { BindTarget = ShowDeleted }, Margin = new MarginPadding { - Horizontal = 70, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 } }, @@ -393,7 +393,7 @@ namespace osu.Game.Overlays.Comments { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 50 }, + Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING }, Text = CommentsStrings.Empty } }); diff --git a/osu.Game/Overlays/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs index e6d44e618b..0ae1f839a1 100644 --- a/osu.Game/Overlays/Comments/CommentsHeader.cs +++ b/osu.Game/Overlays/Comments/CommentsHeader.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Comments new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 50 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Children = new Drawable[] { new OverlaySortTabControl diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 397dd46cdc..a710406548 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -537,7 +537,7 @@ namespace osu.Game.Overlays.Comments { return new MarginPadding { - Horizontal = 70, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 15 }; } diff --git a/osu.Game/Overlays/Comments/TotalCommentsCounter.cs b/osu.Game/Overlays/Comments/TotalCommentsCounter.cs index 38928f6f3d..2065f7a76b 100644 --- a/osu.Game/Overlays/Comments/TotalCommentsCounter.cs +++ b/osu.Game/Overlays/Comments/TotalCommentsCounter.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = 50 }, + Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING }, Spacing = new Vector2(5, 0), Children = new Drawable[] { diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs index 6cfa5cb9e8..dd418a9e58 100644 --- a/osu.Game/Overlays/Comments/VotePill.cs +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -132,11 +132,10 @@ namespace osu.Game.Overlays.Comments }, sideNumber = new OsuSpriteText { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreRight, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, Text = "+1", Font = OsuFont.GetFont(size: 14), - Margin = new MarginPadding { Right = 3 }, Alpha = 0, }, votesCounter = new OsuSpriteText @@ -189,7 +188,7 @@ namespace osu.Game.Overlays.Comments else sideNumber.FadeTo(IsHovered ? 1 : 0); - borderContainer.BorderThickness = IsHovered ? 3 : 0; + borderContainer.BorderThickness = IsHovered ? 2 : 0; } private void onHoverAction() diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 1540aa8fbb..5047992c8b 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Dashboard new Container { RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(padding), + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = padding }, Child = searchTextBox = new BasicSearchTextBox { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 73fab6d62b..e3accfd2ad 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Padding = new MarginPadding { Top = 20, - Horizontal = 45 + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - FriendsOnlineStatusItem.PADDING }, Child = onlineStreamControl = new FriendOnlineStreamControl(), } @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 50 } + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING } }, loading = new LoadingLayer(true) } diff --git a/osu.Game/Overlays/News/Displays/ArticleListing.cs b/osu.Game/Overlays/News/Displays/ArticleListing.cs index b6ce16ae7d..4fc9dde156 100644 --- a/osu.Game/Overlays/News/Displays/ArticleListing.cs +++ b/osu.Game/Overlays/News/Displays/ArticleListing.cs @@ -43,7 +43,7 @@ namespace osu.Game.Overlays.News.Displays { Vertical = 20, Left = 30, - Right = 50 + Right = WaveOverlayContainer.HORIZONTAL_PADDING }; InternalChild = new FillFlowContainer diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 4fdf7cb2b6..4d2c6bc9d0 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,6 +23,7 @@ namespace osu.Game.Overlays protected readonly OverlayScrollContainer ScrollFlow; protected readonly LoadingLayer Loading; + private readonly Container loadingContainer; private readonly Container content; protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true) @@ -65,10 +67,22 @@ namespace osu.Game.Overlays }, } }, - Loading = new LoadingLayer(true) + loadingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = Loading = new LoadingLayer(true), + } }); base.Content.Add(mainContent); } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // don't block header by applying padding equal to the visible header height + loadingContainer.Padding = new MarginPadding { Top = Math.Max(0, Header.Height - ScrollFlow.Current) }; + } } } diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index f28d40c429..93de463204 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays } }); - ContentSidePadding = 50; + ContentSidePadding = WaveOverlayContainer.HORIZONTAL_PADDING; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs index b8c0032e87..93e5e83ffc 100644 --- a/osu.Game/Overlays/OverlaySidebar.cs +++ b/osu.Game/Overlays/OverlaySidebar.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Vertical = 20, - Left = 50, + Left = WaveOverlayContainer.HORIZONTAL_PADDING, Right = 30 }, Child = CreateContent() diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index 8af2ab3823..5c51f5e4d0 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays } } - protected partial class TabButton : HeaderButton + public partial class TabButton : HeaderButton { public readonly BindableBool Active = new BindableBool(); diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index 9b18e5cccf..45181c13e4 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -39,12 +39,14 @@ namespace osu.Game.Overlays private FillFlowContainer text; private ExpandingBar expandingBar; + public const float PADDING = 5; + protected OverlayStreamItem(T value) : base(value) { Height = 50; Width = 90; - Margin = new MarginPadding(5); + Margin = new MarginPadding(PADDING); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs index 508041eb76..24be6ce2f5 100644 --- a/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(10, 10), - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Top = 10 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Top = 10 }, } }; } diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 1e80257a57..08a816930e 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 }, Spacing = new Vector2(0, 10), Children = new Drawable[] { diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 0dab4d582d..d964364510 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Padding = new MarginPadding { Vertical = 10 }, - Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }, + Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING }, Spacing = new Vector2(10, 0), Children = new Drawable[] { @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Header Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Right = UserProfileOverlay.CONTENT_X_MARGIN }, + Margin = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING }, Children = new Drawable[] { levelBadge = new LevelBadge @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Header Origin = Anchor.CentreRight, Width = 200, Height = 6, - Margin = new MarginPadding { Right = 50 }, + Margin = new MarginPadding { Right = WaveOverlayContainer.HORIZONTAL_PADDING }, Child = new LevelProgressBar { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index 1cc3aae735..1f35f39b49 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Header { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 }, Child = new GridContainer { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 2f4f49788f..de678cb5d1 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -31,6 +32,9 @@ namespace osu.Game.Overlays.Profile.Header [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RankingsOverlay? rankingsOverlay { get; set; } + private UserCoverBackground cover = null!; private SupporterIcon supporterTag = null!; private UpdateableAvatar avatar = null!; @@ -38,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; + private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; private ToggleCoverButton coverToggle = null!; @@ -83,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Header Direction = FillDirection.Horizontal, Padding = new MarginPadding { - Left = UserProfileOverlay.CONTENT_X_MARGIN, + Left = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = vertical_padding }, Height = content_height + 2 * vertical_padding, @@ -156,13 +161,17 @@ namespace osu.Game.Overlays.Profile.Header Size = new Vector2(28, 20), ShowPlaceholderOnUnknown = false, }, - userCountryText = new OsuSpriteText + userCountryContainer = new OsuHoverContainer { - Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), - Margin = new MarginPadding { Left = 5 }, - Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, - } + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 5 }, + Child = userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + }, } }, } @@ -202,6 +211,7 @@ namespace osu.Game.Overlays.Profile.Header openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); + userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 363eb5d58e..80d48ae09e 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeader() { - ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN; + ContentSidePadding = WaveOverlayContainer.HORIZONTAL_PADDING; TabControl.AddItem(LayoutStrings.HeaderUsersShow); diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 4ac86924f8..a8a240ddde 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Profile AutoSizeAxes = Axes.Both, Margin = new MarginPadding { - Horizontal = UserProfileOverlay.CONTENT_X_MARGIN - outer_gutter_width, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - outer_gutter_width, Top = 20, Bottom = 20, }, @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile RelativeSizeAxes = Axes.X, Padding = new MarginPadding { - Horizontal = UserProfileOverlay.CONTENT_X_MARGIN - outer_gutter_width, + Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING - outer_gutter_width, Bottom = 20 } }, diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs index 7b26640e50..1a44262ef8 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu set => valueText.Text = value.ToLocalisableString("N0"); } - public CountSection(LocalisableString header) + protected CountSection(LocalisableString header) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Overlays/Rankings/CountryFilter.cs b/osu.Game/Overlays/Rankings/CountryFilter.cs index e27fa7c7bd..525816f8fd 100644 --- a/osu.Game/Overlays/Rankings/CountryFilter.cs +++ b/osu.Game/Overlays/Rankings/CountryFilter.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Rankings Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, Spacing = new Vector2(10, 0), - Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }, + Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING }, Children = new Drawable[] { new OsuSpriteText diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index 31273e3b01..190da04a5d 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Rankings { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }, Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index affd9a2c44..27d894cdc2 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Rankings.Tables public abstract partial class RankingsTable : TableContainer { protected const int TEXT_SIZE = 12; - private const float horizontal_inset = 20; private const float row_height = 32; private const float row_spacing = 3; private const int items_per_page = 50; @@ -39,7 +38,7 @@ namespace osu.Game.Overlays.Rankings.Tables RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Horizontal = horizontal_inset }; + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }; RowSize = new Dimension(GridSizeMode.Absolute, row_height + row_spacing); } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 523b1237fa..0515e8dc97 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -33,7 +33,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private SettingsSlider dimSlider = null!; private readonly Bindable currentDisplay = new Bindable(); - private readonly IBindableList windowModes = new BindableList(); private Bindable scalingMode = null!; private Bindable sizeFullscreen = null!; @@ -79,7 +78,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (window != null) { currentDisplay.BindTo(window.CurrentDisplayBindable); - windowModes.BindTo(window.SupportedWindowModes); window.DisplaysChanged += onDisplaysChanged; } @@ -91,7 +89,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown = new SettingsDropdown { LabelText = GraphicsSettingsStrings.ScreenMode, - ItemSource = windowModes, + Items = window?.SupportedWindowModes, + CanBeShown = { Value = window?.SupportedWindowModes.Count() > 1 }, Current = config.GetBindable(FrameworkSetting.WindowMode), }, displayDropdown = new DisplaySettingsDropdown @@ -193,8 +192,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics updateScreenModeWarning(); }, true); - windowModes.BindCollectionChanged((_, _) => updateDisplaySettingsVisibility()); - currentDisplay.BindValueChanged(display => Schedule(() => { resolutions.RemoveRange(1, resolutions.Count - 1); @@ -254,7 +251,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private void updateDisplaySettingsVisibility() { - windowModeDropdown.CanBeShown.Value = windowModes.Count > 1; resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen; displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1; safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero; @@ -278,7 +274,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics return; } - if (host.Window is WindowsWindow) + if (host.Renderer is IWindowsRenderer) { switch (fullscreenCapability.Value) { diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index f21ef0ee98..93294a9d30 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); // Toolbar and its components need keyboard input even when hidden. - public override bool PropagateNonPositionalInputSubTree => true; + public override bool PropagateNonPositionalInputSubTree => OverlayActivationMode.Value != OverlayActivation.Disabled; public Toolbar() { diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index c5f8a820ea..d1fe877e55 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -45,8 +45,6 @@ namespace osu.Game.Overlays [Resolved] private RulesetStore rulesets { get; set; } = null!; - public const float CONTENT_X_MARGIN = 50; - public UserProfileOverlay() : base(OverlayColourScheme.Pink) { @@ -184,7 +182,7 @@ namespace osu.Game.Overlays public ProfileSectionTabControl() { Height = 40; - Padding = new MarginPadding { Horizontal = CONTENT_X_MARGIN }; + Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING }; TabContainer.Spacing = new Vector2(20); } diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index d25f6a9ae5..00474cc0d8 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -22,6 +22,8 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/wave-pop-in"; + public const float HORIZONTAL_PADDING = 50; + protected WaveOverlayContainer() { AddInternal(Waves = new WaveContainer diff --git a/osu.Game/Overlays/Wiki/WikiArticlePage.cs b/osu.Game/Overlays/Wiki/WikiArticlePage.cs index 6c1dbe3181..342a395871 100644 --- a/osu.Game/Overlays/Wiki/WikiArticlePage.cs +++ b/osu.Game/Overlays/Wiki/WikiArticlePage.cs @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Wiki { Vertical = 20, Left = 30, - Right = 50, + Right = WaveOverlayContainer.HORIZONTAL_PADDING, }, OnAddHeading = sidebar.AddEntry, } diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 88dc2cd7a4..2444aa4fa2 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Vertical = 20, - Horizontal = 50, + Horizontal = HORIZONTAL_PADDING, }, }); } diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 0df481737e..09c6af3820 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -11,8 +11,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -47,8 +45,6 @@ namespace osu.Game.Rulesets.Edit IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; } - private ExpandableSlider> distanceSpacingSlider; private ExpandableButton currentDistanceSpacingButton; @@ -67,47 +63,29 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - AddInternal(new Container + RightToolbox.Add(new EditorToolboxGroup("snapping") { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Children = new Drawable[] { - new Box + distanceSpacingSlider = new ExpandableSlider> { - Colour = colourProvider.Background5, - RelativeSizeAxes = Axes.Both, + KeyboardStep = adjust_step, + // Manual binding in LoadComplete to handle one-way event flow. + Current = DistanceSpacingMultiplier.GetUnboundCopy(), }, - RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250) + currentDistanceSpacingButton = new ExpandableButton { - Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, - Child = new EditorToolboxGroup("snapping") + Action = () => { - Children = new Drawable[] - { - distanceSpacingSlider = new ExpandableSlider> - { - KeyboardStep = adjust_step, - // Manual binding in LoadComplete to handle one-way event flow. - Current = DistanceSpacingMultiplier.GetUnboundCopy(), - }, - currentDistanceSpacingButton = new ExpandableButton - { - Action = () => - { - (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime(); + (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime(); - Debug.Assert(objects != null); + Debug.Assert(objects != null); - DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after); - DistanceSnapToggle.Value = TernaryState.True; - }, - RelativeSizeAxes = Axes.X, - } - } - } + DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after); + DistanceSnapToggle.Value = TernaryState.True; + }, + RelativeSizeAxes = Axes.X, } } }); @@ -115,7 +93,7 @@ namespace osu.Game.Rulesets.Edit private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime() { - HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject; + HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject; if (lastBefore == null) return null; @@ -261,7 +239,8 @@ namespace osu.Game.Rulesets.Edit public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) { - return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor); + return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 + / BeatSnapProvider.BeatDivisor); } public virtual float DurationToDistance(HitObject referenceObject, double duration) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index aee86fd942..653861c11c 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -58,8 +58,15 @@ namespace osu.Game.Rulesets.Edit [Resolved] protected IBeatSnapProvider BeatSnapProvider { get; private set; } + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + protected ComposeBlueprintContainer BlueprintContainer { get; private set; } + protected ExpandingToolboxContainer LeftToolbox { get; private set; } + + protected ExpandingToolboxContainer RightToolbox { get; private set; } + private DrawableEditorRulesetWrapper drawableRulesetWrapper; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; @@ -82,7 +89,7 @@ namespace osu.Game.Rulesets.Edit dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuConfigManager config) + private void load(OsuConfigManager config) { autoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); @@ -131,7 +138,7 @@ namespace osu.Game.Rulesets.Edit Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, }, - new ExpandingToolboxContainer(60, 200) + LeftToolbox = new ExpandingToolboxContainer(60, 200) { Children = new Drawable[] { @@ -153,6 +160,28 @@ namespace osu.Game.Rulesets.Edit }, } }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background5, + RelativeSizeAxes = Axes.Both, + }, + RightToolbox = new ExpandingToolboxContainer(130, 250) + { + Child = new EditorToolboxGroup("inspector") + { + Child = new HitObjectInspector() + }, + } + } + } }; toolboxCollection.Items = CompositionTools diff --git a/osu.Game/Rulesets/Edit/HitObjectInspector.cs b/osu.Game/Rulesets/Edit/HitObjectInspector.cs new file mode 100644 index 0000000000..977d00ede2 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectInspector.cs @@ -0,0 +1,146 @@ +// 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.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Edit; + +namespace osu.Game.Rulesets.Edit +{ + internal partial class HitObjectInspector : CompositeDrawable + { + private OsuTextFlowContainer inspectorText = null!; + + [Resolved] + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + InternalChild = inspectorText = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText(); + EditorBeatmap.TransactionBegan += updateInspectorText; + EditorBeatmap.TransactionEnded += updateInspectorText; + updateInspectorText(); + } + + private ScheduledDelegate? rollingTextUpdate; + + private void updateInspectorText() + { + inspectorText.Clear(); + rollingTextUpdate?.Cancel(); + rollingTextUpdate = null; + + switch (EditorBeatmap.SelectedHitObjects.Count) + { + case 0: + addValue("No selection"); + break; + + case 1: + var selected = EditorBeatmap.SelectedHitObjects.Single(); + + addHeader("Type"); + addValue($"{selected.GetType().ReadableName()}"); + + addHeader("Time"); + addValue($"{selected.StartTime:#,0.##}ms"); + + switch (selected) + { + case IHasPosition pos: + addHeader("Position"); + addValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}"); + break; + + case IHasXPosition x: + addHeader("Position"); + + addValue($"x:{x.X:#,0.##} "); + break; + + case IHasYPosition y: + addHeader("Position"); + + addValue($"y:{y.Y:#,0.##}"); + break; + } + + if (selected is IHasDistance distance) + { + addHeader("Distance"); + addValue($"{distance.Distance:#,0.##}px"); + } + + if (selected is IHasRepeats repeats) + { + addHeader("Repeats"); + addValue($"{repeats.RepeatCount:#,0.##}"); + } + + if (selected is IHasDuration duration) + { + addHeader("End Time"); + addValue($"{duration.EndTime:#,0.##}ms"); + addHeader("Duration"); + addValue($"{duration.Duration:#,0.##}ms"); + } + + // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. + // This is a good middle-ground for the time being. + rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); + break; + + default: + addHeader("Selected Objects"); + addValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}"); + + addHeader("Start Time"); + addValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms"); + + addHeader("End Time"); + addValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms"); + break; + } + + void addHeader(string header) => inspectorText.AddParagraph($"{header}: ", s => + { + s.Padding = new MarginPadding { Top = 2 }; + s.Font = s.Font.With(size: 12); + s.Colour = colourProvider.Content2; + }); + + void addValue(string value) => inspectorText.AddParagraph(value, s => + { + s.Font = s.Font.With(weight: FontWeight.SemiBold); + s.Colour = colourProvider.Content1; + }); + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 9e4469bf25..733610c040 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -24,5 +24,22 @@ namespace osu.Game.Rulesets.Mods MaxValue = 2, Precision = 0.01, }; + + public override double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + // Each 0.1 multiple changes score multiplier by 0.02. + value /= 5; + + return 1 + value; + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 4425ece513..97789b7f5a 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -20,11 +20,31 @@ namespace osu.Game.Rulesets.Mods public virtual bool RestartOnFail => Restart.Value; + private Action? triggerFailureDelegate; + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { + triggerFailureDelegate = healthProcessor.TriggerFailure; healthProcessor.FailConditions += FailCondition; } + /// + /// Immediately triggers a failure on the loaded . + /// + protected void TriggerFailure() => triggerFailureDelegate?.Invoke(); + + /// + /// Determines whether should trigger a failure. Called every time a + /// judgement is applied to . + /// + /// The loaded . + /// The latest . + /// Whether the fail condition has been met. + /// + /// This method should only be used to trigger failures based on . + /// Using outside values to evaluate failure may introduce event ordering discrepancies, use + /// an with instead. + /// protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result); } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 7d858dca6f..06c7750035 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -24,5 +24,19 @@ namespace osu.Game.Rulesets.Mods MaxValue = 0.99, Precision = 0.01, }; + + public override double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + return 1 + value; + } + } } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0c50f8341a..f6c3452e48 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Objects.Drawables OnNestedDrawableCreated?.Invoke(drawableNested); drawableNested.OnNewResult += onNewResult; + drawableNested.OnRevertResult += onNestedRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation(). @@ -312,6 +313,7 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var obj in nestedHitObjects) { obj.OnNewResult -= onNewResult; + obj.OnRevertResult -= onNestedRevertResult; obj.ApplyCustomUpdateState -= onApplyCustomUpdateState; } @@ -376,6 +378,8 @@ namespace osu.Game.Rulesets.Objects.Drawables OnRevertResult?.Invoke(this, Result); } + private void onNestedRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); + private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state); private void onDefaultsApplied(HitObject hitObject) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index b70ddd5e24..3e0b6433c2 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -31,6 +31,15 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } + /// + /// Immediately triggers a failure for this HealthProcessor. + /// + public void TriggerFailure() + { + if (Failed?.Invoke() != false) + HasFailed = true; + } + protected override void ApplyResultInternal(JudgementResult result) { result.HealthAtJudgement = Health.Value; @@ -42,10 +51,7 @@ namespace osu.Game.Rulesets.Scoring Health.Value += GetHealthIncreaseFor(result); if (meetsAnyFailCondition(result)) - { - if (Failed?.Invoke() != false) - HasFailed = true; - } + TriggerFailure(); } protected override void RevertResultInternal(JudgementResult result) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 64fe9c8a86..4f22c0c617 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 96b02ee4dc..e34289c968 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -25,21 +25,28 @@ namespace osu.Game.Rulesets.UI /// /// The texture store to be used for the ruleset. /// + /// + /// Reads textures from the "Textures" folder in ruleset resources. + /// If not available locally, lookups will fallback to the global texture store. + /// public TextureStore TextureStore { get; } /// /// The sample store to be used for the ruleset. /// /// - /// This is the local sample store pointing to the ruleset sample resources, - /// the cached sample store () retrieves from - /// this store and falls back to the parent store if this store doesn't have the requested sample. + /// Reads samples from the "Samples" folder in ruleset resources. + /// If not available locally, lookups will fallback to the global sample store. /// public ISampleStore SampleStore { get; } /// /// The shader manager to be used for the ruleset. /// + /// + /// Reads shaders from the "Shaders" folder in ruleset resources. + /// If not available locally, lookups will fallback to the global shader manager. + /// public ShaderManager ShaderManager { get; } /// @@ -61,8 +68,7 @@ namespace osu.Game.Rulesets.UI SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get())); - ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders")); - CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get())); + CacheAs(ShaderManager = new RulesetShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders"), parent.Get())); RulesetConfigManager = parent.Get().GetConfigFor(ruleset); if (RulesetConfigManager != null) @@ -92,7 +98,7 @@ namespace osu.Game.Rulesets.UI isDisposed = true; - if (ShaderManager.IsNotNull()) SampleStore.Dispose(); + if (SampleStore.IsNotNull()) SampleStore.Dispose(); if (TextureStore.IsNotNull()) TextureStore.Dispose(); if (ShaderManager.IsNotNull()) ShaderManager.Dispose(); } @@ -190,25 +196,21 @@ namespace osu.Game.Rulesets.UI } } - private class FallbackShaderManager : ShaderManager + private class RulesetShaderManager : ShaderManager { - private readonly ShaderManager primary; - private readonly ShaderManager fallback; + private readonly ShaderManager parent; - public FallbackShaderManager(IRenderer renderer, ShaderManager primary, ShaderManager fallback) - : base(renderer, new ResourceStore()) + public RulesetShaderManager(IRenderer renderer, NamespacedResourceStore rulesetResources, ShaderManager parent) + : base(renderer, rulesetResources) { - this.primary = primary; - this.fallback = fallback; + this.parent = parent; } - public override byte[]? LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name); + public override IShader? GetCachedShader(string vertex, string fragment) => base.GetCachedShader(vertex, fragment) ?? parent.GetCachedShader(vertex, fragment); - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (primary.IsNotNull()) primary.Dispose(); - } + public override IShaderPart? GetCachedShaderPart(string name) => base.GetCachedShaderPart(name) ?? parent.GetCachedShaderPart(name); + + public override byte[]? GetRawData(string fileName) => base.GetRawData(fileName) ?? parent.GetRawData(fileName); } } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7bf0482673..2ae54a3afe 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -19,7 +19,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; using static osu.Game.Input.Handlers.ReplayInputHandler; @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.UI .Select(b => b.GetAction()) .Distinct() .OrderBy(action => action) - .Select(action => new KeyCounterAction(action))); + .Select(action => new KeyCounterActionTrigger(action))); } private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler @@ -179,11 +179,14 @@ namespace osu.Game.Rulesets.UI { } - public bool OnPressed(KeyBindingPressEvent e) => Target.Children.OfType>().Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); + public bool OnPressed(KeyBindingPressEvent e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger) + .Select(c => (KeyCounterActionTrigger)c.Trigger) + .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); public void OnReleased(KeyBindingReleaseEvent e) { - foreach (var c in Target.Children.OfType>()) + foreach (var c + in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger).Select(c => (KeyCounterActionTrigger)c.Trigger)) c.OnReleased(e.Action, Clock.Rate >= 0); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 0f702e1c49..c2a3f12efd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -7,14 +7,15 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Localisation; using osuTK; using osuTK.Graphics; +using Key = osuTK.Input.Key; namespace osu.Game.Screens.Edit.Compose.Components { @@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private SpriteIcon icon; + private const float snap_step = 15; + private readonly Bindable cumulativeRotation = new Bindable(); [Resolved] @@ -50,18 +53,14 @@ namespace osu.Game.Screens.Edit.Compose.Components }); } - protected override void LoadComplete() - { - base.LoadComplete(); - cumulativeRotation.BindValueChanged(_ => updateTooltipText(), true); - } - protected override void UpdateHoverState() { base.UpdateHoverState(); icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint); } + private float rawCumulativeRotation; + protected override bool OnDragStart(DragStartEvent e) { bool handle = base.OnDragStart(e); @@ -74,21 +73,36 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.OnDrag(e); - float instantaneousAngle = convertDragEventToAngleOfRotation(e); - cumulativeRotation.Value += instantaneousAngle; + rawCumulativeRotation += convertDragEventToAngleOfRotation(e); - if (cumulativeRotation.Value < -180) - cumulativeRotation.Value += 360; - else if (cumulativeRotation.Value > 180) - cumulativeRotation.Value -= 360; + applyRotation(shouldSnap: e.ShiftPressed); + } - HandleRotate?.Invoke(instantaneousAngle); + protected override bool OnKeyDown(KeyDownEvent e) + { + if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + { + applyRotation(shouldSnap: true); + return true; + } + + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + applyRotation(shouldSnap: false); } protected override void OnDragEnd(DragEndEvent e) { base.OnDragEnd(e); cumulativeRotation.Value = null; + rawCumulativeRotation = 0; + TooltipText = default; } private float convertDragEventToAngleOfRotation(DragEvent e) @@ -100,9 +114,19 @@ namespace osu.Game.Screens.Edit.Compose.Components return (endAngle - startAngle) * 180 / MathF.PI; } - private void updateTooltipText() + private void applyRotation(bool shouldSnap) { - TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default; + float oldRotation = cumulativeRotation.Value ?? 0; + + float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); + newRotation = (newRotation - 180) % 360 + 180; + + cumulativeRotation.Value = newRotation; + + HandleRotate?.Invoke(newRotation - oldRotation); + TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation); } + + private float snap(float value, float step) => MathF.Round(value / step) * step; } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d89392f757..b5d304a031 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -210,7 +210,10 @@ namespace osu.Game.Screens.Edit // this is a bit haphazard, but guards against setting the lease Beatmap bindable if // the editor has already been exited. if (!ValidForPush) + { + beatmapManager.Delete(loadableBeatmap.BeatmapSetInfo); return; + } } try diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index b70c1f7ddf..372cfe748e 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -130,6 +130,8 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); } diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs new file mode 100644 index 0000000000..6818b30823 --- /dev/null +++ b/osu.Game/Screens/Play/ArgonKeyCounter.cs @@ -0,0 +1,76 @@ +// 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.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public partial class ArgonKeyCounter : KeyCounter + { + private Circle inputIndicator = null!; + private OsuSpriteText countText = null!; + + // These values were taken from Figma + private const float line_height = 3; + private const float name_font_size = 10; + private const float count_font_size = 14; + + // Make things look bigger without using Scale + private const float scale_factor = 1.5f; + + public ArgonKeyCounter(InputTrigger trigger) + : base(trigger) + { + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + inputIndicator = new Circle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = line_height * scale_factor, + Alpha = 0.5f + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(0, -13) * scale_factor, + Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), + Colour = colours.Blue0, + Text = Trigger.Name + }, + countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), + }, + }; + + // Values from Figma didn't match visually + // So these were just eyeballed + Height = 30 * scale_factor; + Width = 35 * scale_factor; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsActive.BindValueChanged(e => inputIndicator.Alpha = e.NewValue ? 1 : 0.5f, true); + CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true); + } + } +} diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs new file mode 100644 index 0000000000..984c2a7287 --- /dev/null +++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.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.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public partial class ArgonKeyCounterDisplay : KeyCounterDisplay + { + private const int duration = 100; + + protected override FillFlowContainer KeyFlow { get; } + + public ArgonKeyCounterDisplay() + { + InternalChild = KeyFlow = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Alpha = 0, + Spacing = new Vector2(2), + }; + } + + protected override void Update() + { + base.Update(); + + Size = KeyFlow.Size; + } + + protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger); + + protected override void UpdateVisibility() + => KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); + } +} diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 0214d33549..57bdad079e 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -1,15 +1,13 @@ // 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.Audio; -using osu.Framework.Bindables; -using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; using ManagedBass.Fx; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; +using osu.Framework.Audio; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,6 +19,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -50,8 +49,7 @@ namespace osu.Game.Screens.Play private const float duration = 2500; - private ISample? failSample; - private SampleChannel? failSampleChannel; + private SkinnableSound failSample = null!; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -76,10 +74,10 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, ISkinSource skin, IBindable beatmap) + private void load(AudioManager audio, IBindable beatmap) { track = beatmap.Value.Track; - failSample = skin.GetSample(new SampleInfo(@"Gameplay/failsound")); + AddInternal(failSample = new SkinnableSound(new SampleInfo("Gameplay/failsound"))); AddRangeInternal(new Drawable[] { @@ -126,7 +124,7 @@ namespace osu.Game.Screens.Play failHighPassFilter.CutoffTo(300); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); - failSampleChannel = failSample?.Play(); + failSample.Play(); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); @@ -159,7 +157,7 @@ namespace osu.Game.Screens.Play /// public void Stop() { - failSampleChannel?.Stop(); + failSample.Stop(); removeFilters(); } diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs similarity index 69% rename from osu.Game/Screens/Play/KeyCounter.cs rename to osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs index 4405542b3b..f7ac72035f 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs @@ -1,8 +1,6 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,70 +11,23 @@ using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public abstract partial class KeyCounter : Container + public partial class DefaultKeyCounter : KeyCounter { - private Sprite buttonSprite; - private Sprite glowSprite; - private Container textLayer; - private SpriteText countSpriteText; - - public bool IsCounting { get; set; } = true; - private int countPresses; - - public int CountPresses - { - get => countPresses; - private set - { - if (countPresses != value) - { - countPresses = value; - countSpriteText.Text = value.ToString(@"#,0"); - } - } - } - - private bool isLit; - - public bool IsLit - { - get => isLit; - protected set - { - if (isLit != value) - { - isLit = value; - updateGlowSprite(value); - } - } - } - - public void Increment() - { - if (!IsCounting) - return; - - CountPresses++; - } - - public void Decrement() - { - if (!IsCounting) - return; - - CountPresses--; - } + private Sprite buttonSprite = null!; + private Sprite glowSprite = null!; + private Container textLayer = null!; + private SpriteText countSpriteText = null!; //further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray; public Color4 KeyUpTextColor { get; set; } = Color4.White; public double FadeTime { get; set; } - protected KeyCounter(string name) + public DefaultKeyCounter(InputTrigger trigger) + : base(trigger) { - Name = name; } [BackgroundDependencyLoader(true)] @@ -106,7 +57,7 @@ namespace osu.Game.Screens.Play { new OsuSpriteText { - Text = Name, + Text = Trigger.Name, Font = OsuFont.Numeric.With(size: 12), Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -116,7 +67,7 @@ namespace osu.Game.Screens.Play }, countSpriteText = new OsuSpriteText { - Text = CountPresses.ToString(@"#,0"), + Text = CountPresses.Value.ToString(@"#,0"), Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativePositionAxes = Axes.Both, @@ -130,6 +81,9 @@ namespace osu.Game.Screens.Play // so the size can be changing between buttonSprite and glowSprite. Height = buttonSprite.DrawHeight; Width = buttonSprite.DrawWidth; + + IsActive.BindValueChanged(e => updateGlowSprite(e.NewValue), true); + CountPresses.BindValueChanged(e => countSpriteText.Text = e.NewValue.ToString(@"#,0"), true); } private void updateGlowSprite(bool show) diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs new file mode 100644 index 0000000000..e459574243 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs @@ -0,0 +1,80 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class DefaultKeyCounterDisplay : KeyCounterDisplay + { + private const int duration = 100; + private const double key_fade_time = 80; + + protected override FillFlowContainer KeyFlow { get; } + + public DefaultKeyCounterDisplay() + { + InternalChild = KeyFlow = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Alpha = 0, + }; + } + + protected override void Update() + { + base.Update(); + + // Don't use autosize as it will shrink to zero when KeyFlow is hidden. + // In turn this can cause the display to be masked off screen and never become visible again. + Size = KeyFlow.Size; + } + + protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger) + { + FadeTime = key_fade_time, + KeyDownTextColor = KeyDownTextColor, + KeyUpTextColor = KeyUpTextColor, + }; + + protected override void UpdateVisibility() => + // Isolate changing visibility of the key counters from fading this component. + KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); + + private Color4 keyDownTextColor = Color4.DarkGray; + + public Color4 KeyDownTextColor + { + get => keyDownTextColor; + set + { + if (value != keyDownTextColor) + { + keyDownTextColor = value; + foreach (var child in KeyFlow.Cast()) + child.KeyDownTextColor = value; + } + } + } + + private Color4 keyUpTextColor = Color4.White; + + public Color4 KeyUpTextColor + { + get => keyUpTextColor; + set + { + if (value != keyUpTextColor) + { + keyUpTextColor = value; + foreach (var child in KeyFlow.Cast()) + child.KeyUpTextColor = value; + } + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/InputTrigger.cs b/osu.Game/Screens/Play/HUD/InputTrigger.cs new file mode 100644 index 0000000000..b57f2cdf91 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/InputTrigger.cs @@ -0,0 +1,37 @@ +// 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; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An event trigger which can be used with to create visual tracking of button/key presses. + /// + public abstract partial class InputTrigger : Component + { + /// + /// Callback to invoke when the associated input has been activated. + /// + /// Whether gameplay is progressing in the forward direction time-wise. + public delegate void OnActivateCallback(bool forwardPlayback); + + /// + /// Callback to invoke when the associated input has been deactivated. + /// + /// Whether gameplay is progressing in the forward direction time-wise. + public delegate void OnDeactivateCallback(bool forwardPlayback); + + public event OnActivateCallback? OnActivate; + public event OnDeactivateCallback? OnDeactivate; + + protected InputTrigger(string name) + { + Name = name; + } + + protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback); + + protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback); + } +} diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs new file mode 100644 index 0000000000..7cdd6b025f --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An individual key display which is intended to be displayed within a . + /// + public abstract partial class KeyCounter : Container + { + /// + /// The which activates and deactivates this . + /// + public readonly InputTrigger Trigger; + + /// + /// Whether the actions reported by should be counted. + /// + public Bindable IsCounting { get; } = new BindableBool(true); + + private readonly Bindable countPresses = new BindableInt + { + MinValue = 0 + }; + + /// + /// The current count of registered key presses. + /// + public IBindable CountPresses => countPresses; + + private readonly Container content; + + protected override Container Content => content; + + /// + /// Whether this is currently in the "activated" state because the associated key is currently pressed. + /// + protected readonly Bindable IsActive = new BindableBool(); + + protected KeyCounter(InputTrigger trigger) + { + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both + }, + Trigger = trigger, + }; + + Trigger.OnActivate += Activate; + Trigger.OnDeactivate += Deactivate; + } + + private void increment() + { + if (!IsCounting.Value) + return; + + countPresses.Value++; + } + + private void decrement() + { + if (!IsCounting.Value) + return; + + countPresses.Value--; + } + + protected virtual void Activate(bool forwardPlayback = true) + { + IsActive.Value = true; + if (forwardPlayback) + increment(); + } + + protected virtual void Deactivate(bool forwardPlayback = true) + { + IsActive.Value = false; + if (!forwardPlayback) + decrement(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + Trigger.OnActivate -= Activate; + Trigger.OnDeactivate -= Deactivate; + } + } +} diff --git a/osu.Game/Screens/Play/KeyCounterAction.cs b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs similarity index 70% rename from osu.Game/Screens/Play/KeyCounterAction.cs rename to osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs index 900d9bcd0e..e5951a8bf4 100644 --- a/osu.Game/Screens/Play/KeyCounterAction.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs @@ -1,18 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterAction : KeyCounter + public partial class KeyCounterActionTrigger : InputTrigger where T : struct { public T Action { get; } - public KeyCounterAction(T action) + public KeyCounterActionTrigger(T action) : base($"B{(int)(object)action + 1}") { Action = action; @@ -23,9 +21,7 @@ namespace osu.Game.Screens.Play if (!EqualityComparer.Default.Equals(action, Action)) return false; - IsLit = true; - if (forwards) - Increment(); + Activate(forwards); return false; } @@ -34,9 +30,7 @@ namespace osu.Game.Screens.Play if (!EqualityComparer.Default.Equals(action, Action)) return; - IsLit = false; - if (!forwards) - Decrement(); + Deactivate(forwards); } } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs new file mode 100644 index 0000000000..05427d3a32 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// A flowing display of all gameplay keys. Individual keys can be added using implementations. + /// + public abstract partial class KeyCounterDisplay : CompositeDrawable + { + /// + /// Whether the key counter should be visible regardless of the configuration value. + /// This is true by default, but can be changed. + /// + public Bindable AlwaysVisible { get; } = new Bindable(true); + + /// + /// The s contained in this . + /// + public IEnumerable Counters => KeyFlow; + + protected abstract FillFlowContainer KeyFlow { get; } + + /// + /// Whether the actions reported by all s within this should be counted. + /// + public Bindable IsCounting { get; } = new BindableBool(true); + + protected readonly Bindable ConfigVisibility = new Bindable(); + + protected abstract void UpdateVisibility(); + + private Receptor? receptor; + + public void SetReceptor(Receptor receptor) + { + if (this.receptor != null) + throw new InvalidOperationException("Cannot set a new receptor when one is already active"); + + this.receptor = receptor; + } + + /// + /// Add a to this display. + /// + public void Add(InputTrigger trigger) + { + var keyCounter = CreateCounter(trigger); + + KeyFlow.Add(keyCounter); + + IsCounting.BindTo(keyCounter.IsCounting); + } + + /// + /// Add a range of to this display. + /// + public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); + + protected abstract KeyCounter CreateCounter(InputTrigger trigger); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AlwaysVisible.BindValueChanged(_ => UpdateVisibility()); + ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true); + } + + public override bool HandleNonPositionalInput => receptor == null; + + public override bool HandlePositionalInput => receptor == null; + + public partial class Receptor : Drawable + { + protected readonly KeyCounterDisplay Target; + + public Receptor(KeyCounterDisplay target) + { + RelativeSizeAxes = Axes.Both; + Depth = float.MinValue; + Target = target; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override bool Handle(UIEvent e) + { + switch (e) + { + case KeyDownEvent: + case KeyUpEvent: + case MouseDownEvent: + case MouseUpEvent: + return Target.InternalChildren.Any(c => c.TriggerEvent(e)); + } + + return base.Handle(e); + } + } + } +} diff --git a/osu.Game/Screens/Play/KeyCounterKeyboard.cs b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs similarity index 70% rename from osu.Game/Screens/Play/KeyCounterKeyboard.cs rename to osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs index c5c8b7eeae..3052c1e666 100644 --- a/osu.Game/Screens/Play/KeyCounterKeyboard.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs @@ -1,18 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; using osuTK.Input; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterKeyboard : KeyCounter + public partial class KeyCounterKeyboardTrigger : InputTrigger { public Key Key { get; } - public KeyCounterKeyboard(Key key) + public KeyCounterKeyboardTrigger(Key key) : base(key.ToString()) { Key = key; @@ -22,8 +20,7 @@ namespace osu.Game.Screens.Play { if (e.Key == Key) { - IsLit = true; - Increment(); + Activate(); } return base.OnKeyDown(e); @@ -31,7 +28,9 @@ namespace osu.Game.Screens.Play protected override void OnKeyUp(KeyUpEvent e) { - if (e.Key == Key) IsLit = false; + if (e.Key == Key) + Deactivate(); + base.OnKeyUp(e); } } diff --git a/osu.Game/Screens/Play/KeyCounterMouse.cs b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs similarity index 79% rename from osu.Game/Screens/Play/KeyCounterMouse.cs rename to osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs index cf9c7c029f..369aaa9f74 100644 --- a/osu.Game/Screens/Play/KeyCounterMouse.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs @@ -1,19 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; -using osuTK.Input; using osuTK; +using osuTK.Input; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public partial class KeyCounterMouse : KeyCounter + public partial class KeyCounterMouseTrigger : InputTrigger { public MouseButton Button { get; } - public KeyCounterMouse(MouseButton button) + public KeyCounterMouseTrigger(MouseButton button) : base(getStringRepresentation(button)) { Button = button; @@ -39,17 +37,16 @@ namespace osu.Game.Screens.Play protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button == Button) - { - IsLit = true; - Increment(); - } + Activate(); return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) { - if (e.Button == Button) IsLit = false; + if (e.Button == Button) + Deactivate(); + base.OnMouseUp(e); } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index c8d06b82e8..9f050a07bd 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -331,7 +331,7 @@ namespace osu.Game.Screens.Play ShowHealth = { BindTarget = ShowHealthBar } }; - protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay + protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs deleted file mode 100644 index bb50d4a539..0000000000 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play -{ - public partial class KeyCounterDisplay : Container - { - private const int duration = 100; - private const double key_fade_time = 80; - - private readonly Bindable configVisibility = new Bindable(); - - protected readonly FillFlowContainer KeyFlow; - - protected override Container Content => KeyFlow; - - /// - /// Whether the key counter should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - - public KeyCounterDisplay() - { - InternalChild = KeyFlow = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Alpha = 0, - }; - } - - protected override void Update() - { - base.Update(); - - // Don't use autosize as it will shrink to zero when KeyFlow is hidden. - // In turn this can cause the display to be masked off screen and never become visible again. - Size = KeyFlow.Size; - } - - public override void Add(KeyCounter key) - { - ArgumentNullException.ThrowIfNull(key); - - base.Add(key); - key.IsCounting = IsCounting; - key.FadeTime = key_fade_time; - key.KeyDownTextColor = KeyDownTextColor; - key.KeyUpTextColor = KeyUpTextColor; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.KeyOverlay, configVisibility); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AlwaysVisible.BindValueChanged(_ => updateVisibility()); - configVisibility.BindValueChanged(_ => updateVisibility(), true); - } - - private bool isCounting = true; - - public bool IsCounting - { - get => isCounting; - set - { - if (value == isCounting) return; - - isCounting = value; - foreach (var child in Children) - child.IsCounting = value; - } - } - - private Color4 keyDownTextColor = Color4.DarkGray; - - public Color4 KeyDownTextColor - { - get => keyDownTextColor; - set - { - if (value != keyDownTextColor) - { - keyDownTextColor = value; - foreach (var child in Children) - child.KeyDownTextColor = value; - } - } - } - - private Color4 keyUpTextColor = Color4.White; - - public Color4 KeyUpTextColor - { - get => keyUpTextColor; - set - { - if (value != keyUpTextColor) - { - keyUpTextColor = value; - foreach (var child in Children) - child.KeyUpTextColor = value; - } - } - } - - private void updateVisibility() => - // Isolate changing visibility of the key counters from fading this component. - KeyFlow.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); - - public override bool HandleNonPositionalInput => receptor == null; - public override bool HandlePositionalInput => receptor == null; - - private Receptor receptor; - - public void SetReceptor(Receptor receptor) - { - if (this.receptor != null) - throw new InvalidOperationException("Cannot set a new receptor when one is already active"); - - this.receptor = receptor; - } - - public partial class Receptor : Drawable - { - protected readonly KeyCounterDisplay Target; - - public Receptor(KeyCounterDisplay target) - { - RelativeSizeAxes = Axes.Both; - Depth = float.MinValue; - Target = target; - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - - protected override bool Handle(UIEvent e) - { - switch (e) - { - case KeyDownEvent: - case KeyUpEvent: - case MouseDownEvent: - case MouseUpEvent: - return Target.Children.Any(c => c.TriggerEvent(e)); - } - - return base.Handle(e); - } - } - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 45a671fb89..5174adfc06 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -358,14 +358,10 @@ namespace osu.Game.Screens.Play ScoreProcessor.RevertResult(r); }; - DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => - { - if (storyboardEnded.NewValue) - progressToResults(true); - }; + DimmableStoryboard.HasStoryboardEnded.ValueChanged += _ => checkScoreCompleted(); // Bind the judgement processors to ourselves - ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged); + ScoreProcessor.HasCompleted.BindValueChanged(_ => checkScoreCompleted()); HealthProcessor.Failed += onFail; // Provide judgement processors to mods after they're loaded so that they're on the gameplay clock, @@ -441,8 +437,11 @@ namespace osu.Game.Screens.Play }, KeyCounter = { + IsCounting = + { + Value = false + }, AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, - IsCounting = false }, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -482,7 +481,7 @@ namespace osu.Game.Screens.Play { updateGameplayState(); updatePauseOnFocusLostState(); - HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; + HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue; } private void updateGameplayState() @@ -706,19 +705,20 @@ namespace osu.Game.Screens.Play /// /// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime. /// - /// Thrown if this method is called more than once without changing state. - private void scoreCompletionChanged(ValueChangedEvent completed) + private void checkScoreCompleted() { // If this player instance is in the middle of an exit, don't attempt any kind of state update. if (!this.IsCurrentScreen()) return; - // Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled. - // TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar. - // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run). - // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done, - // but it still doesn't feel right that this exists here. - if (!completed.NewValue) + // Handle cases of arriving at this method when not in a completed state. + // - When a storyboard completion triggered this call earlier than gameplay finishes. + // - When a replay has been rewound before a queued resultsDisplayDelegate has run. + // + // Currently, even if this scenario is hit, prepareAndImportScoreAsync has already been queued (and potentially run). + // In the scenarios above, this is a non-issue, but it still feels a bit convoluted to have to cancel in this method. + // Maybe this can be improved with further refactoring. + if (!ScoreProcessor.HasCompleted.Value) { resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; @@ -742,12 +742,12 @@ namespace osu.Game.Screens.Play if (!Configuration.ShowResults) return; - bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; + bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; - if (storyboardHasOutro) + // If the current beatmap has a storyboard, this method will be called again on storyboard completion. + // Alternatively, the user may press the outro skip button, forcing immediate display of the results screen. + if (storyboardStillRunning) { - // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending - // or the user pressing the skip outro button. skipOutroOverlay.Show(); return; } @@ -793,6 +793,8 @@ namespace osu.Game.Screens.Play // This player instance may already be in the process of exiting. return; + Debug.Assert(ScoreProcessor.Rank.Value != ScoreRank.F); + this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely())); }, Time.Current + delay, 50); @@ -1112,7 +1114,7 @@ namespace osu.Game.Screens.Play GameplayState.HasQuit = true; // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. - if (prepareScoreForDisplayTask == null) + if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4f7e4add32..30ae5ee5aa 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -67,6 +67,8 @@ namespace osu.Game.Screens.Play private OsuScrollContainer settingsScroll = null!; + private Bindable showStoryboards = null!; + private bool backgroundBrightnessReduction; private readonly BindableDouble volumeAdjustment = new BindableDouble(1); @@ -149,10 +151,11 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(SessionStatics sessionStatics, AudioManager audio) + private void load(SessionStatics sessionStatics, AudioManager audio, OsuConfigManager config) { muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); + showStoryboards = config.GetBindable(OsuSetting.ShowStoryboard); const float padding = 25; @@ -463,7 +466,10 @@ namespace osu.Game.Screens.Play // only show if the warning was created (i.e. the beatmap needs it) // and this is not a restart of the map (the warning expires after first load). - if (epilepsyWarning?.IsAlive == true) + // + // note the late check of storyboard enable as the user may have just changed it + // from the settings on the loader screen. + if (epilepsyWarning?.IsAlive == true && showStoryboards.Value) { const double epilepsy_display_length = 3000; @@ -483,6 +489,7 @@ namespace osu.Game.Screens.Play { // This goes hand-in-hand with the restoration of low pass filter in contentOut(). this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); + epilepsyWarning?.Expire(); } pushSequence.Schedule(() => diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 68d3247275..6ba9843f7b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select public float BleedBottom { get; set; } /// - /// Triggered when the loaded change and are completely loaded. + /// Triggered when finish loading, or are subsequently changed. /// public Action? BeatmapSetsChanged; @@ -353,6 +353,8 @@ namespace osu.Game.Screens.Select if (!Scroll.UserScrolling) ScrollToSelected(true); + + BeatmapSetsChanged?.Invoke(); }); public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c5e914b461..4d6a5398c5 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -861,11 +862,9 @@ namespace osu.Game.Screens.Select private void updateVisibleBeatmapCount() { - FilterControl.InformationalText = Carousel.CountDisplayed == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{Carousel.CountDisplayed:#,0} matching beatmap" - : $"{Carousel.CountDisplayed:#,0} matching beatmaps"; + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + FilterControl.InformationalText = $"{"match".ToQuantity(Carousel.CountDisplayed, "#,0")}"; } private bool boundLocalBindables; diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 0158c47ea3..76c2c4d7ec 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -70,20 +69,6 @@ namespace osu.Game.Skinning updateSample(); } - protected override void LoadComplete() - { - base.LoadComplete(); - - CurrentSkin.SourceChanged += skinChangedImmediate; - } - - private void skinChangedImmediate() - { - // Clean up the previous sample immediately on a source change. - // This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled). - clearPreviousSamples(); - } - protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); @@ -109,6 +94,8 @@ namespace osu.Game.Skinning private void updateSample() { + clearPreviousSamples(); + if (sampleInfo == null) return; @@ -129,6 +116,8 @@ namespace osu.Game.Skinning /// public void Play() { + FlushPendingSkinChanges(); + if (Sample == null) return; @@ -172,14 +161,6 @@ namespace osu.Game.Skinning } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (CurrentSkin.IsNotNull()) - CurrentSkin.SourceChanged -= skinChangedImmediate; - } - #region Re-expose AudioContainer public BindableNumber Volume => sampleContainer.Volume; diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index cef1db4bc0..c7b33dc539 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Pooling; +using osu.Framework.Threading; namespace osu.Game.Skinning { @@ -14,6 +15,8 @@ namespace osu.Game.Skinning /// public abstract partial class SkinReloadableDrawable : PoolableDrawable { + private ScheduledDelegate? pendingSkinChange; + /// /// Invoked when has changed. /// @@ -31,21 +34,30 @@ namespace osu.Game.Skinning CurrentSkin.SourceChanged += onChange; } - private void onChange() => - // schedule required to avoid calls after disposed. - // note that this has the side-effect of components only performing a skin change when they are alive. - Scheduler.AddOnce(skinChanged); - protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); skinChanged(); } - private void skinChanged() + /// + /// Force any pending calls to be performed immediately. + /// + /// + /// When a skin change occurs, the handling provided by this class is scheduled. + /// In some cases, such a sample playback, this can result in the sample being played + /// just before it is updated to a potentially different sample. + /// + /// Calling this method will ensure any pending update operations are run immediately. + /// It is recommended to call this before consuming the result of skin changes for anything non-drawable. + /// + protected void FlushPendingSkinChanges() { - SkinChanged(CurrentSkin); - OnSkinChanged?.Invoke(); + if (pendingSkinChange == null) + return; + + pendingSkinChange.RunTask(); + pendingSkinChange = null; } /// @@ -56,6 +68,22 @@ namespace osu.Game.Skinning { } + private void onChange() + { + // schedule required to avoid calls after disposed. + // note that this has the side-effect of components only performing a skin change when they are alive. + pendingSkinChange?.Cancel(); + pendingSkinChange = Scheduler.Add(skinChanged); + } + + private void skinChanged() + { + SkinChanged(CurrentSkin); + OnSkinChanged?.Invoke(); + + pendingSkinChange = null; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 475b79053a..59b3799e0a 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -115,6 +115,8 @@ namespace osu.Game.Skinning /// public virtual void Play() { + FlushPendingSkinChanges(); + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index e598c79b08..be77c9a98e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables Loop = animation.LoopType == AnimationLoopType.LoopForever; LifetimeStart = animation.StartTime; - LifetimeEnd = animation.EndTime; + LifetimeEnd = animation.EndTimeForDisplay; } [Resolved] diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index f9b09ed57c..400d33481c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -82,7 +82,7 @@ namespace osu.Game.Storyboards.Drawables Position = sprite.InitialPosition; LifetimeStart = sprite.StartTime; - LifetimeEnd = sprite.EndTime; + LifetimeEnd = sprite.EndTimeForDisplay; } [Resolved] diff --git a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs index c8daeb3b3d..9eed139ad4 100644 --- a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs +++ b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs @@ -12,9 +12,17 @@ namespace osu.Game.Storyboards { /// /// The time at which the ends. + /// This is consumed to extend the length of a storyboard to ensure all visuals are played to completion. /// double EndTime { get; } + /// + /// The time this element displays until. + /// This is used for lifetime purposes, and includes long playing animations which don't necessarily extend + /// a storyboard's play time. + /// + double EndTimeForDisplay { get; } + /// /// The duration of the StoryboardElement. /// diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs index 16deac8e9e..1a4b6bb923 100644 --- a/osu.Game/Storyboards/StoryboardAnimation.cs +++ b/osu.Game/Storyboards/StoryboardAnimation.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 5b7b194be7..982185d51b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; using osuTK; @@ -84,6 +81,19 @@ namespace osu.Game.Storyboards } } + public double EndTimeForDisplay + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in loops) + latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); + + return latestEndTime; + } + } + public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); private delegate void DrawablePropertyInitializer(Drawable drawable, T value); @@ -114,7 +124,7 @@ namespace osu.Game.Storyboards public virtual Drawable CreateDrawable() => new DrawableStoryboardSprite(this); - public void ApplyTransforms(Drawable drawable, IEnumerable> triggeredGroups = null) + public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null) { // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. // To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list @@ -156,7 +166,7 @@ namespace osu.Game.Storyboards foreach (var command in commands) { - DrawablePropertyInitializer initFunc = null; + DrawablePropertyInitializer? initFunc = null; if (!initialized) { @@ -169,7 +179,7 @@ namespace osu.Game.Storyboards } } - private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable> triggeredGroups) + private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups) { var commands = TimelineGroup.GetCommands(timelineSelector); foreach (var loop in loops) @@ -198,11 +208,11 @@ namespace osu.Game.Storyboards { public double StartTime => command.StartTime; - private readonly DrawablePropertyInitializer initializeProperty; + private readonly DrawablePropertyInitializer? initializeProperty; private readonly DrawableTransformer transform; private readonly CommandTimeline.TypedCommand command; - public GeneratedCommand([NotNull] CommandTimeline.TypedCommand command, [CanBeNull] DrawablePropertyInitializer initializeProperty, [NotNull] DrawableTransformer transform) + public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty, DrawableTransformer transform) { this.command = command; this.initializeProperty = initializeProperty; diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs index cd9e9e1d52..78188d7cf7 100644 --- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -3,9 +3,12 @@ #nullable disable +using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -24,18 +27,27 @@ namespace osu.Game.Tests.Visual protected EditorBeatmap EditorBeatmap => (EditorBeatmap)Editor.Dependencies.Get(typeof(EditorBeatmap)); + [CanBeNull] + protected Func CreateInitialBeatmap { get; set; } + [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); - AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + if (CreateInitialBeatmap == null) + AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + else + { + AddStep("set test beatmap", () => Game.Beatmap.Value = CreateInitialBeatmap?.Invoke()); + } PushAndConfirm(() => new EditorLoader()); AddUntilStep("wait for editor load", () => Editor?.IsLoaded == true); - AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + if (CreateInitialBeatmap == null) + AddUntilStep("wait for metadata screen load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. @@ -50,6 +62,14 @@ namespace osu.Game.Tests.Visual protected void ReloadEditorToSameBeatmap() { + Guid beatmapSetGuid = Guid.Empty; + Guid beatmapGuid = Guid.Empty; + + AddStep("Store beatmap GUIDs", () => + { + beatmapSetGuid = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID; + beatmapGuid = EditorBeatmap.BeatmapInfo.ID; + }); AddStep("Exit", () => InputManager.Key(Key.Escape)); AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); @@ -59,7 +79,8 @@ namespace osu.Game.Tests.Visual PushAndConfirm(() => songSelect = new PlaySongSelect()); AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); - AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault); + AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid)); + AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid); AddStep("Open options", () => InputManager.Key(Key.F3)); AddStep("Enter editor", () => InputManager.Key(Key.Number5)); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3de022e88d..085f78b27b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,8 +36,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index eb7ba24336..127994c670 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ iossimulator-x64 - +