diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 082e0d247c..56b3ebe87b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read # to fetch code (actions/checkout) + jobs: inspect-code: name: Code Quality @@ -85,7 +88,7 @@ jobs: run: dotnet build -c Debug -warnaserror osu.Desktop.slnf - name: Test - run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" + run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0 shell: pwsh # Attempt to upload results even if test fails. diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml index 358cbda17a..bfc9620174 100644 --- a/.github/workflows/report-nunit.yml +++ b/.github/workflows/report-nunit.yml @@ -8,8 +8,12 @@ on: workflows: ["Continuous Integration"] types: - completed +permissions: {} jobs: annotate: + permissions: + checks: write # to create checks (dorny/test-reporter) + name: Annotate CI run with test results runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml index 442b97c473..cce3f23e5f 100644 --- a/.github/workflows/sentry-release.yml +++ b/.github/workflows/sentry-release.yml @@ -5,6 +5,9 @@ on: tags: - '*' +permissions: + contents: read # to fetch code (actions/checkout) + jobs: sentry_release: runs-on: ubuntu-latest diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index b8604169aa..936808f38b 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 4117452579..35e7742172 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 0b119c8680..c1044965b5 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 4117452579..35e7742172 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/osu.Android.props b/osu.Android.props index dd263d6aaa..3f4c8e2d24 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,11 +51,11 @@ - - + + - + diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d9ad95f96a..3ee1b3da30 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -137,12 +137,13 @@ namespace osu.Desktop { base.SetHost(host); - var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); - var desktopWindow = (SDL2DesktopWindow)host.Window; + var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); + if (iconStream != null) + desktopWindow.SetIconFromStream(iconStream); + desktopWindow.CursorState |= CursorState.Hidden; - desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); } diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 36ffd3b5b6..d62d422f33 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs index cbf6e8f202..cf6a8169c4 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs @@ -1,10 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -12,11 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneCatchTouchInput : OsuTestScene { - private CatchTouchInputMapper catchTouchInputMapper = null!; - - [SetUpSteps] - public void SetUpSteps() + [Test] + public void TestBasic() { + CatchTouchInputMapper catchTouchInputMapper = null!; + AddStep("create input overlay", () => { Child = new CatchInputManager(new CatchRuleset().RulesetInfo) @@ -32,12 +37,30 @@ namespace osu.Game.Rulesets.Catch.Tests } }; }); + + AddStep("show overlay", () => catchTouchInputMapper.Show()); } [Test] - public void TestBasic() + public void TestWithoutRelax() { - AddStep("show overlay", () => catchTouchInputMapper.Show()); + AddStep("create drawable ruleset without relax mod", () => + { + Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List()); + }); + AddUntilStep("wait for load", () => Child.IsLoaded); + AddAssert("check touch input is shown", () => this.ChildrenOfType().Any()); + } + + [Test] + public void TestWithRelax() + { + AddStep("create drawable ruleset with relax mod", () => + { + Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List { new CatchModRelax() }); + }); + AddUntilStep("wait for load", () => Child.IsLoaded); + AddAssert("check touch input is not shown", () => !this.ChildrenOfType().Any()); } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 7f513728af..f3161f32be 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -1,10 +1,10 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; @@ -12,6 +12,8 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -19,15 +21,28 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneComboCounter : CatchSkinnableTestScene { - private ScoreProcessor scoreProcessor; + private ScoreProcessor scoreProcessor = null!; private Color4 judgedObjectColour = Color4.White; + private readonly Bindable showHud = new Bindable(true); + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.CacheAs(new TestPlayer + { + ShowingOverlayComponents = { BindTarget = showHud }, + }); + } + [SetUp] public void SetUp() => Schedule(() => { scoreProcessor = new ScoreProcessor(new CatchRuleset()); + showHud.Value = true; + SetContents(_ => new CatchComboDisplay { Anchor = Anchor.Centre, @@ -51,9 +66,15 @@ namespace osu.Game.Rulesets.Catch.Tests 1f ); }); + + AddStep("set hud to never show", () => showHud.Value = false); + AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5); + + AddStep("set hud to show", () => showHud.Value = true); + AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5); } - private void performJudgement(HitResult type, Judgement judgement = null) + private void performJudgement(HitResult type, Judgement? judgement = null) { var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } }; diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index d45f8a9692..c9db824615 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs index 58f493b4b8..a0a11424d0 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -36,5 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit return base.CreateHitObjectBlueprintFor(hitObject); } + + protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs index 9038153e20..19b4a39f97 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs @@ -6,8 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.UI; @@ -17,15 +15,8 @@ namespace osu.Game.Rulesets.Catch.Mods { public override LocalisableString Description => "Where's the catcher?"; - [SettingSource( - "Hidden at combo", - "The combo count at which the catcher becomes completely hidden", - SettingControlType = typeof(SettingsSlider) - )] - public override BindableInt HiddenComboCount { get; } = new BindableInt + public override BindableInt HiddenComboCount { get; } = new BindableInt(10) { - Default = 10, - Value = 10, MinValue = 0, MaxValue = 50, }; diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index b2dd29841b..b4d29988d9 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index e9c289e46a..a5b7d8d0af 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -1,12 +1,13 @@ // 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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK.Graphics; @@ -19,14 +20,29 @@ namespace osu.Game.Rulesets.Catch.UI { private int currentCombo; - [CanBeNull] - public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter; + public ICatchComboCounter? ComboCounter => Drawable as ICatchComboCounter; + + private readonly IBindable showCombo = new BindableBool(true); public CatchComboDisplay() : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty()) { } + [Resolved(canBeNull: true)] + private Player? player { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (player != null) + { + showCombo.BindTo(player.ShowingOverlayComponents); + showCombo.BindValueChanged(s => this.FadeTo(s.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); + } + } + protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index ef2936ac94..27f7886d79 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -4,6 +4,7 @@ #nullable disable using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; @@ -36,7 +37,9 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load() { - KeyBindingInputManager.Add(new CatchTouchInputMapper()); + // With relax mod, input maps directly to x position and left/right buttons are not used. + if (!Mods.Any(m => m is ModRelax)) + KeyBindingInputManager.Add(new CatchTouchInputMapper()); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 6e6e83f9cf..0e4f612999 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -30,15 +31,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Cached(typeof(IScrollingInfo))] private IScrollingInfo scrollingInfo; + [Cached] + private readonly StageDefinition stage = new StageDefinition(5); + protected ManiaPlacementBlueprintTestScene() { scrollingInfo = ((ScrollingTestContainer)HitObjectContainer).ScrollingInfo; - Add(column = new Column(0) + Add(column = new Column(0, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AccentColour = Color4.OrangeRed, + AccentColour = { Value = Color4.OrangeRed }, Clock = new FramedClock(new StopwatchClock()), // No scroll }); } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 679a15e8cb..4cadcf138b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor protected ManiaSelectionBlueprintTestScene(int columns) { - var stageDefinitions = new List { new StageDefinition { Columns = columns } }; + var stageDefinitions = new List { new StageDefinition(columns) }; base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up) { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index ec96205067..ef140995ec 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo(); [Cached(typeof(EditorBeatmap))] - private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()) + private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition(2)) { BeatmapInfo = { @@ -56,8 +56,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { Playfield = new ManiaPlayfield(new List { - new StageDefinition { Columns = 4 }, - new StageDefinition { Columns = 3 } + new StageDefinition(4), + new StageDefinition(3) }) { Clock = new FramedClock(new StopwatchClock()) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index e96a186ae4..e082b90d3b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { AddStep("setup compose screen", () => { - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 }) + var beatmap = new ManiaBeatmap(new StageDefinition(4)) { BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, }; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index fcc9e2e6c3..a3985be936 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { InternalChildren = new Drawable[] { - EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) + EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition(4)) { BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo } }), diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs deleted file mode 100644 index e53deb5269..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs +++ /dev/null @@ -1,52 +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.Collections.Generic; -using osu.Game.Rulesets.Mania.Beatmaps; -using NUnit.Framework; - -namespace osu.Game.Rulesets.Mania.Tests -{ - [TestFixture] - public class ManiaColumnTypeTest - { - [TestCase(new[] - { - ColumnType.Special - }, 1)] - [TestCase(new[] - { - ColumnType.Odd, - ColumnType.Even, - ColumnType.Even, - ColumnType.Odd - }, 4)] - [TestCase(new[] - { - ColumnType.Odd, - ColumnType.Even, - ColumnType.Odd, - ColumnType.Special, - ColumnType.Odd, - ColumnType.Even, - ColumnType.Odd - }, 7)] - public void Test(IEnumerable expected, int columns) - { - var definition = new StageDefinition - { - Columns = columns - }; - var results = getResults(definition); - Assert.AreEqual(expected, results); - } - - private IEnumerable getResults(StageDefinition definition) - { - for (int i = 0; i < definition.Columns; i++) - yield return definition.GetTypeOfColumn(i); - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs index eb380c07a6..e456659ac4 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs @@ -3,9 +3,11 @@ #nullable disable +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests @@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests { } - protected override void ReloadMappings() + protected override void ReloadMappings(IQueryable realmKeyBindings) { KeyBindings = DefaultKeyBindings; } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs index b64006316e..7d1a934456 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(ManiaAction.Key8)] public void TestEncodeDecodeSingleStage(params ManiaAction[] actions) { - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 }); + var beatmap = new ManiaBeatmap(new StageDefinition(9)); var frame = new ManiaReplayFrame(0, actions); var legacyFrame = frame.ToLegacy(beatmap); @@ -38,8 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(ManiaAction.Key8)] public void TestEncodeDecodeDualStage(params ManiaAction[] actions) { - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 }); - beatmap.Stages.Add(new StageDefinition { Columns = 5 }); + var beatmap = new ManiaBeatmap(new StageDefinition(5)); + beatmap.Stages.Add(new StageDefinition(5)); var frame = new ManiaReplayFrame(0, actions); var legacyFrame = frame.ToLegacy(beatmap); diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs new file mode 100644 index 0000000000..3bd654e75e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs @@ -0,0 +1,49 @@ +// 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; +using osu.Game.Rulesets.Mania.Beatmaps; +using NUnit.Framework; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaSpecialColumnTest + { + [TestCase(new[] + { + true + }, 1)] + [TestCase(new[] + { + false, + false, + false, + false + }, 4)] + [TestCase(new[] + { + false, + false, + false, + true, + false, + false, + false + }, 7)] + public void Test(IEnumerable special, int columns) + { + var definition = new StageDefinition(columns); + var results = getResults(definition); + Assert.AreEqual(special, results); + } + + private IEnumerable getResults(StageDefinition definition) + { + for (int i = 0; i < definition.Columns; i++) + yield return definition.IsSpecialColumn(i); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs index 7970d5b594..d27a79c41d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods private static ManiaBeatmap createRawBeatmap() { - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)); beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 }); // Set BPM to 60 // Add test hit objects diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index 1a3513d46c..d3e90170b2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests.Skinning { @@ -24,15 +23,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private readonly Column column; + [Cached] + private readonly StageDefinition stageDefinition = new StageDefinition(5); + public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false) { InternalChildren = new[] { - this.column = new Column(column) + this.column = new Column(column, false) { Action = { Value = action }, - AccentColour = Color4.Orange, - ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd, Alpha = showColumn ? 1 : 0 }, content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index fd82041ad8..75175c43d8 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -61,7 +61,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning c.Add(CreateHitObject().With(h => { h.HitObject.StartTime = Time.Current + 5000; - h.AccentColour.Value = Color4.Orange; })); }) }, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 9f235689b4..2c5535a65f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling.Algorithms; using osu.Game.Tests.Visual; @@ -24,6 +25,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached(Type = typeof(IScrollingInfo))] private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); + [Cached] + private readonly StageDefinition stage = new StageDefinition(4); + protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset(); protected ManiaSkinnableTestScene() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs index ff557638a9..1bfe55b074 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { var stageDefinitions = new List { - new StageDefinition { Columns = 4 }, + new StageDefinition(4), }; SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s => diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs deleted file mode 100644 index bbbd7edb7b..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs +++ /dev/null @@ -1,51 +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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.UI.Components; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Mania.Tests.Skinning -{ - public class TestSceneKeyArea : ManiaSkinnableTestScene - { - [BackgroundDependencyLoader] - private void load() - { - SetContents(_ => new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f), - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new ColumnTestContainer(0, ManiaAction.Key1) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) - { - RelativeSizeAxes = Axes.Both - }, - }, - new ColumnTestContainer(1, ManiaAction.Key2) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) - { - RelativeSizeAxes = Axes.Both - }, - }, - } - }); - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs index 62dadbc3dd..9817719c94 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { stageDefinitions = new List { - new StageDefinition { Columns = 2 } + new StageDefinition(2) }; SetContents(_ => new ManiaPlayfield(stageDefinitions)); @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { stageDefinitions = new List { - new StageDefinition { Columns = 2 }, - new StageDefinition { Columns = 2 } + new StageDefinition(2), + new StageDefinition(2) }; SetContents(_ => new ManiaPlayfield(stageDefinitions)); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs index f3f1b9416f..07aa0b845f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) { - Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction) + Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction) }; }); } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 687b3a747d..0744d7e2e7 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Skinning; @@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }), + SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs index 6cbc172755..979c90c802 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Tests.Skinning @@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null) + SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs index 21ec85bbe6..3abeb8a5f6 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | // | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | // | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | - | // | | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); + var beatmap = new ManiaBeatmap(new StageDefinition(2)); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 }); @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | * | // | | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); + var beatmap = new ManiaBeatmap(new StageDefinition(2)); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 }); @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | | // | | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); + var beatmap = new ManiaBeatmap(new StageDefinition(2)); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 }); @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | | // | | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); + var beatmap = new ManiaBeatmap(new StageDefinition(2)); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 }); @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | | // | | | - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); + var beatmap = new ManiaBeatmap(new StageDefinition(2)); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 }); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index 2922d18713..83491b6fe9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.Tests [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); + [Cached] + private readonly StageDefinition stage = new StageDefinition(1); + private readonly List columns = new List(); public TestSceneColumn() @@ -84,12 +88,12 @@ namespace osu.Game.Rulesets.Mania.Tests private Drawable createColumn(ScrollingDirection direction, ManiaAction action, int index) { - var column = new Column(index) + var column = new Column(index, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Height = 0.85f, - AccentColour = Color4.OrangeRed, + AccentColour = { Value = Color4.OrangeRed }, Action = { Value = action }, }; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs index 223f8dae44..d273f5cb35 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs @@ -4,11 +4,13 @@ #nullable disable using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -24,6 +26,9 @@ namespace osu.Game.Rulesets.Mania.Tests private Column column; + [Cached] + private readonly StageDefinition stage = new StageDefinition(1); + [SetUp] public void SetUp() => Schedule(() => { @@ -35,11 +40,11 @@ namespace osu.Game.Rulesets.Mania.Tests RelativeSizeAxes = Axes.Y, TimeRange = 2000, Clock = new FramedClock(clock), - Child = column = new Column(0) + Child = column = new Column(0, false) { Action = { Value = ManiaAction.Key1 }, Height = 0.85f, - AccentColour = Color4.Gray + AccentColour = { Value = Color4.Gray }, }, }; }); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index a563dc3106..1f139b5b78 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests { AddStep("load player", () => { - Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) + Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition(4)) { HitObjects = hitObjects, BeatmapInfo = diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs index cf8947c1ed..6387dac957 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Tests { var specialAction = ManiaAction.Special1; - var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction); + var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction); stages.Add(stage); return new ScrollingTestContainer(direction) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs index e84d02775a..9f2e3d2502 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Tests { const double beat_length = 500; - var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }) + var beatmap = new ManiaBeatmap(new StageDefinition(1)) { HitObjects = { diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 2951076591..0d7b03d830 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs deleted file mode 100644 index 0114987e3c..0000000000 --- a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs +++ /dev/null @@ -1,14 +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 - -namespace osu.Game.Rulesets.Mania.Beatmaps -{ - public enum ColumnType - { - Even, - Odd, - Special - } -} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 4879ce6748..b5655a4579 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -60,5 +61,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps }, }; } + + public StageDefinition GetStageForColumnIndex(int column) + { + foreach (var stage in Stages) + { + if (column < stage.Columns) + return stage; + + column -= stage.Columns; + } + + throw new ArgumentOutOfRangeException(nameof(column), "Provided index exceeds all available stages"); + } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 90cd7f57b5..632b7cdcc7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -93,10 +93,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap CreateBeatmap() { - beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns); + beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns); if (Dual) - beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns }); + beatmap.Stages.Add(new StageDefinition(TargetColumns)); return beatmap; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index 54e2d4686f..898b558eb3 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -11,32 +11,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// /// Defines properties for each stage in a . /// - public struct StageDefinition + public class StageDefinition { /// /// The number of s which this stage contains. /// - public int Columns; + public readonly int Columns; + + public StageDefinition(int columns) + { + if (columns < 1) + throw new ArgumentException("Column count must be above zero.", nameof(columns)); + + Columns = columns; + } /// /// Whether the column index is a special column for this stage. /// /// The 0-based column index. /// Whether the column is a special column. - public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; - - /// - /// Get the type of column given a column index. - /// - /// The 0-based column index. - /// The type of the column. - public readonly ColumnType GetTypeOfColumn(int column) - { - if (IsSpecialColumn(column)) - return ColumnType.Special; - - int distanceToEdge = Math.Min(column, (Columns - 1) - column); - return distanceToEdge % 2 == 0 ? ColumnType.Odd : ColumnType.Even; - } + public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index a925e7c0ac..440dec82af 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - scoreAccuracy = customAccuracy; + scoreAccuracy = calculateCustomAccuracy(); // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. @@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty /// /// Accuracy used to weight judgements independently from the score's actual accuracy. /// - private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320); + private double calculateCustomAccuracy() + { + if (totalHits == 0) + return 0; + + return (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320); + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index ad75afff8e..f438d6497c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -33,5 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit } protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler(); + + protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 061dedb07a..6162184c9a 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mania.Edit.Setup; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mania.Skinning.Argon; +using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Mania.Skinning.Legacy; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -66,6 +68,15 @@ namespace osu.Game.Rulesets.Mania { switch (skin) { + case TrianglesSkin: + return new ManiaTrianglesSkinTransformer(skin, beatmap); + + case ArgonSkin: + return new ManiaArgonSkinTransformer(skin, beatmap); + + case DefaultLegacySkin: + return new ManiaClassicSkinTransformer(skin, beatmap); + case LegacySkin: return new ManiaLegacySkinTransformer(skin, beatmap); } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 21b362df00..f05edb4677 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -3,29 +3,19 @@ #nullable disable -using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania { public class ManiaSkinComponent : GameplaySkinComponent { - /// - /// The intended for this component. - /// May be null if the component is not a direct member of a . - /// - public readonly StageDefinition? StageDefinition; - /// /// Creates a new . /// /// The component. - /// The intended for this component. May be null if the component is not a direct member of a . - public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null) + public ManiaSkinComponent(ManiaSkinComponents component) : base(component) { - StageDefinition = stageDefinition; } protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 6020348938..a607ed572d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override void UpdateInitialTransforms() - { - } - protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 19792086a7..48647f9f5f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -4,12 +4,14 @@ #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.Bindings; using osu.Framework.Input.Events; +using osu.Game.Audio; using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Container tailContainer; private Container tickContainer; + private PausableSkinnableSound slidingSample; + /// /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. /// @@ -108,6 +112,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + slidingSample = new PausableSkinnableSound { Looping = true } }); maskedContents.AddRange(new[] @@ -118,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + isHitting.BindValueChanged(updateSlidingSample, true); + } + protected override void OnApply() { base.OnApply(); @@ -322,5 +334,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables HoldStartTime = null; isHitting.Value = false; } + + protected override void LoadSamples() + { + // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. + + if (HitObject.SampleControlPoint == null) + { + throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); + } + + slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + } + + public override void StopAllSamples() + { + base.StopAllSamples(); + slidingSample?.Stop(); + } + + private void updateSlidingSample(ValueChangedEvent tracking) + { + if (tracking.NewValue) + slidingSample?.Play(); + else + slidingSample?.Stop(); + } + + protected override void OnFree() + { + slidingSample.Samples = null; + base.OnFree(); + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index d374e935ec..ac646ea427 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -30,20 +30,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public bool UpdateResult() => base.UpdateResult(true); - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - // This hitobject should never expire, so this is just a safe maximum. - LifetimeEnd = LifetimeStart + 30000; - } - protected override void UpdateHitStateTransforms(ArmedState state) { // suppress the base call explicitly. // the hold note head should never change its visual state on its own due to the "freezing" mechanic // (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line). // it will be hidden along with its parenting hold note when required. + + // Set `LifetimeEnd` explicitly to a non-`double.MaxValue` because otherwise this DHO is automatically expired. + LifetimeEnd = double.PositiveInfinity; } public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index a7bdcd047e..3084f71be2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience; + public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience; protected override void CheckForResult(bool userTriggered, double timeOffset) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index bcc10ab7bc..73dc937a00 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); - // Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms. - // Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1. - protected override double InitialLifetimeOffset => 30000; - [Resolved(canBeNull: true)] private ManiaPlayfield playfield { get; set; } @@ -69,22 +65,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Direction.BindValueChanged(OnDirectionChanged, true); } - protected override void OnApply() - { - base.OnApply(); - - if (ParentHitObject != null) - AccentColour.BindTo(ParentHitObject.AccentColour); - } - - protected override void OnFree() - { - base.OnFree(); - - if (ParentHitObject != null) - AccentColour.UnbindFrom(ParentHitObject.AccentColour); - } - protected virtual void OnDirectionChanged(ValueChangedEvent e) { Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs similarity index 50% rename from osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs rename to osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs index 5bd2d3ab48..598a765d3c 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.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.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -12,26 +10,38 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.UI.Components +namespace osu.Game.Rulesets.Mania.Skinning.Argon { - public class ColumnBackground : CompositeDrawable, IKeyBindingHandler, IHasAccentColour + public class ArgonColumnBackground : CompositeDrawable, IKeyBindingHandler { - private readonly IBindable action = new Bindable(); - - private Box background; - private Box backgroundOverlay; - private readonly IBindable direction = new Bindable(); - [BackgroundDependencyLoader] - private void load(IBindable action, IScrollingInfo scrollingInfo) - { - this.action.BindTo(action); + private Color4 brightColour; + private Color4 dimColour; + private Box background = null!; + private Box backgroundOverlay = null!; + + [Resolved] + private Column column { get; set; } = null!; + + private Bindable accentColour = null!; + + public ArgonColumnBackground() + { + RelativeSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = ArgonNotePiece.CORNER_RADIUS; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { InternalChildren = new[] { background = new Box @@ -49,61 +59,42 @@ namespace osu.Game.Rulesets.Mania.UI.Components } }; - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(dir => + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => { - backgroundOverlay.Anchor = backgroundOverlay.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; - updateColours(); + background.Colour = colour.NewValue.Darken(3).Opacity(0.8f); + brightColour = colour.NewValue.Opacity(0.6f); + dimColour = colour.NewValue.Opacity(0); }, true); + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); } - protected override void LoadComplete() + private void onDirectionChanged(ValueChangedEvent direction) { - base.LoadComplete(); - updateColours(); - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set + if (direction.NewValue == ScrollingDirection.Up) { - if (accentColour == value) - return; - - accentColour = value; - - updateColours(); + backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.TopLeft; + backgroundOverlay.Colour = ColourInfo.GradientVertical(brightColour, dimColour); + } + else + { + backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.BottomLeft; + backgroundOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour); } - } - - private void updateColours() - { - if (!IsLoaded) - return; - - background.Colour = AccentColour.Darken(5); - - var brightPoint = AccentColour.Opacity(0.6f); - var dimPoint = AccentColour.Opacity(0); - - backgroundOverlay.Colour = ColourInfo.GradientVertical( - direction.Value == ScrollingDirection.Up ? brightPoint : dimPoint, - direction.Value == ScrollingDirection.Up ? dimPoint : brightPoint); } public bool OnPressed(KeyBindingPressEvent e) { - if (e.Action == action.Value) + if (e.Action == column.Action.Value) backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); return false; } public void OnReleased(KeyBindingReleaseEvent e) { - if (e.Action == action.Value) + if (e.Action == column.Action.Value) backgroundOverlay.FadeTo(0, 250, Easing.OutQuint); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs new file mode 100644 index 0000000000..af179d5580 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs @@ -0,0 +1,97 @@ +// 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.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public class ArgonHitExplosion : CompositeDrawable, IHitExplosion + { + public override bool RemoveWhenNotAlive => true; + + [Resolved] + private Column column { get; set; } = null!; + + private readonly IBindable direction = new Bindable(); + + private Container largeFaint = null!; + + private Bindable accentColour = null!; + + public ArgonHitExplosion() + { + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.X; + Height = ArgonNotePiece.NOTE_HEIGHT; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + InternalChildren = new Drawable[] + { + largeFaint = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = ArgonNotePiece.CORNER_RADIUS, + Blending = BlendingParameters.Additive, + Child = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + }, + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => + { + largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1); + + largeFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour.NewValue, + Roundness = 40, + Radius = 60, + }; + }, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + Anchor = Anchor.TopCentre; + Y = ArgonNotePiece.NOTE_HEIGHT / 2; + } + else + { + Anchor = Anchor.BottomCentre; + Y = -ArgonNotePiece.NOTE_HEIGHT / 2; + } + } + + public void Animate(JudgementResult result) + { + this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs new file mode 100644 index 0000000000..9e449623d5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs @@ -0,0 +1,47 @@ +// 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.Shapes; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public class ArgonHitTarget : CompositeDrawable + { + private readonly IBindable direction = new Bindable(); + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + RelativeSizeAxes = Axes.X; + Height = ArgonNotePiece.NOTE_HEIGHT; + + Masking = true; + CornerRadius = ArgonNotePiece.CORNER_RADIUS; + + InternalChildren = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.3f, + Blending = BlendingParameters.Additive, + Colour = Color4.White + }, + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + Anchor = Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs new file mode 100644 index 0000000000..757190c4ae --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs @@ -0,0 +1,97 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Skinning.Default; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + /// + /// Represents length-wise portion of a hold note. + /// + public class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody + { + protected readonly Bindable AccentColour = new Bindable(); + protected readonly IBindable IsHitting = new Bindable(); + + private Drawable background = null!; + private Box foreground = null!; + + public ArgonHoldBodyPiece() + { + RelativeSizeAxes = Axes.Both; + + // Without this, the width of the body will be slightly larger than the head/tail. + Masking = true; + CornerRadius = ArgonNotePiece.CORNER_RADIUS; + Blending = BlendingParameters.Additive; + } + + [BackgroundDependencyLoader(true)] + private void load(DrawableHitObject? drawableObject) + { + InternalChildren = new[] + { + background = new Box { RelativeSizeAxes = Axes.Both }, + foreground = new Box + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + }; + + if (drawableObject != null) + { + var holdNote = (DrawableHoldNote)drawableObject; + + AccentColour.BindTo(holdNote.AccentColour); + IsHitting.BindTo(holdNote.IsHitting); + } + + AccentColour.BindValueChanged(colour => + { + background.Colour = colour.NewValue.Darken(1.2f); + foreground.Colour = colour.NewValue.Opacity(0.2f); + }, true); + + IsHitting.BindValueChanged(hitting => + { + const float animation_length = 50; + + foreground.ClearTransforms(); + + if (hitting.NewValue) + { + // wait for the next sync point + double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2); + + using (foreground.BeginDelayedSequence(synchronisedOffset)) + { + foreground.FadeTo(1, animation_length).Then() + .FadeTo(0.5f, animation_length) + .Loop(); + } + } + else + { + foreground.FadeOut(animation_length); + } + }); + } + + public void Recycle() + { + foreground.ClearTransforms(); + foreground.Alpha = 0; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs new file mode 100644 index 0000000000..e1068c6cd8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs @@ -0,0 +1,91 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + internal class ArgonHoldNoteTailPiece : CompositeDrawable + { + private readonly IBindable direction = new Bindable(); + private readonly IBindable accentColour = new Bindable(); + + private readonly Box colouredBox; + private readonly Box shadow; + + public ArgonHoldNoteTailPiece() + { + RelativeSizeAxes = Axes.X; + Height = ArgonNotePiece.NOTE_HEIGHT; + + CornerRadius = ArgonNotePiece.CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + shadow = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Height = 0.82f, + Masking = true, + CornerRadius = ArgonNotePiece.CORNER_RADIUS, + Children = new Drawable[] + { + colouredBox = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + new Circle + { + RelativeSizeAxes = Axes.X, + Height = ArgonNotePiece.CORNER_RADIUS * 2, + }, + }; + } + + [BackgroundDependencyLoader(true)] + private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + if (drawableObject != null) + { + accentColour.BindTo(drawableObject.AccentColour); + accentColour.BindValueChanged(onAccentChanged, true); + } + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up + ? Anchor.TopCentre + : Anchor.BottomCentre; + } + + private void onAccentChanged(ValueChangedEvent accent) + { + colouredBox.Colour = ColourInfo.GradientVertical( + accent.NewValue, + accent.NewValue.Darken(0.1f) + ); + + shadow.Colour = accent.NewValue.Darken(0.5f); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs new file mode 100644 index 0000000000..e7dfec256d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -0,0 +1,193 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement + { + protected readonly HitResult Result; + + protected SpriteText JudgementText { get; private set; } = null!; + + private RingExplosion? ringExplosion; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ArgonJudgementPiece(HitResult result) + { + Result = result; + Origin = Anchor.Centre; + Y = 160; + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + JudgementText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = Result.GetDescription().ToUpperInvariant(), + Colour = colours.ForHitResult(Result), + Blending = BlendingParameters.Additive, + Spacing = new Vector2(10, 0), + Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular), + }, + }; + + if (Result.IsHit()) + { + AddInternal(ringExplosion = new RingExplosion(Result) + { + Colour = colours.ForHitResult(Result), + }); + } + } + + /// + /// Plays the default animation for this judgement piece. + /// + /// + /// The base implementation only handles fade (for all result types) and misses. + /// Individual rulesets are recommended to implement their appropriate hit animations. + /// + public virtual void PlayAnimation() + { + switch (Result) + { + default: + JudgementText + .ScaleTo(Vector2.One) + .ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint); + break; + + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveTo(Vector2.Zero); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + break; + } + + this.FadeOutFromOne(800); + + ringExplosion?.PlayAnimation(); + } + + public Drawable? GetAboveHitObjectsProxiedContent() => null; + + private class RingExplosion : CompositeDrawable + { + private readonly float travel = 52; + + public RingExplosion(HitResult result) + { + const float thickness = 4; + + const float small_size = 9; + const float large_size = 14; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Blending = BlendingParameters.Additive; + + int countSmall = 0; + int countLarge = 0; + + switch (result) + { + case HitResult.Meh: + countSmall = 3; + travel *= 0.3f; + break; + + case HitResult.Ok: + case HitResult.Good: + countSmall = 4; + travel *= 0.6f; + break; + + case HitResult.Great: + case HitResult.Perfect: + countSmall = 4; + countLarge = 4; + break; + } + + for (int i = 0; i < countSmall; i++) + AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) }); + + for (int i = 0; i < countLarge; i++) + AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) }); + } + + public void PlayAnimation() + { + foreach (var c in InternalChildren) + { + const float start_position_ratio = 0.3f; + + float direction = RNG.NextSingle(0, 360); + float distance = RNG.NextSingle(travel / 2, travel); + + c.MoveTo(new Vector2( + MathF.Cos(direction) * distance * start_position_ratio, + MathF.Sin(direction) * distance * start_position_ratio + )); + + c.MoveTo(new Vector2( + MathF.Cos(direction) * distance, + MathF.Sin(direction) * distance + ), 600, Easing.OutQuint); + } + + this.FadeOutFromOne(1000, Easing.OutQuint); + } + + public class RingPiece : CircularContainer + { + public RingPiece(float thickness = 9) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Masking = true; + BorderThickness = thickness; + BorderColour = Color4.White; + + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + }; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs new file mode 100644 index 0000000000..7670c9bdf2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs @@ -0,0 +1,272 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public class ArgonKeyArea : CompositeDrawable, IKeyBindingHandler + { + private readonly IBindable direction = new Bindable(); + + private Container directionContainer = null!; + private Drawable background = null!; + + private Circle hitTargetLine = null!; + + private Container bottomIcon = null!; + private CircularContainer topIcon = null!; + + private Bindable accentColour = null!; + + [Resolved] + private Column column { get; set; } = null!; + + public ArgonKeyArea() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + const float icon_circle_size = 8; + const float icon_spacing = 7; + const float icon_vertical_offset = -30; + + InternalChild = directionContainer = new Container + { + RelativeSizeAxes = Axes.X, + // Ensure the area is tall enough to put the target line in the correct location. + // This is to also allow the main background component to overlap the target line + // and avoid an inner corner radius being shown below the target line. + Height = Stage.HIT_TARGET_POSITION + ArgonNotePiece.CORNER_RADIUS * 2, + Children = new[] + { + new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + CornerRadius = ArgonNotePiece.CORNER_RADIUS, + Child = background = new Box + { + Name = "Key gradient", + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + }, + hitTargetLine = new Circle + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Colour = OsuColour.Gray(196 / 255f), + Height = ArgonNotePiece.CORNER_RADIUS * 2, + Masking = true, + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow }, + }, + new Container + { + Name = "Icons", + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + bottomIcon = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Y = icon_vertical_offset, + Children = new[] + { + new Circle + { + Size = new Vector2(icon_circle_size), + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow }, + }, + new Circle + { + X = -icon_spacing, + Y = icon_spacing * 1.2f, + Size = new Vector2(icon_circle_size), + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow }, + }, + new Circle + { + X = icon_spacing, + Y = icon_spacing * 1.2f, + Size = new Vector2(icon_circle_size), + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow }, + }, + } + }, + topIcon = new CircularContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Y = -icon_vertical_offset, + Size = new Vector2(22, 14), + Masking = true, + BorderThickness = 4, + BorderColour = Color4.White, + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + }, + } + } + }, + } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => + { + background.Colour = colour.NewValue.Darken(0.2f); + bottomIcon.Colour = colour.NewValue; + }, + true); + + // Yes, proxy everything. + column.TopLevelContainer.Add(CreateProxy()); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + switch (direction.NewValue) + { + case ScrollingDirection.Up: + directionContainer.Scale = new Vector2(1, -1); + directionContainer.Anchor = Anchor.TopLeft; + directionContainer.Origin = Anchor.BottomLeft; + break; + + case ScrollingDirection.Down: + directionContainer.Scale = new Vector2(1, 1); + directionContainer.Anchor = Anchor.BottomLeft; + directionContainer.Origin = Anchor.BottomLeft; + break; + } + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action != column.Action.Value) return false; + + const double lighting_fade_in_duration = 70; + Color4 lightingColour = getLightingColour(); + + background + .FlashColour(accentColour.Value.Lighten(0.8f), 200, Easing.OutQuint) + .FadeTo(1, lighting_fade_in_duration, Easing.OutQuint) + .Then() + .FadeTo(0.8f, 500); + + hitTargetLine.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint); + hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour.Opacity(0.4f), + Radius = 20, + }, lighting_fade_in_duration, Easing.OutQuint); + + topIcon.ScaleTo(0.9f, lighting_fade_in_duration, Easing.OutQuint); + topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour.Opacity(0.1f), + Radius = 20, + }, lighting_fade_in_duration, Easing.OutQuint); + + bottomIcon.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint); + + foreach (var circle in bottomIcon) + { + circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour.Opacity(0.2f), + Radius = 60, + }, lighting_fade_in_duration, Easing.OutQuint); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + if (e.Action != column.Action.Value) return; + + const double lighting_fade_out_duration = 800; + + Color4 lightingColour = getLightingColour().Opacity(0); + + // background fades out faster than lighting elements to give better definition to the player. + background.FadeTo(0.3f, 50, Easing.OutQuint) + .Then() + .FadeOut(lighting_fade_out_duration, Easing.OutQuint); + + topIcon.ScaleTo(1f, 200, Easing.OutQuint); + topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour, + Radius = 20, + }, lighting_fade_out_duration, Easing.OutQuint); + + hitTargetLine.FadeColour(OsuColour.Gray(196 / 255f), lighting_fade_out_duration, Easing.OutQuint); + hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour, + Radius = 25, + }, lighting_fade_out_duration, Easing.OutQuint); + + bottomIcon.FadeColour(accentColour.Value, lighting_fade_out_duration, Easing.OutQuint); + + foreach (var circle in bottomIcon) + { + circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour, + Radius = 30, + }, lighting_fade_out_duration, Easing.OutQuint); + } + } + + private Color4 getLightingColour() => Interpolation.ValueAt(0.2f, accentColour.Value, Color4.White, 0, 1); + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs new file mode 100644 index 0000000000..454a6b012b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs @@ -0,0 +1,110 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + internal class ArgonNotePiece : CompositeDrawable + { + public const float NOTE_HEIGHT = 42; + + public const float CORNER_RADIUS = 3.4f; + + private readonly IBindable direction = new Bindable(); + private readonly IBindable accentColour = new Bindable(); + + private readonly Box colouredBox; + private readonly Box shadow; + + public ArgonNotePiece() + { + RelativeSizeAxes = Axes.X; + Height = NOTE_HEIGHT; + + CornerRadius = CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + shadow = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Height = 0.82f, + Masking = true, + CornerRadius = CORNER_RADIUS, + Children = new Drawable[] + { + colouredBox = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + new Circle + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = CORNER_RADIUS * 2, + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 4, + Icon = FontAwesome.Solid.AngleDown, + Size = new Vector2(20), + Scale = new Vector2(1, 0.7f) + } + }; + } + + [BackgroundDependencyLoader(true)] + private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + if (drawableObject != null) + { + accentColour.BindTo(drawableObject.AccentColour); + accentColour.BindValueChanged(onAccentChanged, true); + } + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up + ? Anchor.TopCentre + : Anchor.BottomCentre; + } + + private void onAccentChanged(ValueChangedEvent accent) + { + colouredBox.Colour = ColourInfo.GradientVertical( + accent.NewValue.Lighten(0.1f), + accent.NewValue + ); + + shadow.Colour = accent.NewValue.Darken(0.5f); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs new file mode 100644 index 0000000000..1881695b14 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs @@ -0,0 +1,16 @@ +// 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; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public class ArgonStageBackground : CompositeDrawable + { + public ArgonStageBackground() + { + RelativeSizeAxes = Axes.Both; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs new file mode 100644 index 0000000000..ae313e0b91 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -0,0 +1,141 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public class ManiaArgonSkinTransformer : SkinTransformer + { + private readonly ManiaBeatmap beatmap; + + public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap) + : base(skin) + { + this.beatmap = (ManiaBeatmap)beatmap; + } + + public override Drawable? GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case GameplaySkinComponent resultComponent: + return new ArgonJudgementPiece(resultComponent.Component); + + case ManiaSkinComponent maniaComponent: + // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. + switch (maniaComponent.Component) + { + case ManiaSkinComponents.StageBackground: + return new ArgonStageBackground(); + + case ManiaSkinComponents.ColumnBackground: + return new ArgonColumnBackground(); + + case ManiaSkinComponents.HoldNoteBody: + return new ArgonHoldBodyPiece(); + + case ManiaSkinComponents.HoldNoteTail: + return new ArgonHoldNoteTailPiece(); + + case ManiaSkinComponents.HoldNoteHead: + case ManiaSkinComponents.Note: + return new ArgonNotePiece(); + + case ManiaSkinComponents.HitTarget: + return new ArgonHitTarget(); + + case ManiaSkinComponents.KeyArea: + return new ArgonKeyArea(); + + case ManiaSkinComponents.HitExplosion: + return new ArgonHitExplosion(); + } + + break; + } + + return base.GetDrawableComponent(component); + } + + public override IBindable? GetConfig(TLookup lookup) + { + if (lookup is ManiaSkinConfigurationLookup maniaLookup) + { + int column = maniaLookup.ColumnIndex ?? 0; + var stage = beatmap.GetStageForColumnIndex(column); + + switch (maniaLookup.Lookup) + { + case LegacyManiaSkinConfigurationLookups.ColumnSpacing: + return SkinUtils.As(new Bindable(2)); + + case LegacyManiaSkinConfigurationLookups.StagePaddingBottom: + case LegacyManiaSkinConfigurationLookups.StagePaddingTop: + return SkinUtils.As(new Bindable(30)); + + case LegacyManiaSkinConfigurationLookups.ColumnWidth: + return SkinUtils.As(new Bindable( + stage.IsSpecialColumn(column) ? 120 : 60 + )); + + case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: + + Color4 colour; + + const int total_colours = 7; + + if (stage.IsSpecialColumn(column)) + colour = new Color4(159, 101, 255, 255); + else + { + switch (column % total_colours) + { + case 0: + colour = new Color4(240, 216, 0, 255); + break; + + case 1: + colour = new Color4(240, 101, 0, 255); + break; + + case 2: + colour = new Color4(240, 0, 130, 255); + break; + + case 3: + colour = new Color4(192, 0, 240, 255); + break; + + case 4: + colour = new Color4(0, 96, 240, 255); + break; + + case 5: + colour = new Color4(0, 226, 240, 255); + break; + + case 6: + colour = new Color4(0, 240, 96, 255); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + return SkinUtils.As(new Bindable(colour)); + } + } + + return base.GetConfig(lookup); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs new file mode 100644 index 0000000000..eb51179cea --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Default +{ + public class ManiaTrianglesSkinTransformer : SkinTransformer + { + private readonly ManiaBeatmap beatmap; + + public ManiaTrianglesSkinTransformer(ISkin skin, IBeatmap beatmap) + : base(skin) + { + this.beatmap = (ManiaBeatmap)beatmap; + } + + private readonly Color4 colourEven = new Color4(6, 84, 0, 255); + private readonly Color4 colourOdd = new Color4(94, 0, 57, 255); + private readonly Color4 colourSpecial = new Color4(0, 48, 63, 255); + + public override IBindable? GetConfig(TLookup lookup) + { + if (lookup is ManiaSkinConfigurationLookup maniaLookup) + { + switch (maniaLookup.Lookup) + { + case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: + int column = maniaLookup.ColumnIndex ?? 0; + + var stage = beatmap.GetStageForColumnIndex(column); + + if (stage.IsSpecialColumn(column)) + return SkinUtils.As(new Bindable(colourSpecial)); + + int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column); + return SkinUtils.As(new Bindable(distanceToEdge % 2 == 0 ? colourOdd : colourEven)); + } + } + + return base.GetConfig(lookup); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs index ab953ccfb9..e227c80845 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -20,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy [Resolved] protected Column Column { get; private set; } + [Resolved] + private StageDefinition stage { get; set; } + /// /// The column type identifier to use for texture lookups, in the case of no user-provided configuration. /// @@ -28,19 +32,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy [BackgroundDependencyLoader] private void load() { - switch (Column.ColumnType) + if (Column.IsSpecial) + FallbackColumnIndex = "S"; + else { - case ColumnType.Special: - FallbackColumnIndex = "S"; - break; - - case ColumnType.Odd: - FallbackColumnIndex = "1"; - break; - - case ColumnType.Even: - FallbackColumnIndex = "2"; - break; + int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index); + FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2"; } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs index 740ccbfe27..d039551cd7 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs @@ -18,20 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyStageBackground : CompositeDrawable { - private readonly StageDefinition stageDefinition; - private Drawable leftSprite; private Drawable rightSprite; private ColumnFlow columnBackgrounds; - public LegacyStageBackground(StageDefinition stageDefinition) + public LegacyStageBackground() { - this.stageDefinition = stageDefinition; RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, StageDefinition stageDefinition) { string leftImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value ?? "mania-stage-left"; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs new file mode 100644 index 0000000000..e57927897c --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public class ManiaClassicSkinTransformer : ManiaLegacySkinTransformer + { + public ManiaClassicSkinTransformer(ISkin skin, IBeatmap beatmap) + : base(skin, beatmap) + { + } + + public override IBindable GetConfig(TLookup lookup) + { + if (lookup is ManiaSkinConfigurationLookup maniaLookup) + { + var baseLookup = base.GetConfig(lookup); + + if (baseLookup != null) + return baseLookup; + + // default provisioning. + switch (maniaLookup.Lookup) + { + case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: + return SkinUtils.As(new Bindable(Color4.Black)); + } + } + + return base.GetConfig(lookup); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index dd5baa8150..1d39721a2b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,8 +19,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class ManiaLegacySkinTransformer : LegacySkinTransformer { - private readonly ManiaBeatmap beatmap; - /// /// Mapping of to their corresponding /// value. @@ -60,6 +57,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy /// private readonly Lazy hasKeyTexture; + private readonly ManiaBeatmap beatmap; + public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap) : base(skin) { @@ -113,8 +112,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new LegacyHitExplosion(); case ManiaSkinComponents.StageBackground: - Debug.Assert(maniaComponent.StageDefinition != null); - return new LegacyStageBackground(maniaComponent.StageDefinition.Value); + return new LegacyStageBackground(); case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); @@ -151,7 +149,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy public override IBindable GetConfig(TLookup lookup) { if (lookup is ManiaSkinConfigurationLookup maniaLookup) - return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); + { + return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.ColumnIndex)); + } return base.GetConfig(lookup); } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs index 4d0c321116..e22bf63049 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Mania.Skinning /// /// The skin from which configuration is retrieved. /// The value to retrieve. - /// If not null, denotes the index of the column to which the entry applies. - public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + /// If not null, denotes the index of the column to which the entry applies. + public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null) => skin.GetConfig( - new ManiaSkinConfigurationLookup(lookup, index)); + new ManiaSkinConfigurationLookup(lookup, columnIndex)); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs index e9005a3da0..59188f02f9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs @@ -16,20 +16,21 @@ namespace osu.Game.Rulesets.Mania.Skinning public readonly LegacyManiaSkinConfigurationLookups Lookup; /// - /// The intended index for the configuration. + /// The column which is being looked up. /// May be null if the configuration does not apply to a . + /// Note that this is the absolute index across all stages. /// - public readonly int? TargetColumn; + public readonly int? ColumnIndex; /// /// Creates a new . /// /// The lookup value. - /// The intended index for the configuration. May be null if the configuration does not apply to a . - public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null) + /// The intended index for the configuration. May be null if the configuration does not apply to a . + public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null) { Lookup = lookup; - TargetColumn = targetColumn; + ColumnIndex = columnIndex; } } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index deb1b155b5..3d46bdaa7b 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -3,30 +3,30 @@ #nullable disable -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects.Drawables; 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.Framework.Platform; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.UI; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { [Cached] - public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour + public class Column : ScrollingPlayfield, IKeyBindingHandler { public const float COLUMN_WIDTH = 80; public const float SPECIAL_COLUMN_WIDTH = 70; @@ -39,23 +39,46 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); public readonly ColumnHitObjectArea HitObjectArea; - internal readonly Container TopLevelContainer; - private readonly DrawablePool hitExplosionPool; + internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; + private DrawablePool hitExplosionPool; private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; - private readonly GameplaySampleTriggerSource sampleTriggerSource; + private GameplaySampleTriggerSource sampleTriggerSource; - public Column(int index) + /// + /// Whether this is a special (ie. scratch) column. + /// + public readonly bool IsSpecial; + + public readonly Bindable AccentColour = new Bindable(Color4.Black); + + public Column(int index, bool isSpecial) { Index = index; + IsSpecial = isSpecial; RelativeSizeAxes = Axes.Y; Width = COLUMN_WIDTH; + hitPolicy = new OrderedHitPolicy(HitObjectContainer); + HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; + } + + [Resolved] + private ISkinSource skin { get; set; } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + SkinnableDrawable keyArea; + + skin.SourceChanged += onSourceChanged; + onSourceChanged(); + Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, }; InternalChildren = new[] @@ -64,17 +87,18 @@ namespace osu.Game.Rulesets.Mania.UI sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer), // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), - HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) + HitObjectArea, + keyArea = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, }, background, - TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }, + TopLevelContainer, new ColumnTouchInputArea(this) }; - hitPolicy = new OrderedHitPolicy(HitObjectContainer); + applyGameWideClock(background); + applyGameWideClock(keyArea); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); @@ -83,20 +107,38 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(50, 250); + + // Some elements don't handle rewind correctly and fixing them is non-trivial. + // In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide + // clock so they don't need to worry about rewind. + // This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding. + // + // This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind. + void applyGameWideClock(Drawable drawable) + { + drawable.Clock = host.UpdateThread.Clock; + drawable.ProcessCustomClock = false; + } + } + + private void onSourceChanged() + { + AccentColour.Value = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black; } protected override void LoadComplete() { base.LoadComplete(); - NewResult += OnNewResult; } - public ColumnType ColumnType { get; set; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); - public bool IsSpecial => ColumnType == ColumnType.Special; - - public Color4 AccentColour { get; set; } + if (skin != null) + skin.SourceChanged -= onSourceChanged; + } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -111,7 +153,7 @@ namespace osu.Game.Rulesets.Mania.UI DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject; - maniaObject.AccentColour.Value = AccentColour; + maniaObject.AccentColour.BindTo(AccentColour); maniaObject.CheckHittable = hitPolicy.IsHittable; } diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 871ec9f1a3..9b3f6d7033 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X; + Masking = true; + InternalChild = columns = new FillFlowContainer { RelativeSizeAxes = Axes.Y, diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs index 39d17db6be..3680e7ea0a 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components [Resolved] private Column column { get; set; } + private Bindable accentColour; + public DefaultColumnBackground() { RelativeSizeAxes = Axes.Both; @@ -55,9 +57,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components } }; - background.Colour = column.AccentColour.Darken(5); - brightColour = column.AccentColour.Opacity(0.6f); - dimColour = column.AccentColour.Opacity(0); + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => + { + background.Colour = colour.NewValue.Darken(5); + brightColour = colour.NewValue.Opacity(0.6f); + dimColour = colour.NewValue.Opacity(0); + }, true); direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs index 53fa86125f..97aa897782 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components private Container hitTargetLine; private Drawable hitTargetBar; + private Bindable accentColour; + [Resolved] private Column column { get; set; } @@ -54,12 +56,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components }, }; - hitTargetLine.EdgeEffect = new EdgeEffectParameters + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = column.AccentColour.Opacity(0.5f), - }; + hitTargetLine.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = colour.NewValue.Opacity(0.5f), + }; + }, true); direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs index 5a0fab2ff4..600c9feb73 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components private Container keyIcon; private Drawable gradient; + private Bindable accentColour; + [Resolved] private Column column { get; set; } @@ -75,15 +77,19 @@ namespace osu.Game.Rulesets.Mania.UI.Components } }; - keyIcon.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = column.AccentColour.Opacity(0.5f), - }; - direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => + { + keyIcon.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = colour.NewValue.Opacity(0.5f), + }; + }, true); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index e83cd10d2d..59716ee3e2 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -32,6 +32,10 @@ namespace osu.Game.Rulesets.Mania.UI private CircularContainer largeFaint; private CircularContainer mainGlow1; + private CircularContainer mainGlow2; + private CircularContainer mainGlow3; + + private Bindable accentColour; public DefaultHitExplosion() { @@ -48,8 +52,6 @@ namespace osu.Game.Rulesets.Mania.UI const float roundness = 80; const float initial_height = 10; - var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1); - InternalChildren = new Drawable[] { largeFaint = new CircularContainer @@ -61,13 +63,6 @@ namespace osu.Game.Rulesets.Mania.UI // we want our size to be very small so the glow dominates it. Size = new Vector2(default_large_faint_size), Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f), - Roundness = 160, - Radius = 200, - }, }, mainGlow1 = new CircularContainer { @@ -76,15 +71,8 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both, Masking = true, Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1), - Roundness = 20, - Radius = 50, - }, }, - new CircularContainer + mainGlow2 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -93,15 +81,8 @@ namespace osu.Game.Rulesets.Mania.UI Size = new Vector2(0.01f, initial_height), Blending = BlendingParameters.Additive, Rotation = RNG.NextSingle(-angle_variance, angle_variance), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, }, - new CircularContainer + mainGlow3 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -110,18 +91,44 @@ namespace osu.Game.Rulesets.Mania.UI Size = new Vector2(0.01f, initial_height), Blending = BlendingParameters.Additive, Rotation = RNG.NextSingle(-angle_variance, angle_variance), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, } }; direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => + { + largeFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, colour.NewValue, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }; + mainGlow1.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, colour.NewValue, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }; + mainGlow2.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.4f, colour.NewValue, Color4.White, 0, 1), + Roundness = roundness, + Radius = 40, + }; + mainGlow3.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.4f, colour.NewValue, Color4.White, 0, 1), + Roundness = roundness, + Radius = 40, + }; + }, true); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs index 28509d1f4e..a7b94f9f22 100644 --- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Mania.UI { base.PrepareForUse(); + LifetimeStart = Time.Current; + (skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result); this.Delay(DURATION).Then().Expire(); diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index c578bbb703..1273cb3d32 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -19,7 +21,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { @@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.UI /// public class Stage : ScrollingPlayfield { + [Cached] + public readonly StageDefinition Definition; + public const float COLUMN_SPACING = 1; public const float HIT_TARGET_POSITION = 110; @@ -40,13 +44,6 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Drawable barLineContainer; - private readonly Dictionary columnColours = new Dictionary - { - { ColumnType.Even, new Color4(6, 84, 0, 255) }, - { ColumnType.Odd, new Color4(94, 0, 57, 255) }, - { ColumnType.Special, new Color4(0, 48, 63, 255) } - }; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos)); private readonly int firstColumnIndex; @@ -54,6 +51,7 @@ namespace osu.Game.Rulesets.Mania.UI public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) { this.firstColumnIndex = firstColumnIndex; + Definition = definition; Name = "Stage"; @@ -75,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground()) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) { RelativeSizeAxes = Axes.Both }, @@ -100,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y, } }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) { RelativeSizeAxes = Axes.Both }, @@ -118,15 +116,13 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { - var columnType = definition.GetTypeOfColumn(i); + bool isSpecial = definition.IsSpecialColumn(i); - var column = new Column(firstColumnIndex + i) + var column = new Column(firstColumnIndex + i, isSpecial) { RelativeSizeAxes = Axes.Both, Width = 1, - ColumnType = columnType, - AccentColour = columnColours[columnType], - Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ } + Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ } }; topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); @@ -135,6 +131,37 @@ namespace osu.Game.Rulesets.Mania.UI } } + private ISkinSource currentSkin; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + currentSkin = skin; + + skin.SourceChanged += onSkinChanged; + onSkinChanged(); + } + + private void onSkinChanged() + { + float paddingTop = currentSkin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.StagePaddingTop))?.Value ?? 0; + float paddingBottom = currentSkin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.StagePaddingBottom))?.Value ?? 0; + + Padding = new MarginPadding + { + Top = paddingTop, + Bottom = paddingBottom, + }; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (currentSkin != null) + currentSkin.SourceChanged -= onSkinChanged; + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs index 51871dd9e5..0601dc6068 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs @@ -148,6 +148,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestFloatEdgeCaseConversion() + { + Slider slider = null; + + AddStep("select first slider", () => + { + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + EditorClock.Seek(slider.StartTime); + EditorBeatmap.SelectedHitObjects.Add(slider); + }); + + AddStep("change to these specific circumstances", () => + { + EditorBeatmap.Difficulty.SliderMultiplier = 1; + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(slider.StartTime); + timingPoint.BeatLength = 352.941176470588; + slider.Path.ControlPoints[^1].Position = new Vector2(-110, 16); + slider.Path.ExpectedDistance.Value = 100; + }); + + convertToStream(); + + AddAssert("stream created", () => streamCreatedFor(slider, + (time: 0, pathPosition: 0), + (time: 0.25, pathPosition: 0.25), + (time: 0.5, pathPosition: 0.5), + (time: 0.75, pathPosition: 0.75), + (time: 1, pathPosition: 1))); + } + private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles) { if (EditorBeatmap.HitObjects.Contains(slider)) diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png new file mode 100644 index 0000000000..b1380a47a4 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png new file mode 100644 index 0000000000..5f7beae4e9 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs new file mode 100644 index 0000000000..1cb64b71fc --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs @@ -0,0 +1,136 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Framework.Logging; +using osu.Framework.Testing.Input; +using osu.Game.Rulesets.Osu.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneSmoke : OsuSkinnableTestScene + { + [Test] + public void TestSmoking() + { + addStep("Create short smoke", 2_000); + addStep("Create medium smoke", 5_000); + addStep("Create long smoke", 10_000); + } + + private void addStep(string stepName, double duration) + { + var smokeContainers = new List(); + + AddStep(stepName, () => + { + smokeContainers.Clear(); + SetContents(_ => + { + smokeContainers.Add(new TestSmokeContainer + { + Duration = duration, + RelativeSizeAxes = Axes.Both + }); + + return new SmokingInputManager + { + Duration = duration, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.95f), + Child = smokeContainers[^1], + }; + }); + }); + + AddUntilStep("Until skinnable expires", () => + { + if (smokeContainers.Count == 0) + return false; + + Logger.Log("How many: " + smokeContainers.Count); + + foreach (var smokeContainer in smokeContainers) + { + if (smokeContainer.Children.Count != 0) + return false; + } + + return true; + }); + } + + private class SmokingInputManager : ManualInputManager + { + public double Duration { get; init; } + + private double? startTime; + + public SmokingInputManager() + { + UseParentInput = false; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + MoveMouseTo(ToScreenSpace(DrawSize / 2)); + } + + protected override void Update() + { + base.Update(); + + const float spin_angle = 4 * MathF.PI; + + startTime ??= Time.Current; + + float fraction = (float)((Time.Current - startTime) / Duration); + + float angle = fraction * spin_angle; + float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2; + + Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2; + MoveMouseTo(ToScreenSpace(pos)); + } + } + + private class TestSmokeContainer : SmokeContainer + { + public double Duration { get; init; } + + private bool isPressing; + private bool isFinished; + + private double? startTime; + + protected override void Update() + { + base.Update(); + + startTime ??= Time.Current + 0.1; + + if (!isPressing && !isFinished && Time.Current > startTime) + { + OnPressed(new KeyBindingPressEvent(new InputState(), OsuAction.Smoke)); + isPressing = true; + isFinished = false; + } + + if (isPressing && Time.Current > startTime + Duration) + { + OnReleased(new KeyBindingReleaseEvent(new InputState(), OsuAction.Smoke)); + isPressing = false; + isFinished = true; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index c2973644cf..1eb1c85d93 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -1,10 +1,10 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 2ba856d014..dabbfcd2fb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (!(currentObj.BaseObject is Spinner)) { - double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length; + double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length; cumulativeStrainTime += lastObj.StrainTime; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 7c289b5b05..265a1d21b1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount(); double pathPosition = positionWithRepeats - (int)positionWithRepeats; // every second span is in the reverse direction - need to reverse the path position. - if (Precision.AlmostBigger(positionWithRepeats % 2, 1)) + if (positionWithRepeats % 2 >= 1) pathPosition = 1 - pathPosition; Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index e624660410..f6622c268d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -4,7 +4,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods { @@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Hit them at the right size!"; - [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] - public override BindableNumber StartScale { get; } = new BindableFloat + public override BindableNumber StartScale { get; } = new BindableFloat(2) { MinValue = 1f, MaxValue = 25f, - Default = 2f, - Value = 2f, Precision = 0.1f, }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index b77c887cd3..3d066d3ada 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -4,7 +4,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods { @@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Hit them at the right size!"; - [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] - public override BindableNumber StartScale { get; } = new BindableFloat + public override BindableNumber StartScale { get; } = new BindableFloat(0.5f) { MinValue = 0f, MaxValue = 0.99f, - Default = 0.5f, - Value = 0.5f, Precision = 0.01f, }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs index 817f7b599c..2f84c30581 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs @@ -7,8 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; @@ -22,15 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods private PeriodTracker spinnerPeriods = null!; - [SettingSource( - "Hidden at combo", - "The combo count at which the cursor becomes completely hidden", - SettingControlType = typeof(SettingsSlider) - )] - public override BindableInt HiddenComboCount { get; } = new BindableInt + public override BindableInt HiddenComboCount { get; } = new BindableInt(10) { - Default = 10, - Value = 10, MinValue = 0, MaxValue = 50, }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 59984f9a7b..6f1206382a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; + [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] public abstract BindableNumber StartScale { get; } protected virtual float EndScale => 1; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 78c7aa53f4..618fcfe05d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -29,10 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray(); [SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider))] - public BindableFloat AngleSharpness { get; } = new BindableFloat + public BindableFloat AngleSharpness { get; } = new BindableFloat(7) { - Default = 7, - Value = 7, MinValue = 1, MaxValue = 10, Precision = 0.1f diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index fac1cbfd47..753de6231a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer { public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray(); + + public override Type[] IncompatibleMods => + base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray(); /// /// How early before a hitobject's start time to trigger a hit. @@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods return; } - osuInputManager.AllowUserPresses = false; + osuInputManager.AllowGameplayInputs = false; } public void Update(Playfield playfield) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 861ad80b7f..406968ba08 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -53,11 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods }).ToArray(); [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] - public Bindable Seed { get; } = new Bindable - { - Default = null, - Value = null - }; + public Bindable Seed { get; } = new Bindable(); [SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")] public Bindable Metronome { get; } = new BindableBool(true); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c5992b359d..23db29b9a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -204,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // todo: temporary / arbitrary, used for lifetime optimisation. this.Delay(800).FadeOut(); - // in the case of an early state change, the fade should be expedited to the current point in time. - if (HitStateUpdateTime < HitObject.StartTime) - ApproachCircle.FadeOut(50); - switch (state) { + default: + ApproachCircle.FadeOut(); + break; + case ArmedState.Idle: HitArea.HitAction = null; break; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 6f4ca30bd0..d9d0d28477 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -11,7 +11,9 @@ using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -64,6 +66,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.UnbindFrom(HitObject.ScaleBindable); } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + // Dim should only be applied at a top level, as it will be implicitly applied to nested objects. + if (ParentHitObject == null) + { + // Of note, no one noticed this was missing for years, but it definitely feels like it should still exist. + // For now this is applied across all skins, and matches stable. + // For simplicity, dim colour is applied to the DrawableHitObject itself. + // We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod). + this.FadeColour(new Color4(195, 195, 195, 255)); + using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) + this.FadeColour(Color4.White, 100); + } + } + protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; private OsuInputManager osuActionInputManager; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 6bfb4e8aae..a2fe623897 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -186,17 +186,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Vector2? lastPosition; + private bool rewinding; + public void UpdateProgress(double completionProgress) { Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); + if (Clock.ElapsedFrameTime != 0) + rewinding = Clock.ElapsedFrameTime < 0; + // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. if (diff.LengthFast < 0.01f) return; - ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); + ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0); lastPosition = Position; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 6a15463a32..4975ca1248 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; } - protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration; + public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration; /// /// Apply a judgement result. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e3c1b1e168..6c2be8a49a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -34,21 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects public override IList AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray(); - public IList CreateSlidingSamples() - { - var slidingSamples = new List(); - - var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); - if (normalSample != null) - slidingSamples.Add(normalSample.With("sliderslide")); - - var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); - if (whistleSample != null) - slidingSamples.Add(whistleSample.With("sliderwhistle")); - - return slidingSamples; - } - private readonly Cached endPositionCache = new Cached(); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 569e9b7c1c..676ff62455 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects // This is so on repeats ticks don't appear too late to be visually processed by the player. offset = 200; else - offset = TimeFadeIn * 0.66f; + offset = TimePreempt * 0.66f; TimePreempt = (StartTime - SpanStartTime) / 2 + offset; } diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index 12256e93d0..1e59e19246 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges.Events; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu @@ -17,9 +19,16 @@ namespace osu.Game.Rulesets.Osu { public IEnumerable PressedActions => KeyBindingContainer.PressedActions; - public bool AllowUserPresses + /// + /// Whether gameplay input buttons should be allowed. + /// Defaults to true, generally used for mods like Relax which turn off main inputs. + /// + /// + /// Of note, auxiliary inputs like the "smoke" key are left usable. + /// + public bool AllowGameplayInputs { - set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value; + set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value; } /// @@ -58,18 +67,36 @@ namespace osu.Game.Rulesets.Osu private class OsuKeyBindingContainer : RulesetKeyBindingContainer { - public bool AllowUserPresses = true; + private bool allowGameplayInputs = true; + + /// + /// Whether gameplay input buttons should be allowed. + /// Defaults to true, generally used for mods like Relax which turn off main inputs. + /// + /// + /// Of note, auxiliary inputs like the "smoke" key are left usable. + /// + public bool AllowGameplayInputs + { + get => allowGameplayInputs; + set + { + allowGameplayInputs = value; + ReloadMappings(); + } + } public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) { } - protected override bool Handle(UIEvent e) + protected override void ReloadMappings(IQueryable realmKeyBindings) { - if (!AllowUserPresses) return false; + base.ReloadMappings(realmKeyBindings); - return base.Handle(e); + if (!AllowGameplayInputs) + KeyBindings = KeyBindings.Where(b => b.GetAction() == OsuAction.Smoke).ToList(); } } } @@ -80,6 +107,9 @@ namespace osu.Game.Rulesets.Osu LeftButton, [Description("Right button")] - RightButton + RightButton, + + [Description("Smoke")] + Smoke, } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 3f5e728651..e823053be9 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu { new KeyBinding(InputKey.Z, OsuAction.LeftButton), new KeyBinding(InputKey.X, OsuAction.RightButton), + new KeyBinding(InputKey.C, OsuAction.Smoke), new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton), new KeyBinding(InputKey.MouseRight, OsuAction.RightButton), }; diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index fcf079b6aa..4248cce55a 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu SliderBall, SliderBody, SpinnerBody, + CursorSmoke, ApproachCircle, } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs index 85060261fe..8082c5aef4 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays Position = currentFrame.Position; if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton); + if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke); } public LegacyReplayFrame ToLegacy(IBeatmap beatmap) @@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays state |= ReplayButtonState.Left1; if (Actions.Contains(OsuAction.RightButton)) state |= ReplayButtonState.Right1; + if (Actions.Contains(OsuAction.Smoke)) + state |= ReplayButtonState.Smoke; return new LegacyReplayFrame(Time, Position.X, Position.Y, state); } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index 05fbac625e..6f55e1790f 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -1,20 +1,23 @@ // 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.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuHitWindows : HitWindows { + /// + /// osu! ruleset has a fixed miss window regardless of difficulty settings. + /// + public const double MISS_WINDOW = 400; + private static readonly DifficultyRange[] osu_ranges = { new DifficultyRange(HitResult.Great, 80, 50, 20), new DifficultyRange(HitResult.Ok, 140, 100, 60), new DifficultyRange(HitResult.Meh, 200, 150, 100), - new DifficultyRange(HitResult.Miss, 400, 400, 400), + new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW), }; public override bool IsHitResultAllowed(HitResult result) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index 7bc6723afb..bf507db50c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon return new ArgonJudgementPiece(resultComponent.Component); case OsuSkinComponent osuComponent: + // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. switch (osuComponent.Component) { case OsuSkinComponents.HitCircle: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs new file mode 100644 index 0000000000..27a2dc3960 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs @@ -0,0 +1,19 @@ +// 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.Textures; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public class DefaultSmokeSegment : SmokeSegment + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + // ISkinSource doesn't currently fallback to global textures. + // We might want to change this in the future if the intention is to allow the user to skin this as per legacy skins. + Texture = textures.Get("Gameplay/osu/cursor-smoke"); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs new file mode 100644 index 0000000000..c9c7e86e86 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs @@ -0,0 +1,19 @@ +// 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.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning.Legacy +{ + public class LegacySmokeSegment : SmokeSegment + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + base.LoadComplete(); + + Texture = skin.GetTexture("cursor-smoke"); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 885a2c12fb..b778bc21d1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; + case OsuSkinComponents.CursorSmoke: + if (GetTexture("cursor-smoke") != null) + return new LegacySmokeSegment(); + + return null; + case OsuSkinComponents.HitCircleText: if (!this.HasFont(LegacyFont.HitCircle)) return null; diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs new file mode 100644 index 0000000000..6c998e244c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -0,0 +1,366 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable + { + private const int max_point_count = 18_000; + + // fade anim values + private const double initial_fade_out_duration = 4000; + + private const double re_fade_in_speed = 3; + private const double re_fade_in_duration = 50; + + private const double final_fade_out_speed = 2; + private const double final_fade_out_duration = 8000; + + private const float initial_alpha = 0.6f; + private const float re_fade_in_alpha = 1f; + + private readonly int rotationSeed = RNG.Next(); + + // scale anim values + private const double scale_duration = 1200; + + private const float initial_scale = 0.65f; + private const float final_scale = 1f; + + // rotation anim values + private const double rotation_duration = 500; + + private const float max_rotation = 0.25f; + + public IShader? TextureShader { get; private set; } + public IShader? RoundedTextureShader { get; private set; } + + protected Texture? Texture { get; set; } + + private float radius => Texture?.DisplayWidth * 0.165f ?? 3; + + protected readonly List SmokePoints = new List(); + + private float pointInterval => radius * 7f / 8; + + private double smokeStartTime { get; set; } = double.MinValue; + + private double smokeEndTime { get; set; } = double.MaxValue; + + private float totalDistance; + private Vector2? lastPosition; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + RelativeSizeAxes = Axes.Both; + + LifetimeStart = smokeStartTime = Time.Current; + + totalDistance = pointInterval; + } + + private Vector2 nextPointDirection() + { + float angle = RNG.NextSingle(0, 2 * MathF.PI); + return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); + } + + public void AddPosition(Vector2 position, double time) + { + lastPosition ??= position; + + float delta = (position - (Vector2)lastPosition).LengthFast; + totalDistance += delta; + int count = (int)(totalDistance / pointInterval); + + if (count > 0) + { + Vector2 increment = position - (Vector2)lastPosition; + increment.NormalizeFast(); + + Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition; + increment *= pointInterval; + + if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time) + { + int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer()); + SmokePoints.RemoveRange(index, SmokePoints.Count - index); + } + + totalDistance %= pointInterval; + + for (int i = 0; i < count; i++) + { + SmokePoints.Add(new SmokePoint + { + Position = pointPos, + Time = time, + Direction = nextPointDirection(), + }); + + pointPos += increment; + } + + Invalidate(Invalidation.DrawNode); + } + + lastPosition = position; + + if (SmokePoints.Count >= max_point_count) + FinishDrawing(time); + } + + public void FinishDrawing(double time) + { + smokeEndTime = time; + + double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, smokeEndTime - smokeStartTime); + LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed; + } + + protected override DrawNode CreateDrawNode() => new SmokeDrawNode(this); + + protected override void Update() + { + base.Update(); + + Invalidate(Invalidation.DrawNode); + } + + protected struct SmokePoint + { + public Vector2 Position; + public double Time; + public Vector2 Direction; + + public struct UpperBoundComparer : IComparer + { + public int Compare(SmokePoint x, SmokePoint target) + { + // By returning -1 when the target value is equal to x, guarantees that the + // element at BinarySearch's returned index will always be the first element + // larger. Since 0 is never returned, the target is never "found", so the return + // value will be the index's complement. + + return x.Time > target.Time ? 1 : -1; + } + } + } + + protected class SmokeDrawNode : TexturedShaderDrawNode + { + protected new SmokeSegment Source => (SmokeSegment)base.Source; + + protected double SmokeStartTime { get; private set; } + protected double SmokeEndTime { get; private set; } + protected double CurrentTime { get; private set; } + + private readonly List points = new List(); + private IVertexBatch? quadBatch; + private float radius; + private Vector2 drawSize; + private Texture? texture; + + // anim calculation vars (color, scale, direction) + private double initialFadeOutDurationTrunc; + private double firstVisiblePointTime; + + private double initialFadeOutTime; + private double reFadeInTime; + private double finalFadeOutTime; + + private Random rotationRNG = new Random(); + + public SmokeDrawNode(ITexturedShaderDrawable source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + points.Clear(); + points.AddRange(Source.SmokePoints); + + radius = Source.radius; + drawSize = Source.DrawSize; + texture = Source.Texture; + + SmokeStartTime = Source.smokeStartTime; + SmokeEndTime = Source.smokeEndTime; + CurrentTime = Source.Clock.CurrentTime; + + rotationRNG = new Random(Source.rotationSeed); + + initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime); + firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc; + + initialFadeOutTime = CurrentTime; + reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed); + finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed); + } + + public sealed override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + if (points.Count == 0) + return; + + quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10); + texture ??= renderer.WhitePixel; + RectangleF textureRect = texture.GetTextureRect(); + + var shader = GetAppropriateShader(renderer); + + renderer.SetBlend(BlendingParameters.Additive); + renderer.PushLocalMatrix(DrawInfo.Matrix); + + shader.Bind(); + texture.Bind(); + + foreach (var point in points) + drawPointQuad(point, textureRect); + + shader.Unbind(); + renderer.PopLocalMatrix(); + } + + protected Color4 ColourAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour + ? ((SRGBColour)DrawColourInfo.Colour).Linear + : DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, drawSize)).Linear; + + protected virtual Color4 PointColour(SmokePoint point) + { + var color = Color4.White; + + double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time; + + if (timeDoingInitialFadeOut > 0) + { + float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1); + color.A = (1 - fraction) * initial_alpha; + } + + if (color.A > 0) + { + double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed; + double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed; + + if (timeDoingFinalFadeOut > 0) + { + float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1); + fraction = MathF.Pow(fraction, 5); + color.A = (1 - fraction) * re_fade_in_alpha; + } + else if (timeDoingReFadeIn > 0) + { + float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1); + fraction = 1 - MathF.Pow(1 - fraction, 5); + color.A = fraction * (re_fade_in_alpha - color.A) + color.A; + } + } + + return color; + } + + protected virtual float PointScale(SmokePoint point) + { + double timeDoingScale = CurrentTime - point.Time; + float fraction = Math.Clamp((float)(timeDoingScale / scale_duration), 0, 1); + fraction = 1 - MathF.Pow(1 - fraction, 5); + return fraction * (final_scale - initial_scale) + initial_scale; + } + + protected virtual Vector2 PointDirection(SmokePoint point) + { + float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X); + float finalAngle = initialAngle + nextRotation(); + + double timeDoingRotation = CurrentTime - point.Time; + float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1); + fraction = 1 - MathF.Pow(1 - fraction, 5); + float angle = fraction * (finalAngle - initialAngle) + initialAngle; + + return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); + } + + private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1); + + private void drawPointQuad(SmokePoint point, RectangleF textureRect) + { + Debug.Assert(quadBatch != null); + + var colour = PointColour(point); + float scale = PointScale(point); + var dir = PointDirection(point); + var ortho = dir.PerpendicularLeft; + + if (colour.A == 0 || scale == 0) + return; + + var localTopLeft = point.Position + (radius * scale * (-ortho - dir)); + var localTopRight = point.Position + (radius * scale * (-ortho + dir)); + var localBotLeft = point.Position + (radius * scale * (ortho - dir)); + var localBotRight = point.Position + (radius * scale * (ortho + dir)); + + quadBatch.Add(new TexturedVertex2D + { + Position = localTopLeft, + TexturePosition = textureRect.TopLeft, + Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour), + }); + quadBatch.Add(new TexturedVertex2D + { + Position = localTopRight, + TexturePosition = textureRect.TopRight, + Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour), + }); + quadBatch.Add(new TexturedVertex2D + { + Position = localBotRight, + TexturePosition = textureRect.BottomRight, + Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour), + }); + quadBatch.Add(new TexturedVertex2D + { + Position = localBotLeft, + TexturePosition = textureRect.BottomLeft, + Colour = Color4Extensions.Multiply(ColourAtPosition(localBotLeft), colour), + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + quadBatch?.Dispose(); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index fc2ba8ea2f..2e67e91460 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, + new SmokeContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, diff --git a/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs new file mode 100644 index 0000000000..beba834e88 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Manages smoke trails generated from user input. + /// + public class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler + { + private SmokeSkinnableDrawable? currentSegmentSkinnable; + + private Vector2 lastMousePosition; + + public override bool ReceivePositionalInputAt(Vector2 _) => true; + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == OsuAction.Smoke) + { + AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())); + + // Add initial position immediately. + addPosition(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + if (e.Action == OsuAction.Smoke) + { + if (currentSegmentSkinnable?.Drawable is SmokeSegment segment) + { + segment.FinishDrawing(Time.Current); + currentSegmentSkinnable = null; + } + } + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + lastMousePosition = e.MousePosition; + addPosition(); + + return base.OnMouseMove(e); + } + + private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current); + + private class SmokeSkinnableDrawable : SkinnableDrawable + { + public override bool RemoveWhenNotAlive => true; + + public override double LifetimeStart => Drawable.LifetimeStart; + public override double LifetimeEnd => Drawable.LifetimeEnd; + + public SmokeSkinnableDrawable(ISkinComponent component, Func? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) + : base(component, defaultImplementation, confineMode) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index a405f0e8ba..d2d5cdb6ac 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NUnit.Framework; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; @@ -129,5 +130,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.Miss); AssertResult(0, HitResult.IgnoreMiss); } + + [Test] + public void TestHighVelocityHit() + { + const double hit_time = 1000; + + var beatmap = CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + }); + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 }); + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 }); + + var hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre), + }, beatmap); + + AssertJudgementCount(1); + AssertResult(0, HitResult.Ok); + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs new file mode 100644 index 0000000000..095fddc33f --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneBarLineGeneration : OsuTestScene + { + [Test] + public void TestCloseBarLineGeneration() + { + const double start_time = 1000; + + var beatmap = new Beatmap + { + HitObjects = + { + new Hit + { + Type = HitType.Centre, + StartTime = start_time + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint()); + beatmap.ControlPointInfo.Add(start_time + 1, new TimingControlPoint()); + + var barlines = new BarLineGenerator(beatmap).BarLines; + + AddAssert("first barline generated", () => barlines.Any(b => b.StartTime == start_time)); + AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + 1)); + } + + [Test] + public void TestOmitBarLineEffectPoint() + { + const double start_time = 1000; + const double beat_length = 500; + + const int time_signature_numerator = 4; + + var beatmap = new Beatmap + { + HitObjects = + { + new Hit + { + Type = HitType.Centre, + StartTime = start_time + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint + { + BeatLength = beat_length, + TimeSignature = new TimeSignature(time_signature_numerator) + }); + + beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true }); + + var barlines = new BarLineGenerator(beatmap).BarLines; + + AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time)); + AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + (beat_length * time_signature_numerator))); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index e8eaff4368..38e61f5624 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 95a1e8bc66..dc7bad2f75 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countOk; private int countMeh; private int countMiss; + private double accuracy; private double effectiveMissCount; @@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + accuracy = customAccuracy; // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.050 * lengthBonus; - return difficultyValue * Math.Pow(score.Accuracy, 2.0); + return difficultyValue * Math.Pow(accuracy, 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) @@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0; + double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); accuracyValue *= lengthBonus; @@ -110,5 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; + + private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 705a0a8047..451c5a793b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Filled = HitObject.FirstTick }); - protected override double MaximumJudgementOffset => HitObject.HitWindow; + public override double MaximumJudgementOffset => HitObject.HitWindow; protected override void CheckForResult(bool userTriggered, double timeOffset) { diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs index f14288e7ba..4ef466efbf 100644 --- a/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapManagerTest.cs @@ -9,8 +9,10 @@ using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; @@ -96,5 +98,41 @@ namespace osu.Game.Tests.Beatmaps var second = beatmaps.GetWorkingBeatmap(beatmap, true); Assert.That(first, Is.Not.SameAs(second)); }); + + [Test] + public void TestSavePreservesCollections() => AddStep("run test", () => + { + var beatmap = Realm.Run(r => r.Find(importedSet.Beatmaps.First().ID).Detach()); + + var working = beatmaps.GetWorkingBeatmap(beatmap); + + Assert.That(working.BeatmapInfo.BeatmapSet?.Files, Has.Count.GreaterThan(0)); + + string initialHash = working.BeatmapInfo.MD5Hash; + + var preserveCollection = new BeatmapCollection("test contained"); + preserveCollection.BeatmapMD5Hashes.Add(initialHash); + + var noNewCollection = new BeatmapCollection("test not contained"); + + Realm.Write(r => + { + r.Add(preserveCollection); + r.Add(noNewCollection); + }); + + Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(initialHash)); + Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash)); + + beatmaps.Save(working.BeatmapInfo, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo)); + + string finalHash = working.BeatmapInfo.MD5Hash; + + Assert.That(finalHash, Is.Not.SameAs(initialHash)); + + Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash)); + Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(finalHash)); + Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(finalHash)); + }); } } diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 17709fb10f..da250c1e05 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -204,31 +204,23 @@ namespace osu.Game.Tests.Online public override double ScoreMultiplier => 1; [SettingSource("Initial rate", "The starting speed of the track")] - public override BindableNumber InitialRate { get; } = new BindableDouble + public override BindableNumber InitialRate { get; } = new BindableDouble(1.5) { MinValue = 1, MaxValue = 2, - Default = 1.5, - Value = 1.5, Precision = 0.01, }; [SettingSource("Final rate", "The speed increase to ramp towards")] - public override BindableNumber FinalRate { get; } = new BindableDouble + public override BindableNumber FinalRate { get; } = new BindableDouble(0.5) { MinValue = 0, MaxValue = 1, - Default = 0.5, - Value = 0.5, Precision = 0.01, }; [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] - public override BindableBool AdjustPitch { get; } = new BindableBool - { - Default = true, - Value = true - }; + public override BindableBool AdjustPitch { get; } = new BindableBool(true); } private class TestModDifficultyAdjust : ModDifficultyAdjust diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs index b17414e026..1d8cbffcdb 100644 --- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs @@ -124,31 +124,23 @@ namespace osu.Game.Tests.Online public override double ScoreMultiplier => 1; [SettingSource("Initial rate", "The starting speed of the track")] - public override BindableNumber InitialRate { get; } = new BindableDouble + public override BindableNumber InitialRate { get; } = new BindableDouble(1.5) { MinValue = 1, MaxValue = 2, - Default = 1.5, - Value = 1.5, Precision = 0.01, }; [SettingSource("Final rate", "The speed increase to ramp towards")] - public override BindableNumber FinalRate { get; } = new BindableDouble + public override BindableNumber FinalRate { get; } = new BindableDouble(0.5) { MinValue = 0, MaxValue = 1, - Default = 0.5, - Value = 0.5, Precision = 0.01, }; [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] - public override BindableBool AdjustPitch { get; } = new BindableBool - { - Default = true, - Value = true - }; + public override BindableBool AdjustPitch { get; } = new BindableBool(true); } private class TestModEnum : Mod diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 3f20f843a7..e7590df3e0 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; -using System.Threading.Tasks; using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; @@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online } }; - beatmaps.AllowImport = new TaskCompletionSource(); + beatmaps.AllowImport.Reset(); testBeatmapFile = TestResources.GetQuickTestBeatmapForImport(); @@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile)); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); - AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); + AddStep("allow importing", () => beatmaps.AllowImport.Set()); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet)); addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable); @@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online [Test] public void TestTrackerRespectsSoftDeleting() { - AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); + AddStep("allow importing", () => beatmaps.AllowImport.Set()); AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely()); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); @@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online [Test] public void TestTrackerRespectsChecksum() { - AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); + AddStep("allow importing", () => beatmaps.AllowImport.Set()); AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely()); addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable); @@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online private class TestBeatmapManager : BeatmapManager { - public TaskCompletionSource AllowImport = new TaskCompletionSource(); + public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim(); public Live CurrentImport { get; private set; } @@ -229,7 +228,9 @@ namespace osu.Game.Tests.Online public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) { - testBeatmapManager.AllowImport.Task.WaitSafely(); + if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken)) + throw new TimeoutException("Timeout waiting for import to be allowed."); + return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken)); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 7e0981ce69..54ad4e25e4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -29,16 +29,18 @@ namespace osu.Game.Tests.Visual.Editing private TimelineBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); + private Vector2 getPosition(HitObject hitObject) => + blueprintContainer.SelectionBlueprints.First(s => s.Item == hitObject).ScreenSpaceDrawQuad.Centre; + + private Vector2 getMiddlePosition(HitObject hitObject1, HitObject hitObject2) => + (getPosition(hitObject1) + getPosition(hitObject2)) / 2; + private void moveMouseToObject(Func targetFunc) { AddStep("move mouse to object", () => { - var pos = blueprintContainer.SelectionBlueprints - .First(s => s.Item == targetFunc()) - .ChildrenOfType() - .First().ScreenSpaceDrawQuad.Centre; - - InputManager.MoveMouseTo(pos); + var hitObject = targetFunc(); + InputManager.MoveMouseTo(getPosition(hitObject)); }); } @@ -262,6 +264,56 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } + [Test] + public void TestBasicDragSelection() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 500, Position = new Vector2(100) }, + new HitCircle { StartTime = 1000, Position = new Vector2(200) }, + new HitCircle { StartTime = 1500, Position = new Vector2(300) }, + }; + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1]))); + AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("drag to select", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[2], addedObjects[3]))); + assertSelectionIs(new[] { addedObjects[1], addedObjects[2] }); + + AddStep("drag to deselect", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[1], addedObjects[2]))); + assertSelectionIs(new[] { addedObjects[1] }); + + AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left)); + assertSelectionIs(new[] { addedObjects[1] }); + } + + [Test] + public void TestFastDragSelection() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 20000, Position = new Vector2(100) }, + new HitCircle { StartTime = 31000, Position = new Vector2(200) }, + new HitCircle { StartTime = 60000, Position = new Vector2(300) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1]))); + AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left)); + AddStep("start drag", () => InputManager.MoveMouseTo(getPosition(addedObjects[1]))); + + AddStep("jump editor clock", () => EditorClock.Seek(30000)); + AddStep("jump editor clock", () => EditorClock.Seek(60000)); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + assertSelectionIs(addedObjects.Skip(1)); + AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count); + } + private void assertSelectionIs(IEnumerable hitObjects) => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 72656c29b1..171ae829a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -58,6 +58,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for some scores not masked away", () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); + + AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); + + AddStep("change score to middle", () => playerScore.Value = 1000000); + AddWaitStep("wait for movement", 5); + AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); + + AddStep("change score to first", () => playerScore.Value = 5000000); + AddWaitStep("wait for movement", 5); + AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 0a32513834..bd55ed8bd6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -31,7 +31,7 @@ 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.All(kc => kc.CountPresses >= 7)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15); AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index da6604a653..a984f508ea 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -16,6 +16,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.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; using osu.Game.Tests.Gameplay; @@ -148,6 +149,42 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); } + [Test] + public void TestInputDoesntWorkWhenHUDHidden() + { + SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType().Single(); + + bool seeked = false; + + createNew(); + + AddStep("bind seek", () => + { + seeked = false; + + var progress = getSongProgress(); + + progress.ShowHandle = true; + progress.OnSeek += _ => seeked = true; + }); + + AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); + AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + + AddStep("attempt seek", () => + { + InputManager.MoveMouseTo(getSongProgress()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("seek not performed", () => !seeked); + + AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); + + AddStep("attempt seek", () => InputManager.Click(MouseButton.Left)); + AddAssert("seek performed", () => seeked); + } + [Test] public void TestHiddenHUDDoesntBlockComponentUpdates() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 8a4818d2f8..156a1ee34a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -11,8 +11,10 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; +using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -167,14 +169,39 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); } - private void addHitObject(double time) + [Test] + public void TestVeryFlowScroll() + { + const double long_time_range = 100000; + var manualClock = new ManualClock(); + + AddStep("set manual clock", () => + { + manualClock.CurrentTime = 0; + scrollContainers.ForEach(c => c.Clock = new FramedClock(manualClock)); + + setScrollAlgorithm(ScrollVisualisationMethod.Constant); + scrollContainers.ForEach(c => c.TimeRange = long_time_range); + }); + + AddStep("add hit objects", () => + { + addHitObject(long_time_range); + addHitObject(long_time_range + 100, 250); + }); + + AddAssert("hit objects are alive", () => playfields.All(p => p.HitObjectContainer.AliveObjects.Count() == 2)); + } + + private void addHitObject(double time, float size = 75) { playfields.ForEach(p => { - var hitObject = new TestDrawableHitObject(time); - setAnchor(hitObject, p); + var hitObject = new TestHitObject(size) { StartTime = time }; + var drawable = new TestDrawableHitObject(hitObject); - p.Add(hitObject); + setAnchor(drawable, p); + p.Add(drawable); }); } @@ -248,6 +275,8 @@ namespace osu.Game.Tests.Visual.Gameplay } }; } + + protected override ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new TestScrollingHitObjectContainer(); } private class TestDrawableControlPoint : DrawableHitObject @@ -281,22 +310,41 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private class TestDrawableHitObject : DrawableHitObject + private class TestHitObject : HitObject { - public TestDrawableHitObject(double time) - : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty }) - { - Origin = Anchor.Custom; - OriginPosition = new Vector2(75 / 4.0f); + public readonly float Size; - AutoSizeAxes = Axes.Both; + public TestHitObject(float size) + { + Size = size; + } + } + + private class TestDrawableHitObject : DrawableHitObject + { + public TestDrawableHitObject(TestHitObject hitObject) + : base(hitObject) + { + Origin = Anchor.Centre; + Size = new Vector2(hitObject.Size); AddInternal(new Box { - Size = new Vector2(75), + RelativeSizeAxes = Axes.Both, Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1) }); } } + + private class TestScrollingHitObjectContainer : ScrollingHitObjectContainer + { + protected override RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry) + { + if (entry.HitObject is TestHitObject testObject) + return new RectangleF().Inflate(testObject.Size / 2); + + return base.GetConservativeBoundingBox(entry); + } + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index ac73e88468..60ed0012ae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; @@ -24,6 +25,16 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly BindableList scores = new BindableList(); + private readonly Bindable configVisibility = new Bindable(); + + private SoloGameplayLeaderboard leaderboard = null!; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + } + [SetUpSteps] public void SetUpSteps() { @@ -37,11 +48,12 @@ namespace osu.Game.Tests.Visual.Gameplay Id = 2, }; - Child = new SoloGameplayLeaderboard(trackingUser) + Child = leaderboard = new SoloGameplayLeaderboard(trackingUser) { Scores = { BindTarget = scores }, Anchor = Anchor.Centre, Origin = Anchor.Centre, + AlwaysVisible = { Value = false }, Expanded = { Value = true }, }; }); @@ -54,7 +66,24 @@ namespace osu.Game.Tests.Visual.Gameplay { AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v); AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v); - AddSliderStep("combo", 0, 1000, 0, v => scoreProcessor.Combo.Value = v); + AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v); + AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value); + } + + [Test] + public void TestVisibility() + { + AddStep("set config visible true", () => configVisibility.Value = true); + AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); + + AddStep("set config visible false", () => configVisibility.Value = false); + AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0); + + AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true); + AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); + + AddStep("set config visible true", () => configVisibility.Value = true); + AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); } private static List createSampleScores() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index e2c825df0b..a26a7e97be 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected new OutroPlayer Player => (OutroPlayer)base.Player; + private double currentBeatmapDuration; private double currentStoryboardDuration; private bool showResults = true; @@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("set dim level to 0", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0)); AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false); - AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000); + AddStep("set beatmap duration to 0s", () => currentBeatmapDuration = 0); + AddStep("set storyboard duration to 8s", () => currentStoryboardDuration = 8000); AddStep("set ShowResults = true", () => showResults = true); } @@ -151,6 +153,24 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("player exited", () => Stack.CurrentScreen == null); } + [Test] + public void TestPerformExitAfterOutro() + { + CreateTest(() => + { + AddStep("set beatmap duration to 4s", () => currentBeatmapDuration = 4000); + AddStep("set storyboard duration to 1s", () => currentStoryboardDuration = 1000); + }); + + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); + AddStep("exit via pause", () => Player.ExitViaPause()); + AddAssert("player paused", () => !Player.IsResuming); + + AddStep("resume player", () => Player.Resume()); + AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); + AddUntilStep("wait for score shown", () => Player.IsScoreShown); + } + protected override bool AllowFail => true; protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); @@ -160,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap(); - beatmap.HitObjects.Add(new HitCircle()); + beatmap.HitObjects.Add(new HitCircle { StartTime = currentBeatmapDuration }); return beatmap; } @@ -189,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay private event Func failConditions; public OutroPlayer(Func failConditions, bool showResults = true) - : base(false, showResults) + : base(showResults: showResults) { this.failConditions = failConditions; } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs index 3757c49f18..010ed23c9b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestEditDefaultSkin() { - AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.TRIANGLES_SKIN); + AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.ARGON_SKIN); AddStep("open settings", () => { Game.Settings.Show(); }); @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("open skin editor", () => skinEditor.Show()); // Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part). - AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.TRIANGLES_SKIN); + AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.ARGON_SKIN); AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected)); AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType().SingleOrDefault()?.Enabled.Value == true); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 5e76fe1519..003cec0d07 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -9,8 +9,10 @@ using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; @@ -92,6 +94,31 @@ namespace osu.Game.Tests.Visual.Navigation returnToMenu(); } + [Test] + public void TestFromSongSelectWithFilter([Values] ScorePresentType type) + { + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + + AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq"); + AddUntilStep("wait for no results", () => Beatmap.IsDefault); + + var firstImport = importScore(1, new CatchRuleset().RulesetInfo); + presentAndConfirm(firstImport, type); + } + + [Test] + public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type) + { + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + + AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); + + var firstImport = importScore(1, new CatchRuleset().RulesetInfo); + presentAndConfirm(firstImport, type); + } + [Test] public void TestFromSongSelect([Values] ScorePresentType type) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 39432ee059..863b352618 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -189,6 +189,16 @@ Line after image"; }); } + [Test] + public void TestFlag() + { + AddStep("Add flag", () => + { + markdownContainer.CurrentPath = @"https://dev.ppy.sh"; + markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::"; + }); + } + private class TestMarkdownContainer : WikiMarkdownContainer { public LinkInline Link; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs index 558bff2f3c..27cd74bb1f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Online Locale = "en", Subtitle = "Article styling criteria", Markdown = - "# Formatting\n\n*For the writing standards, see: [Article style criteria/Writing](../Writing)*\n\n*Notice: This article uses [RFC 2119](https://tools.ietf.org/html/rfc2119 \"IETF Tools\") to describe requirement levels.*\n\n## Locales\n\nListed below are the properly-supported locales for the wiki:\n\n| File Name | Locale Name | Native Script |\n| :-- | :-- | :-- |\n| `en.md` | English | English |\n| `ar.md` | Arabic | اَلْعَرَبِيَّةُ |\n| `be.md` | Belarusian | Беларуская мова |\n| `bg.md` | Bulgarian | Български |\n| `cs.md` | Czech | Česky |\n| `da.md` | Danish | Dansk |\n| `de.md` | German | Deutsch |\n| `gr.md` | Greek | Ελληνικά |\n| `es.md` | Spanish | Español |\n| `fi.md` | Finnish | Suomi |\n| `fr.md` | French | Français |\n| `hu.md` | Hungarian | Magyar |\n| `id.md` | Indonesian | Bahasa Indonesia |\n| `it.md` | Italian | Italiano |\n| `ja.md` | Japanese | 日本語 |\n| `ko.md` | Korean | 한국어 |\n| `nl.md` | Dutch | Nederlands |\n| `no.md` | Norwegian | Norsk |\n| `pl.md` | Polish | Polski |\n| `pt.md` | Portuguese | Português |\n| `pt-br.md` | Brazilian Portuguese | Português (Brasil) |\n| `ro.md` | Romanian | Română |\n| `ru.md` | Russian | Русский |\n| `sk.md` | Slovak | Slovenčina |\n| `sv.md` | Swedish | Svenska |\n| `th.md` | Thai | ไทย |\n| `tr.md` | Turkish | Türkçe |\n| `uk.md` | Ukrainian | Українська мова |\n| `vi.md` | Vietnamese | Tiếng Việt |\n| `zh.md` | Chinese (Simplified) | 简体中文 |\n| `zh-tw.md` | Traditional Chinese (Taiwan) | 繁體中文(台灣) |\n\n*Note: The website will give readers their selected language's version of an article. If it is not available, the English version will be given.*\n\n### Content parity\n\nTranslations are subject to strict content parity with their English article, in the sense that they must have the same message, regardless of grammar and syntax. Any changes to the translations' meanings must be accompanied by equivalent changes to the English article.\n\nThere are some cases where the content is allowed to differ:\n\n- Articles originally written in a language other than English (in this case, English should act as the translation)\n- Explanations of English words that are common terms in the osu! community\n- External links\n- Tags\n- Subcommunity-specific explanations\n\n## Front matter\n\nFront matter must be placed at the very top of the file. It is written in [YAML](https://en.wikipedia.org/wiki/YAML#Example \"YAML Wikipedia article\") and describes additional information about the article. This must be surrounded by three hyphens (`---`) on the lines above and below it, and an empty line must follow it before the title heading.\n\n### Articles that need help\n\n*Note: Avoid translating English articles with this tag. In addition to this, this tag should be added when the translation needs its own clean up.*\n\nThe `needs_cleanup` tag may be added to articles that need rewriting or formatting help. It is also acceptable to open an issue on GitHub for this purpose. This tag must be written as shown below:\n\n```yaml\nneeds_cleanup: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be done to remove the tag.\n\n### Outdated articles\n\n*Note: Avoid translating English articles with this tag. If the English article has this tag, the translation must also have this tag.*\n\nTranslated articles that are outdated must use the `outdated` tag when the English variant is updated. English articles may also become outdated when the content they contain is misleading or no longer relevant. This tag must be written as shown below:\n\n```yaml\noutdated: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be updated to remove the tag.\n\n### Tagging articles\n\nTags help the website's search engine query articles better. Tags should be written in the same language as the article and include the original list of tags. Tags should use lowercase letters where applicable.\n\nFor example, an article called \"Beatmap discussion\" may include the following tags:\n\n```yaml\ntags:\n - beatmap discussions\n - modding V2\n - MV2\n```\n\n### Translations without reviews\n\n*Note: Wiki maintainers will determine and apply this mark prior to merging.*\n\nSometimes, translations are added to the wiki without review from other native speakers of the language. In this case, the `no_native_review` mark is added to let future translators know that it may need to be checked again. This tag must be written as shown below:\n\n```yaml\nno_native_review: true\n```\n\n## Article naming\n\n*See also: [Folder names](#folder-names) and [Titles](#titles)*\n\nArticle titles should be singular and use sentence case. See [Wikipedia's naming conventions article](https://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(plurals) \"Wikipedia\") for more details.\n\nArticle titles should match the folder name it is in (spaces may replace underscores (`_`) where appropriate). If the folder name changes, the article title should be changed to match it and vice versa.\n\n---\n\nContest and tournament articles are an exception. The folder name must use abbreviations, acronyms, or initialisms. The article's title must be the full name of the contest or tournament.\n\n## Folder and file structure\n\n### Folder names\n\n*See also: [Article naming](#article-naming)*\n\nFolder names must be in English and use sentence case.\n\nFolder names must only use these characters:\n\n- uppercase and lowercase letters\n- numbers\n- underscores (`_`)\n- hyphens (`-`)\n- exclamation marks (`!`)\n\n### Article file names\n\nThe file name of an article can be found in the `File Name` column of the [locales section](#locales). The location of a translated article must be placed in the same folder as the English article.\n\n### Index articles\n\nAn index article must be created if the folder is intended to only hold other articles. Index articles must contain a list of articles that are inside its own folder. They may also contain other information, such as a lead paragraph or descriptions of the linked articles.\n\n### Disambiguation articles\n\n[Disambiguation](/wiki/Disambiguation) articles must be placed in the `/wiki/Disambiguation` folder. The main page must be updated to include the disambiguation article. Refer to [Disambiguation/Mod](/wiki/Disambiguation/Mod) as an example.\n\nRedirects must be updated to have the ambiguous keyword(s) redirect to the disambiguation article.\n\nArticles linked from a disambiguation article must have a [For other uses](#for-other-uses) hatnote.\n\n## HTML\n\nHTML must not be used, with exception for [comments](#comments). The structure of the article must be redone if HTML is used.\n\n### Comments\n\nHTML comments should be used for marking to-dos, but may also be used to annotate text. They should be on their own line, but can be placed inline in a paragraph. If placed inline, the start of the comment must not have a space.\n\nBad example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\nGood example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\n## Editing\n\n### End of line sequence\n\n*Caution: Uploading Markdown files using `CRLF` (carriage return and line feed) via GitHub will result in those files using `CRLF`. To prevent this, set the line ending to `LF` (line feed) before uploading.*\n\nMarkdown files must be checked in using the `LF` end of line sequence.\n\n### Escaping\n\nMarkdown syntax should be escaped as needed. However, article titles are parsed as plain text and so must not be escaped.\n\n### Paragraphs\n\nEach paragraph must be followed by one empty line.\n\n### Line breaks\n\nLine breaks must use a backslash (`\\`).\n\nLine breaks must be used sparingly.\n\n## Hatnote\n\n*Not to be confused with [Notice](#notice).*\n\nHatnotes are short notes placed at the top of an article or section to help readers navigate to related articles or inform them about related topics.\n\nHatnotes must be italicised and be placed immediately after the heading. If multiple hatnotes are used, they must be on the same paragraph separated with a line break.\n\n### Main page\n\n*Main page* hatnotes direct the reader to the main article of a topic. When this hatnote is used, it implies that the section it is on is a summary of what the linked page is about. This hatnote should have only one link. These must be formatted as follows:\n\n```markdown\n*Main page: {article}*\n\n*Main pages: {article} and {article}*\n```\n\n### See also\n\n*See also* hatnotes suggest to readers other points of interest from a given article or section. These must be formatted as follows:\n\n```markdown\n*See also: {article}*\n\n*See also: {article} and {article}*\n```\n\n### For see\n\n*For see* hatnotes are similar to *see also* hatnotes, but are generally more descriptive and direct. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*For {description}, see: {article}`*\n\n*For {description}, see: {article} and {article}`*\n```\n\n### Not to be confused with\n\n*Not to be confused with* hatnotes help distinguish ambiguous or misunderstood article titles or sections. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*Not to be confused with {article}.*\n\n*Not to be confused with {article} or {article}.*\n```\n\n### For other uses\n\n*For other uses* hatnotes are similar to *not to be confused with* hatnotes, but links directly to the [disambiguation article](#disambiguation-articles). This hatnote must only link to the disambiguation article. These must be formatted as follows:\n\n```markdown\n*For other uses, see {disambiguation article}.*\n```\n\n## Notice\n\n*Not to be confused with [Hatnote](#hatnote).*\n\nA notice should be placed where appropriate in a section, but must start off the paragraph and use italics. Notices may contain bolding where appropriate, but should be kept to a minimum. Notices must be written as complete sentences. Thus, unlike most [hatnotes](#hatnotes), must use a full stop (`.`) or an exclamation mark (`!`) if appropriate. Anything within the same paragraph of a notice must also be italicised. These must be formatted as follows:\n\n```markdown\n*Note: {note}.*\n\n*Notice: {notice}.*\n\n*Caution: {caution}.*\n\n*Warning: {warning}.*\n```\n\n- `Note` should be used for factual or trivial details.\n- `Notice` should be used for reminders or to draw attention to something that the reader should be made aware of.\n- `Caution` should be used to warn the reader to avoid unintended consequences.\n- `Warning` should be used to warn the reader that action may be taken against them.\n\n## Emphasising\n\n### Bold\n\nBold must use double asterisks (`**`).\n\nLead paragraphs may bold the first occurrence of the article's title.\n\n### Italics\n\nItalics must use single asterisks (`*`).\n\nNames of work or video games should be italicised. osu!—the game—is exempt from this.\n\nThe first occurrence of an abbreviation, acronym, or initialism may be italicised.\n\nItalics may also be used to provide emphasis or help with readability.\n\n## Headings\n\nAll headings must use sentence case.\n\nHeadings must use the [ATX (hash) style](https://github.github.com/gfm/#atx-headings \"GitHub\") and must have an empty line before and after the heading. The title heading is an exception when it is on the first line. If this is the case, there only needs to be an empty line after the title heading.\n\nHeadings must not exceed a heading level of 5 and must not be used to style or format text.\n\n### Titles\n\n*See also: [Article naming](#article-naming)*\n\n*Caution: Titles are parsed as plain text; they must not be escaped.*\n\nThe first heading in all articles must be a level 1 heading, being the article's title. All headings afterwards must be [section headings](#sections). Titles must not contain formatting, links, or images.\n\nThe title heading must be on the first line, unless [front matter](#front-matter) is being used. If that is the case, the title heading must go after it and have an empty line before the title heading.\n\n### Sections\n\nSection headings must use levels 2 to 5. The section heading proceeding the [title heading](#titles) must be a level 2 heading. Unlike titles, section headings may have small image icons.\n\nSection headings must not skip a heading level (i.e. do not go from a level 2 heading to a level 4 heading) and must not contain formatting or links.\n\n*Notice: On the website, heading levels 4 and 5 will not appear in the table of contents. They cannot be linked to directly either.*\n\n## Lists\n\nLists should not go over 4 levels of indentation and should not have an empty line in between each item.\n\nFor nested lists, bullets or numbers must align with the item content of their parent lists.\n\nThe following example was done incorrectly (take note of the spacing before the bullet):\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\nThe following example was done correctly:\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\n### Bulleted\n\nBulleted lists must use a hyphen (`-`). These must then be followed by one space. (Example shown below.)\n\n```markdown\n- osu!\n - Hit circle\n - Combo number\n - Approach circle\n - Slider\n - Hit circles\n - Slider body\n - Slider ticks\n - Spinner\n- osu!taiko\n```\n\n### Numbered\n\nThe numbers in a numbered list must be incremented to represent their step.\n\n```markdown\n1. Download the osu! installer.\n2. Run the installer.\n 1. To change the installation location, click the text underneath the progression bar.\n 2. The installer will prompt for a new location, choose the installation folder.\n3. osu! will start up once installation is complete.\n4. Sign in.\n```\n\n### Mixed\n\nCombining both bulleted and numbered lists should be done sparingly.\n\n```markdown\n1. Download a skin from the forums.\n2. Load the skin file into osu!.\n - If the file is a `.zip`, unzip it and move the contents into the `Skins/` folder (found in your osu! installation folder).\n - If the file is a `.osk`, open it on your desktop or drag-and-drop it into the game client.\n3. Open osu!, if it is not opened, and select the skin in the options.\n - This may have been completed if you opened the `.osk` file or drag-and-dropped it into the game client.\n```\n\n## Code\n\nThe markup for code is a grave mark (`` ` ``). To put grave marks in code, use double grave marks instead. If the grave mark is at the start or end, pad it with one space. (Example shown below.)\n\n```markdown\n`` ` ``\n`` `Space` ``\n```\n\n### Keyboard keys\n\n*Notice: When denoting the letter itself, and not the keyboard key, use quotation marks instead.*\n\nWhen representing keyboard keys, use capital letters for single characters and title case for modifiers. Use the plus symbol (`+`) (without code) to represent key combinations. (Example shown below.)\n\n```markdown\npippi is spelt with a lowercase \"p\" like peppy.\n\nPress `Ctrl` + `O` to open the open dialog.\n```\n\nWhen representing a space or the spacebar, use `` `Space` ``.\n\n### Button and menu text\n\nWhen copying the text from a menu or button, the letter casing should be copied as it appears. (Example shown below.)\n\n```markdown\nThe `osu!direct` button is visible in the main menu on the right side, if you have an active osu!supporter tag.\n```\n\n### Folder and directory names\n\nWhen copying the name of a folder or directory, the letter casing should be copied as it appears, but prefer lowercased paths when possible. Directory paths must not be absolute (i.e. do not start the directory name from the drive letter or from the root folder). (Example shown below.)\n\n```markdown\nosu! is installed in the `AppData/Local` folder by default, unless specified otherwise during installation.\n```\n\n### Keywords and commands\n\nWhen copying a keyword or command, the letter casing should be copied as it appears or how someone normally would type it. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nAs of now, the `Name` and `Author` commands in the skin configuration file (`skin.ini`) do nothing.\n```\n\n### File names\n\nWhen copying the name of a file, the letter casing should be copied as it appears. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nTo play osu!, double click the `osu!.exe` icon.\n```\n\n### File extensions\n\n*Notice: File formats (not to be confused with file extensions) must be written in capital letters without the prefixed fullstop (`.`).*\n\nFile extensions must be prefixed with a fullstop (`.`) and be followed by the file extension in lowercase letters. (Example shown below.)\n\n```markdown\nThe JPG (or JPEG) file format has the `.jpg` (or `.jpeg`) extension.\n```\n\n### Chat channels\n\nWhen copying the name of a chat channel, start it with a hash (`#`), followed by the channel name in lowercase letters. (Example shown below.)\n\n```markdown\n`#lobby` is where you can advertise your multi room.\n```\n\n## Preformatted text (code blocks)\n\n*Notice: Syntax highlighting for preformatted text is not implemented on the website yet.*\n\nPreformatted text (also known as code blocks) must be fenced using three grave marks. They should set the language identifier for syntax highlighting.\n\n## Links\n\nThere are two types of links: inline and reference. Inline has two styles.\n\nThe following is an example of both inline styles:\n\n```markdown\n[Game Modifiers](/wiki/Game_Modifiers)\n\n\n```\n\nThe following is an example of the reference style:\n\n```markdown\n[Game Modifiers][game mods link]\n\n[game mods link]: /wiki/Game_Modifiers\n```\n\n---\n\nLinks must use the inline style if they are only referenced once. The inline angle brackets style should be avoided. References to reference links must be placed at the bottom of the article.\n\n### Internal links\n\n*Note: Internal links refer to links that stay inside the `https://osu.ppy.sh/` domain.*\n\n#### Wiki links\n\nAll links that point to an wiki article should start with `/wiki/` followed by the path to get to the article you are targeting. Relative links may also be used. Some examples include the following:\n\n```markdown\n[FAQ](/wiki/FAQ)\n[pippi](/wiki/Mascots#-pippi)\n[Beatmaps](../)\n[Pattern](./Pattern)\n```\n\nWiki links must not use redirects and must not have a trailing forward slash (`/`).\n\nBad examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/ASC)\n[Developers](/wiki/Developers/)\n[Developers](/wiki/Developers/#game-client-developers)\n```\n\nGood examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/Article_styling_criteria)\n[Developers](/wiki/Developers)\n[Developers](/wiki/Developers#game-client-developers)\n```\n\n##### Sub-article links\n\nWiki links that point to a sub-article should include the parent article's folder name in its link text. See the following example:\n\n```markdown\n*See also: [Beatmap Editor/Design](/wiki/Beatmap_Editor/Design)*\n```\n\n##### Section links\n\n*Notice: On the website, heading levels 4 and 5 are not given the id attribute. This means that they can not be linked to directly.*\n\nWiki links that point to a section of an article may use the section sign symbol (`§`). See the following example:\n\n```markdown\n*For timing rules, see: [Ranking Criteria § Timing](/wiki/Ranking_Criteria#timing)*\n```\n\n#### Other osu! links\n\nThe URL from the address bar of your web browser should be copied as it is when linking to other osu! web pages. The `https://osu.ppy.sh` part of the URL must be kept.\n\n##### User profiles\n\nAll usernames must be linked on first occurrence. Other occurrences are optional, but must be consistent throughout the entire article for all usernames. If it is difficult to determine the user's id, it may be skipped over.\n\nWhen linking to a user profile, the user's id number must be used. Use the new website (`https://osu.ppy.sh/users/{username})`) to get the user's id.\n\nThe link text of the user link should be the user's current name.\n\n##### Difficulties\n\nWhenever linking to a single difficulty, use this format as the link text:\n\n```\n{artist} - {title} ({creator}) [{difficuty_name}]\n```\n\nThe link must actually link to that difficulty. Beatmap difficulty URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}#{mode}/{BeatmapID}\n```\n\nThe difficulty name may be left outside of the link text, but doing so must be consistent throughout the entire article.\n\n##### Beatmaps\n\nWhenever linking to a beatmap, use this format as the link text:\n\n```\n{artist} - {title} ({creator})\n```\n\nAll beatmap URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}\n```\n\n### External links\n\n*Notice: External links refers to links that go outside the `https://osu.ppy.sh/` domain.*\n\nThe `https` protocol must be used, unless the site does not support it. External links must be a clean and direct link to a reputable source. The link text should be the title of the page it is linking to. The URL from the address bar of your web browser should be copied as it is when linking to other external pages.\n\nThere are no visual differences between external and osu! web links. Due to this, the website name should be included in the title text. See the following example:\n\n```markdown\n*For more information about music theory, see: [Music theory](https://en.wikipedia.org/wiki/Music_theory \"Wikipedia\")*\n```\n\n## Images\n\nThere are two types of image links: inline and reference. Examples:\n\n**Inline style:**\n\n```markdown\n![](/wiki/shared/flag/AU.gif)\n```\n\n**Reference style:**\n\n```markdown\n![][flag_AU]\n\n[flag_AU]: /wiki/shared/flag/AU.gif\n```\n\nImages should use the inline linking style. References to reference links must be placed at the bottom of the article.\n\nImages must be placed in a folder named `img`, located in the article's folder. Images that are used in multiple articles should be stored in the `/wiki/shared/` folder.\n\n### Image caching\n\nImages on the website are cached for up to 60 days. The cached image is matched with the image link's URL.\n\nWhen updating an image, either change the image's name or append a query string to the URL. In both cases, all translations linking to the updated image should also be updated.\n\n### Formats and quality\n\nImages should use the JPG format at quality 8 (80 or 80%, depending on the program). If the image contains transparency or has text that must be readable, use the PNG format instead. If the image contains an animation, the GIF format can be used; however, this should be used sparingly as these may take longer to load or can be bigger then the [max file size](#file-size).\n\n### File size\n\nImages must be under 1 megabyte, otherwise they will fail to load. Downscaling and using JPG at 80% is almost always under the size limit.\n\nAll images should be optimised as much as possible. Use [jpeg-archive](https://github.com/danielgtaylor/jpeg-archive \"GitHub\") to compress JPEG images. For consistency, use the following command for jpeg-archive:\n\n```sh\njpeg-recompress -am smallfry \n```\n\nWhere `` is the file name to be compressed and `` is the compressed file name.\n\n### File names\n\n*Notice: File extensions must use lowercase letters, otherwise they will fail to load!*\n\nUse hyphens (`-`) when spacing words. When naming an image, the file name should be meaningful or descriptive but short.\n\n### Formatting and positioning\n\n*Note: It is currently not possible to float an image or have text wrap around it.*\n\nImages on the website will be centred when it is on a single line, by themself. Otherwise, they will be positioned inline with the paragraph. The following example will place the image in the center:\n\n```markdown\nInstalling osu! is easy. First, download the installer from the download page.\n\n![](img/download-page.jpg)\n\nThen locate the installer and run it.\n```\n\n### Alt text\n\nImages should have alt text unless it is for decorative purposes.\n\n### Captions\n\nImages are given captions on the website if they fulfill these conditions:\n\n1. The image is by itself.\n2. The image is not inside a heading.\n3. The image has title text.\n\nCaptions are assumed via the title text, which must be in plain text. Images with captions are also centred with the image on the website.\n\n### Max image width\n\nThe website's max image width is the width of the article body. Images should be no wider than 800 pixels.\n\n### Annotating images\n\nWhen annotating images, use *Torus Regular*. For Chinese, Korean, Japanese characters, use *Microsoft YaHei*.\n\nAnnotating images should be avoided, as it is difficult for translators (and other editors) to edit them.\n\n#### Translating annotated images\n\nWhen translating annotated images, the localised image version must be placed in the same directory as the original version (i.e. the English version). The filename of a localised image version must start with the original version's name, followed by a hyphen, followed by the locale name (in capital letters). See the following examples:\n\n- `hardrock-mod-vs-easy-mod.jpg` for English\n- `hardrock-mod-vs-easy-mod-DE.jpg` for German\n- `hardrock-mod-vs-easy-mod-ZH-TW.jpg` for Traditional Chinese\n\n### Screenshots of gameplay\n\nAll screenshots of gameplay must be done in the stable build, unless it is for a specific feature that is unavailable in the stable build. You should use the in-game screenshot feature (`F12`).\n\n#### Game client settings\n\n*Note: If you do not want to change your current settings for the wiki, you can move your `osu!..cfg` out of the osu! folder and move it back later.*\n\nYou must set these settings before taking a screenshot of the game client (settings not stated below are assumed to be at their defaults):\n\n- Select language: `English`\n- Prefer metadata in original language: `Enabled`\n- Resolution: `1280x720`\n- Fullscreen mode: `Disabled`\n- Parallax: `Disabled`\n- Menu tips: `Disabled`\n- Seasonal backgrounds: `Never`\n- Always show key overlay: `Enabled`\n- Current skin: `Default` (first option)\n\n*Notice to translators: If you are translating an article containing screenshots of the game, you may set the game client's language to the language you are translating in.*\n\n### Image links\n\nImages must not be part of a link text.\n\nFlag icons next to user links must be separate from the link text. See the following example:\n\n```markdown\n![][flag_AU] [peppy](https://osu.ppy.sh/users/2)\n```\n\n### Flag icons\n\n*For a list of flag icons, see: [issue \\#328](https://github.com/ppy/osu-wiki/issues/328 \"GitHub\")*\n\nThe flag icons use the two letter code (in all capital letters) and end with `.gif`. When adding a flag inline, use this format:\n\n```markdown\n![](/wiki/shared/flag/xx.gif)\n```\n\nWhere `xx` is the [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 \"Wikipedia\") two-lettered country code for the flag.\n\nThe full country name should be added in the title text. The country code in the alternate text is optional, but must be applied to all flag icons in the article.\n\n## Tables\n\nTables on the website only support headings along the first row.\n\nTables must not be beautified (do not pad cells with extra spaces to make their widths uniform). They must have a vertical bar (`|`) on the left and right sides and the text of each cell must be padded with one space on both sides. Empty cells must use a vertical bar (`|`) followed by two spaces then another vertical bar (`|`).\n\nThe delimiter row (the next line after the table heading) must use only three characters per column (and be padded with a space on both sides), which must look like one of the following:\n\n- `:--` (for left align)\n- `:-:` (for centre align)\n- `--:` (for right align)\n\n---\n\nThe following is an example of what a table should look like:\n\n```markdown\n| Team \"Picturesque\" Red | Score | Team \"Statuesque\" Blue | Average Beatmap Stars |\n| :-- | :-: | --: | :-- |\n| **peppy** | 5 - 2 | pippi | 9.3 stars |\n| Aiko | 1 - 6 | **Alisa** | 4.2 stars |\n| Ryūta | 3 - 4 | **Yuzu** | 5.1 stars |\n| **Taikonator** | 7 - 0 | Tama | 13.37 stars |\n| Maria | No Contest | Mocha | |\n```\n\n## Blockquotes\n\nThe blockquote is limited to quoting text from someone. It must not be used to format text otherwise.\n\n## Thematic breaks\n\nThe thematic break (also known as the horizontal rule or line) should be used sparingly. A few uses of the thematic break may include (but is not limited to):\n\n- separating images from text\n- separating multiple images that follow one another\n- shifting the topic within a section\n\nThese must have an empty line before and after the markup. Thematic breaks must use only three hyphens, as depicted below:\n\n```markdown\n---\n```\n" + "# Formatting\n\n*For the writing standards, see: [Article style criteria/Writing](../Writing)*\\\n*Notice: This article uses [RFC 2119](https://tools.ietf.org/html/rfc2119) to describe requirement levels.*\n\n## Locales\n\nListed below are the properly-supported locales for the wiki:\n\n| File Name | Locale Name | Native Script |\n| :-- | :-- | :-- |\n| `en.md` | English | English |\n| `ar.md` | Arabic | \u0627\u064e\u0644\u0652\u0639\u064e\u0631\u064e\u0628\u0650\u064a\u064e\u0651\u0629\u064f |\n| `be.md` | Belarusian | \u0411\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f \u043c\u043e\u0432\u0430 |\n| `bg.md` | Bulgarian | \u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438 |\n| `cs.md` | Czech | \u010cesky |\n| `da.md` | Danish | Dansk |\n| `de.md` | German | Deutsch |\n| `el.md` | Greek | \u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac |\n| `es.md` | Spanish | Espa\u00f1ol |\n| `fi.md` | Finnish | Suomi |\n| `fr.md` | French | Fran\u00e7ais |\n| `hu.md` | Hungarian | Magyar |\n| `id.md` | Indonesian | Bahasa Indonesia |\n| `it.md` | Italian | Italiano |\n| `ja.md` | Japanese | \u65e5\u672c\u8a9e |\n| `ko.md` | Korean | \ud55c\uad6d\uc5b4 |\n| `nl.md` | Dutch | Nederlands |\n| `no.md` | Norwegian | Norsk |\n| `pl.md` | Polish | Polski |\n| `pt.md` | Portuguese | Portugu\u00eas |\n| `pt-br.md` | Brazilian Portuguese | Portugu\u00eas (Brasil) |\n| `ro.md` | Romanian | Rom\u00e2n\u0103 |\n| `ru.md` | Russian | \u0420\u0443\u0441\u0441\u043a\u0438\u0439 |\n| `sk.md` | Slovak | Sloven\u010dina |\n| `sv.md` | Swedish | Svenska |\n| `th.md` | Thai | \u0e44\u0e17\u0e22 |\n| `tr.md` | Turkish | T\u00fcrk\u00e7e |\n| `uk.md` | Ukrainian | \u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430 \u043c\u043e\u0432\u0430 |\n| `vi.md` | Vietnamese | Ti\u1ebfng Vi\u1ec7t |\n| `zh.md` | Chinese (Simplified) | \u7b80\u4f53\u4e2d\u6587 |\n| `zh-tw.md` | Traditional Chinese (Taiwan) | \u7e41\u9ad4\u4e2d\u6587\uff08\u53f0\u7063\uff09 |\n\n*Note: The website will give readers their selected language's version of an article. If it is not available, the English version will be given.*\n\n## Front matter\n\nFront matter must be placed at the very top of the file. It is written in [YAML](https://en.wikipedia.org/wiki/YAML#Example) and describes additional information about the article. This must be surrounded by three hyphens (`---`) on the lines above and below it, and an empty line must follow it before the title heading.\n\n### Articles that need help\n\n*Note: Avoid translating English articles with this tag. In addition to this, this tag should be added when the translation needs its own clean up.*\n\nThe `needs_cleanup` tag may be added to articles that need rewriting or formatting help. It is also acceptable to open an issue on GitHub for this purpose. This tag must be written as shown below:\n\n```yaml\nneeds_cleanup: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be done to remove the tag.\n\n### Outdated articles\n\n*Note: Avoid translating English articles with this tag. If the English article has this tag, the translation must also have this tag.*\n\nEnglish articles may become outdated when the content they contain is misleading or no longer relevant. These should receive an `outdated` tag, which must be written as shown below:\n\n```yaml\noutdated: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be updated to remove the tag.\n\n### Outdated translations\n\nTranslated articles that are outdated must use the `outdated_translation` tag when the English variant is updated, except for minor wording, grammar changes, and the like, that do not affect the meaning of the article.\n\n```yaml\noutdated_translation: true\n```\n\nWhen outdating translations, they must also receive an `outdated_since` tag that points to the first commit where the English version is updated.\n\n```yaml\noutdated_since: 29eac89cd535f8b071ca000af8fe4f0be22bdc9b\n```\n\n### Tagging articles\n\nTags help the website's search engine query articles better. Tags should be written in the same language as the article and include the original list of tags. Tags should use lowercase letters where applicable.\n\nFor example, an article called \"Beatmap discussion\" may include the following tags:\n\n```yaml\ntags:\n - beatmap discussions\n - modding V2\n - MV2\n```\n\n### Translations without reviews\n\n*Note: Wiki maintainers will determine and apply this mark prior to merging.*\n\nSometimes, translations are added to the wiki without review from other native speakers of the language. In this case, the `no_native_review` mark is added to let future translators know that it may need to be checked again. This tag must be written as shown below:\n\n```yaml\nno_native_review: true\n```\n\n## Article naming\n\n*See also: [Folder names](#folder-names) and [Titles](#titles)*\n\nArticle titles should be singular and use sentence case. See [Wikipedia's naming conventions article](https://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(plurals)) for more details.\n\nArticle titles should match the folder name it is in (spaces may replace underscores (`_`) where appropriate). If the folder name changes, the article title should be changed to match it and vice versa.\n\n---\n\nContest and tournament articles are an exception. The folder name must use abbreviations, acronyms, or initialisms. The article's title must be the full name of the contest or tournament.\n\n## Folder and file structure\n\n### Folder names\n\n*See also: [Article naming](#article-naming)*\n\nFolder names must be in English and use sentence case.\n\nFolder names must only use these characters:\n\n- uppercase and lowercase letters\n- numbers\n- underscores (`_`)\n- hyphens (`-`)\n- exclamation marks (`!`)\n\n### Article file names\n\nThe file name of an article can be found in the `File Name` column of the [locales section](#locales). The location of a translated article must be placed in the same folder as the English article.\n\n### Index articles\n\nAn index article must be created if the folder is intended to only hold other articles. Index articles must contain a list of articles that are inside its own folder. They may also contain other information, such as a lead paragraph or descriptions of the linked articles.\n\n### Disambiguation articles\n\n[Disambiguation](/wiki/Disambiguation) articles must be placed in the `/wiki/Disambiguation` folder. The main page must be updated to include the disambiguation article. Refer to [Disambiguation/Mod](/wiki/Disambiguation/Mod) as an example.\n\nRedirects must be updated to have the ambiguous keyword(s) redirect to the disambiguation article.\n\nArticles linked from a disambiguation article must have a [For other uses](#for-other-uses) hatnote.\n\n## HTML\n\nHTML must not be used, with exception for [comments](#comments). The structure of the article must be redone if HTML is used.\n\n### Comments\n\nHTML comments should be used for marking to-dos, but may also be used to annotate text. They should be on their own line, but can be placed inline in a paragraph. If placed inline, the start of the comment must not have a space.\n\nBad example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\nGood example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\n## Editing\n\n### End of line sequence\n\n*Caution: Uploading Markdown files using `CRLF` (carriage return and line feed) via GitHub will result in those files using `CRLF`. To prevent this, set the line ending to `LF` (line feed) before uploading.*\n\nMarkdown files must be checked in using the `LF` end of line sequence.\n\n### Escaping\n\nMarkdown syntax should be escaped as needed. However, article titles are parsed as plain text and so must not be escaped.\n\n### Paragraphs\n\nEach paragraph must be followed by one empty line.\n\n### Line breaks\n\nLine breaks must use a backslash (`\\`).\n\nLine breaks must be used sparingly.\n\n## Hatnote\n\n*Not to be confused with [Notice](#notice).*\n\nHatnotes are short notes placed at the top of an article or section to help readers navigate to related articles or inform them about related topics.\n\nHatnotes must be italicised and be placed immediately after the heading. If multiple hatnotes are used, they must be on the same paragraph separated with a line break.\n\n### Main page\n\n*Main page* hatnotes direct the reader to the main article of a topic. When this hatnote is used, it implies that the section it is on is a summary of what the linked page is about. This hatnote should have only one link. These must be formatted as follows:\n\n```markdown\n*Main page: {article}*\n\n*Main pages: {article} and {article}*\n```\n\n### See also\n\n*See also* hatnotes suggest to readers other points of interest from a given article or section. These must be formatted as follows:\n\n```markdown\n*See also: {article}*\n\n*See also: {article} and {article}*\n```\n\n### For see\n\n*For see* hatnotes are similar to *see also* hatnotes, but are generally more descriptive and direct. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*For {description}, see: {article}*\n\n*For {description}, see: {article} and {article}*\n```\n\n### Not to be confused with\n\n*Not to be confused with* hatnotes help distinguish ambiguous or misunderstood article titles or sections. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*Not to be confused with {article}.*\n\n*Not to be confused with {article} or {article}.*\n```\n\n### For other uses\n\n*For other uses* hatnotes are similar to *not to be confused with* hatnotes, but links directly to the [disambiguation article](#disambiguation-articles). This hatnote must only link to the disambiguation article. These must be formatted as follows:\n\n```markdown\n*For other uses, see {disambiguation article}.*\n```\n\n## Notice\n\n*Not to be confused with [Hatnote](#hatnote).*\n\nA notice should be placed where appropriate in a section, but must start off the paragraph and use italics. Notices may contain bolding where appropriate, but should be kept to a minimum. Notices must be written as complete sentences. Thus, unlike most [hatnotes](#hatnote), notices must use a full stop (`.`) or an exclamation mark (`!`) if appropriate. Anything within the same paragraph of a notice must also be italicised. These must be formatted as follows:\n\n```markdown\n*Note: {note}.*\n\n*Notice: {notice}.*\n\n*Caution: {caution}.*\n\n*Warning: {warning}.*\n```\n\n- `Note` should be used for factual or trivial details.\n- `Notice` should be used for reminders or to draw attention to something that the reader should be made aware of.\n- `Caution` should be used to warn the reader to avoid unintended consequences.\n- `Warning` should be used to warn the reader that action may be taken against them.\n\n## Stacked hatnotes and notices\n\nMultiple hatnotes and notices may be stacked when necessary. When doing this, they must be stacked without blank lines and use trailing backslashes:\n\n```markdown\n*Warning: {warning}.*\\\n*See also: {article}*\n```\n\nIn many cases, it may be more fitting to embed extraneous hatnotes or notices into paragraph text instead of stacking many of them.\n\n## Emphasising\n\n### Bolding\n\nBold text must use double asterisks (`**`).\n\nLead paragraphs may bold the first occurrence of the article's title.\n\n### Italics\n\nItalics must use single asterisks (`*`).\n\nThe first occurrence of an abbreviation, acronym, or initialism may be italicised.\n\nItalics may also be used to provide emphasis or help with readability.\n\nNames of work or video games should be italicised. osu! \u2014 the game \u2014 is exempt from this.\n\nAs an example, when referring to songs in the format of `{artist} - {title}`, the whole part is a reference to the work and should therefore be italicised:\n\n```markdown\n*cYsmix - triangles* is a one of the three intro songs that can be heard when starting the game client.\n```\n\nArtist names are otherwise generally not italicised. This means that in free-form references, only the title should be italicised, because the artist name is then not part of the name of the work:\n\n```markdown\n*Blue Zenith* by xi is an infamous song in the osu! community due to a famous score set by a top player on a certain beatmap.\n```\n\n### Emphasis and links\n\nLinked text appears in a different colour which already provides emphasis, and therefore does not need further emphasis:\n\n```markdown\n[Camellia - OOPARTS](https://cametek.bandcamp.com/track/parts) is an example of a song officially created specifically for osu!, otherwise known as an *osu! original*, since it was specifically commissioned for the osu! World Cup 2020 tiebreaker.\n```\n\nThis however does not apply if the referenced work is not the only part of the link:\n\n```markdown\n[Voltaeyx's beatmap of *TheFatRat - Mayday (feat. Laura Brehm)*](https://osu.ppy.sh/beatmapsets/756794) amassed considerable popularity in 2018 due to its unique overlapping slider patterns.\n```\n\nThe above type of construction should be used sparingly, and must not be used in places with many links, such as tables or lists.\n\n## Headings\n\nAll headings must use sentence case.\n\nHeadings must use the [ATX (hash) style](https://github.github.com/gfm/#atx-headings) and must have an empty line before and after the heading. The title heading is an exception when it is on the first line. If this is the case, there only needs to be an empty line after the title heading.\n\nHeadings must not exceed a heading level of 5 and must not be used to style or format text.\n\n### Titles\n\n*See also: [Article naming](#article-naming)*\\\n*Caution: Titles are parsed as plain text; they must not be escaped.*\n\nThe first heading in all articles must be a level 1 heading, being the article's title. All headings afterwards must be [section headings](#sections). Titles must not contain formatting, links, or images.\n\nThe title heading must be on the first line, unless [front matter](#front-matter) is being used. If that is the case, the title heading must go after it and have an empty line before the title heading.\n\n### Sections\n\nSection headings must use levels 2 to 5. The section heading proceeding the [title heading](#titles) must be a level 2 heading. Unlike titles, section headings may have small image icons.\n\nSection headings must not skip a heading level (i.e. do not go from a level 2 heading to a level 4 heading) and must not contain formatting or links.\n\n*Notice: On the website, heading levels 4 and 5 will not appear in the table of contents.*\n\n### Custom identifiers\n\nIt is possible to redefine a section's identifier, which is used for linking to it directly. Custom identifiers should be used in case the automatically generated ones are too long or contain tricky punctuation marks or images:\n\n\n\n```markdown\n## My cooldown has passed. How do I appeal? {#appeal}\n\n## Common restriction reasons and cooldowns {#common-reasons}\n\n## Ideas for a multiplayer match {id=\u0438\u0434\u0435\u0438-\u0434\u043b\u044f-\u043c\u0443\u043b\u044c\u0442\u0438\u043f\u043b\u0435\u0435\u0440\u0430} \n```\n\nThis feature can also be used for tagging a specific part of the article which doesn't have a heading. Use it sparingly:\n\n```markdown\n> That's it! You're well on your way to becoming an osu! rhythm champion!\n{#tutorial-quote}\n```\n\n## Lists\n\nLists should not go over 4 levels of indentation and should not have an empty line in between each item.\n\nFor nested lists, bullets or numbers must align with the item content of their parent lists.\n\nThe following example was done incorrectly (take note of the spacing before the bullet):\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\nThe following example was done correctly:\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\n### Bulleted\n\nBulleted lists must use a hyphen (`-`). These must then be followed by one space. (Example shown below.)\n\n```markdown\n- osu!\n - Hit circle\n - Combo number\n - Approach circle\n - Slider\n - Hit circles\n - Slider body\n - Slider ticks\n - Spinner\n- osu!taiko\n```\n\n### Numbered\n\nThe numbers in a numbered list must be incremented to represent their step.\n\n```markdown\n1. Download the osu! installer.\n2. Run the installer.\n 1. To change the installation location, click the text underneath the progression bar.\n 2. The installer will prompt for a new location, choose the installation folder.\n3. osu! will start up once installation is complete.\n4. Sign in.\n```\n\n### Mixed\n\nCombining both bulleted and numbered lists should be done sparingly.\n\n```markdown\n1. Download a skin from the forums.\n2. Load the skin file into osu!.\n - If the file is a `.zip`, unzip it and move the contents into the `Skins/` folder (found in your osu! installation folder).\n - If the file is a `.osk`, open it on your desktop or drag-and-drop it into the game client.\n3. Open osu!, if it is not opened, and select the skin in the options.\n - This may have been completed if you opened the `.osk` file or drag-and-dropped it into the game client.\n```\n\n## Code\n\nThe markup for code is a grave mark (`` ` ``). To put grave marks in code, use double grave marks instead. If the grave mark is at the start or end, pad it with one space. (Example shown below.)\n\n```markdown\n`` ` ``\n`` `Space` ``\n```\n\n### Keyboard keys\n\n*Notice: When denoting the letter itself, and not the keyboard key, use quotation marks instead.*\n\nWhen representing keyboard keys, use capital letters for single characters and title case for modifiers. Use the plus symbol (`+`) (without code) to represent key combinations. (Example shown below.)\n\n```markdown\npippi is spelt with a lowercase \"p\" like peppy.\n\nPress `Ctrl` + `O` to open the open dialog.\n```\n\nWhen representing a space or the spacebar, use `` `Space` ``.\n\n### Button and menu text\n\nWhen copying the text from a menu or button, the letter casing should be copied as it appears. (Example shown below.)\n\n```markdown\nThe `osu!direct` button is visible in the main menu on the right side, if you have an active osu!supporter tag.\n```\n\n### Folder and directory names\n\nWhen copying the name of a folder or directory, the letter casing should be copied as it appears, but prefer lowercased paths when possible. Directory paths must not be absolute (i.e. do not start the directory name from the drive letter or from the root folder). (Example shown below.)\n\n```markdown\nosu! is installed in the `AppData/Local` folder by default, unless specified otherwise during installation.\n```\n\n### Keywords and commands\n\nWhen copying a keyword or command, the letter casing should be copied as it appears or how someone normally would type it. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nAs of now, the `Name` and `Author` commands in the skin configuration file (`skin.ini`) do nothing.\n```\n\n### File names\n\nWhen copying the name of a file, the letter casing should be copied as it appears. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nTo play osu!, double click the `osu!.exe` icon.\n```\n\n### File extensions\n\n*Notice: File formats (not to be confused with file extensions) must be written in capital letters without the prefixed fullstop (`.`).*\n\nFile extensions must be prefixed with a fullstop (`.`) and be followed by the file extension in lowercase letters. (Example shown below.)\n\n```markdown\nThe JPG (or JPEG) file format has the `.jpg` (or `.jpeg`) extension.\n```\n\n### Chat channels\n\nWhen copying the name of a chat channel, start it with a hash (`#`), followed by the channel name in lowercase letters. (Example shown below.)\n\n```markdown\n`#lobby` is where you can advertise your multi room.\n```\n\n## Preformatted text (code blocks)\n\n*Notice: Syntax highlighting for preformatted text is not implemented on the website yet.*\n\nPreformatted text (also known as code blocks) must be fenced using three grave marks. They should set the language identifier for syntax highlighting.\n\n## Links\n\n*See also: [Footnotes](#footnotes)*\n\nThere are two types of links: inline and reference. Inline has two styles.\n\nThe following is an example of both inline styles:\n\n```markdown\n[Game modifier](/wiki/Game_modifier)\n\n\n```\n\nThe following is an example of the reference style:\n\n```markdown\n[Game modifier][game mods link]\n\n[game mods link]: /wiki/Game_modifier\n```\n\n---\n\nLinks must use the inline style if they are only referenced once. The inline angle brackets style should be avoided. References to reference links must be placed at the bottom of the article.\n\n### Internal links\n\n*Note: Internal links refer to links that stay inside the `https://osu.ppy.sh/` domain.*\n\n#### Wiki links\n\nAll links that point to an wiki article should start with `/wiki/` followed by the path to get to the article you are targeting. Relative links may also be used. Some examples include the following:\n\n```markdown\n[FAQ](/wiki/FAQ)\n[pippi](/wiki/Mascots#pippi)\n[Beatmaps](../)\n[Pattern](./Pattern)\n```\n\nWiki links must not use redirects and must not have a trailing forward slash (`/`).\n\nBad examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/ASC)\n[Developers](/wiki/Developers/)\n[Developers](/wiki/Developers/#game-client-developers)\n```\n\nGood examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/Article_styling_criteria)\n[Developers](/wiki/People/The_Team/Developers)\n[Developers](/wiki/People/The_Team/Developers#game-client-developers)\n```\n\n##### Sub-article links\n\nWiki links that point to a sub-article should include the parent article's folder name in its link text. See the following example:\n\n```markdown\n*See also: [Beatmap Editor/Design](/wiki/Client/Beatmap_editor/Design)*\n```\n\n##### Section links\n\n*Notice: On the website, heading levels 4 and 5 are not given the id attribute. This means that they can not be linked to directly.*\n\nWiki links that point to a section of an article may use the section sign symbol (`\u00a7`). See the following example:\n\n```markdown\n*For timing rules, see: [Ranking Criteria \u00a7 Timing](/wiki/Ranking_Criteria#timing)*\n```\n\n#### Other osu! links\n\nThe URL from the address bar of your web browser should be copied as it is when linking to other osu! web pages. The `https://osu.ppy.sh` part of the URL must be kept.\n\n##### User profiles\n\nAll usernames must be linked on first occurrence. Other occurrences are optional, but must be consistent throughout the entire article for all usernames. If it is difficult to determine the user's id, it may be skipped over.\n\nWhen linking to a user profile, the user's id number must be used. Use the new website (`https://osu.ppy.sh/users/{username})`) to get the user's id.\n\nThe link text of the user link should be the user's current name.\n\n##### Difficulties\n\nWhenever linking to a single difficulty, use this format as the link text:\n\n```\n{artist} - {title} ({creator}) [{difficulty_name}]\n```\n\nThe link must actually link to that difficulty. Beatmap difficulty URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}#{mode}/{BeatmapID}\n```\n\nThe difficulty name may be left outside of the link text, but doing so must be consistent throughout the entire article.\n\n##### Beatmaps\n\nWhenever linking to a beatmap, use this format as the link text:\n\n```\n{artist} - {title} ({creator})\n```\n\nAll beatmap URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}\n```\n\n### External links\n\n*Notice: External links refers to links that go outside the `https://osu.ppy.sh/` domain.*\n\nThe `https` protocol must be used, unless the site does not support it. External links must be a clean and direct link to a reputable source. The URL from the address bar of your web browser should be copied as it is when linking to other external pages.\n\nThere are no visual differences between external and osu! web links. Due to this, the website name should be included in the title text. See the following example:\n\n```markdown\n*For more information about music theory, see: [Music theory](https://en.wikipedia.org/wiki/Music_theory)*\n```\n\n## Images\n\nThere are two types of image links: inline and reference. Examples:\n\n**Inline style:**\n\n```markdown\n![Gold crown](/wiki/shared/crown-gold.png \"1st place\")\n```\n\n**Reference style:**\n\n```markdown\n![Gold crown][GCrown]\n\n[GCrown]: /wiki/shared/crown-gold.png \"1st place\"\n```\n\nImages should use the inline linking style. Reference link definitions must be placed at the bottom of the article.\n\nAll block images on the page (that have nothing else on the same line) are combined into a single gallery, which can be navigated using arrow icons on both sides of the screen, keyboard shortcuts, or screen swipes on mobile devices.\n\n### Alternative and title text\n\nThe text in the first pair of square brackets (*alternative text*) should describe the image literally. It is used by screen readers or when the image fails to load. It can be omitted if it is identical to the title text or if the image is included only for decorative purposes.\n\nThe text in the quotation marks (*title text*) should give additional context to the image or indicate its meaning. It is displayed as a tooltip when hovering over the image and used as a caption if applicable. It does not support any markdown formatting.\n\n### Display\n\nIf an image is the sole content of a paragraph, it displays as a centred block. Otherwise, it flows with the surrounding inline text.\n\nBlock images with title text display the title text as a caption below the image. Avoid adding [HTML comment](#comments) or any other text on the same line as the image, as this will cause the caption not to be rendered.\n\nBlock images are commonly paired with [infobox](#infoboxes) formatting to reduce their initial size and float them to the side of other content:\n\n```markdown\n::: Infobox\n![](img/mod-response.png \"An example of a response to a mod\")\n:::\n```\n\n### Image caching\n\nImages on the website are cached for up to 60 days. The cached image is matched with the image link's URL.\n\nWhen updating an image, either change the image's name or append a query string to the URL. In both cases, all translations linking to the updated image should also be updated.\n\n### Formats and quality\n\nImages should use the JPG format at quality 8 (80 or 80%, depending on the program). If the image contains transparency or has text that must be readable, use the PNG format instead. If the image contains an animation, the GIF format can be used; however, this should be used sparingly as these may take longer to load or can be bigger then the [max file size](#file-size).\n\n### File size\n\nImages must be under 1 megabyte, otherwise they will fail to load. Downscaling and using JPG at 80% is almost always under the size limit.\n\nAll images should be optimised as much as possible. Use [jpeg-archive](https://github.com/danielgtaylor/jpeg-archive) to compress JPEG images. For consistency, use the following command for jpeg-archive:\n\n```sh\njpeg-recompress -am smallfry \n```\n\nWhere `` is the file name to be compressed and `` is the compressed file name.\n\n### File names\n\n*Notice: File extensions must use lowercase letters, otherwise they will fail to load!*\n\nUse hyphens (`-`) when spacing words. When naming an image, the file name should be meaningful or descriptive but short.\n\nImages must be placed in a folder named `img` under the article's folder. Images that are used in multiple articles should be stored in the `/wiki/shared` folder.\n\n### Max image width\n\nThe website's max image width is the width of the article body. Images should be no wider than 800 pixels.\n\n### Annotating images\n\nWhen annotating images, use *Torus Regular*. For Chinese, Korean, Japanese characters, use *Microsoft YaHei*.\n\nAnnotating images should be avoided, as it is difficult for translators (and other editors) to edit them.\n\n#### Translating annotated images\n\nWhen translating annotated images, the localised image version must be placed in the same directory as the original version (i.e. the English version). The filename of a localised image version must start with the original version's name, followed by a hyphen, followed by the locale name (in capital letters). See the following examples:\n\n- `hardrock-mod-vs-easy-mod.jpg` for English\n- `hardrock-mod-vs-easy-mod-DE.jpg` for German\n- `hardrock-mod-vs-easy-mod-ZH-TW.jpg` for Traditional Chinese\n\n### Screenshots of gameplay\n\nAll screenshots of gameplay must be done in the stable build, unless it is for a specific feature that is unavailable in the stable build. You should use the in-game screenshot feature (`F12`).\n\n#### Game client settings\n\n*Note: If you do not want to change your current settings for the wiki, you can move your `osu!..cfg` out of the osu! folder and move it back later.*\n\nYou must set these settings before taking a screenshot of the game client (settings not stated below are assumed to be at their defaults):\n\n- Select language: `English`\n- Prefer metadata in original language: `Enabled`\n- Resolution: `1280x720`\n- Fullscreen mode: `Disabled`\n- Parallax: `Disabled`\n- Menu tips: `Disabled`\n- Seasonal backgrounds: `Never`\n- Always show key overlay: `Enabled`\n- Current skin: `Default` (first option)\n\n*Notice to translators: If you are translating an article containing screenshots of the game, you may set the game client's language to the language you are translating in.*\n\n### Image links\n\nImages must not be part of a link text.\n\n## Flag icons\n\nThe flag icons use the two letter code (in all capital letters) to match a certain territory. When adding a flag inline, use this format:\n\n```markdown\n::{ flag=XX }::\n```\n\nWhere `XX` is the [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) two-lettered country code for the flag.\n\n## Tables\n\nTables on the website only support headings along the first row.\n\nTables must not be beautified (do not pad cells with extra spaces to make their widths uniform). They must have a vertical bar (`|`) on the left and right sides and the text of each cell must be padded with one space on both sides. Empty cells must use a vertical bar (`|`) followed by two spaces then another vertical bar (`|`).\n\nThe delimiter row (the next line after the table heading) must use only three characters per column (and be padded with a space on both sides), which must look like one of the following:\n\n- `:--` (for left align)\n- `:-:` (for centre align)\n- `--:` (for right align)\n\n---\n\nThe following is an example of what a table should look like:\n\n```markdown\n| Team \"Picturesque\" Red | Score | Team \"Statuesque\" Blue | Average Beatmap Stars |\n| :-- | :-: | --: | :-- |\n| **peppy** | 5 - 2 | pippi | 9.3 stars |\n| Aiko | 1 - 6 | **Alisa** | 4.2 stars |\n| Ry\u016bta | 3 - 4 | **Yuzu** | 5.1 stars |\n| **Taikonator** | 7 - 0 | Tama | 13.37 stars |\n| Maria | No Contest | Mocha | |\n```\n\n## Infoboxes\n\nAn infobox is a fixed-width block which is aligned to the right side of the article. It may contain a relevant image, which explains the surrounding text, or a block of navigation that links to other articles from the same category.\n\nExample use, rendered on the right:\n\n\n\n::: Infobox\n![](/wiki/shared/mods/SD.png \"Sudden Death mod icon\")\n:::\n\n```markdown\n::: Infobox\n![](/wiki/shared/mods/SD.png \"Sudden Death mod icon\")\n:::\n```\n\nInfoboxes should be used with caution in the following cases:\n\n- Short sections: the next section's heading appears below any infoboxes, leaving a large gap after the text.\n- Several images at once: instead, use individual infoboxes for better design.\n\n## Footnotes\n\nFootnotes are short notes located at the end of the page. They are used for citing sources, or providing background information that would otherwise disrupt the flow of the article. Footnotes may contain text formatting and links.\n\nIn the osu! wiki, footnotes are implemented using special syntax (`[^identifier]`). Footnotes can use any identifier, but they will automatically be rendered as superscripts with increasing numbers in order of their first appearance. Translations must not modify identifiers of footnotes.\n\nFootnote references are placed directly after the words, phrases, or sentences they explain, with no space in between. These references must be placed after punctuation, except for parentheses, when they pertain to the contents inside, and dashes.\n\nThe footnotes themselves must be placed in a separate second-level heading at the end of the article. Depending on the content, the heading used may be `References`, `Notes`, or `Notes and references`.\n\nCorrect usage examples:\n\n```markdown\nThe osu! wiki is a project that was meant to replace the old FAQ system.[^wiki-faq] It was named after the rhythm game osu![^osu] and the largest open online encyclopedia, Wikipedia. From the very start, it had attracted skillful translators[^wiki-tl] and editors.\n\n## References\n\n[^wiki-faq]: https://osu.ppy.sh/community/forums/topics/68525\n[^wiki-tl]: https://osu.ppy.sh/community/forums/posts/1177500\n[^osu]: https://osu.ppy.sh/community/forums/posts/1178153\n```\n\n### Citations\n\nCitations, or references, are used to identify a source of information. Citations via footnotes should be preferred over inline links.\n\nReferences should whenever applicable specify author, date, service/platform, and title. The exact format may vary depending on the referenced material with a preference for brevity.\n\nExamples:\n\n```markdown\nThe first version of the osu!api was made available on July 2, 2013.[^api-first-usage] It had received critical acclaim from users.[^api-praise] A new version of API, released several years later, contains many more capabilities.[^api-v2-2020] Endpoint versioning is common among web APIs.[^web-api]\n\n## References\n\n[^api-first-usage]: [Forum thread by peppy (2013-07-02) \"osu!api open beta\"](https://osu.ppy.sh/community/forums/posts/2403913)\n[^api-praise]: [Forum post by Menchi (2013-11-02) in \"osu!api open beta\"](https://osu.ppy.sh/community/forums/posts/2662247)\n[^api-v2-2020]: [Tweet by @ppy (2020-03-20)](https://twitter.com/ppy/status/1263083636363948032)\n[^web-api]: [\"Web API\" on Wikipedia](https://en.wikipedia.org/wiki/Web_API)\n```\n\n### Notes\n\nFootnotes may be used for storing explanations or tangential remarks which cannot be inlined without worsening the article's readability, or are less significant than the article itself. Such footnotes may use free-form text.\n\nExample:\n\n```markdown\nA tournament must not be organised and run by an inexperienced team of unaccomplished and irreputable staff.[^staff]\n\n## Notes\n\n[^staff]: An *inexperienced* staff member is loosely defined as someone who has been playing osu! for less than an hour in total.\n```\n\n## Blockquotes\n\nThe blockquote is limited to [quoting someone or something](/wiki/Article_styling_criteria/Writing#block-quotation). It must not be used to format text otherwise.\n\n```markdown\n> plz enjoy game\n\n\u2014rrtyui\n```\n\n## Thematic breaks\n\nThe thematic break (also known as the horizontal rule or line) should be used sparingly. A few uses of the thematic break may include (but is not limited to):\n\n- separating images from text\n- separating multiple images that follow one another\n- shifting the topic within a section\n\nThese must have an empty line before and after the markup. Thematic breaks must use only three hyphens, as depicted below:\n\n```markdown\n---\n```\n" }; // From https://osu.ppy.sh/api/v2/wiki/en/Article_styling_criteria diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index b3d1966511..6ff53663ba 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -29,11 +29,7 @@ namespace osu.Game.Tests.Visual.Settings { Child = textBox = new SettingsTextBox { - Current = new Bindable - { - Default = "test", - Value = "test" - } + Current = new Bindable("test") }; }); AddUntilStep("wait for loaded", () => textBox.IsLoaded); @@ -59,11 +55,7 @@ namespace osu.Game.Tests.Visual.Settings { Child = textBox = new SettingsTextBox { - Current = new Bindable - { - Default = "test", - Value = "test" - } + Current = new Bindable("test") }; }); AddUntilStep("wait for loaded", () => textBox.IsLoaded); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index dc2a687bd5..3cf6f7febf 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -67,11 +67,7 @@ namespace osu.Game.Tests.Visual.Settings }; [SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))] - public Bindable IntTextBoxBindable { get; } = new Bindable - { - Default = null, - Value = null - }; + public Bindable IntTextBoxBindable { get; } = new Bindable(); } private enum TestEnum diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 89ee6ec7c6..f97ba219cd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.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; @@ -36,10 +34,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Cached(typeof(IDialogOverlay))] private readonly DialogOverlay dialogOverlay; - private ScoreManager scoreManager; - - private RulesetStore rulesetStore; - private BeatmapManager beatmapManager; + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -74,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestLocalScoresDisplay() { - BeatmapInfo beatmapInfo = null; + BeatmapInfo beatmapInfo = null!; AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); @@ -387,7 +384,7 @@ namespace osu.Game.Tests.Visual.SongSelect private class FailableLeaderboard : BeatmapLeaderboard { public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state); - public new void SetScores(IEnumerable scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore); + public new void SetScores(IEnumerable? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index db380cfdb7..c1a9768cf0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.PressButton(MouseButton.Left); }); - AddUntilStep("wait for fetch", () => leaderboard.Scores != null); + AddUntilStep("wait for fetch", () => leaderboard.Scores.Any()); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID)); // "Clean up" @@ -174,7 +174,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestDeleteViaDatabase() { AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); - AddUntilStep("wait for fetch", () => leaderboard.Scores != null); + AddUntilStep("wait for fetch", () => leaderboard.Scores.Any()); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID)); } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index b314d95597..7ed08d8dff 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -107,9 +107,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("start drag", () => { - InputManager.MoveMouseTo(notification.ChildrenOfType().Single()); + InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single()); InputManager.PressButton(MouseButton.Left); - InputManager.MoveMouseTo(notification.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0)); + InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0)); }); AddStep("fling away", () => @@ -123,6 +123,45 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("unread count zero", () => notificationOverlay.UnreadCount.Value == 0); } + [Test] + public void TestProgressNotificationCantBeFlung() + { + bool activated = false; + ProgressNotification notification = null!; + + AddStep("post", () => + { + activated = false; + notificationOverlay.Post(notification = new ProgressNotification + { + Text = @"Uploading to BSS...", + CompletionText = "Uploaded to BSS!", + Activated = () => activated = true, + }); + + progressingNotifications.Add(notification); + }); + + AddStep("start drag", () => + { + InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single()); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0)); + }); + + AddStep("attempt fling", () => + { + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddUntilStep("was not closed", () => !notification.WasClosed); + AddUntilStep("was not cancelled", () => notification.State == ProgressNotificationState.Active); + AddAssert("was not activated", () => !activated); + AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero)); + + AddUntilStep("was completed", () => notification.State == ProgressNotificationState.Completed); + } + [Test] public void TestDismissWithoutActivationCloseButton() { @@ -465,7 +504,7 @@ namespace osu.Game.Tests.Visual.UserInterface { base.Update(); - progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed); + progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed && n.WasClosed); if (progressingNotifications.Count(n => n.State == ProgressNotificationState.Active) < 3) { diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 2572864e09..bdf8cc5136 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,11 +1,11 @@  - - + + - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 5512b26863..bdef46a6b2 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,9 +4,9 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + - + WinExe diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 292caa4397..bcb1d7f961 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -141,18 +141,9 @@ namespace osu.Game.Beatmaps // Handle collections using permissive difficulty name to track difficulties. foreach (var originalBeatmap in original.Beatmaps) { - var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName); - - if (updatedBeatmap == null) - continue; - - var collections = realm.All().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash)); - - foreach (var c in collections) - { - c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash); - c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash); - } + updated.Beatmaps + .FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName)? + .TransferCollectionReferences(realm, originalBeatmap.MD5Hash); } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 32b7f0b29b..6f9df1ba7f 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; @@ -213,6 +214,23 @@ namespace osu.Game.Beatmaps return fileHashX == fileHashY; } + /// + /// When updating a beatmap, its hashes will change. Collections currently track beatmaps by hash, so they need to be updated. + /// This method will handle updating + /// + /// A realm instance in an active write transaction. + /// The previous MD5 hash of the beatmap before update. + public void TransferCollectionReferences(Realm realm, string previousMD5Hash) + { + var collections = realm.All().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(previousMD5Hash)); + + foreach (var c in collections) + { + c.BeatmapMD5Hashes.Remove(previousMD5Hash); + c.BeatmapMD5Hashes.Add(MD5Hash); + } + } + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2c6edb64f8..befc56d244 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -311,6 +311,8 @@ namespace osu.Game.Beatmaps if (existingFileInfo != null) DeleteFile(setInfo, existingFileInfo); + string oldMd5Hash = beatmapInfo.MD5Hash; + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); @@ -327,6 +329,8 @@ namespace osu.Game.Beatmaps setInfo.CopyChangesToRealm(liveBeatmapSet); + beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + ProcessBeatmap?.Invoke((liveBeatmapSet, false)); }); } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index c199d1da59..e6f1609d7f 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -24,7 +24,6 @@ namespace osu.Game.Beatmaps.ControlPoints public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1) { Precision = 0.01, - Default = 1, MinValue = 0.1, MaxValue = 10 }; diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index ead07b4eaa..7c4313a015 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -28,7 +28,6 @@ namespace osu.Game.Beatmaps.ControlPoints public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1) { Precision = 0.01, - Default = 1, MinValue = 0.01, MaxValue = 10 }; diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 78dec67937..c454439c5c 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -45,7 +45,6 @@ namespace osu.Game.Beatmaps.ControlPoints { MinValue = 0, MaxValue = 100, - Default = 100 }; /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 23d4d10fd8..61cc060594 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -49,7 +49,6 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH) { - Default = DEFAULT_BEAT_LENGTH, MinValue = 6, MaxValue = 60000 }; diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index a39766abe1..0f0e72b0ac 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps /// /// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// - void PrepareTrackForPreview(bool looping); + void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 0a51c843cd..393c4ba892 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000); - public void PrepareTrackForPreview(bool looping) + public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0) { Track.Looping = looping; Track.RestartPoint = Metadata.PreviewTime; @@ -125,6 +125,8 @@ namespace osu.Game.Beatmaps Track.RestartPoint = 0.4f * Track.Length; } + + Track.RestartPoint += offsetFromPreviewPoint; } /// diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 54c545e367..1378e1691a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -39,7 +39,7 @@ namespace osu.Game.Configuration { // UI/selection defaults SetDefault(OsuSetting.Ruleset, string.Empty); - SetDefault(OsuSetting.Skin, SkinInfo.TRIANGLES_SKIN.ToString()); + SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString()); SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); SetDefault(OsuSetting.BeatmapDetailModsFilter, false); @@ -120,7 +120,7 @@ namespace osu.Game.Configuration // Gameplay SetDefault(OsuSetting.PositionalHitsounds, true); // replaced by level setting below, can be removed 20220703. SetDefault(OsuSetting.PositionalHitsoundsLevel, 0.2f, 0, 1); - SetDefault(OsuSetting.DimLevel, 0.8, 0, 1, 0.01); + SetDefault(OsuSetting.DimLevel, 0.7, 0, 1, 0.01); SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01); SetDefault(OsuSetting.LightenDuringBreaks, true); @@ -131,6 +131,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); + SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); SetDefault(OsuSetting.FloatingComments, false); @@ -294,6 +295,7 @@ namespace osu.Game.Configuration LightenDuringBreaks, ShowStoryboard, KeyOverlay, + GameplayLeaderboard, PositionalHitsounds, PositionalHitsoundsLevel, AlwaysPlayFirstComboBreak, diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index 877c90a534..6cba8fe819 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -45,7 +45,7 @@ namespace osu.Game.Database public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null); - public void DownloadAsUpdate(TModel originalModel) => Download(originalModel, false, originalModel); + public void DownloadAsUpdate(TModel originalModel, bool minimiseDownloadSize) => Download(originalModel, minimiseDownloadSize, originalModel); protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel) { diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index f1bc0bfe0e..0286815569 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -294,15 +294,38 @@ namespace osu.Game.Database // Log output here will be missing a valid hash in non-batch imports. LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}..."); + List files = new List(); + + if (archive != null) + { + // Import files to the disk store. + // We intentionally delay adding to realm to avoid blocking on a write during disk operations. + foreach (var filenames in getShortenedFilenames(archive)) + { + using (Stream s = archive.GetStream(filenames.original)) + files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened)); + } + } + + using (var transaction = realm.BeginWrite()) + { + // Add all files to realm in one go. + // This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails. + foreach (var file in files) + { + if (!file.File.IsManaged) + realm.Add(file.File, true); + } + + transaction.Commit(); + } + + item.Files.AddRange(files); + item.Hash = ComputeHash(item); + // TODO: do we want to make the transaction this local? not 100% sure, will need further investigation. using (var transaction = realm.BeginWrite()) { - if (archive != null) - // TODO: look into rollback of file additions (or delayed commit). - item.Files.AddRange(createFileInfos(archive, Files, realm)); - - item.Hash = ComputeHash(item); - // TODO: we may want to run this outside of the transaction. Populate(item, archive, realm, cancellationToken); @@ -425,16 +448,6 @@ namespace osu.Game.Database { var fileInfos = new List(); - // import files to manager - foreach (var filenames in getShortenedFilenames(reader)) - { - using (Stream s = reader.GetStream(filenames.original)) - { - var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened); - fileInfos.Add(item); - } - } - return fileInfos; } diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs index ecb152c45c..036b15ea17 100644 --- a/osu.Game/Database/RealmFileStore.cs +++ b/osu.Game/Database/RealmFileStore.cs @@ -40,8 +40,8 @@ namespace osu.Game.Database /// /// The file data stream. /// The realm instance to add to. Should already be in a transaction. - /// - public RealmFile Add(Stream data, Realm realm) + /// Whether the should immediately be added to the underlying realm. If false is provided here, the instance must be manually added. + public RealmFile Add(Stream data, Realm realm, bool addToRealm = true) { string hash = data.ComputeSHA2Hash(); @@ -52,7 +52,7 @@ namespace osu.Game.Database if (!checkFileExistsAndMatchesHash(file)) copyToStore(file, data); - if (!file.IsManaged) + if (addToRealm && !file.IsManaged) realm.Add(file); return file; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 62fefe201d..3855ed6d4e 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -5,6 +5,7 @@ using Markdig; using Markdig.Extensions.AutoLinks; +using Markdig.Extensions.CustomContainers; using Markdig.Extensions.EmphasisExtras; using Markdig.Extensions.Footnotes; using Markdig.Extensions.Tables; @@ -32,6 +33,12 @@ namespace osu.Game.Graphics.Containers.Markdown /// protected virtual bool Autolinks => false; + /// + /// Allows this markdown container to parse custom containers (used for flags and infoboxes). + /// + /// + protected virtual bool CustomContainers => false; + public OsuMarkdownContainer() { LineSpacing = 21; @@ -107,6 +114,9 @@ namespace osu.Game.Graphics.Containers.Markdown if (Autolinks) pipeline = pipeline.UseAutoLinks(); + if (CustomContainers) + pipeline.UseCustomContainers(); + return pipeline.Build(); } } diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs index db8abfb269..9d7b47281f 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs @@ -3,6 +3,9 @@ #nullable disable +using System; +using System.Linq; +using Markdig.Extensions.CustomContainers; using Markdig.Syntax.Inlines; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,6 +14,9 @@ using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Graphics.Containers.Markdown { @@ -33,6 +39,31 @@ namespace osu.Game.Graphics.Containers.Markdown protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic) => CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic)); + protected override void AddCustomComponent(CustomContainerInline inline) + { + if (!(inline.FirstChild is LiteralInline literal)) + { + base.AddCustomComponent(inline); + return; + } + + string[] attributes = literal.Content.ToString().Trim(' ', '{', '}').Split(); + string flagAttribute = attributes.SingleOrDefault(a => a.StartsWith(@"flag", StringComparison.Ordinal)); + + if (flagAttribute == null) + { + base.AddCustomComponent(inline); + return; + } + + string flag = flagAttribute.Split('=').Last().Trim('"'); + + if (!Enum.TryParse(flag, out var countryCode)) + countryCode = CountryCode.Unknown; + + AddDrawable(new DrawableFlag(countryCode) { Size = new Vector2(20, 15) }); + } + private class OsuMarkdownInlineCode : Container { [Resolved] diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 14a041b459..4f079ab435 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -55,13 +55,13 @@ namespace osu.Game.Input.Bindings { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. - reloadMappings(sender.AsQueryable()); + ReloadMappings(sender.AsQueryable()); }); base.LoadComplete(); } - protected override void ReloadMappings() => reloadMappings(queryRealmKeyBindings(realm.Realm)); + protected sealed override void ReloadMappings() => ReloadMappings(queryRealmKeyBindings(realm.Realm)); private IQueryable queryRealmKeyBindings(Realm realm) { @@ -70,7 +70,7 @@ namespace osu.Game.Input.Bindings .Where(b => b.RulesetName == rulesetName && b.Variant == variant); } - private void reloadMappings(IQueryable realmKeyBindings) + protected virtual void ReloadMappings(IQueryable realmKeyBindings) { var defaults = DefaultKeyBindings.ToList(); diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 93e3276f59..385ebd0593 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -89,6 +89,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections"); + /// + /// "Mod presets" + /// + public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"Mod presets"); + /// /// "Name" /// diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs index 74b2c8d892..dd21739096 100644 --- a/osu.Game/Localisation/DebugSettingsStrings.cs +++ b/osu.Game/Localisation/DebugSettingsStrings.cs @@ -44,11 +44,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches"); - /// - /// "Compact realm" - /// - public static LocalisableString CompactRealm => new TranslatableString(getKey(@"compact_realm"), @"Compact realm"); - private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 13cfcc3a19..40f39d927d 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -79,6 +79,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AlwaysShowKeyOverlay => new TranslatableString(getKey(@"key_overlay"), @"Always show key overlay"); + /// + /// "Always show gameplay leaderboard" + /// + public static LocalisableString AlwaysShowGameplayLeaderboard => new TranslatableString(getKey(@"gameplay_leaderboard"), @"Always show gameplay leaderboard"); + /// /// "Always play first combo break sound" /// diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 2aa91f5245..3278b20983 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -64,6 +64,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard"); + /// + /// "You are running the latest release ({0})" + /// + public static LocalisableString RunningLatestRelease(string version) => new TranslatableString(getKey(@"running_latest_release"), @"You are running the latest release ({0})", version); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs index a398eced07..8aa0adf7a0 100644 --- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs @@ -19,6 +19,41 @@ namespace osu.Game.Localisation /// public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory"); + /// + /// "Migration in progress" + /// + public static LocalisableString MigrationInProgress => new TranslatableString(getKey(@"migration_in_progress"), @"Migration in progress"); + + /// + /// "This could take a few minutes depending on the speed of your disk(s)." + /// + public static LocalisableString MigrationDescription => new TranslatableString(getKey(@"migration_description"), @"This could take a few minutes depending on the speed of your disk(s)."); + + /// + /// "Please avoid interacting with the game!" + /// + public static LocalisableString ProhibitedInteractDuringMigration => new TranslatableString(getKey(@"prohibited_interact_during_migration"), @"Please avoid interacting with the game!"); + + /// + /// "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up." + /// + public static LocalisableString FailedCleanupNotification => new TranslatableString(getKey(@"failed_cleanup_notification"), @"Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up."); + + /// + /// "Please select a new location" + /// + public static LocalisableString SelectNewLocation => new TranslatableString(getKey(@"select_new_location"), @"Please select a new location"); + + /// + /// "The target directory already seems to have an osu! install. Use that data instead?" + /// + public static LocalisableString TargetDirectoryAlreadyInstalledOsu => new TranslatableString(getKey(@"target_directory_already_installed_osu"), @"The target directory already seems to have an osu! install. Use that data instead?"); + + /// + /// "To complete this operation, osu! will close. Please open it again to use the new data location." + /// + public static LocalisableString RestartAndReOpenRequiredForCompletion => new TranslatableString(getKey(@"restart_and_re_open_required_for_completion"), @"To complete this operation, osu! will close. Please open it again to use the new data location."); + /// /// "Import beatmaps from stable" /// @@ -84,6 +119,26 @@ namespace osu.Game.Localisation /// public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets"); + /// + /// "Deleted all collections!" + /// + public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!"); + + /// + /// "Deleted all mod presets!" + /// + public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!"); + + /// + /// "Restored all deleted mod presets!" + /// + public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!"); + + /// + /// "Please select your osu!stable install location" + /// + public static LocalisableString StableDirectorySelectHeader => new TranslatableString(getKey(@"stable_directory_select_header"), @"Please select your osu!stable install location"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs index d62d348df9..6c2e3c1f9c 100644 --- a/osu.Game/Localisation/TabletSettingsStrings.cs +++ b/osu.Game/Localisation/TabletSettingsStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString NoTabletDetected => new TranslatableString(getKey(@"no_tablet_detected"), @"No tablet detected!"); + /// + /// "If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps." + /// + public static LocalisableString NoTabletDetectedDescription(string url) => new TranslatableString(getKey(@"no_tablet_detected_description"), @"If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps.", url); + /// /// "Reset to full area" /// diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs index efecc0fc25..3383d21dfc 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -9,27 +9,25 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapRequest : APIRequest { - private readonly IBeatmapInfo beatmapInfo; - - private readonly string filename; + public readonly IBeatmapInfo BeatmapInfo; + public readonly string Filename; public GetBeatmapRequest(IBeatmapInfo beatmapInfo) { - this.beatmapInfo = beatmapInfo; - - filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty; + BeatmapInfo = beatmapInfo; + Filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty; } protected override WebRequest CreateWebRequest() { var request = base.CreateWebRequest(); - if (beatmapInfo.OnlineID > 0) - request.AddParameter(@"id", beatmapInfo.OnlineID.ToString()); - if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash)) - request.AddParameter(@"checksum", beatmapInfo.MD5Hash); - if (!string.IsNullOrEmpty(filename)) - request.AddParameter(@"filename", filename); + if (BeatmapInfo.OnlineID > 0) + request.AddParameter(@"id", BeatmapInfo.OnlineID.ToString()); + if (!string.IsNullOrEmpty(BeatmapInfo.MD5Hash)) + request.AddParameter(@"checksum", BeatmapInfo.MD5Hash); + if (!string.IsNullOrEmpty(Filename)) + request.AddParameter(@"filename", Filename); return request; } diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 5f843e9a7b..d3ddcffaf5 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -228,7 +228,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"rank_history")] private APIRankHistory rankHistory { - set => statistics.RankHistory = value; + set => Statistics.RankHistory = value; } [JsonProperty("badges")] diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 206c15afde..69b4e5b209 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -1,14 +1,11 @@ // 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.Diagnostics; using System.Linq; using System.Threading; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; @@ -54,23 +51,23 @@ namespace osu.Game.Online.Leaderboards private readonly Container placeholderContainer; private readonly UserTopScoreContainer userScoreContainer; - private FillFlowContainer scoreFlowContainer; + private FillFlowContainer? scoreFlowContainer; private readonly LoadingSpinner loading; - private CancellationTokenSource currentFetchCancellationSource; - private CancellationTokenSource currentScoresAsyncLoadCancellationSource; + private CancellationTokenSource? currentFetchCancellationSource; + private CancellationTokenSource? currentScoresAsyncLoadCancellationSource; - private APIRequest fetchScoresRequest; + private APIRequest? fetchScoresRequest; private LeaderboardState state; [Resolved(CanBeNull = true)] - private IAPIProvider api { get; set; } + private IAPIProvider? api { get; set; } private readonly IBindable apiState = new Bindable(); - private TScope scope; + private TScope scope = default!; public TScope Scope { @@ -179,16 +176,21 @@ namespace osu.Game.Online.Leaderboards /// /// The scores to display. /// The user top score, if any. - protected void SetScores(IEnumerable scores, TScoreInfo userScore = default) + protected void SetScores(IEnumerable? scores, TScoreInfo? userScore = default) { this.scores.Clear(); if (scores != null) this.scores.AddRange(scores); - // Schedule needs to be non-delayed here for the weird logic in refetchScores to work. - // If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving". - // This whole flow should be refactored in the future. - Scheduler.Add(applyNewScores, false); + // Non-delayed schedule may potentially run inline (due to IsMainThread check passing) after leaderboard is disposed. + // This is guarded against in BeatmapLeaderboard via web request cancellation, but let's be extra safe. + if (!IsDisposed) + { + // Schedule needs to be non-delayed here for the weird logic in refetchScores to work. + // If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving". + // This whole flow should be refactored in the future. + Scheduler.Add(applyNewScores, false); + } void applyNewScores() { @@ -208,8 +210,7 @@ namespace osu.Game.Online.Leaderboards /// /// /// An responsible for the fetch operation. This will be queued and performed automatically. - [CanBeNull] - protected abstract APIRequest FetchScores(CancellationToken cancellationToken); + protected abstract APIRequest? FetchScores(CancellationToken cancellationToken); protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); @@ -293,7 +294,7 @@ namespace osu.Game.Online.Leaderboards #region Placeholder handling - private Placeholder placeholder; + private Placeholder? placeholder; private void setState(LeaderboardState state) { @@ -320,7 +321,7 @@ namespace osu.Game.Online.Leaderboards placeholder.FadeInFromZero(fade_duration, Easing.OutQuint); } - private Placeholder getPlaceholderFor(LeaderboardState state) + private Placeholder? getPlaceholderFor(LeaderboardState state) { switch (state) { diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index 2d2d82821c..391e8804f0 100644 --- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.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.Threading; using osu.Framework.Bindables; @@ -18,13 +16,15 @@ namespace osu.Game.Online.Leaderboards { private const int duration = 500; - public Bindable Score = new Bindable(); + public Bindable Score = new Bindable(); private readonly Container scoreContainer; private readonly Func createScoreDelegate; protected override bool StartHidden => true; + private CancellationTokenSource? loadScoreCancellation; + public UserTopScoreContainer(Func createScoreDelegate) { this.createScoreDelegate = createScoreDelegate; @@ -65,9 +65,7 @@ namespace osu.Game.Online.Leaderboards Score.BindValueChanged(onScoreChanged); } - private CancellationTokenSource loadScoreCancellation; - - private void onScoreChanged(ValueChangedEvent score) + private void onScoreChanged(ValueChangedEvent score) { var newScore = score.NewValue; diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index a012bf49b6..48d5c0bea9 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Diagnostics; using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -58,7 +58,7 @@ namespace osu.Game.Online.Spectator { await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state); } - catch (HubException exception) + catch (Exception exception) { if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 59c0af1533..1716e48395 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -561,9 +561,20 @@ namespace osu.Game return; } + // This should be able to be performed from song select, but that is disabled for now + // due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios). + // + // As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select. + // This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the + // song select leaderboard). + IEnumerable validScreens = + Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset) + ? new[] { typeof(SongSelect) } + : Array.Empty(); + PerformFromScreen(screen => { - Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset} to match score"); + Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score"); Ruleset.Value = databasedScore.ScoreInfo.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); @@ -578,7 +589,7 @@ namespace osu.Game screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false)); break; } - }, validScreens: new[] { typeof(PlaySongSelect) }); + }, validScreens: validScreens); } public override Task Import(params ImportTask[] imports) diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs index 8be865ee16..c17080f602 100644 --- a/osu.Game/Overlays/Dialog/ConfirmDialog.cs +++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Dialog @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Dialog /// The description of the action to be displayed to the user. /// An action to perform on confirmation. /// An optional action to perform on cancel. - public ConfirmDialog(string message, Action onConfirm, Action onCancel = null) + public ConfirmDialog(LocalisableString message, Action onConfirm, Action onCancel = null) { HeaderText = message; BodyText = "Last chance to turn back"; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 82f49e0aef..4963de7251 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup beatmapSubscription?.Dispose(); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => Schedule(() => { currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count); @@ -139,7 +139,7 @@ namespace osu.Game.Overlays.FirstRunSetup currentlyLoadedBeatmaps.ScaleTo(1.1f) .ScaleTo(1, 1500, Easing.OutQuint); } - } + }); private void downloadTutorial() { diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 9fe2fd5279..63f1aa248c 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -24,7 +25,7 @@ namespace osu.Game.Overlays.Music public class PlaylistOverlay : VisibilityContainer { private const float transition_duration = 600; - private const float playlist_height = 510; + public const float PLAYLIST_HEIGHT = 510; private readonly BindableList> beatmapSets = new BindableList>(); @@ -130,7 +131,7 @@ namespace osu.Game.Overlays.Music filter.Search.HoldFocus = true; Schedule(() => filter.Search.TakeFocus()); - this.ResizeTo(new Vector2(1, playlist_height), transition_duration, Easing.OutQuint); + this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlagFast(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); this.FadeIn(transition_duration, Easing.OutQuint); } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index da87336039..793b7e294f 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -58,12 +58,11 @@ namespace osu.Game.Overlays [Resolved] private RealmAccess realm { get; set; } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now. - // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load(). - beatmap.BindValueChanged(beatmapChanged, true); + base.LoadComplete(); + + beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } @@ -263,8 +262,6 @@ namespace osu.Game.Overlays private IQueryable getBeatmapSets() => realm.Realm.All().Where(s => !s.DeletePending); - private void beatmapChanged(ValueChangedEvent beatmap) => changeBeatmap(beatmap.NewValue); - private void changeBeatmap(WorkingBeatmap newWorking) { // This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index fad8afd371..36548c893c 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -113,9 +113,12 @@ namespace osu.Game.Overlays if (enabled) // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. - notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 100); + notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 250); else + { processingPosts = false; + toastTray.FlushAllToasts(); + } } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index ea654e1272..8be9d2072b 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -68,6 +68,8 @@ namespace osu.Game.Overlays.Notifications public virtual bool Read { get; set; } + protected virtual bool AllowFlingDismiss => true; + public new bool IsDragged => dragContainer.IsDragged; protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check; @@ -315,7 +317,7 @@ namespace osu.Game.Overlays.Notifications protected override void OnDragEnd(DragEndEvent e) { - if (Rotation < -10 || velocity.X < -0.3f) + if (notification.AllowFlingDismiss && (Rotation < -10 || velocity.X < -0.3f)) notification.Close(true); else if (X > 30 || velocity.X > 0.3f) notification.ForwardToOverlay?.Invoke(); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 61bb22041e..4cf47013bd 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Notifications public Func? CancelRequested { get; set; } + protected override bool AllowFlingDismiss => false; + /// /// The function to post completion notifications back to. /// diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 6eddc7da83..900b4bebf0 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -38,6 +38,7 @@ namespace osu.Game.Overlays private const float transition_length = 800; private const float progress_height = 10; private const float bottom_black_area_height = 55; + private const float margin = 10; private Drawable background; private ProgressBar progressBar; @@ -53,6 +54,7 @@ namespace osu.Game.Overlays private Container dragContainer; private Container playerContainer; + private Container playlistContainer; protected override string PopInSampleName => "UI/now-playing-pop-in"; protected override string PopOutSampleName => "UI/now-playing-pop-out"; @@ -69,7 +71,7 @@ namespace osu.Game.Overlays public NowPlayingOverlay() { Width = 400; - Margin = new MarginPadding(10); + Margin = new MarginPadding(margin); } [BackgroundDependencyLoader] @@ -82,7 +84,6 @@ namespace osu.Game.Overlays Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, Children = new Drawable[] { playerContainer = new Container @@ -182,8 +183,13 @@ namespace osu.Game.Overlays } }, }, + playlistContainer = new Container + { + RelativeSizeAxes = Axes.X, + Y = player_height + margin, + } } - } + }, }; } @@ -193,11 +199,10 @@ namespace osu.Game.Overlays { LoadComponentAsync(playlist = new PlaylistOverlay { - RelativeSizeAxes = Axes.X, - Y = player_height + 10, + RelativeSizeAxes = Axes.Both, }, _ => { - dragContainer.Add(playlist); + playlistContainer.Add(playlist); playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); @@ -242,7 +247,18 @@ namespace osu.Game.Overlays { base.UpdateAfterChildren(); - Height = dragContainer.Height; + playlistContainer.Height = MathF.Min(Parent.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT); + + float height = player_height; + + if (playlist != null) + { + height += playlist.DrawHeight; + if (playlist.State.Value == Visibility.Visible) + height += margin; + } + + Height = dragContainer.Height = height; } protected override void Update() diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 42ac4adb34..3afb060e49 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings }, new SettingsButton { - Text = DebugSettingsStrings.CompactRealm, + Text = "Compact realm", Action = () => { // Blocking operations implicitly causes a Compact(). diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index 0893af7d3e..88a27840d8 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -38,6 +38,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Current = config.GetBindable(OsuSetting.KeyOverlay), Keywords = new[] { "counter" }, }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard, + Current = config.GetBindable(OsuSetting.GameplayLeaderboard), + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index 63f0dec953..0f77e6609b 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -44,9 +44,12 @@ namespace osu.Game.Overlays.Settings.Sections.General }, }; - if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale)) - locale = Language.en; - languageSelection.Current.Value = locale; + frameworkLocale.BindValueChanged(locale => + { + if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language)) + language = Language.en; + languageSelection.Current.Value = language; + }, true); languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode()); } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index b68a4fed48..d97cf699e5 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { notifications?.Post(new SimpleNotification { - Text = $"You are running the latest release ({game.Version})", + Text = GeneralSettingsStrings.RunningLatestRelease(game.Version), Icon = FontAwesome.Solid.CheckCircle, }); } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 28642f12a1..59b56522a4 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.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.Collections.Generic; using System.Drawing; using System.Linq; using osu.Framework; @@ -29,37 +28,41 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader; - private FillFlowContainer> scalingSettings; + private FillFlowContainer> scalingSettings = null!; private readonly Bindable currentDisplay = new Bindable(); private readonly IBindableList windowModes = new BindableList(); - private Bindable scalingMode; - private Bindable sizeFullscreen; + private Bindable scalingMode = null!; + private Bindable sizeFullscreen = null!; private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) }); private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable); [Resolved] - private OsuGameBase game { get; set; } + private OsuGameBase game { get; set; } = null!; [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; - private SettingsDropdown resolutionDropdown; - private SettingsDropdown displayDropdown; - private SettingsDropdown windowModeDropdown; + private IWindow? window; - private Bindable scalingPositionX; - private Bindable scalingPositionY; - private Bindable scalingSizeX; - private Bindable scalingSizeY; + private SettingsDropdown resolutionDropdown = null!; + private SettingsDropdown displayDropdown = null!; + private SettingsDropdown windowModeDropdown = null!; + + private Bindable scalingPositionX = null!; + private Bindable scalingPositionY = null!; + private Bindable scalingSizeX = null!; + private Bindable scalingSizeY = null!; private const int transition_duration = 400; [BackgroundDependencyLoader] private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, GameHost host) { + window = host.Window; + scalingMode = osuConfig.GetBindable(OsuSetting.Scaling); sizeFullscreen = config.GetBindable(FrameworkSetting.SizeFullscreen); scalingSizeX = osuConfig.GetBindable(OsuSetting.ScalingSizeX); @@ -67,14 +70,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingPositionX = osuConfig.GetBindable(OsuSetting.ScalingPositionX); scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); - if (host.Window != null) + if (window != null) { - currentDisplay.BindTo(host.Window.CurrentDisplayBindable); - windowModes.BindTo(host.Window.SupportedWindowModes); + currentDisplay.BindTo(window.CurrentDisplayBindable); + windowModes.BindTo(window.SupportedWindowModes); + window.DisplaysChanged += onDisplaysChanged; } - if (host.Window is WindowsWindow windowsWindow) - fullscreenCapability.BindTo(windowsWindow.FullscreenCapability); + if (host.Renderer is IWindowsRenderer windowsRenderer) + fullscreenCapability.BindTo(windowsRenderer.FullscreenCapability); Children = new Drawable[] { @@ -87,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics displayDropdown = new DisplaySettingsDropdown { LabelText = GraphicsSettingsStrings.Display, - Items = host.Window?.Displays, + Items = window?.Displays, Current = currentDisplay, }, resolutionDropdown = new ResolutionSettingsDropdown @@ -202,19 +206,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics // initial update bypasses transforms updateScalingModeVisibility(); - void updateDisplayModeDropdowns() - { - if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen) - resolutionDropdown.Show(); - else - resolutionDropdown.Hide(); - - if (displayDropdown.Items.Count() > 1) - displayDropdown.Show(); - else - displayDropdown.Hide(); - } - void updateScalingModeVisibility() { if (scalingMode.Value == ScalingMode.Off) @@ -225,6 +216,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } } + private void onDisplaysChanged(IEnumerable displays) + { + Scheduler.AddOnce(d => + { + displayDropdown.Items = d; + updateDisplayModeDropdowns(); + }, displays); + } + + private void updateDisplayModeDropdowns() + { + if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen) + resolutionDropdown.Show(); + else + resolutionDropdown.Hide(); + + if (displayDropdown.Items.Count() > 1) + displayDropdown.Show(); + else + displayDropdown.Hide(); + } + private void updateScreenModeWarning() { if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS) @@ -280,7 +293,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }; } - private Drawable preview; + private Drawable? preview; private void showPreview() { @@ -291,6 +304,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics preview.Expire(); } + protected override void Dispose(bool isDisposing) + { + if (window != null) + window.DisplaysChanged -= onDisplaysChanged; + + base.Dispose(isDisposing); + } + private class ScalingPreview : ScalingContainer { public ScalingPreview() diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 9ff47578e9..2fea2e34b2 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -387,14 +387,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (bindTarget != null) bindTarget.IsBinding = true; } - private void updateStoreFromButton(KeyButton button) - { - realm.Run(r => - { - var binding = r.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); - }); - } + private void updateStoreFromButton(KeyButton button) => + realm.WriteAsync(r => r.Find(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString); private void updateIsDefaultValue() { diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 271438ed14..f1e216f538 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, LocalisationManager localisation) { Children = new Drawable[] { @@ -110,11 +110,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) { t.NewLine(); - t.AddText("If your tablet is not detected, please read "); - t.AddLink("this FAQ", LinkAction.External, RuntimeInfo.OS == RuntimeInfo.Platform.Windows + var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? @"https://opentabletdriver.net/Wiki/FAQ/Windows" - : @"https://opentabletdriver.net/Wiki/FAQ/Linux"); - t.AddText(" for troubleshooting steps."); + : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value); + t.AddLinks(formattedSource.Text, formattedSource.Links); } }), } @@ -215,21 +214,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input rotation.BindTo(tabletHandler.Rotation); areaOffset.BindTo(tabletHandler.AreaOffset); - areaOffset.BindValueChanged(val => + areaOffset.BindValueChanged(val => Schedule(() => { offsetX.Value = val.NewValue.X; offsetY.Value = val.NewValue.Y; - }, true); + }), true); offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y)); offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue)); areaSize.BindTo(tabletHandler.AreaSize); - areaSize.BindValueChanged(val => + areaSize.BindValueChanged(val => Schedule(() => { sizeX.Value = val.NewValue.X; sizeY.Value = val.NewValue.Y; - }, true); + }), true); sizeX.BindValueChanged(val => { @@ -255,7 +254,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }); tablet.BindTo(tabletHandler.Tablet); - tablet.BindValueChanged(val => + tablet.BindValueChanged(val => Schedule(() => { Scheduler.AddOnce(updateVisibility); @@ -274,7 +273,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input sizeY.Default = sizeY.MaxValue = tab.Size.Y; areaSize.Default = new Vector2(sizeX.Default, sizeY.Default); - }, true); + }), true); } private void updateVisibility() diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs index 453dbd2e18..beae5a6aad 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class BeatmapSettings : SettingsSubsection { - protected override LocalisableString Header => "Beatmaps"; + protected override LocalisableString Header => CommonStrings.Beatmaps; private SettingsButton importBeatmapsButton = null!; private SettingsButton deleteBeatmapsButton = null!; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs index 5a91213eb8..17fef37e40 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class CollectionsSettings : SettingsSubsection { - protected override LocalisableString Header => "Collections"; + protected override LocalisableString Header => CommonStrings.Collections; private SettingsButton importCollectionsButton = null!; @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private void deleteAllCollections() { realm.Write(r => r.RemoveAll()); - notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" }); + notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllCollections }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index d565576d09..158e1a8aa0 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osuTK; @@ -71,14 +72,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Migration in progress", + Text = MaintenanceSettingsStrings.MigrationInProgress, Font = OsuFont.Default.With(size: 40) }, new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "This could take a few minutes depending on the speed of your disk(s).", + Text = MaintenanceSettingsStrings.MigrationDescription, Font = OsuFont.Default.With(size: 30) }, new LoadingSpinner(true) @@ -89,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Please avoid interacting with the game!", + Text = MaintenanceSettingsStrings.ProhibitedInteractDuringMigration, Font = OsuFont.Default.With(size: 30) }, } @@ -111,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { notifications.Post(new SimpleNotification { - Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.", + Text = MaintenanceSettingsStrings.FailedCleanupNotification, Activated = () => { originalStorage.PresentExternally(); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 0d32e33d87..5de33fdd55 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.IO; +using osu.Game.Localisation; using osu.Game.Overlays.Dialog; namespace osu.Game.Overlays.Settings.Sections.Maintenance @@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public override bool HideOverlaysOnEnter => true; - public override LocalisableString HeaderText => "Please select a new location"; + public override LocalisableString HeaderText => MaintenanceSettingsStrings.SelectNewLocation; protected override void OnSelection(DirectoryInfo directory) { @@ -51,9 +52,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance // Quick test for whether there's already an osu! install at the target path. if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME)) { - dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () => + dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.TargetDirectoryAlreadyInstalledOsu, () => { - dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () => + dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.RestartAndReOpenRequiredForCompletion, () => { (storage as OsuStorage)?.ChangeDataPath(target.FullName); game.Exit(); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs index d35d3ff468..51f6e1bf60 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class ModPresetSettings : SettingsSubsection { - protected override LocalisableString Header => "Mod presets"; + protected override LocalisableString Header => CommonStrings.ModPresets; [Resolved] private RealmAccess realm { get; set; } = null!; @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance deleteAllButton.Enabled.Value = true; if (deletionTask.IsCompletedSuccessfully) - notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all mod presets!" }); + notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllModPresets }); else if (deletionTask.IsFaulted) Logger.Error(deletionTask.Exception, "Failed to delete all mod presets"); } @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance undeleteButton.Enabled.Value = true; if (undeletionTask.IsCompletedSuccessfully) - notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Restored all deleted mod presets!" }); + notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.RestoredAllDeletedModPresets }); else if (undeletionTask.IsFaulted) Logger.Error(undeletionTask.Exception, "Failed to restore mod presets"); } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs index 70e11d45dc..eb2d3171ea 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class ScoreSettings : SettingsSubsection { - protected override LocalisableString Header => "Scores"; + protected override LocalisableString Header => CommonStrings.Scores; private SettingsButton importScoresButton = null!; private SettingsButton deleteScoresButton = null!; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs index c95b1d4dd8..93c65513b7 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class SkinSettings : SettingsSubsection { - protected override LocalisableString Header => "Skins"; + protected override LocalisableString Header => CommonStrings.Skins; private SettingsButton importSkinsButton = null!; private SettingsButton deleteSkinsButton = null!; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 4f1083a75c..15c455416c 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -4,6 +4,7 @@ #nullable disable using System.Linq; +using Markdig.Extensions.CustomContainers; using Markdig.Extensions.Yaml; using Markdig.Syntax; using Markdig.Syntax.Inlines; @@ -16,6 +17,7 @@ namespace osu.Game.Overlays.Wiki.Markdown public class WikiMarkdownContainer : OsuMarkdownContainer { protected override bool Footnotes => true; + protected override bool CustomContainers => true; public string CurrentPath { @@ -26,6 +28,11 @@ namespace osu.Game.Overlays.Wiki.Markdown { switch (markdownObject) { + case CustomContainer: + // infoboxes are parsed into CustomContainer objects, but we don't have support for infoboxes yet. + // todo: add support for infobox. + break; + case YamlFrontMatterBlock yamlFrontMatterBlock: container.Add(new WikiNoticeContainer(yamlFrontMatterBlock)); break; diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index f6abf259e8..f345504ca1 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -46,6 +46,10 @@ namespace osu.Game.Replays.Legacy [IgnoreMember] public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); + [JsonIgnore] + [IgnoreMember] + public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke); + [Key(3)] public ReplayButtonState ButtonState; diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 5a03d66b84..4ff4f66665 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -58,6 +58,9 @@ namespace osu.Game.Rulesets.Configuration pendingWrites.Clear(); } + if (!changed.Any()) + return true; + realm?.Write(r => { foreach (var c in changed) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 697b303689..e7127abcf0 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -36,32 +36,24 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) }; [SettingSource("Initial rate", "The starting speed of the track")] - public BindableNumber InitialRate { get; } = new BindableDouble + public BindableNumber InitialRate { get; } = new BindableDouble(1) { MinValue = 0.5, MaxValue = 2, - Default = 1, - Value = 1, Precision = 0.01 }; [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] - public BindableBool AdjustPitch { get; } = new BindableBool - { - Default = true, - Value = true - }; + public BindableBool AdjustPitch { get; } = new BindableBool(true); /// /// The instantaneous rate of the track. /// Every frame this mod will attempt to smoothly adjust this to meet . /// - public BindableNumber SpeedChange { get; } = new BindableDouble + public BindableNumber SpeedChange { get; } = new BindableDouble(1) { MinValue = min_allowable_rate, MaxValue = max_allowable_rate, - Default = 1, - Value = 1 }; // The two constants below denote the maximum allowable range of rates that `SpeedChange` can take. diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index d8a41ae658..9e4469bf25 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -18,12 +18,10 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Zoooooooooom..."; [SettingSource("Speed increase", "The actual increase to apply")] - public override BindableNumber SpeedChange { get; } = new BindableDouble + public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5) { MinValue = 1.01, MaxValue = 2, - Default = 1.5, - Value = 1.5, Precision = 0.01, }; } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 8d8b97e79e..7d858dca6f 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -18,12 +18,10 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Less zoom..."; [SettingSource("Speed decrease", "The actual decrease to apply")] - public override BindableNumber SpeedChange { get; } = new BindableDouble + public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75) { MinValue = 0.5, MaxValue = 0.99, - Default = 0.75, - Value = 0.75, Precision = 0.01, }; } diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs index 9735d6b536..05ecd37000 100644 --- a/osu.Game/Rulesets/Mods/ModMuted.cs +++ b/osu.Game/Rulesets/Mods/ModMuted.cs @@ -36,34 +36,20 @@ namespace osu.Game.Rulesets.Mods private readonly BindableNumber currentCombo = new BindableInt(); [SettingSource("Enable metronome", "Add a metronome beat to help you keep track of the rhythm.")] - public BindableBool EnableMetronome { get; } = new BindableBool - { - Default = true, - Value = true - }; + public BindableBool EnableMetronome { get; } = new BindableBool(true); [SettingSource("Final volume at combo", "The combo count at which point the track reaches its final volume.", SettingControlType = typeof(SettingsSlider))] - public BindableInt MuteComboCount { get; } = new BindableInt + public BindableInt MuteComboCount { get; } = new BindableInt(100) { - Default = 100, - Value = 100, MinValue = 0, MaxValue = 500, }; [SettingSource("Start muted", "Increase volume as combo builds.")] - public BindableBool InverseMuting { get; } = new BindableBool - { - Default = false, - Value = false - }; + public BindableBool InverseMuting { get; } = new BindableBool(); [SettingSource("Mute hit sounds", "Hit sounds are also muted alongside the track.")] - public BindableBool AffectsHitSounds { get; } = new BindableBool - { - Default = true, - Value = true - }; + public BindableBool AffectsHitSounds { get; } = new BindableBool(true); protected ModMuted() { diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs index 1b9ce833ad..36fbb88943 100644 --- a/osu.Game/Rulesets/Mods/ModNoScope.cs +++ b/osu.Game/Rulesets/Mods/ModNoScope.cs @@ -6,7 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -34,6 +36,11 @@ namespace osu.Game.Rulesets.Mods protected float ComboBasedAlpha; + [SettingSource( + "Hidden at combo", + "The combo count at which the cursor becomes completely hidden", + SettingControlType = typeof(SettingsSlider) + )] public abstract BindableInt HiddenComboCount { get; } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 1f7742b075..178b9fb619 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -18,10 +18,6 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] - public Bindable Seed { get; } = new Bindable - { - Default = null, - Value = null - }; + public Bindable Seed { get; } = new Bindable(); } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 72a7f4b9a3..c4cb41fb6a 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -39,10 +39,8 @@ namespace osu.Game.Rulesets.Mods private double finalRateTime; private double beginRampTime; - public BindableNumber SpeedChange { get; } = new BindableDouble + public BindableNumber SpeedChange { get; } = new BindableDouble(1) { - Default = 1, - Value = 1, Precision = 0.01, }; diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 22ed7c2efd..35a673093b 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods { @@ -17,32 +16,21 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Sloooow doooown..."; public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown; - [SettingSource("Initial rate", "The starting speed of the track")] - public override BindableNumber InitialRate { get; } = new BindableDouble + public override BindableNumber InitialRate { get; } = new BindableDouble(1) { MinValue = 0.51, MaxValue = 2, - Default = 1, - Value = 1, Precision = 0.01, }; - [SettingSource("Final rate", "The speed increase to ramp towards")] - public override BindableNumber FinalRate { get; } = new BindableDouble + public override BindableNumber FinalRate { get; } = new BindableDouble(0.75) { MinValue = 0.5, MaxValue = 1.99, - Default = 0.75, - Value = 0.75, Precision = 0.01, }; - [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] - public override BindableBool AdjustPitch { get; } = new BindableBool - { - Default = true, - Value = true - }; + public override BindableBool AdjustPitch { get; } = new BindableBool(true); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 13ece6d9a3..bbc8382055 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods { @@ -17,32 +16,21 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Can you keep up?"; public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp; - [SettingSource("Initial rate", "The starting speed of the track")] - public override BindableNumber InitialRate { get; } = new BindableDouble + public override BindableNumber InitialRate { get; } = new BindableDouble(1) { MinValue = 0.5, MaxValue = 1.99, - Default = 1, - Value = 1, Precision = 0.01, }; - [SettingSource("Final rate", "The speed increase to ramp towards")] - public override BindableNumber FinalRate { get; } = new BindableDouble + public override BindableNumber FinalRate { get; } = new BindableDouble(1.5) { MinValue = 0.51, MaxValue = 2, - Default = 1.5, - Value = 1.5, Precision = 0.01, }; - [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] - public override BindableBool AdjustPitch { get; } = new BindableBool - { - Default = true, - Value = true - }; + public override BindableBool AdjustPitch { get; } = new BindableBool(true); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs index dec81d9bbd..c2709db747 100644 --- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs +++ b/osu.Game/Rulesets/Objects/BarLineGenerator.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; @@ -40,14 +38,21 @@ namespace osu.Game.Rulesets.Objects for (int i = 0; i < timingPoints.Count; i++) { TimingControlPoint currentTimingPoint = timingPoints[i]; + EffectControlPoint currentEffectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTimingPoint.Time); int currentBeat = 0; - // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object - double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; + // Stop on the next timing point, or if there is no next timing point stop slightly past the last object + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; + double startTime = currentTimingPoint.Time; double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; - for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++) + if (currentEffectPoint.OmitFirstBarLine) + { + startTime += barLength; + } + + for (double t = startTime; Precision.AlmostBigger(endTime, t); t += barLength, currentBeat++) { double roundedTime = Math.Round(t, MidpointRounding.AwayFromZero); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e218da918c..dec68a6c22 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -651,7 +651,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// This does not affect the time offset provided to invocations of . /// - protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0; + public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0; /// /// Applies the of this , notifying responders such as diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index d20e0616e5..0f79e58201 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; @@ -198,6 +199,21 @@ namespace osu.Game.Rulesets.Objects /// [NotNull] protected virtual HitWindows CreateHitWindows() => new HitWindows(); + + public IList CreateSlidingSamples() + { + var slidingSamples = new List(); + + var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + if (normalSample != null) + slidingSamples.Add(normalSample.With("sliderslide")); + + var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); + if (whistleSample != null) + slidingSamples.Add(whistleSample.With("sliderwhistle")); + + return slidingSamples; + } } public static class HitObjectExtensions diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 1a97153f2f..64ac021204 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -230,9 +230,9 @@ namespace osu.Game.Rulesets.UI { } - protected override void ReloadMappings() + protected override void ReloadMappings(IQueryable realmKeyBindings) { - base.ReloadMappings(); + base.ReloadMappings(realmKeyBindings); KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList(); } diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index d8a8a6ccd8..825aba5bc2 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default) { - Default = time_span_default, MinValue = time_span_min, MaxValue = time_span_max }; diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 0ed3ca1e63..37da157cc1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -3,11 +3,12 @@ #nullable disable +using System; using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Framework.Layout; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -126,6 +127,16 @@ namespace osu.Game.Rulesets.UI.Scrolling private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight; + public override void Add(HitObjectLifetimeEntry entry) + { + // Scroll info is not available until loaded. + // The lifetime of all entries will be updated in the first Update. + if (IsLoaded) + setComputedLifetimeStart(entry); + + base.Add(entry); + } + protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) { base.AddDrawable(entry, drawable); @@ -144,7 +155,6 @@ namespace osu.Game.Rulesets.UI.Scrolling private void invalidateHitObject(DrawableHitObject hitObject) { - hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); layoutComputed.Remove(hitObject); } @@ -156,10 +166,8 @@ namespace osu.Game.Rulesets.UI.Scrolling layoutComputed.Clear(); - // Reset lifetime to the conservative estimation. - // If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update. foreach (var entry in Entries) - entry.SetInitialLifetime(); + setComputedLifetimeStart(entry); scrollingInfo.Algorithm.Reset(); @@ -186,35 +194,46 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) + /// + /// Get a conservative maximum bounding box of a corresponding to . + /// It is used to calculate when the hit object appears. + /// + protected virtual RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry) => new RectangleF().Inflate(100); + + private double computeDisplayStartTime(HitObjectLifetimeEntry entry) { - // Origin position may be relative to the parent size - Debug.Assert(hitObject.Parent != null); + RectangleF boundingBox = GetConservativeBoundingBox(entry); + float startOffset = 0; - float originAdjustment = 0.0f; - - // calculate the dimension of the part of the hitobject that should already be visible - // when the hitobject origin first appears inside the scrolling container switch (direction.Value) { - case ScrollingDirection.Up: - originAdjustment = hitObject.OriginPosition.Y; + case ScrollingDirection.Right: + startOffset = boundingBox.Right; break; case ScrollingDirection.Down: - originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y; + startOffset = boundingBox.Bottom; break; case ScrollingDirection.Left: - originAdjustment = hitObject.OriginPosition.X; + startOffset = -boundingBox.Left; break; - case ScrollingDirection.Right: - originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X; + case ScrollingDirection.Up: + startOffset = -boundingBox.Top; break; } - return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); + return scrollingInfo.Algorithm.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength); + } + + private void setComputedLifetimeStart(HitObjectLifetimeEntry entry) + { + double computedStartTime = computeDisplayStartTime(entry); + + // always load the hitobject before its first judgement offset + double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0; + entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime); } private void updateLayoutRecursive(DrawableHitObject hitObject) @@ -232,8 +251,9 @@ namespace osu.Game.Rulesets.UI.Scrolling { updateLayoutRecursive(obj); - // Nested hitobjects don't need to scroll, but they do need accurate positions + // Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime updatePosition(obj, hitObject.HitObject.StartTime); + setComputedLifetimeStart(obj.Entry); } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 078f06b745..34e5b7f9de 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time); - protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); + protected sealed override HitObjectContainer CreateHitObjectContainer() => CreateScrollingHitObjectContainer(); + + protected virtual ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new ScrollingHitObjectContainer(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8b38d9c612..43ad270c16 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -13,7 +12,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -61,25 +59,31 @@ namespace osu.Game.Screens.Edit.Compose.Components { case NotifyCollectionChangedAction.Add: foreach (object o in args.NewItems) - SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select(); + { + if (blueprintMap.TryGetValue((T)o, out var blueprint)) + blueprint.Select(); + } break; case NotifyCollectionChangedAction.Remove: foreach (object o in args.OldItems) - SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect(); + { + if (blueprintMap.TryGetValue((T)o, out var blueprint)) + blueprint.Deselect(); + } break; } }; SelectionHandler = CreateSelectionHandler(); - SelectionHandler.DeselectAll = deselectAll; + SelectionHandler.DeselectAll = DeselectAll; SelectionHandler.SelectedItems.BindTo(SelectedItems); AddRangeInternal(new[] { - DragBox = CreateDragBox(selectBlueprintsFromDragRectangle), + DragBox = CreateDragBox(), SelectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), SelectionHandler.CreateProxy(), @@ -101,12 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [CanBeNull] protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null; - protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect); - - /// - /// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to. - /// - protected virtual bool AllowDeselectionDuringDrag => true; + protected virtual DragBox CreateDragBox() => new DragBox(); protected override bool OnMouseDown(MouseDownEvent e) { @@ -142,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (endClickSelection(e) || ClickedBlueprint != null) return true; - deselectAll(); + DeselectAll(); return true; } @@ -171,11 +170,15 @@ namespace osu.Game.Screens.Edit.Compose.Components finishSelectionMovement(); } + private MouseButtonEvent lastDragEvent; + protected override bool OnDragStart(DragStartEvent e) { if (e.Button == MouseButton.Right) return false; + lastDragEvent = e; + if (movementBlueprints != null) { isDraggingBlueprint = true; @@ -183,30 +186,21 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - if (DragBox.HandleDrag(e)) - { - DragBox.Show(); - return true; - } - - return false; + DragBox.HandleDrag(e); + DragBox.Show(); + return true; } protected override void OnDrag(DragEvent e) { - if (e.Button == MouseButton.Right) - return; - - if (DragBox.State == Visibility.Visible) - DragBox.HandleDrag(e); + lastDragEvent = e; moveCurrentSelection(e); } protected override void OnDragEnd(DragEndEvent e) { - if (e.Button == MouseButton.Right) - return; + lastDragEvent = null; if (isDraggingBlueprint) { @@ -214,8 +208,19 @@ namespace osu.Game.Screens.Edit.Compose.Components changeHandler?.EndChange(); } - if (DragBox.State == Visibility.Visible) - DragBox.Hide(); + DragBox.Hide(); + } + + protected override void Update() + { + base.Update(); + + if (lastDragEvent != null && DragBox.State == Visibility.Visible) + { + lastDragEvent.Target = this; + DragBox.HandleDrag(lastDragEvent); + UpdateSelectionFromDragBox(); + } } /// @@ -233,7 +238,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!SelectionHandler.SelectedBlueprints.Any()) return false; - deselectAll(); + DeselectAll(); return true; } @@ -380,44 +385,39 @@ namespace osu.Game.Screens.Edit.Compose.Components } /// - /// Select all masks in a given rectangle selection area. + /// Select all blueprints in a selection area specified by . /// - /// The rectangle to perform a selection on in screen-space coordinates. - private void selectBlueprintsFromDragRectangle(RectangleF rect) + protected virtual void UpdateSelectionFromDragBox() { + var quad = DragBox.Box.ScreenSpaceDrawQuad; + foreach (var blueprint in SelectionBlueprints) { - // only run when utmost necessary to avoid unnecessary rect computations. - bool isValidForSelection() => blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint); - switch (blueprint.State) { - case SelectionState.NotSelected: - if (isValidForSelection()) - blueprint.Select(); + case SelectionState.Selected: + // Selection is preserved even after blueprint becomes dead. + if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint)) + blueprint.Deselect(); break; - case SelectionState.Selected: - if (AllowDeselectionDuringDrag && !isValidForSelection()) - blueprint.Deselect(); + case SelectionState.NotSelected: + if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint)) + blueprint.Select(); break; } } } /// - /// Selects all s. + /// Select all currently-present items. /// - protected virtual void SelectAll() - { - // Scheduled to allow the change in lifetime to take place. - Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); - } + protected abstract void SelectAll(); /// - /// Deselects all selected s. + /// Deselect all selected items. /// - private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); + protected void DeselectAll() => SelectedItems.Clear(); protected virtual void OnBlueprintSelected(SelectionBlueprint blueprint) { diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 4c37d200bc..ec07da43a0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics.UserInterface; @@ -37,7 +36,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler; private PlacementBlueprint currentPlacement; - private InputManager inputManager; /// /// Positional input must be received outside the container's bounds, @@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - inputManager = GetContainingInputManager(); - Beatmap.HitObjectAdded += hitObjectAdded; // updates to selected are handled for us by SelectionHandler. @@ -220,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition() { - var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position); + var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position); // if no time was found from positional snapping, we should still quantize to the beat. snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 838562719d..905d47533a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -8,7 +8,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Layout; @@ -21,18 +20,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class DragBox : CompositeDrawable, IStateful { - protected readonly Action PerformSelection; - - protected Drawable Box; + public Drawable Box { get; private set; } /// /// Creates a new . /// - /// A delegate that performs drag selection. - public DragBox(Action performSelection) + public DragBox() { - PerformSelection = performSelection; - RelativeSizeAxes = Axes.Both; AlwaysPresent = true; Alpha = 0; @@ -46,30 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual Drawable CreateBox() => new BoxWithBorders(); - private RectangleF? dragRectangle; - /// /// Handle a forwarded mouse event. /// /// The mouse event. - /// Whether the event should be handled and blocking. - public virtual bool HandleDrag(MouseButtonEvent e) + public virtual void HandleDrag(MouseButtonEvent e) { - var dragPosition = e.ScreenSpaceMousePosition; - var dragStartPosition = e.ScreenSpaceMouseDownPosition; - - var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y); - - // We use AABBFloat instead of RectangleF since it handles negative sizes for us - var rec = dragQuad.AABBFloat; - dragRectangle = rec; - - var topLeft = ToLocalSpace(rec.TopLeft); - var bottomRight = ToLocalSpace(rec.BottomRight); - - Box.Position = topLeft; - Box.Size = bottomRight - topLeft; - return true; + Box.Position = Vector2.ComponentMin(e.MouseDownPosition, e.MousePosition); + Box.Size = Vector2.ComponentMax(e.MouseDownPosition, e.MousePosition) - Box.Position; } private Visibility state; @@ -87,19 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - protected override void Update() - { - base.Update(); - - if (dragRectangle != null) - PerformSelection?.Invoke(dragRectangle.Value); - } - - public override void Hide() - { - State = Visibility.Hidden; - dragRectangle = null; - } + public override void Hide() => State = Visibility.Hidden; public override void Show() => State = Visibility.Visible; diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 6a4fe27f04..7423b368b4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -27,6 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private HitObjectUsageEventBuffer usageEventBuffer; + protected InputManager InputManager { get; private set; } + protected EditorBlueprintContainer(HitObjectComposer composer) { Composer = composer; @@ -42,6 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); + InputManager = GetContainingInputManager(); + Beatmap.HitObjectAdded += AddBlueprintFor; Beatmap.HitObjectRemoved += RemoveBlueprintFor; @@ -66,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) => blueprints.OrderBy(b => b.Item.StartTime); - protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning; - protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) { if (!base.ApplySnapResult(blueprints, result)) @@ -133,8 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void SelectAll() { Composer.Playfield.KeepAllAlive(); - - base.SelectAll(); + SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } protected override void OnBlueprintSelected(SelectionBlueprint blueprint) diff --git a/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs new file mode 100644 index 0000000000..58bfaf56ff --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// A that scrolls along with the scrolling playfield. + /// + public class ScrollingDragBox : DragBox + { + public double MinTime { get; private set; } + + public double MaxTime { get; private set; } + + private double? startTime; + + private readonly ScrollingPlayfield playfield; + + public ScrollingDragBox(Playfield playfield) + { + this.playfield = playfield as ScrollingPlayfield ?? throw new ArgumentException("Playfield must be of type {nameof(ScrollingPlayfield)} to use this class.", nameof(playfield)); + } + + public override void HandleDrag(MouseButtonEvent e) + { + base.HandleDrag(e); + + startTime ??= playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMouseDownPosition); + double endTime = playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMousePosition); + + MinTime = Math.Min(startTime.Value, endTime); + MaxTime = Math.Max(startTime.Value, endTime); + + var startPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(startTime.Value)); + var endPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(endTime)); + + switch (playfield.ScrollingInfo.Direction.Value) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + Box.Y = Math.Min(startPos.Y, endPos.Y); + Box.Height = Math.Max(startPos.Y, endPos.Y) - Box.Y; + break; + + case ScrollingDirection.Left: + case ScrollingDirection.Right: + Box.X = Math.Min(startPos.X, endPos.X); + Box.Width = Math.Max(startPos.X, endPos.X) - Box.X; + break; + } + } + + public override void Hide() + { + base.Hide(); + startTime = null; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 8419d3b380..269c19f846 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -305,7 +305,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void DeleteSelected() { - DeleteItems(selectedBlueprints.Select(b => b.Item)); + DeleteItems(SelectedItems.ToArray()); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 721f0c4e3b..a73ada76f5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -304,10 +304,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => editorClock.TrackLength / Zoom; - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) => - new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); + public double TimeAtPosition(float x) + { + return x / Content.DrawWidth * editorClock.TrackLength; + } - private double getTimeFromPosition(Vector2 localPosition) => - (localPosition.X / Content.DrawWidth) * editorClock.TrackLength; + public float PositionAtTime(double time) + { + return (float)(time / editorClock.TrackLength * Content.DrawWidth); + } + + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + { + double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); + return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 590f92d281..b79c2675c8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -13,7 +12,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -31,10 +29,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved(CanBeNull = true)] private Timeline timeline { get; set; } - private DragEvent lastDragEvent; private Bindable placement; private SelectionBlueprint placementBlueprint; + private bool hitObjectDragged; + /// /// Positional input must be received outside the container's bounds, /// in order to handle timeline blueprints which are stacked offscreen. @@ -65,7 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void LoadComplete() { base.LoadComplete(); - DragBox.Alpha = 0; placement = Beatmap.PlacementObject.GetBoundCopy(); placement.ValueChanged += placementChanged; @@ -93,24 +91,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Container> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; - protected override void OnDrag(DragEvent e) + protected override bool OnDragStart(DragStartEvent e) { - handleScrollViaDrag(e); + if (!base.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition)) + return false; - base.OnDrag(e); - } - - protected override void OnDragEnd(DragEndEvent e) - { - base.OnDragEnd(e); - lastDragEvent = null; + return base.OnDragStart(e); } protected override void Update() { - // trigger every frame so drags continue to update selection while playback is scrolling the timeline. - if (lastDragEvent != null) - OnDrag(lastDragEvent); + if (IsDragged || hitObjectDragged) + handleScrollViaDrag(); if (Composer != null && timeline != null) { @@ -165,30 +157,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { return new TimelineHitObjectBlueprint(item) { - OnDragHandled = handleScrollViaDrag, + OnDragHandled = e => hitObjectDragged = e != null, }; } - protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); + protected sealed override DragBox CreateDragBox() => new TimelineDragBox(); - private void handleScrollViaDrag(DragEvent e) + protected override void UpdateSelectionFromDragBox() { - lastDragEvent = e; + var dragBox = (TimelineDragBox)DragBox; + double minTime = dragBox.MinTime; + double maxTime = dragBox.MaxTime; - if (lastDragEvent == null) - return; + SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject)); - if (timeline != null) + foreach (var hitObject in Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected)) { - var timelineQuad = timeline.ScreenSpaceDrawQuad; - float mouseX = e.ScreenSpaceMousePosition.X; - - // scroll if in a drag and dragging outside visible extents - if (mouseX > timelineQuad.TopRight.X) - timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime)); - else if (mouseX < timelineQuad.TopLeft.X) - timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime)); + Composer.Playfield.SetKeepAlive(hitObject, true); + SelectedItems.Add(hitObject); } + + bool shouldBeSelected(HitObject hitObject) + { + double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2; + return minTime <= midTime && midTime <= maxTime; + } + } + + private void handleScrollViaDrag() + { + if (timeline == null) return; + + var timelineQuad = timeline.ScreenSpaceDrawQuad; + float mouseX = InputManager.CurrentState.Mouse.Position.X; + + // scroll if in a drag and dragging outside visible extents + if (mouseX > timelineQuad.TopRight.X) + timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime)); + else if (mouseX < timelineQuad.TopLeft.X) + timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime)); } private class SelectableAreaBackground : CompositeDrawable diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs index c026c169d6..65d9293b7e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs @@ -6,76 +6,44 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineDragBox : DragBox { - // the following values hold the start and end X positions of the drag box in the timeline's local space, - // but with zoom unapplied in order to be able to compensate for positional changes - // while the timeline is being zoomed in/out. - private float? selectionStart; - private float selectionEnd; + public double MinTime { get; private set; } + + public double MaxTime { get; private set; } + + private double? startTime; [Resolved] private Timeline timeline { get; set; } - public TimelineDragBox(Action performSelect) - : base(performSelect) - { - } - protected override Drawable CreateBox() => new Box { RelativeSizeAxes = Axes.Y, Alpha = 0.3f }; - public override bool HandleDrag(MouseButtonEvent e) + public override void HandleDrag(MouseButtonEvent e) { - // The dragbox should only be active if the mouseDownPosition.Y is within this drawable's bounds. - float localY = ToLocalSpace(e.ScreenSpaceMouseDownPosition).Y; - if (DrawRectangle.Top > localY || DrawRectangle.Bottom < localY) - return false; + startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X); + double endTime = timeline.TimeAtPosition(e.MousePosition.X); - selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom; + MinTime = Math.Min(startTime.Value, endTime); + MaxTime = Math.Max(startTime.Value, endTime); - // only calculate end when a transition is not in progress to avoid bouncing. - if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom)) - selectionEnd = e.MousePosition.X / timeline.CurrentZoom; - - updateDragBoxPosition(); - return true; - } - - private void updateDragBoxPosition() - { - if (selectionStart == null) - return; - - float rescaledStart = selectionStart.Value * timeline.CurrentZoom; - float rescaledEnd = selectionEnd * timeline.CurrentZoom; - - Box.X = Math.Min(rescaledStart, rescaledEnd); - Box.Width = Math.Abs(rescaledStart - rescaledEnd); - - var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat; - - // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment. - boxScreenRect.Y -= boxScreenRect.Height; - boxScreenRect.Height *= 2; - - PerformSelection?.Invoke(boxScreenRect); + Box.X = timeline.PositionAtTime(MinTime); + Box.Width = timeline.PositionAtTime(MaxTime) - Box.X; } public override void Hide() { base.Hide(); - selectionStart = null; + startTime = null; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 076ce224f0..c1c9b2493b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -4,11 +4,12 @@ #nullable disable using System; -using System.Linq; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -34,8 +35,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private OsuColour colours { get; set; } - private static readonly int highest_divisor = BindableBeatDivisor.PREDEFINED_DIVISORS.Last(); - public TimelineTickDisplay() { RelativeSizeAxes = Axes.Both; @@ -80,20 +79,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.Update(); - if (timeline != null) + if (timeline == null || DrawWidth <= 0) return; + + (float, float) newRange = ( + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); + + if (visibleRange != newRange) { - var newRange = ( - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); + visibleRange = newRange; - if (visibleRange != newRange) - { - visibleRange = newRange; - - // actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries. - if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick)) - tickCache.Invalidate(); - } + // actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries. + if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick)) + tickCache.Invalidate(); } if (!tickCache.IsValid) @@ -151,6 +149,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } + if (Children.Count > 512) + { + // There should always be a sanely small number of ticks rendered. + // If this assertion triggers, either the zoom logic is broken or a beatmap is + // probably doing weird things... + // + // Let's hope the latter never happens. + // If it does, we can choose to either fix it or ignore it as an outlier. + string message = $"Timeline is rendering many ticks ({Children.Count})"; + + Logger.Log(message); + Debug.Fail(message); + } + int usedDrawables = drawableIndex; // save a few drawables beyond the currently used for edge cases. diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 0fb59a8a1f..839b2b5bad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -56,7 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected ZoomableScrollContainer() : base(Direction.Horizontal) { - base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y }); + base.Content.Add(zoomedContent = new Container + { + RelativeSizeAxes = Axes.Y, + // We must hide content until SetupZoom is called. + // If not, a child component that relies on its DrawWidth (via RelativeSizeAxes) may see a very incorrect value + // momentarily, as noticed in the TimelineTickDisplay, which would render thousands of ticks incorrectly. + Alpha = 0, + }); AddLayout(zoomedContentWidthCache); } @@ -94,6 +101,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline maxZoom = maximum; CurrentZoom = zoomTarget = initial; isZoomSetUp = true; + + zoomedContent.Show(); } /// @@ -118,9 +127,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline CurrentZoom = zoomTarget = newZoom; } - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); if (!zoomedContentWidthCache.IsValid) updateZoomedContentWidth(); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 16c0064e80..839535b99f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -352,6 +352,8 @@ namespace osu.Game.Screens.Edit var updates = batchPendingUpdates.ToArray(); batchPendingUpdates.Clear(); + foreach (var h in deletes) SelectedHitObjects.Remove(h); + foreach (var h in deletes) HitObjectRemoved?.Invoke(h); foreach (var h in inserts) HitObjectAdded?.Invoke(h); foreach (var h in updates) HitObjectUpdated?.Invoke(h); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 409c7d6c8d..dcead4a3a8 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -278,11 +278,11 @@ namespace osu.Game.Screens.Menu if (!UsingThemedIntro) { - initialBeatmap?.PrepareTrackForPreview(false); + initialBeatmap?.PrepareTrackForPreview(false, -2600); drawableTrack.VolumeTo(0); drawableTrack.Restart(); - drawableTrack.VolumeTo(1, 2200, Easing.InCubic); + drawableTrack.VolumeTo(1, 2600, Easing.InCubic); } else { diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 9e56a3a0b7..5ae2158172 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -78,13 +78,17 @@ namespace osu.Game.Screens.Menu if (reverbChannel != null) intro.LogoVisualisation.AddAmplitudeSource(reverbChannel); - Scheduler.AddDelayed(() => - { + if (!UsingThemedIntro) StartTrack(); - // this classic intro loops forever. + Scheduler.AddDelayed(() => + { if (UsingThemedIntro) + { + StartTrack(); + // this classic intro loops forever. Track.Looping = true; + } const float fade_in_time = 200; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 64644be965..ee5ee576d8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.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.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -16,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components public class MatchLeaderboard : Leaderboard { [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + private Bindable roomId { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -33,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override bool IsOnlineScope => true; - protected override APIRequest FetchScores(CancellationToken cancellationToken) + protected override APIRequest? FetchScores(CancellationToken cancellationToken) { if (roomId.Value == null) return null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 15138b18b0..bbdfed0a00 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { private const float disabled_alpha = 0.2f; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + public Action? SettingsApplied; public OsuTextBox NameField = null!; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index c7af87a91d..4e9ab07e4c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.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 osu.Framework.Timing; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 61ea7d68ee..7e5d90bd4f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -105,7 +105,8 @@ namespace osu.Game.Screens.OnlinePlay while (this.IsCurrentScreen()) this.Exit(); } - else + // Also handle the case where a child screen is current (ie. gameplay). + else if (this.GetChildScreen() != null) { this.MakeCurrent(); Schedule(forcefullyExit); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 2d816fbd55..48908fb9a0 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD private bool requiresScroll; private readonly OsuScrollContainer scroll; - private GameplayLeaderboardScore? trackedScore; + public GameplayLeaderboardScore? TrackedScore { get; private set; } private const int max_panels = 8; @@ -42,6 +42,7 @@ namespace osu.Game.Screens.Play.HUD { scroll = new InputDisabledScrollContainer { + ClampExtension = 0, RelativeSizeAxes = Axes.Both, Child = Flow = new FillFlowContainer { @@ -78,10 +79,10 @@ namespace osu.Game.Screens.Play.HUD if (isTracked) { - if (trackedScore != null) + if (TrackedScore != null) throw new InvalidOperationException("Cannot track more than one score."); - trackedScore = drawable; + TrackedScore = drawable; } drawable.Expanded.BindTo(Expanded); @@ -92,14 +93,6 @@ namespace osu.Game.Screens.Play.HUD int displayCount = Math.Min(Flow.Count, max_panels); Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); - // Add extra margin space to flow equal to height of leaderboard. - // This ensures the content is always on screen, but also accounts for the fact that scroll operations - // without animation were actually forcing the local score to a location it can't usually reside at. - // - // Basically, the local score was in the scroll extension region (due to always trying to scroll the - // local player to the middle of the display, but there being no other content below the local player - // to scroll up by). - Flow.Margin = new MarginPadding { Bottom = Height }; requiresScroll = displayCount != Flow.Count; return drawable; @@ -108,7 +101,7 @@ namespace osu.Game.Screens.Play.HUD public void Clear() { Flow.Clear(); - trackedScore = null; + TrackedScore = null; scroll.ScrollToStart(false); } @@ -119,9 +112,10 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); - if (requiresScroll && trackedScore != null) + if (requiresScroll && TrackedScore != null) { - float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; + float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; + scroll.ScrollTo(scrollTarget); } @@ -130,7 +124,7 @@ namespace osu.Game.Screens.Play.HUD float fadeBottom = scroll.Current + scroll.DrawHeight; float fadeTop = scroll.Current + panel_height; - if (scroll.Current <= 0) fadeTop -= panel_height; + if (scroll.IsScrolledToStart()) fadeTop -= panel_height; if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; // logic is mostly shared with Leaderboard, copied here for simplicity. diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 29354e610d..2eec8253b3 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD private const float rank_text_width = 35f; - private const float score_components_width = 85f; - private const float avatar_size = 25f; private const double panel_transition_duration = 500; @@ -161,7 +160,7 @@ namespace osu.Game.Screens.Play.HUD { new Dimension(GridSizeMode.Absolute, rank_text_width), new Dimension(), - new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -286,8 +285,19 @@ namespace osu.Game.Screens.Play.HUD LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); - Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); - Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); + + Accuracy.BindValueChanged(v => + { + accuracyText.Text = v.NewValue.FormatAccuracy(); + updateDetailsWidth(); + }, true); + + Combo.BindValueChanged(v => + { + comboText.Text = $"{v.NewValue}x"; + updateDetailsWidth(); + }, true); + HasQuit.BindValueChanged(_ => updateState()); } @@ -303,13 +313,10 @@ namespace osu.Game.Screens.Play.HUD private void changeExpandedState(ValueChangedEvent expanded) { - scoreComponents.ClearTransforms(); - if (expanded.NewValue) { gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint); - scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint); scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint); usernameText.FadeIn(panel_transition_duration, Easing.OutQuint); @@ -318,11 +325,29 @@ namespace osu.Game.Screens.Play.HUD { gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint); - scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint); scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint); usernameText.FadeOut(text_transition_duration, Easing.OutQuint); } + + updateDetailsWidth(); + } + + private float? scoreComponentsTargetWidth; + + private void updateDetailsWidth() + { + const float score_components_min_width = 88f; + + float newWidth = Expanded.Value + ? Math.Max(score_components_min_width, comboText.DrawWidth + accuracyText.DrawWidth + 25) + : 0; + + if (scoreComponentsTargetWidth == newWidth) + return; + + scoreComponentsTargetWidth = newWidth; + scoreComponents.ResizeWidthTo(newWidth, panel_transition_duration, Easing.OutQuint); } private void updateState() diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 56756249b3..4201b3f4c9 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.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.Collections.Specialized; @@ -12,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -34,19 +33,20 @@ namespace osu.Game.Screens.Play.HUD public readonly SortedDictionary TeamScores = new SortedDictionary(); [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private MultiplayerClient multiplayerClient { get; set; } + private MultiplayerClient multiplayerClient { get; set; } = null!; [Resolved] - private UserLookupCache userLookupCache { get; set; } + private UserLookupCache userLookupCache { get; set; } = null!; + + private Bindable scoringMode = null!; private readonly MultiplayerRoomUser[] playingUsers; - private Bindable scoringMode; private readonly IBindableList playingUserIds = new BindableList(); @@ -126,14 +126,17 @@ namespace osu.Game.Screens.Play.HUD playingUserIds.BindCollectionChanged(playingUsersChanged); } - protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser user, bool isTracked) + protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) { var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked); - if (UserScores[user.OnlineID].Team is int team) + if (user != null) { - leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f); - leaderboardScore.TextColour = Color4.White; + if (UserScores[user.OnlineID].Team is int team) + { + leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f); + leaderboardScore.TextColour = Color4.White; + } } return leaderboardScore; @@ -189,7 +192,7 @@ namespace osu.Game.Screens.Play.HUD { base.Dispose(isDisposing); - if (spectatorClient != null) + if (spectatorClient.IsNotNull()) { foreach (var user in playingUsers) spectatorClient.StopWatchingUser(user.UserID); diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index eff7870d89..ab3cf2950c 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; @@ -13,6 +15,9 @@ namespace osu.Game.Screens.Play.HUD { public class SoloGameplayLeaderboard : GameplayLeaderboard { + private const int duration = 100; + + private readonly Bindable configVisibility = new Bindable(); private readonly IUser trackingUser; public readonly IBindableList Scores = new BindableList(); @@ -26,15 +31,33 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private ScoreManager scoreManager { get; set; } = null!; + /// + /// Whether the leaderboard 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 SoloGameplayLeaderboard(IUser trackingUser) { this.trackingUser = trackingUser; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + } + protected override void LoadComplete() { base.LoadComplete(); Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true); + + // Alpha will be updated via `updateVisibility` below. + Alpha = 0; + + AlwaysVisible.BindValueChanged(_ => updateVisibility()); + configVisibility.BindValueChanged(_ => updateVisibility(), true); } private void showScores() @@ -64,10 +87,13 @@ namespace osu.Game.Screens.Play.HUD local.TotalScore.BindTarget = scoreProcessor.TotalScore; local.Accuracy.BindTarget = scoreProcessor.Accuracy; - local.Combo.BindTarget = scoreProcessor.Combo; + local.Combo.BindTarget = scoreProcessor.HighestCombo; // Local score should always show lower than any existing scores in cases of ties. local.DisplayOrder.Value = long.MaxValue; } + + private void updateVisibility() => + this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 3fbb051c3b..7833c2d7fa 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -39,6 +39,10 @@ namespace osu.Game.Screens.Play /// public float BottomScoringElementsHeight { get; private set; } + // HUD uses AlwaysVisible on child components so they can be in an updated state for next display. + // Without blocking input, this would also allow them to be interacted with in such a state. + public override bool PropagatePositionalInputSubTree => ShowHud.Value; + public readonly KeyCounterDisplay KeyCounter; public readonly ModDisplay ModDisplay; public readonly HoldForMenuButton HoldToQuit; diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index b6094726c0..1b726b0f7b 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Play { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, + Alpha = 0, }; } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 047f25a111..c3c351ac36 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -34,7 +34,6 @@ namespace osu.Game.Screens.Play public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { - Default = 1, MinValue = 0.5, MaxValue = 2, Precision = 0.1, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e31e18046e..68b623b781 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -94,6 +94,11 @@ namespace osu.Game.Screens.Play public int RestartCount; + /// + /// Whether the is currently visible. + /// + public IBindable ShowingOverlayComponents = new Bindable(); + [Resolved] private ScoreManager scoreManager { get; set; } @@ -566,9 +571,6 @@ namespace osu.Game.Screens.Play /// protected void PerformExit(bool showDialogFirst) { - // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). - resultsDisplayDelegate?.Cancel(); - // there is a chance that an exit request occurs after the transition to results has already started. // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). if (!this.IsCurrentScreen()) @@ -603,6 +605,9 @@ namespace osu.Game.Screens.Play } } + // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). + resultsDisplayDelegate?.Cancel(); + // The actual exit is performed if // - the pause / fail dialog was not requested // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). @@ -780,19 +785,11 @@ namespace osu.Game.Screens.Play /// /// /// A final display will only occur once all work is completed in . This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . - /// - /// Calling this method multiple times will have no effect. /// /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) { - if (resultsDisplayDelegate != null) - // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be - // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued - // may take x00 more milliseconds than expected in the very rare edge case). - // - // If required we can handle this more correctly by rescheduling here. - return; + resultsDisplayDelegate?.Cancel(); double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; @@ -1023,6 +1020,8 @@ namespace osu.Game.Screens.Play }); HUDOverlay.IsPlaying.BindTo(localUserPlaying); + ShowingOverlayComponents.BindTo(HUDOverlay.ShowHud); + DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 75da8e7b9d..537f4d811a 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play.PlayerSettings public BindableDouble Current { get; } = new BindableDouble { - Default = 0, - Value = 0, MinValue = -50, MaxValue = 50, Precision = 0.1, diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 12646d656a..14e3123028 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -17,7 +17,6 @@ namespace osu.Game.Screens.Play.PlayerSettings public readonly Bindable UserPlaybackRate = new BindableDouble(1) { - Default = 1, MinValue = 0.5, MaxValue = 2, Precision = 0.1, diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 163b9c593f..5382e283e0 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { + AlwaysVisible = { Value = true }, Scores = { BindTarget = LeaderboardScores } }; diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index d7730737d6..ee19391b89 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -48,6 +48,7 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { + AlwaysVisible = { Value = false }, Scores = { BindTarget = LeaderboardScores } }; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index be77304076..d56b9c23c8 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -76,6 +76,7 @@ namespace osu.Game.Screens.Play req.Success += r => { + Logger.Log($"Score submission token retrieved ({r.ID})"); token = r.ID; tcs.SetResult(true); }; @@ -91,6 +92,11 @@ namespace osu.Game.Screens.Play void handleTokenFailure(Exception exception) { + // This method may be invoked multiple times due to the Task.Wait call above. + // We only really care about the first error. + if (!tcs.TrySetResult(false)) + return; + if (HandleTokenRetrievalFailure(exception)) { if (string.IsNullOrEmpty(exception.Message)) @@ -104,8 +110,12 @@ namespace osu.Game.Screens.Play this.Exit(); }); } - - tcs.SetResult(false); + else + { + // Gameplay is allowed to continue, but we still should keep track of the error. + // In the future, this should be visible to the user in some way. + Logger.Log($"Score submission token retrieval failed ({exception.Message})"); + } } } diff --git a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs index 3c4ed4734b..023d3627b0 100644 --- a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs +++ b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -43,11 +45,15 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.CentreLeft; } + private Bindable preferNoVideo = null!; + [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { const float icon_size = 14; + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); + Content.Anchor = Anchor.CentreLeft; Content.Origin = Anchor.CentreLeft; @@ -104,7 +110,7 @@ namespace osu.Game.Screens.Select.Carousel return; } - beatmapDownloader.DownloadAsUpdate(beatmapSetInfo); + beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value); attachExistingDownload(); }; } diff --git a/osu.Game/Screens/Select/FooterButtonRandom.cs b/osu.Game/Screens/Select/FooterButtonRandom.cs index 1f56915f62..aad7fdff39 100644 --- a/osu.Game/Screens/Select/FooterButtonRandom.cs +++ b/osu.Game/Screens/Select/FooterButtonRandom.cs @@ -138,7 +138,8 @@ namespace osu.Game.Screens.Select return false; } - TriggerClick(); + if (!e.Repeat) + TriggerClick(); return true; } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 0f6ac2b455..161d4847bf 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.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.Diagnostics; @@ -25,11 +23,11 @@ namespace osu.Game.Screens.Select.Leaderboards { public class BeatmapLeaderboard : Leaderboard { - public Action ScoreSelected; + public Action? ScoreSelected; - private BeatmapInfo beatmapInfo; + private BeatmapInfo? beatmapInfo; - public BeatmapInfo BeatmapInfo + public BeatmapInfo? BeatmapInfo { get => beatmapInfo; set @@ -70,24 +68,26 @@ namespace osu.Game.Screens.Select.Leaderboards } [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; [Resolved] - private IBindable ruleset { get; set; } + private IBindable ruleset { get; set; } = null!; [Resolved] - private IBindable> mods { get; set; } + private IBindable> mods { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } + private RulesetStore rulesets { get; set; } = null!; [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; - private IDisposable scoreSubscription; + private IDisposable? scoreSubscription; + + private GetScoresRequest? scoreRetrievalRequest; [BackgroundDependencyLoader] private void load() @@ -102,10 +102,9 @@ namespace osu.Game.Screens.Select.Leaderboards protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; - protected override APIRequest FetchScores(CancellationToken cancellationToken) + protected override APIRequest? FetchScores(CancellationToken cancellationToken) { var fetchBeatmapInfo = BeatmapInfo; - var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; if (fetchBeatmapInfo == null) { @@ -113,13 +112,15 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; + if (Scope == BeatmapLeaderboardScope.Local) { subscribeToLocalScores(fetchBeatmapInfo, cancellationToken); return null; } - if (api?.IsLoggedIn != true) + if (!api.IsLoggedIn) { SetErrorState(LeaderboardState.NotLoggedIn); return null; @@ -143,7 +144,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - IReadOnlyList requestMods = null; + IReadOnlyList? requestMods = null; if (filterMods && !mods.Value.Any()) // add nomod for the request @@ -151,14 +152,14 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); + scoreRetrievalRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); - req.Success += r => SetScores( - scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))), - r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) + scoreRetrievalRequest.Success += response => SetScores( + scoreManager.OrderByTotalScore(response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))), + response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) ); - return req; + return scoreRetrievalRequest; } protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope) @@ -184,7 +185,7 @@ namespace osu.Game.Screens.Select.Leaderboards + $" AND {nameof(ScoreInfo.DeletePending)} == false" , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) + void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception exception) { if (cancellationToken.IsCancellationRequested) return; @@ -218,7 +219,9 @@ namespace osu.Game.Screens.Select.Leaderboards protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + scoreSubscription?.Dispose(); + scoreRetrievalRequest?.Cancel(); } } } diff --git a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs index c9d4dc7811..bacaccd68e 100644 --- a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs +++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs @@ -261,8 +261,8 @@ namespace osu.Game.Screens.Utility string exclusive = "unknown"; - if (host.Window is WindowsWindow windowsWindow) - exclusive = windowsWindow.FullscreenCapability.ToString(); + if (host.Renderer is IWindowsRenderer windowsRenderer) + exclusive = windowsRenderer.FullscreenCapability.ToString(); statusText.Clear(); diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 5a1ef34151..2937b62eec 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -117,6 +117,11 @@ namespace osu.Game.Skinning.Editor return false; } + protected override void SelectAll() + { + SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); + } + /// /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs deleted file mode 100644 index 586882d790..0000000000 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ /dev/null @@ -1,18 +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 - -namespace osu.Game.Skinning -{ - public enum HUDSkinComponents - { - ComboCounter, - ScoreCounter, - AccuracyCounter, - HealthDisplay, - SongProgress, - BarHitErrorMeter, - ColourHitErrorMeter, - } -} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 45454be4a5..3ec0ee6006 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -1,21 +1,34 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Skinning { + /// + /// This class exists for the explicit purpose of ferrying information from ManiaBeatmap in a way LegacySkin can use it. + /// This is because half of the mania legacy skin implementation is in LegacySkin (osu.Game project) which doesn't have visibility + /// over ManiaBeatmap / StageDefinition. + /// public class LegacyManiaSkinConfigurationLookup { - public readonly int Keys; - public readonly LegacyManiaSkinConfigurationLookups Lookup; - public readonly int? TargetColumn; + /// + /// Total columns across all stages. + /// + public readonly int TotalColumns; - public LegacyManiaSkinConfigurationLookup(int keys, LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null) + /// + /// The column which is being looked up. + /// May be null if the configuration does not apply to a specific column. + /// Note that this is the absolute index across all stages. + /// + public readonly int? ColumnIndex; + + public readonly LegacyManiaSkinConfigurationLookups Lookup; + + public LegacyManiaSkinConfigurationLookup(int totalColumns, LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null) { - Keys = keys; + TotalColumns = totalColumns; Lookup = lookup; - TargetColumn = targetColumn; + ColumnIndex = columnIndex; } } @@ -29,6 +42,8 @@ namespace osu.Game.Skinning HitPosition, ScorePosition, LightPosition, + StagePaddingTop, + StagePaddingBottom, HitTargetImage, ShowJudgementLine, KeyImage, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 1e096702b3..646746a0f3 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -128,18 +128,18 @@ namespace osu.Game.Skinning private IBindable? lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup) { - if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) - maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); + if (!maniaConfigurations.TryGetValue(maniaLookup.TotalColumns, out var existing)) + maniaConfigurations[maniaLookup.TotalColumns] = existing = new LegacyManiaSkinConfiguration(maniaLookup.TotalColumns); switch (maniaLookup.Lookup) { case LegacyManiaSkinConfigurationLookups.ColumnWidth: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value])); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value])); case LegacyManiaSkinConfigurationLookups.ColumnSpacing: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.TargetColumn.Value])); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value])); case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); @@ -157,15 +157,15 @@ namespace osu.Game.Skinning return SkinUtils.As(getManiaImage(existing, "LightingN")); case LegacyManiaSkinConfigurationLookups.ExplosionScale: - Debug.Assert(maniaLookup.TargetColumn != null); + Debug.Assert(maniaLookup.ColumnIndex != null); if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m) return SkinUtils.As(new Bindable(1)); - if (existing.ExplosionWidth[maniaLookup.TargetColumn.Value] != 0) - return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + if (existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] != 0) + return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); - return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); case LegacyManiaSkinConfigurationLookups.ColumnLineColour: return SkinUtils.As(getCustomColour(existing, "ColourColumnLine")); @@ -174,53 +174,53 @@ namespace osu.Game.Skinning return SkinUtils.As(getCustomColour(existing, "ColourJudgementLine")); case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.TargetColumn + 1}")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.ColumnIndex + 1}")); case LegacyManiaSkinConfigurationLookups.ColumnLightColour: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.TargetColumn + 1}")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.ColumnIndex + 1}")); case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: return SkinUtils.As(new Bindable(existing.MinimumColumnWidth)); case LegacyManiaSkinConfigurationLookups.NoteImage: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}")); case LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}H")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}H")); case LegacyManiaSkinConfigurationLookups.HoldNoteTailImage: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}T")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}T")); case LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}L")); case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: return SkinUtils.As(getManiaImage(existing, "LightingL")); case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: - Debug.Assert(maniaLookup.TargetColumn != null); + Debug.Assert(maniaLookup.ColumnIndex != null); if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m) return SkinUtils.As(new Bindable(1)); - if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) - return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + if (existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); - return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); case LegacyManiaSkinConfigurationLookups.KeyImage: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.ColumnIndex}")); case LegacyManiaSkinConfigurationLookups.KeyImageDown: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}D")); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.ColumnIndex}D")); case LegacyManiaSkinConfigurationLookups.LeftStageImage: return SkinUtils.As(getManiaImage(existing, "StageLeft")); @@ -238,12 +238,12 @@ namespace osu.Game.Skinning return SkinUtils.As(getManiaImage(existing, "StageHint")); case LegacyManiaSkinConfigurationLookups.LeftLineWidth: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value])); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value])); case LegacyManiaSkinConfigurationLookups.RightLineWidth: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value + 1])); + Debug.Assert(maniaLookup.ColumnIndex != null); + return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.ColumnIndex.Value + 1])); case LegacyManiaSkinConfigurationLookups.Hit0: case LegacyManiaSkinConfigurationLookups.Hit50: diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs index 4d0da9597b..d1a1edcd03 100644 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ b/osu.Game/Storyboards/CommandTimeline.cs @@ -27,7 +27,10 @@ namespace osu.Game.Storyboards public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue) { if (endTime < startTime) - return; + { + (startTime, endTime) = (endTime, startTime); + (startValue, endValue) = (endValue, startValue); + } commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue }); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index fa7ade2c07..ef4539ba56 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -135,25 +135,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); return true; + case GetBeatmapRequest getBeatmapRequest: + { + getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.BeatmapInfo.OnlineID).Single()); + return true; + } + case GetBeatmapsRequest getBeatmapsRequest: { - var result = new List(); - - foreach (int id in getBeatmapsRequest.BeatmapIds) - { - var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); - - if (baseBeatmap == null) - { - baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; - baseBeatmap.OnlineID = id; - baseBeatmap.BeatmapSet!.OnlineID = id; - } - - result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap)); - } - - getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = result }); + getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = createResponseBeatmaps(getBeatmapsRequest.BeatmapIds.ToArray()) }); return true; } @@ -175,6 +165,27 @@ namespace osu.Game.Tests.Visual.OnlinePlay } } + List createResponseBeatmaps(params int[] beatmapIds) + { + var result = new List(); + + foreach (int id in beatmapIds) + { + var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); + + if (baseBeatmap == null) + { + baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; + baseBeatmap.OnlineID = id; + baseBeatmap.BeatmapSet!.OnlineID = id; + } + + result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap)); + } + + return result; + } + return false; } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index a8193701ef..f8f15e2729 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -140,6 +140,7 @@ namespace osu.Game.Tests.Visual { c.RelativeSizeAxes = Axes.None; c.AutoSizeAxes = Axes.None; + c.Size = Vector2.Zero; c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None; c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None; diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 100464029b..7b540cb564 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -156,6 +156,7 @@ namespace osu.Game.Updater switch (State) { case ProgressNotificationState.Cancelled: + case ProgressNotificationState.Completed: base.Close(runFlingAnimation); break; } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c4e2a5168e..f1fed6913b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,15 +18,15 @@ - + - + - - - - + + + + @@ -34,13 +34,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - + diff --git a/osu.iOS.props b/osu.iOS.props index 622efcd63d..c79d0e4864 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,8 +61,8 @@ - - + + @@ -82,11 +82,11 @@ - - + + - +