diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..0c6b80e97e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://osu.ppy.sh/home/support diff --git a/README.md b/README.md index abddb1faa1..0df99f7d6b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ This project is still heavily under development, but is in a state where users a We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below. +Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh/home/changelog). + ## Requirements - A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed. @@ -20,17 +22,24 @@ We are accepting bug reports (please report with as much detail as possible). Fe ### Releases -If you are not interested in developing the game, please head over to the [releases](https://github.com/ppy/osu/releases) to download a precompiled build with automatic updating enabled. +![](https://puu.sh/DCmvA/f6a74f5fbb.png) -- Windows (x64) users should download and run `install.exe`. -- macOS users (10.12 "Sierra" and higher) should download and run `osu.app.zip`. -- iOS users can join the [TestFlight beta program](https://t.co/xQJmHkfC18). +If you are not interested in developing the game, you can consume our [binary releases](https://github.com/ppy/osu/releases). + +**Latest build:*** + +| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | +| ------------- | ------------- | + +- **Linux** users are recommended to self-compile until we have official deployment in place. +- **iOS** users can join the [TestFlight beta program](https://t.co/PasE1zrHhw) (note that due to high demand this is regularly full). +- **Android** users can self-compile, and expect a public beta soon. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. ### Downloading the source code -Clone the repository **including submodules**: +Clone the repository: ```shell git clone https://github.com/ppy/osu @@ -45,7 +54,7 @@ git pull ### Building -Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided below. +Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided [below](#contributing). > Visual Studio Code users must run the `Restore` task before any build attempt. diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 00cabbadf7..975b7f9f5a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using osu.Desktop.Overlays; -using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Game; using osuTK.Input; @@ -56,7 +55,7 @@ namespace osu.Desktop LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, v => { Add(v); - v.State = Visibility.Visible; + v.Show(); }); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) @@ -74,13 +73,11 @@ namespace osu.Desktop { case Intro _: case MainMenu _: - if (versionManager != null) - versionManager.State = Visibility.Visible; + versionManager?.Show(); break; default: - if (versionManager != null) - versionManager.State = Visibility.Hidden; + versionManager?.Hide(); break; } } diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index e9c5d06f3c..5a8cf32f14 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Platform; using osu.Game; using osu.Game.Configuration; using osu.Game.Graphics; @@ -25,15 +23,13 @@ namespace osu.Desktop.Overlays private OsuConfigManager config; private OsuGameBase game; private NotificationOverlay notificationOverlay; - private GameHost host; [BackgroundDependencyLoader] - private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config, GameHost host) + private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config) { notificationOverlay = notification; this.config = config; this.game = game; - this.host = host; AutoSizeAxes = Axes.Both; Anchor = Anchor.BottomCentre; @@ -102,27 +98,31 @@ namespace osu.Desktop.Overlays // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) - notificationOverlay.Post(new UpdateCompleteNotification(version, host.OpenUrlExternally)); + notificationOverlay.Post(new UpdateCompleteNotification(version)); } } private class UpdateCompleteNotification : SimpleNotification { - public UpdateCompleteNotification(string version, Action openUrl = null) + private readonly string version; + + public UpdateCompleteNotification(string version) { + this.version = version; Text = $"You are now running osu!lazer {version}.\nClick to see what's new!"; - Icon = FontAwesome.Solid.CheckSquare; - Activated = delegate - { - openUrl?.Invoke($"https://osu.ppy.sh/home/changelog/lazer/{version}"); - return true; - }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, ChangelogOverlay changelog) { + Icon = FontAwesome.Solid.CheckSquare; IconBackgound.Colour = colours.BlueDark; + + Activated = delegate + { + changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); + return true; + }; } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 9cec0d280d..ab3c040b4e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -20,14 +20,14 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override IBeatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 }, - Ruleset = ruleset.RulesetInfo + Ruleset = ruleset } }; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 035bbe4b4e..0ad72412fc 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs @@ -29,14 +29,14 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override IBeatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, - Ruleset = ruleset.RulesetInfo + Ruleset = ruleset } }; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index 7d7528372a..9ce46ad6ba 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs @@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override IBeatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, - Ruleset = ruleset.RulesetInfo + Ruleset = ruleset } }; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 7393f75e5a..9cbff8c5d3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -23,13 +23,13 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); } - protected override IBeatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap { BeatmapInfo = { - Ruleset = ruleset.RulesetInfo, + Ruleset = ruleset, BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f } } }; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index d6a1ed632b..44e1a8e5cc 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new CatchDifficultyAttributes { Mods = mods }; + return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; @@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, - MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)) + MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), + Skills = skills }; } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index e7c7fd77df..90052d9b11 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -379,8 +379,8 @@ namespace osu.Game.Rulesets.Catch.UI X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1); // Correct overshooting. - if (hyperDashDirection > 0 && hyperDashTargetPosition < X || - hyperDashDirection < 0 && hyperDashTargetPosition > X) + if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || + (hyperDashDirection < 0 && hyperDashTargetPosition > X)) { X = hyperDashTargetPosition; SetHyperDashState(); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 59fed1031f..4a9c22d339 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new ManiaDifficultyAttributes { Mods = mods }; + return new ManiaDifficultyAttributes { Mods = mods, Skills = skills }; return new ManiaDifficultyAttributes { @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate, + Skills = skills }; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs index 921246751c..399cf22599 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs @@ -17,14 +17,14 @@ namespace osu.Game.Rulesets.Osu.Tests { } - protected override IBeatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, - Ruleset = ruleset.RulesetInfo + Ruleset = ruleset } }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 12a3a8d27e..8e73d6152f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -46,11 +46,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("move mouse away", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft)); AddStep("click", () => osuInputManager.GameClick()); - AddAssert("not dismissed", () => !resumeFired && resume.State == Visibility.Visible); + AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible); AddStep("move mouse back", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); AddStep("click", () => osuInputManager.GameClick()); - AddAssert("dismissed", () => resumeFired && resume.State == Visibility.Hidden); + AddAssert("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden); } private class ManualOsuInputManager : OsuInputManager diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 193cfe9c94..2eb783233a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual; using osuTK; @@ -299,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("load player", () => { - Beatmap.Value = new TestWorkingBeatmap(new Beatmap + Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = { @@ -323,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Tests BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Ruleset = new OsuRuleset().RulesetInfo }, - }, Clock); + }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index b2beda18f4..7bb1f42802 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps break; if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < stack_distance - || stackBaseObject is Slider && Vector2Extensions.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance) + || (stackBaseObject is Slider && Vector2Extensions.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance)) { stackBaseIndex = n; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index e2a1542574..c197933233 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new OsuDifficultyAttributes { Mods = mods }; + return new OsuDifficultyAttributes { Mods = mods, Skills = skills }; double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; @@ -50,7 +50,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedStrain = speedRating, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, - MaxCombo = maxCombo + MaxCombo = maxCombo, + Skills = skills }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index a2da2bbf53..8072dc09c1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -2,8 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -11,7 +14,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModGrow : Mod, IApplicableToDrawableHitObjects + internal class OsuModGrow : Mod, IReadFromConfig, IApplicableToDrawableHitObjects { public override string Name => "Grow"; @@ -25,9 +28,16 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; + private Bindable increaseFirstObjectVisibility = new Bindable(); + + public void ReadFromConfig(OsuConfigManager config) + { + increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + } + public void ApplyToDrawableHitObjects(IEnumerable drawables) { - foreach (var drawable in drawables) + foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) { switch (drawable) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index ec23570f54..bc5d02258f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -37,11 +37,11 @@ namespace osu.Game.Rulesets.Osu.Mods if (time < osuHit.HitObject.StartTime - relax_leniency) continue; - if (osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime || osuHit.IsHit) + if ((osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime) || osuHit.IsHit) continue; requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime); - requiresHold |= osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered) || osuHit is DrawableSpinner; + requiresHold |= (osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered)) || osuHit is DrawableSpinner; } if (requiresHit) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 25e1aebd18..33b3667c4f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -2,13 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Primitives; using osuTK; using osuTK.Graphics; -using osuTK.Graphics.ES30; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -19,8 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private readonly SliderPath path; protected Path Path => path; - private readonly BufferedContainer container; - public float PathRadius { get => path.PathRadius; @@ -44,8 +39,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; path.AccentColour = value; - - container.ForceRedraw(); } } @@ -61,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; path.BorderColour = value; - - container.ForceRedraw(); } } @@ -78,23 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; path.BorderSize = value; - - container.ForceRedraw(); } } - public Quad PathDrawQuad => container.ScreenSpaceDrawQuad; - protected SliderBody() { - InternalChild = container = new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - CacheDrawnFrameBuffer = true, - Child = path = new SliderPath { Blending = BlendingMode.None } - }; - - container.Attach(RenderbufferInternalFormat.DepthComponent16); + InternalChild = path = new SliderPath(); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => path.ReceivePositionalInputAt(screenSpacePos); @@ -103,11 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// Sets the vertices of the path which should be drawn by this . /// /// The vertices - protected void SetVertices(IReadOnlyList vertices) - { - path.Vertices = vertices; - container.ForceRedraw(); - } + protected void SetVertices(IReadOnlyList vertices) => path.Vertices = vertices; private class SliderPath : SmoothPath { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 1b8fa0de01..b986076593 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -6,7 +6,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Buffers; +using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shaders; @@ -54,8 +54,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor for (int i = 0; i < max_sprites; i++) { - parts[i].InvalidationID = 0; - parts[i].WasUpdated = true; + // InvalidationID 1 forces an update of each part of the cursor trail the first time ApplyState is run on the draw node + // This is to prevent garbage data from being sent to the vertex shader, resulting in visual issues on some platforms + parts[i].InvalidationID = 1; } } @@ -147,7 +148,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public Vector2 Position; public float Time; public long InvalidationID; - public bool WasUpdated; } private class TrailDrawNode : DrawNode @@ -162,16 +162,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly TrailPart[] parts = new TrailPart[max_sprites]; private Vector2 size; - private readonly VertexBuffer vertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw); + private readonly VertexBatch vertexBatch = new QuadBatch(max_sprites, 1); public TrailDrawNode(CursorTrail source) : base(source) { for (int i = 0; i < max_sprites; i++) - { parts[i].InvalidationID = 0; - parts[i].WasUpdated = false; - } } public override void ApplyState() @@ -192,55 +189,29 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public override void Draw(Action vertexAction) { - shader.GetUniform("g_FadeClock").UpdateValue(ref time); - - int updateStart = -1, updateEnd = 0; - - for (int i = 0; i < parts.Length; ++i) - { - if (parts[i].WasUpdated) - { - if (updateStart == -1) - updateStart = i; - updateEnd = i + 1; - - int start = i * 4; - int end = start; - - Vector2 pos = parts[i].Position; - float localTime = parts[i].Time; - - texture.DrawQuad( - new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y), - DrawColourInfo.Colour, - null, - v => vertexBuffer.Vertices[end++] = new TexturedTrailVertex - { - Position = v.Position, - TexturePosition = v.TexturePosition, - Time = localTime + 1, - Colour = v.Colour, - }); - - parts[i].WasUpdated = false; - } - else if (updateStart != -1) - { - vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4); - updateStart = -1; - } - } - - // Update all remaining vertices that have been changed. - if (updateStart != -1) - vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4); - base.Draw(vertexAction); shader.Bind(); + shader.GetUniform("g_FadeClock").UpdateValue(ref time); - texture.TextureGL.Bind(); - vertexBuffer.Draw(); + for (int i = 0; i < parts.Length; ++i) + { + Vector2 pos = parts[i].Position; + float localTime = parts[i].Time; + + DrawQuad( + texture, + new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y), + DrawColourInfo.Colour, + null, + v => vertexBatch.Add(new TexturedTrailVertex + { + Position = v.Position, + TexturePosition = v.TexturePosition, + Time = localTime + 1, + Colour = v.Colour, + })); + } shader.Unbind(); } @@ -249,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { base.Dispose(isDisposing); - vertexBuffer.Dispose(); + vertexBatch.Dispose(); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 0d4e7edb7b..9e5df0d6b1 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI private GameplayCursorContainer localCursorContainer; - public override CursorContainer LocalCursor => State == Visibility.Visible ? localCursorContainer : null; + public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; protected override string Message => "Click the orange cursor to resume"; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs index 3634ec7d4a..6f9856df83 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs @@ -18,7 +18,6 @@ using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual; using osuTK; using osu.Game.Rulesets.Scoring; @@ -64,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var controlPointInfo = new ControlPointInfo(); controlPointInfo.TimingPoints.Add(new TimingControlPoint()); - WorkingBeatmap beatmap = new TestWorkingBeatmap(new Beatmap + WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap { HitObjects = new List { new CentreHit() }, BeatmapInfo = new BeatmapInfo @@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Ruleset = new TaikoRuleset().RulesetInfo }, ControlPointInfo = controlPointInfo - }, Clock); + }); Add(playfieldContainer = new Container { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 685ad9949b..c8f3e18911 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) - return new TaikoDifficultyAttributes { Mods = mods }; + return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; return new TaikoDifficultyAttributes { @@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + Skills = skills }; } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index f020c2a805..5fc05a4b2f 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -11,7 +11,9 @@ using NUnit.Framework; using osu.Framework.Platform; using osu.Game.IPC; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.IO; using osu.Game.Tests.Resources; using SharpCompress.Archives.Zip; @@ -21,14 +23,14 @@ namespace osu.Game.Tests.Beatmaps.IO public class ImportBeatmapTest { [Test] - public void TestImportWhenClosed() + public async Task TestImportWhenClosed() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed")) { try { - LoadOszIntoOsu(loadOsu(host)); + await LoadOszIntoOsu(loadOsu(host)); } finally { @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestImportThenDelete() + public async Task TestImportThenDelete() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete")) @@ -47,7 +49,7 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = LoadOszIntoOsu(osu); + var imported = await LoadOszIntoOsu(osu); deleteBeatmapSet(imported, osu); } @@ -59,7 +61,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestImportThenImport() + public async Task TestImportThenImport() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport")) @@ -68,17 +70,15 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = LoadOszIntoOsu(osu); - var importedSecondTime = LoadOszIntoOsu(osu); + var imported = await LoadOszIntoOsu(osu); + var importedSecondTime = await LoadOszIntoOsu(osu); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - var manager = osu.Dependencies.Get(); - - Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); - Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); + checkBeatmapSetCount(osu, 1); + checkSingleReferencedFileCount(osu, 18); } finally { @@ -88,30 +88,41 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestRollbackOnFailure() + public async Task TestRollbackOnFailure() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure")) { try { + int itemAddRemoveFireCount = 0; + int loggedExceptionCount = 0; + + Logger.NewEntry += l => + { + if (l.Target == LoggingTarget.Database && l.Exception != null) + Interlocked.Increment(ref loggedExceptionCount); + }; + var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - int fireCount = 0; - // ReSharper disable once AccessToModifiedClosure - manager.ItemAdded += (_, __) => fireCount++; - manager.ItemRemoved += _ => fireCount++; + manager.ItemAdded += (_, __) => Interlocked.Increment(ref itemAddRemoveFireCount); + manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount); - var imported = LoadOszIntoOsu(osu); + var imported = await LoadOszIntoOsu(osu); - Assert.AreEqual(0, fireCount -= 1); + Assert.AreEqual(0, itemAddRemoveFireCount -= 1); imported.Hash += "-changed"; manager.Update(imported); - Assert.AreEqual(0, fireCount -= 2); + Assert.AreEqual(0, itemAddRemoveFireCount -= 2); + + checkBeatmapSetCount(osu, 1); + checkBeatmapCount(osu, 12); + checkSingleReferencedFileCount(osu, 18); var breakTemp = TestResources.GetTestBeatmapForImport(); @@ -127,19 +138,24 @@ namespace osu.Game.Tests.Beatmaps.IO zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate); } - Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); - Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); - Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count); - // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. - manager.Import(breakTemp); + try + { + await manager.Import(breakTemp); + } + catch + { + } // no events should be fired in the case of a rollback. - Assert.AreEqual(0, fireCount); + Assert.AreEqual(0, itemAddRemoveFireCount); - Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); - Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); - Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count); + checkBeatmapSetCount(osu, 1); + checkBeatmapCount(osu, 12); + + checkSingleReferencedFileCount(osu, 18); + + Assert.AreEqual(1, loggedExceptionCount); } finally { @@ -149,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestImportThenImportDifferentHash() + public async Task TestImportThenImportDifferentHash() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash")) @@ -159,19 +175,18 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - var imported = LoadOszIntoOsu(osu); + var imported = await LoadOszIntoOsu(osu); imported.Hash += "-changed"; manager.Update(imported); - var importedSecondTime = LoadOszIntoOsu(osu); + var importedSecondTime = await LoadOszIntoOsu(osu); Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); // only one beatmap will exist as the online set ID matched, causing purging of the first import. - Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); - Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); + checkBeatmapSetCount(osu, 1); } finally { @@ -181,7 +196,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestImportThenDeleteThenImport() + public async Task TestImportThenDeleteThenImport() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) @@ -190,11 +205,11 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = LoadOszIntoOsu(osu); + var imported = await LoadOszIntoOsu(osu); deleteBeatmapSet(imported, osu); - var importedSecondTime = LoadOszIntoOsu(osu); + var importedSecondTime = await LoadOszIntoOsu(osu); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -209,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.IO [TestCase(true)] [TestCase(false)] - public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) + public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}")) @@ -218,7 +233,7 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = loadOsu(host); - var imported = LoadOszIntoOsu(osu); + var imported = await LoadOszIntoOsu(osu); if (set) imported.OnlineBeatmapSetID = 1234; @@ -229,7 +244,7 @@ namespace osu.Game.Tests.Beatmaps.IO deleteBeatmapSet(imported, osu); - var importedSecondTime = LoadOszIntoOsu(osu); + var importedSecondTime = await LoadOszIntoOsu(osu); // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) Assert.IsTrue(imported.ID != importedSecondTime.ID); @@ -243,7 +258,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestImportWithDuplicateBeatmapIDs() + public async Task TestImportWithDuplicateBeatmapIDs() { //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDuplicateBeatmapID")) @@ -284,7 +299,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var imported = manager.Import(toImport); + var imported = await manager.Import(toImport); Assert.NotNull(imported); Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID); @@ -330,7 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public void TestImportWhenFileOpen() + public async Task TestImportWhenFileOpen() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen")) { @@ -339,7 +354,7 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) - osu.Dependencies.Get().Import(temp); + await osu.Dependencies.Get().Import(temp); ensureLoaded(osu); File.Delete(temp); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); @@ -351,13 +366,13 @@ namespace osu.Game.Tests.Beatmaps.IO } } - public static BeatmapSetInfo LoadOszIntoOsu(OsuGameBase osu, string path = null) + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null) { var temp = path ?? TestResources.GetTestBeatmapForImport(); var manager = osu.Dependencies.Get(); - manager.Import(temp); + await manager.Import(temp); var imported = manager.GetAllUsableBeatmapSets(); @@ -373,11 +388,32 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); manager.Delete(imported); - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); + checkBeatmapSetCount(osu, 0); + checkBeatmapSetCount(osu, 1, true); + checkSingleReferencedFileCount(osu, 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } + private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) + { + var manager = osu.Dependencies.Get(); + + Assert.AreEqual(expected, includeDeletePending + ? manager.QueryBeatmapSets(_ => true).ToList().Count + : manager.GetAllUsableBeatmapSets().Count); + } + + private void checkBeatmapCount(OsuGameBase osu, int expected) + { + Assert.AreEqual(expected, osu.Dependencies.Get().QueryBeatmaps(_ => true).ToList().Count); + } + + private void checkSingleReferencedFileCount(OsuGameBase osu, int expected) + { + Assert.AreEqual(expected, osu.Dependencies.Get().QueryFiles(f => f.ReferenceCount == 1).Count()); + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index e39f18c3cd..4babb07213 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -23,13 +23,13 @@ namespace osu.Game.Tests.Scores.IO public class ImportScoreTest { [Test] - public void TestBasicImport() + public async Task TestBasicImport() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport")) { try { - var osu = loadOsu(host); + var osu = await loadOsu(host); var toImport = new ScoreInfo { @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO OnlineScoreID = 12345, }; - var imported = loadIntoOsu(osu, toImport); + var imported = await loadIntoOsu(osu, toImport); Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.TotalScore, imported.TotalScore); @@ -62,20 +62,20 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public void TestImportMods() + public async Task TestImportMods() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods")) { try { - var osu = loadOsu(host); + var osu = await loadOsu(host); var toImport = new ScoreInfo { Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; - var imported = loadIntoOsu(osu, toImport); + var imported = await loadIntoOsu(osu, toImport); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); @@ -88,13 +88,13 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public void TestImportStatistics() + public async Task TestImportStatistics() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics")) { try { - var osu = loadOsu(host); + var osu = await loadOsu(host); var toImport = new ScoreInfo { @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = loadIntoOsu(osu, toImport); + var imported = await loadIntoOsu(osu, toImport); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Scores.IO } } - private ScoreInfo loadIntoOsu(OsuGameBase osu, ScoreInfo score) + private async Task loadIntoOsu(OsuGameBase osu, ScoreInfo score) { var beatmapManager = osu.Dependencies.Get(); @@ -125,20 +125,24 @@ namespace osu.Game.Tests.Scores.IO score.Ruleset = new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); - scoreManager.Import(score); + await scoreManager.Import(score); return scoreManager.GetAllUsableScores().First(); } - private OsuGameBase loadOsu(GameHost host) + private async Task loadOsu(GameHost host) { var osu = new OsuGameBase(); + +#pragma warning disable 4014 Task.Run(() => host.Run(osu)); +#pragma warning restore 4014 + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); var beatmapFile = TestResources.GetTestBeatmapForImport(); var beatmapManager = osu.Dependencies.Get(); - beatmapManager.Import(beatmapFile); + await beatmapManager.Import(beatmapFile); return osu; } diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs index c9bdcf928f..8b941e4633 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; @@ -54,7 +55,7 @@ namespace osu.Game.Tests.Visual.Background private RulesetStore rulesets; [BackgroundDependencyLoader] - private void load(GameHost host) + private void load(GameHost host, AudioManager audio) { factory = new DatabaseContextFactory(LocalStorage); factory.ResetDatabase(); @@ -68,10 +69,10 @@ namespace osu.Game.Tests.Visual.Background usage.Migrate(); Dependencies.Cache(rulesets = new RulesetStore(factory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, host, Beatmap.Default)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - manager.Import(TestResources.GetTestBeatmapForImport()); + manager.Import(TestResources.GetTestBeatmapForImport()).Wait(); Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index 94412455a0..df6740421b 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -111,16 +111,19 @@ namespace osu.Game.Tests.Visual.Components private class TestPreviewTrackManager : PreviewTrackManager { - protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TestPreviewTrack(beatmapSetInfo, trackManager); + protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore); protected class TestPreviewTrack : TrackManagerPreviewTrack { - public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) + private readonly ITrackStore trackManager; + + public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager) : base(beatmapSetInfo, trackManager) { + this.trackManager = trackManager; } - protected override Track GetTrack() => new TrackVirtual { Length = 100000 }; + protected override Track GetTrack() => trackManager.GetVirtual(100000); } } } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs index b537cb0beb..608df1965e 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit.Compose; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Editor { @@ -19,7 +18,7 @@ namespace osu.Game.Tests.Visual.Editor [BackgroundDependencyLoader] private void load() { - Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, Clock); + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Child = new ComposeScreen(); } } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs index 154c58dd99..a8c2362910 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Editor }; [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { - Beatmap.Value = new WaveformTestBeatmap(); + Beatmap.Value = new WaveformTestBeatmap(audio); Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs index 590fa59107..b997d6aaeb 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs @@ -1,4 +1,4 @@ -// 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. using NUnit.Framework; @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Graphics; @@ -48,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editor } }; - Beatmap.Value = new TestWorkingBeatmap(testBeatmap, Clock); + Beatmap.Value = CreateWorkingBeatmap(testBeatmap); Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs index f20c921ff2..2e04eb50ca 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit.Components.Timelines.Summary; -using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Editor @@ -21,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editor [BackgroundDependencyLoader] private void load() { - Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null); + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Add(new SummaryTimeline { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs index 47aa059b62..7accbe2fa8 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs @@ -18,7 +18,6 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; -using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Editor @@ -45,7 +44,7 @@ namespace osu.Game.Tests.Visual.Editor [BackgroundDependencyLoader] private void load() { - Beatmap.Value = new TestWorkingBeatmap(new Beatmap + Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = new List { diff --git a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs index 126ab98291..0d4fe4366d 100644 --- a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs @@ -1,4 +1,4 @@ -// 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. using NUnit.Framework; @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Components; -using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Editor @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Editor Size = new Vector2(200, 100) }; - Beatmap.Value = new TestWorkingBeatmap(new Beatmap(), Clock); + Beatmap.Value = CreateWorkingBeatmap(new Beatmap()); Child = playback; } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs index e93789b1d3..e2762f3d5f 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Osu; using osuTK.Graphics; namespace osu.Game.Tests.Visual.Editor @@ -20,9 +22,9 @@ namespace osu.Game.Tests.Visual.Editor private WorkingBeatmap waveformBeatmap; [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { - waveformBeatmap = new WaveformTestBeatmap(); + waveformBeatmap = new WaveformTestBeatmap(audio); } [TestCase(1f)] @@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Editor Child = graph = new TestWaveformGraph { RelativeSizeAxes = Axes.Both, - Waveform = new DummyWorkingBeatmap().Waveform, + Waveform = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Waveform, }, }; }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs new file mode 100644 index 0000000000..f06f72615b --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneFailAnimation : AllPlayersTestScene + { + protected override Player CreatePlayer(Ruleset ruleset) + { + Mods.Value = Array.Empty(); + return new FailPlayer(); + } + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(AllPlayersTestScene), + typeof(TestPlayer), + typeof(Player), + }; + + protected override void AddCheckSteps() + { + AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); + } + + private class FailPlayer : TestPlayer + { + public new FailOverlay FailOverlay => base.FailOverlay; + + public FailPlayer() + : base(false, false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + ScoreProcessor.FailConditions += _ => true; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index ba9c583b08..4727140d99 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Show overlay", () => pauseOverlay.Show()); AddStep("Press select", () => press(GlobalAction.Select)); - AddAssert("Overlay still open", () => pauseOverlay.State == Visibility.Visible); + AddAssert("Overlay still open", () => pauseOverlay.State.Value == Visibility.Visible); AddStep("Hide overlay", () => pauseOverlay.Hide()); } @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddAssert("Action was triggered", () => triggered); - AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden); + AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden); } /// @@ -272,7 +272,7 @@ namespace osu.Game.Tests.Visual.Gameplay return triggered; }); - AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden); + AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden); } private void press(Key key) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index b6f8638f4a..ac10c77a78 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestPauseAfterFail() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddAssert("fail overlay shown", () => Player.FailOverlayVisible); + AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); private void confirmClockRunning(bool isRunning) => - AddAssert("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); + AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); protected override bool AllowFail => true; @@ -203,9 +203,9 @@ namespace osu.Game.Tests.Visual.Gameplay public new HUDOverlay HUDOverlay => base.HUDOverlay; - public bool FailOverlayVisible => FailOverlay.State == Visibility.Visible; + public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible; - public bool PauseOverlayVisible => PauseOverlay.State == Visibility.Visible; + public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; public override void OnEntering(IScreen last) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 5c26f733ab..daee3a520c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Gameplay { @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void Setup() => Schedule(() => { InputManager.Child = stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }; - Beatmap.Value = new TestWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), Clock); + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); }); [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs index c75fb2567b..65b56319e8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Lists; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Screens.Play; @@ -43,9 +42,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock clock) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) { - var working = base.CreateWorkingBeatmap(beatmap, clock); + var working = base.CreateWorkingBeatmap(beatmap); workingWeakReferences.Add(working); return working; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 213cdf5e48..ead7a4b7fc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Origin = Anchor.TopRight, Anchor = Anchor.TopRight, - State = Visibility.Visible, + State = { Value = Visibility.Visible }, }); AddStep("Restart", restart); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 0c789d8cb7..0df6605cdd 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Menus public TestSceneToolbar() { - var toolbar = new Toolbar { State = Visibility.Visible }; + var toolbar = new Toolbar { State = { Value = Visibility.Visible } }; ToolbarNotificationButton notificationButton = null; AddStep("create toolbar", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 21b97fe73b..8d842fc865 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; @@ -37,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer settings = new TestRoomSettings { RelativeSizeAxes = Axes.Both, - State = Visibility.Visible + State = { Value = Visibility.Visible } }; Child = settings; @@ -57,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set name", () => Room.Name.Value = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = new DummyWorkingBeatmap().BeatmapInfo })); + AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo })); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); AddStep("clear name", () => Room.Name.Value = ""); diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index a7e725ec3f..35449f5687 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online api.Logout(); api.LocalUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); - AddStep("show", () => accountCreation.State = Visibility.Visible); + AddStep("show", () => accountCreation.Show()); AddStep("logout", () => api.Logout()); } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs new file mode 100644 index 0000000000..0655611230 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Changelog; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChangelogOverlay : OsuTestScene + { + private ChangelogOverlay changelog; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(UpdateStreamBadgeArea), + typeof(UpdateStreamBadge), + typeof(ChangelogHeader), + typeof(ChangelogContent), + typeof(ChangelogListing), + typeof(ChangelogSingleBuild), + typeof(ChangelogBuild), + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Add(changelog = new ChangelogOverlay()); + AddStep(@"Show", changelog.Show); + AddStep(@"Hide", changelog.Hide); + + AddWaitStep("wait for hide", 3); + + AddStep(@"Show with Lazer 2018.712.0", () => + { + changelog.ShowBuild(new APIChangelogBuild + { + Version = "2018.712.0", + DisplayVersion = "2018.712.0", + UpdateStream = new APIUpdateStream { Name = OsuGameBase.CLIENT_STREAM_NAME }, + ChangelogEntries = new List + { + new APIChangelogEntry + { + Category = "Test", + Title = "Title", + MessageHtml = "Message", + } + } + }); + changelog.Show(); + }); + + AddWaitStep("wait for show", 3); + AddStep(@"Hide", changelog.Hide); + AddWaitStep("wait for hide", 3); + + AddStep(@"Show with listing", () => + { + changelog.ShowListing(); + changelog.Show(); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatDisplay.cs index 634176e65f..2789feef3d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatDisplay.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Online Children = new Drawable[] { channelManager, - new ChatOverlay { State = Visibility.Visible } + new ChatOverlay { State = { Value = Visibility.Visible } } }; } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index a3d932a383..8b67892fbb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Direct; using osu.Game.Rulesets.Osu; -using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Online @@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online [BackgroundDependencyLoader] private void load() { - var beatmap = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null); + var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true; beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true; diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs index 6dc3428bff..fe8437be17 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs @@ -18,8 +18,24 @@ namespace osu.Game.Tests.Visual.Online { base.LoadComplete(); + int fireCount = 0; + Add(overlay = new TestFullscreenOverlay()); - AddStep(@"toggle", overlay.ToggleVisibility); + + overlay.State.ValueChanged += _ => fireCount++; + + AddStep(@"show", overlay.Show); + + AddAssert("fire count 1", () => fireCount == 1); + + AddStep(@"show again", overlay.Show); + + // this logic is specific to FullscreenOverlay + AddAssert("fire count 2", () => fireCount == 2); + + AddStep(@"hide", overlay.Hide); + + AddAssert("fire count 3", () => fireCount == 3); } private class TestFullscreenOverlay : FullscreenOverlay diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs index 6815018be6..2f88a4b01d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Online FlagName = @"TH", }, }, - Rank = ScoreRank.F, + Rank = ScoreRank.D, PP = 160, MaxCombo = 1234, TotalScore = 123456, diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs new file mode 100644 index 0000000000..bccb263600 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.Profile.Sections; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneShowMoreButton : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ShowMoreButton), + }; + + public TestSceneShowMoreButton() + { + ShowMoreButton button = null; + + int fireCount = 0; + + Add(button = new ShowMoreButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => + { + fireCount++; + // ReSharper disable once AccessToModifiedClosure + // ReSharper disable once PossibleNullReferenceException + Scheduler.AddDelayed(() => button.IsLoading = false, 2000); + } + }); + + AddStep("click button", () => button.Click()); + + AddAssert("action fired once", () => fireCount == 1); + AddAssert("is in loading state", () => button.IsLoading); + + AddStep("click button", () => button.Click()); + + AddAssert("action not fired", () => fireCount == 1); + AddAssert("is in loading state", () => button.IsLoading); + + AddUntilStep("wait for loaded", () => !button.IsLoading); + + AddStep("click button", () => button.Click()); + + AddAssert("action fired twice", () => fireCount == 2); + AddAssert("is in loading state", () => button.IsLoading); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index fca18a9263..54f06d6ad2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; @@ -12,10 +13,12 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserPanel : OsuTestScene { + private readonly UserPanel peppy; + public TestSceneUserPanel() { UserPanel flyte; - UserPanel peppy; + Add(new FillFlowContainer { Anchor = Anchor.Centre, @@ -44,13 +47,31 @@ namespace osu.Game.Tests.Visual.Online }); flyte.Status.Value = new UserStatusOnline(); - peppy.Status.Value = new UserStatusSoloGame(); + peppy.Status.Value = null; + } - AddStep(@"spectating", () => { flyte.Status.Value = new UserStatusSpectating(); }); - AddStep(@"multiplaying", () => { flyte.Status.Value = new UserStatusMultiplayerGame(); }); - AddStep(@"modding", () => { flyte.Status.Value = new UserStatusModding(); }); - AddStep(@"offline", () => { flyte.Status.Value = new UserStatusOffline(); }); - AddStep(@"null status", () => { flyte.Status.Value = null; }); + [Test] + public void UserStatusesTests() + { + AddStep("online", () => { peppy.Status.Value = new UserStatusOnline(); }); + AddStep(@"do not disturb", () => { peppy.Status.Value = new UserStatusDoNotDisturb(); }); + AddStep(@"offline", () => { peppy.Status.Value = new UserStatusOffline(); }); + AddStep(@"null status", () => { peppy.Status.Value = null; }); + } + + [Test] + public void UserActivitiesTests() + { + Bindable activity = new Bindable(); + + peppy.Activity.BindTo(activity); + + AddStep("idle", () => { activity.Value = null; }); + AddStep("spectating", () => { activity.Value = new UserActivity.Spectating(); }); + AddStep("solo", () => { activity.Value = new UserActivity.SoloGame(null, null); }); + AddStep("choosing", () => { activity.Value = new UserActivity.ChoosingBeatmap(); }); + AddStep("editing", () => { activity.Value = new UserActivity.Editing(null); }); + AddStep("modding", () => { activity.Value = new UserActivity.Modding(); }); } } } diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs similarity index 67% rename from osu.Game.Tests/Visual/Settings/TestSceneSettings.cs rename to osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 964754f8d0..668fdf2c20 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -1,24 +1,33 @@ // 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.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Overlays.Settings; namespace osu.Game.Tests.Visual.Settings { [TestFixture] - public class TestSceneSettings : OsuTestScene + public class TestSceneSettingsPanel : OsuTestScene { private readonly SettingsPanel settings; private readonly DialogOverlay dialogOverlay; - public TestSceneSettings() + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SettingsFooter), + typeof(SettingsOverlay), + }; + + public TestSceneSettingsPanel() { settings = new SettingsOverlay { - State = Visibility.Visible + State = { Value = Visibility.Visible } }; Add(dialogOverlay = new DialogOverlay { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index cf4362ba28..8395ece457 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Screens.Select; @@ -18,7 +19,8 @@ namespace osu.Game.Tests.Visual.SongSelect { public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; - public TestSceneBeatmapDetailArea() + [BackgroundDependencyLoader] + private void load(OsuGameBase game) { BeatmapDetailArea detailsArea; Add(detailsArea = new BeatmapDetailArea @@ -28,7 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelect Size = new Vector2(550f, 450f), }); - AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap + AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) { BeatmapInfo = { @@ -56,7 +58,7 @@ namespace osu.Game.Tests.Visual.SongSelect } ); - AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap + AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) { BeatmapInfo = { @@ -82,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap + AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) { BeatmapInfo = { @@ -107,7 +109,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap + AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) { BeatmapInfo = { @@ -133,7 +135,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap + AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) { BeatmapInfo = { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index b1ed5c46c2..932e114580 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; @@ -18,7 +17,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Select; -using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.SongSelect @@ -49,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("show", () => { - infoWedge.State = Visibility.Visible; + infoWedge.Show(); infoWedge.Beatmap = Beatmap.Value; }); @@ -58,11 +56,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddWaitStep("wait for select", 3); - AddStep("hide", () => { infoWedge.State = Visibility.Hidden; }); + AddStep("hide", () => { infoWedge.Hide(); }); AddWaitStep("wait for hide", 3); - AddStep("show", () => { infoWedge.State = Visibility.Visible; }); + AddStep("show", () => { infoWedge.Show(); }); foreach (var rulesetInfo in rulesets.AvailableRulesets) { @@ -136,7 +134,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => { infoBefore = infoWedge.Info; - infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b); + infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); }); AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs index 3d75470328..9365e2c5b1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, new ScoreInfo { - Rank = ScoreRank.F, + Rank = ScoreRank.D, Accuracy = 0.6025, MaxCombo = 244, TotalScore = 1707827, @@ -206,7 +206,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, new ScoreInfo { - Rank = ScoreRank.F, + Rank = ScoreRank.D, Accuracy = 0.5140, MaxCombo = 244, TotalScore = 1707827, @@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, new ScoreInfo { - Rank = ScoreRank.F, + Rank = ScoreRank.D, Accuracy = 0.4222, MaxCombo = 244, TotalScore = 1707827, diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 7e962dbc06..f5115c50a9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.MathUtils; @@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [BackgroundDependencyLoader] - private void load(GameHost host) + private void load(GameHost host, AudioManager audio) { factory = new DatabaseContextFactory(LocalStorage); factory.ResetDatabase(); @@ -93,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelect usage.Migrate(); Dependencies.Cache(rulesets = new RulesetStore(factory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, defaultBeatmap = Beatmap.Default)); Beatmap.SetDefault(); } @@ -137,7 +138,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); changeRuleset(2); - importForRuleset(0); + addRulesetImportStep(0); AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); } @@ -146,8 +147,8 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); changeRuleset(2); - importForRuleset(2); - importForRuleset(1); + addRulesetImportStep(2); + addRulesetImportStep(1); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); changeRuleset(1); @@ -209,7 +210,52 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("start not requested", () => !startRequested); } - private void importForRuleset(int id) => AddStep($"import test map for ruleset {id}", () => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray()))); + [Test] + public void TestAddNewBeatmapWhileSelectingRandom() + { + const int test_count = 10; + int beatmapChangedCount = 0; + int debounceCount = 0; + createSongSelect(); + AddStep("Setup counters", () => + { + beatmapChangedCount = 0; + debounceCount = 0; + songSelect.Carousel.SelectionChanged += _ => beatmapChangedCount++; + }); + AddRepeatStep($"Create beatmaps {test_count} times", () => + { + importForRuleset(0); + + Scheduler.AddDelayed(() => + { + // Wait for debounce + songSelect.Carousel.SelectNextRandom(); + ++debounceCount; + }, 400); + }, test_count); + + AddUntilStep("Debounce limit reached", () => debounceCount == test_count); + + // The selected beatmap should have changed an additional 2 times since both initially loading songselect and the first import also triggers selectionChanged + AddAssert($"Beatmap changed {test_count + 2} times", () => beatmapChangedCount == test_count + 2); + } + + [Test] + public void TestHideSetSelectsCorrectBeatmap() + { + int? previousID = null; + createSongSelect(); + addRulesetImportStep(0); + AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last())); + AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); + AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First())); + AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + } + + private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); + + private void importForRuleset(int id) => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray())).Wait(); private static int importId; private int getImportId() => ++importId; @@ -231,7 +277,7 @@ namespace osu.Game.Tests.Visual.SongSelect var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); for (int i = 0; i < 100; i += 10) - manager.Import(createTestBeatmapSet(i, usableRulesets)); + manager.Import(createTestBeatmapSet(i, usableRulesets)).Wait(); }); } diff --git a/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs b/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs new file mode 100644 index 0000000000..096ac951de --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs @@ -0,0 +1,50 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Tests.Visual.UserInterface +{ + /// + /// An abstract test case which exposes small cells arranged in a grid. + /// Useful for displaying multiple configurations of a tested component at a glance. + /// + public abstract class OsuGridTestScene : OsuTestScene + { + private readonly Drawable[,] cells; + + /// + /// The amount of rows in the grid. + /// + protected readonly int Rows; + + /// + /// The amount of columns in the grid. + /// + protected readonly int Cols; + + /// + /// Constructs a grid test case with the given dimensions. + /// + protected OsuGridTestScene(int rows, int cols) + { + Rows = rows; + Cols = cols; + + GridContainer testContainer; + Add(testContainer = new GridContainer { RelativeSizeAxes = Axes.Both }); + + cells = new Drawable[rows, cols]; + for (int r = 0; r < rows; r++) + for (int c = 0; c < cols; c++) + cells[r, c] = new Container { RelativeSizeAxes = Axes.Both }; + + testContainer.Content = cells.ToJagged(); + } + + protected Container Cell(int index) => (Container)cells[index / Cols, index % Cols]; + protected Container Cell(int row, int col) => (Container)cells[row, col]; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs index 590ee4e720..23d9112b25 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs @@ -84,7 +84,6 @@ namespace osu.Game.Tests.Visual.UserInterface testLocalCursor(); testUserCursorOverride(); testMultipleLocalCursors(); - ReturnUserInput(); } /// @@ -177,7 +176,7 @@ namespace osu.Game.Tests.Visual.UserInterface /// Checks if a cursor is visible. /// /// The cursor to check. - private bool checkVisible(CursorContainer cursorContainer) => cursorContainer.State == Visibility.Visible; + private bool checkVisible(CursorContainer cursorContainer) => cursorContainer.State.Value == Visibility.Visible; /// /// Checks if a cursor is at the current inputmanager screen position. @@ -193,7 +192,7 @@ namespace osu.Game.Tests.Visual.UserInterface public CursorContainer Cursor { get; } public bool ProvidingUserCursor { get; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || SmoothTransition && !ProvidingUserCursor; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor); private readonly Box background; @@ -219,7 +218,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Cursor = new TestCursorContainer { - State = providesUserCursor ? Visibility.Hidden : Visibility.Visible, + State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible }, } }; } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs index b9a6d74f19..b0233d35f9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs @@ -3,13 +3,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneLoadingAnimation : GridTestScene //todo: this should be an OsuTestScene + public class TestSceneLoadingAnimation : OsuGridTestScene { public TestSceneLoadingAnimation() : base(2, 2) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs index a62fd6467b..2f2a40925f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs @@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.UserInterface }; Add(mc); - AddToggleStep(@"toggle visibility", state => mc.State = state ? Visibility.Visible : Visibility.Hidden); - AddStep(@"show", () => mc.State = Visibility.Visible); + AddToggleStep(@"toggle visibility", state => mc.State.Value = state ? Visibility.Visible : Visibility.Hidden); + AddStep(@"show", () => mc.Show()); AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 71033fcd2f..6b7427cef5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface Content.Add(displayedCount); - void setState(Visibility state) => AddStep(state.ToString(), () => manager.State = state); + void setState(Visibility state) => AddStep(state.ToString(), () => manager.State.Value = state); void checkProgressingCount(int expected) => AddAssert($"progressing count is {expected}", () => progressingNotifications.Count == expected); manager.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs new file mode 100644 index 0000000000..6a41d08f01 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs @@ -0,0 +1,72 @@ +// 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.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuAnimatedButton : OsuGridTestScene + { + public TestSceneOsuAnimatedButton() + : base(3, 2) + { + Cell(0).Add(new BaseContainer("relative sized") + { + RelativeSizeAxes = Axes.Both, + }); + + Cell(1).Add(new BaseContainer("auto sized") + { + AutoSizeAxes = Axes.Both + }); + + Cell(2).Add(new BaseContainer("relative Y auto X") + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }); + + Cell(3).Add(new BaseContainer("relative X auto Y") + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }); + + Cell(4).Add(new BaseContainer("fixed") + { + Size = new Vector2(100), + }); + + Cell(5).Add(new BaseContainer("fixed") + { + Size = new Vector2(100, 50), + }); + + AddToggleStep("toggle enabled", toggle => + { + for (int i = 0; i < 6; i++) + ((BaseContainer)Cell(i).Child).Action = toggle ? () => { } : (Action)null; + }); + } + + public class BaseContainer : OsuAnimatedButton + { + public BaseContainer(string text) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = text + }); + } + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs new file mode 100644 index 0000000000..dbef7d1686 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs @@ -0,0 +1,194 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneOsuHoverContainer : ManualInputManagerTestScene + { + private OsuHoverTestContainer hoverContainer; + private Box colourContainer; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = hoverContainer = new OsuHoverTestContainer + { + Enabled = { Value = true }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(100), + Child = colourContainer = new Box + { + RelativeSizeAxes = Axes.Both, + }, + }; + + doMoveOut(); + }); + + [Description("Checks IsHovered property value on a container when it is hovered/unhovered.")] + [TestCase(true, TestName = "Enabled_Check_IsHovered")] + [TestCase(false, TestName = "Disabled_Check_IsHovered")] + public void TestIsHoveredHasProperValue(bool isEnabled) + { + setContainerEnabledTo(isEnabled); + + checkNotHovered(); + + moveToText(); + checkHovered(); + + moveOut(); + checkNotHovered(); + + moveToText(); + checkHovered(); + + moveOut(); + checkNotHovered(); + } + + [Test] + [Description("Checks colour fading on an enabled container when it is hovered/unhovered.")] + public void TestTransitionWhileEnabled() + { + enableContainer(); + + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR); + + moveOut(); + waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR); + + moveOut(); + waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR); + } + + [Test] + [Description("Checks colour fading on a disabled container when it is hovered/unhovered.")] + public void TestNoTransitionWhileDisabled() + { + disableContainer(); + + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveOut(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveOut(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + } + + [Test] + [Description("Checks that when a disabled & hovered container gets enabled, colour fading happens")] + public void TestBecomesEnabledTransition() + { + disableContainer(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + enableContainer(); + waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR); + } + + [Test] + [Description("Checks that when an enabled & hovered container gets disabled, colour fading happens")] + public void TestBecomesDisabledTransition() + { + enableContainer(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR); + + disableContainer(); + waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR); + } + + [Test] + [Description("Checks that when a hovered container gets enabled and disabled multiple times, colour fading happens")] + public void TestDisabledChangesMultipleTimes() + { + enableContainer(); + checkColour(OsuHoverTestContainer.IDLE_COLOUR); + + moveToText(); + waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR); + + disableContainer(); + waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR); + + enableContainer(); + waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR); + + disableContainer(); + waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR); + } + + private void enableContainer() => setContainerEnabledTo(true); + + private void disableContainer() => setContainerEnabledTo(false); + + private void setContainerEnabledTo(bool newValue) + { + string word = newValue ? "Enable" : "Disable"; + AddStep($"{word} container", () => hoverContainer.Enabled.Value = newValue); + } + + private void moveToText() => AddStep("Move mouse to text", () => InputManager.MoveMouseTo(hoverContainer)); + + private void moveOut() => AddStep("Move out", doMoveOut); + + private void checkHovered() => AddAssert("Check hovered", () => hoverContainer.IsHovered); + + private void checkNotHovered() => AddAssert("Check not hovered", () => !hoverContainer.IsHovered); + + private void checkColour(ColourInfo expectedColour) + => AddAssert($"Check colour to be '{expectedColour}'", () => currentColour.Equals(expectedColour)); + + private void waitUntilColourIs(ColourInfo expectedColour) + => AddUntilStep($"Wait until hover colour is {expectedColour}", () => currentColour.Equals(expectedColour)); + + private ColourInfo currentColour => colourContainer.DrawColourInfo.Colour; + + /// + /// Moves the cursor to top left corner of the screen + /// + private void doMoveOut() + => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.TopLeft.X, InputManager.ScreenSpaceDrawQuad.TopLeft.Y)); + + private sealed class OsuHoverTestContainer : OsuHoverContainer + { + public static readonly Color4 HOVER_COLOUR = Color4.Red; + public static readonly Color4 IDLE_COLOUR = Color4.Green; + + public OsuHoverTestContainer() + { + HoverColour = HOVER_COLOUR; + IdleColour = IDLE_COLOUR; + } + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 24140125e0..9ddd8f4038 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface var popup = new PopupDialog { RelativeSizeAxes = Axes.Both, - State = Framework.Graphics.Containers.Visibility.Visible, + State = { Value = Framework.Graphics.Containers.Visibility.Visible }, Icon = FontAwesome.Solid.AssistiveListeningSystems, HeaderText = @"This is a test popup", BodyText = "I can say lots of stuff and even wrap my words!", diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index 23065629a6..c361598354 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface this.api = api; this.rulesets = rulesets; - testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu); + testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu).Result; } [Test] @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.UserInterface TestUpdateableBeatmapBackgroundSprite background = null; AddStep("load null beatmap", () => Child = background = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }); - AddUntilStep("wait for load", () => background.ContentLoaded); + AddUntilStep("content loaded", () => background.ContentLoaded); } [Test] diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index f66b374cd7..fdb91b7c5b 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; @@ -19,12 +20,14 @@ namespace osu.Game.Tests { private readonly ZipArchiveReader reader; private readonly Stream stream; + private readonly ITrackStore trackStore; - public WaveformTestBeatmap() - : base(new BeatmapInfo()) + public WaveformTestBeatmap(AudioManager audioManager) + : base(new BeatmapInfo(), audioManager) { stream = TestResources.GetTestBeatmapStream(); reader = new ZipArchiveReader(stream); + trackStore = audioManager.GetTrackStore(reader); } public override void Dispose() @@ -32,17 +35,19 @@ namespace osu.Game.Tests base.Dispose(); stream?.Dispose(); reader?.Dispose(); + trackStore?.Dispose(); } protected override IBeatmap GetBeatmap() => createTestBeatmap(); protected override Texture GetBackground() => null; - protected override Waveform GetWaveform() => new Waveform(getAudioStream()); + protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); - protected override Track GetTrack() => new TrackBass(getAudioStream()); + protected override Track GetTrack() => trackStore.Get(firstAudioFile); + + private string firstAudioFile => reader.Filenames.First(f => f.EndsWith(".mp3")); - private Stream getAudioStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".mp3"))); private Stream getBeatmapStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))); private Beatmap createTestBeatmap() diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 99c0d70ac9..6e162ca95e 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -1,6 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -12,27 +16,28 @@ using osu.Game.Beatmaps; namespace osu.Game.Audio { - /// - /// A central store for the retrieval of s. - /// public class PreviewTrackManager : Component { private readonly BindableDouble muteBindable = new BindableDouble(); private AudioManager audio; - private TrackManager trackManager; + private PreviewTrackStore trackStore; private TrackManagerPreviewTrack current; [BackgroundDependencyLoader] private void load(AudioManager audio, FrameworkConfigManager config) { - trackManager = new TrackManager(new OnlineStore()); + // this is a temporary solution to get around muting ourselves. + // todo: update this once we have a BackgroundTrackManager or similar. + trackStore = new PreviewTrackStore(new OnlineStore()); + + audio.AddItem(trackStore); + trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack); this.audio = audio; - audio.AddItem(trackManager); - config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); + config.BindWith(FrameworkSetting.VolumeMusic, trackStore.Volume); } /// @@ -42,19 +47,19 @@ namespace osu.Game.Audio /// The playable . public PreviewTrack Get(BeatmapSetInfo beatmapSetInfo) { - var track = CreatePreviewTrack(beatmapSetInfo, trackManager); + var track = CreatePreviewTrack(beatmapSetInfo, trackStore); track.Started += () => { current?.Stop(); current = track; - audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable); + audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable); }; track.Stopped += () => { current = null; - audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); + audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); }; return track; @@ -81,16 +86,16 @@ namespace osu.Game.Audio /// /// Creates the . /// - protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TrackManagerPreviewTrack(beatmapSetInfo, trackManager); + protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TrackManagerPreviewTrack(beatmapSetInfo, trackStore); protected class TrackManagerPreviewTrack : PreviewTrack { public IPreviewTrackOwner Owner { get; private set; } private readonly BeatmapSetInfo beatmapSetInfo; - private readonly TrackManager trackManager; + private readonly ITrackStore trackManager; - public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) + public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager) { this.beatmapSetInfo = beatmapSetInfo; this.trackManager = trackManager; @@ -104,5 +109,46 @@ namespace osu.Game.Audio protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3"); } + + private class PreviewTrackStore : AudioCollectionManager, ITrackStore + { + private readonly IResourceStore store; + + internal PreviewTrackStore(IResourceStore store) + { + this.store = store; + } + + public Track GetVirtual(double length = double.PositiveInfinity) + { + if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}"); + + var track = new TrackVirtual(length); + AddItem(track); + return track; + } + + public Track Get(string name) + { + if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}"); + + if (string.IsNullOrEmpty(name)) return null; + + var dataStream = store.GetStream(name); + + if (dataStream == null) + return null; + + Track track = new TrackBass(dataStream); + AddItem(track); + return track; + } + + public Task GetAsync(string name) => Task.Run(() => Get(name)); + + public Stream GetStream(string name) => store.GetStream(name); + + public IEnumerable GetAvailableResources() => store.GetAvailableResources(); + } } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 52238c26fe..3c082bb71e 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -119,7 +119,7 @@ namespace osu.Game.Beatmaps /// public List Scores { get; set; } - public override string ToString() => $"{Metadata} [{Version}]"; + public override string ToString() => $"{Metadata} [{Version}]".Trim(); public bool Equals(BeatmapInfo other) { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 798bca3ada..3734c8d05c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; @@ -14,6 +15,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Threading; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO.Archives; @@ -72,6 +74,8 @@ namespace osu.Game.Beatmaps private readonly List currentDownloads = new List(); + private readonly BeatmapUpdateQueue updateQueue; + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, new BeatmapStore(contextFactory), host) @@ -86,9 +90,11 @@ namespace osu.Game.Beatmaps beatmaps = (BeatmapStore)ModelStore; beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); + + updateQueue = new BeatmapUpdateQueue(api); } - protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive) + protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps = createBeatmapDifficulties(archive); @@ -104,8 +110,7 @@ namespace osu.Game.Beatmaps validateOnlineIds(beatmapSet); - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - fetchAndPopulateOnlineValues(b); + return updateQueue.UpdateAsync(beatmapSet, cancellationToken); } protected override void PreImport(BeatmapSetInfo beatmapSet) @@ -122,7 +127,7 @@ namespace osu.Game.Beatmaps { Delete(existingOnlineId); beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); - Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database); + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged."); } } } @@ -181,10 +186,10 @@ namespace osu.Game.Beatmaps request.Success += filename => { - Task.Factory.StartNew(() => + Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - Import(downloadNotification, filename); + await Import(downloadNotification, filename); currentDownloads.Remove(request); }, TaskCreationOptions.LongRunning); }; @@ -322,6 +327,8 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + protected override string HumanisedModelName => "beatmap"; + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) { // let's make sure there are actually .osu files to import. @@ -342,6 +349,7 @@ namespace osu.Game.Beatmaps OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, Beatmaps = new List(), Metadata = beatmap.Metadata, + DateAdded = DateTimeOffset.UtcNow }; } @@ -380,47 +388,6 @@ namespace osu.Game.Beatmaps return beatmapInfos; } - /// - /// Query the API to populate missing values like OnlineBeatmapID / OnlineBeatmapSetID or (Rank-)Status. - /// - /// The beatmap to populate. - /// Whether to re-query if the provided beatmap already has populated values. - /// True if population was successful. - private bool fetchAndPopulateOnlineValues(BeatmapInfo beatmap, bool force = false) - { - if (api?.State != APIState.Online) - return false; - - if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null - && beatmap.Status != BeatmapSetOnlineStatus.None && beatmap.BeatmapSet.Status != BeatmapSetOnlineStatus.None) - return true; - - Logger.Log("Attempting online lookup for the missing values...", LoggingTarget.Database); - - try - { - var req = new GetBeatmapRequest(beatmap); - - req.Perform(api); - - var res = req.Result; - - Logger.Log($"Successfully mapped to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.", LoggingTarget.Database); - - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - - return true; - } - catch (Exception e) - { - Logger.Log($"Failed ({e})", LoggingTarget.Database); - return false; - } - } - /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// @@ -429,7 +396,7 @@ namespace osu.Game.Beatmaps private readonly IBeatmap beatmap; public DummyConversionBeatmap(IBeatmap beatmap) - : base(beatmap.BeatmapInfo) + : base(beatmap.BeatmapInfo, null) { this.beatmap = beatmap; } @@ -454,5 +421,55 @@ namespace osu.Game.Beatmaps public override bool IsImportant => false; } } + + private class BeatmapUpdateQueue + { + private readonly IAPIProvider api; + + private const int update_queue_request_concurrency = 4; + + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapUpdateQueue)); + + public BeatmapUpdateQueue(IAPIProvider api) + { + this.api = api; + } + + public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + { + if (api?.State != APIState.Online) + return Task.CompletedTask; + + LogForModel(beatmapSet, "Performing online lookups..."); + return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); + } + + // todo: expose this when we need to do individual difficulty lookups. + protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) + => Task.Factory.StartNew(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler); + + private void update(BeatmapSetInfo set, BeatmapInfo beatmap) + { + if (api?.State != APIState.Online) + return; + + var req = new GetBeatmapRequest(beatmap); + + req.Success += res => + { + LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + + beatmap.Status = res.Status; + beatmap.BeatmapSet.Status = res.BeatmapSet.Status; + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + }; + + req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap}", e); }; + + // intentionally blocking to limit web request concurrency + req.Perform(api); + } + } } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 0bdab22dd2..4b1bddbf0d 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -20,14 +20,12 @@ namespace osu.Game.Beatmaps protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap { private readonly IResourceStore store; - private readonly AudioManager audioManager; public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, BeatmapInfo beatmapInfo, AudioManager audioManager) - : base(beatmapInfo) + : base(beatmapInfo, audioManager) { this.store = store; this.textureStore = textureStore; - this.audioManager = audioManager; } protected override IBeatmap GetBeatmap() @@ -47,6 +45,8 @@ namespace osu.Game.Beatmaps private TextureStore textureStore; + private ITrackStore trackStore; + protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. protected override Texture GetBackground() @@ -68,8 +68,7 @@ namespace osu.Game.Beatmaps { try { - var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); - return trackData == null ? null : new TrackBass(trackData); + return (trackStore ?? (trackStore = AudioManager.GetTrackStore(store))).Get(getPathForFile(Metadata.AudioFile)); } catch { @@ -77,6 +76,14 @@ namespace osu.Game.Beatmaps } } + public override void RecycleTrack() + { + base.RecycleTrack(); + + trackStore?.Dispose(); + trackStore = null; + } + public override void TransferTo(WorkingBeatmap other) { base.TransferTo(other); @@ -135,7 +142,7 @@ namespace osu.Game.Beatmaps try { - skin = new LegacyBeatmapSkin(BeatmapInfo, store, audioManager); + skin = new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager); } catch (Exception e) { diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index e111f77ba1..390236e053 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; @@ -20,6 +21,8 @@ namespace osu.Game.Beatmaps set => onlineBeatmapSetID = value > 0 ? value : null; } + public DateTimeOffset DateAdded { get; set; } + public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None; public BeatmapMetadata Metadata { get; set; } diff --git a/osu.Game/Beatmaps/BindableBeatmap.cs b/osu.Game/Beatmaps/BindableBeatmap.cs index 27bad65062..af627cc6a9 100644 --- a/osu.Game/Beatmaps/BindableBeatmap.cs +++ b/osu.Game/Beatmaps/BindableBeatmap.cs @@ -1,11 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Diagnostics; -using JetBrains.Annotations; -using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; namespace osu.Game.Beatmaps @@ -16,32 +12,15 @@ namespace osu.Game.Beatmaps /// public abstract class BindableBeatmap : NonNullableBindable { - private AudioManager audioManager; private WorkingBeatmap lastBeatmap; protected BindableBeatmap(WorkingBeatmap defaultValue) : base(defaultValue) { + BindValueChanged(b => updateAudioTrack(b.NewValue), true); } - /// - /// Registers an for s to be added to. - /// - /// The to register. - protected void RegisterAudioManager([NotNull] AudioManager audioManager) - { - if (this.audioManager != null) throw new InvalidOperationException($"Cannot register multiple {nameof(AudioManager)}s."); - - this.audioManager = audioManager; - - ValueChanged += b => registerAudioTrack(b.NewValue); - - // If the track has changed prior to this being called, let's register it - if (Value != Default) - registerAudioTrack(Value); - } - - private void registerAudioTrack(WorkingBeatmap beatmap) + private void updateAudioTrack(WorkingBeatmap beatmap) { var trackLoaded = lastBeatmap?.TrackLoaded ?? false; @@ -55,18 +34,9 @@ namespace osu.Game.Beatmaps lastBeatmap.RecycleTrack(); } - - audioManager.Track.AddItem(beatmap.Track); } lastBeatmap = beatmap; } - - /// - /// Retrieve a new instance weakly bound to this . - /// If you are further binding to events of the retrieved , ensure a local reference is held. - /// - [NotNull] - public new abstract BindableBeatmap GetBoundCopy(); } } diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 96786f5f49..1fd3502799 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -31,24 +32,8 @@ namespace osu.Game.Beatmaps.Drawables /// protected virtual double UnloadDelay => 10000; - private BeatmapInfo lastModel; - private bool firstLoad = true; - - protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad) - { - return new DelayedLoadUnloadWrapper(() => - { - // If DelayedLoadUnloadWrapper is attempting to RELOAD the same content (Beatmap), that means that it was - // previously UNLOADED and thus its children have been disposed of, so we need to recreate them here. - if (!firstLoad && lastModel == Beatmap.Value) - return CreateDrawable(Beatmap.Value); - - // If the model has changed since the previous unload (or if there was no load), then we can safely use the given content - lastModel = Beatmap.Value; - firstLoad = false; - return content; - }, timeBeforeLoad, UnloadDelay); - } + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) + => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay); protected override Drawable CreateDrawable(BeatmapInfo model) { diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 7d25ca3ede..3a4c677bd1 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; @@ -16,9 +17,9 @@ namespace osu.Game.Beatmaps { public class DummyWorkingBeatmap : WorkingBeatmap { - private readonly OsuGameBase game; + private readonly TextureStore textures; - public DummyWorkingBeatmap(OsuGameBase game = null) + public DummyWorkingBeatmap(AudioManager audio, TextureStore textures) : base(new BeatmapInfo { Metadata = new BeatmapMetadata @@ -34,16 +35,16 @@ namespace osu.Game.Beatmaps OverallDifficulty = 0, }, Ruleset = new DummyRulesetInfo() - }) + }, audio) { - this.game = game; + this.textures = textures; } protected override IBeatmap GetBeatmap() => new Beatmap(); - protected override Texture GetBackground() => game?.Textures.Get(@"Backgrounds/bg4"); + protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); - protected override Track GetTrack() => new TrackVirtual { Length = 1000 }; + protected override Track GetTrack() => GetVirtualTrack(); private class DummyRulesetInfo : RulesetInfo { diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 4b0720d867..328763fc9f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -11,15 +11,17 @@ using osu.Framework.IO.File; using System.IO; using System.Linq; using System.Threading; +using osu.Framework.Audio; using osu.Game.IO.Serialization; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Skinning; namespace osu.Game.Beatmaps { - public abstract partial class WorkingBeatmap : IDisposable + public abstract class WorkingBeatmap : IDisposable { public readonly BeatmapInfo BeatmapInfo; @@ -27,8 +29,11 @@ namespace osu.Game.Beatmaps public readonly BeatmapMetadata Metadata; - protected WorkingBeatmap(BeatmapInfo beatmapInfo) + protected AudioManager AudioManager { get; } + + protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) { + AudioManager = audioManager; BeatmapInfo = beatmapInfo; BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); @@ -46,13 +51,39 @@ namespace osu.Game.Beatmaps return b; }); - track = new RecyclableLazy(() => GetTrack() ?? new VirtualBeatmapTrack(Beatmap)); + track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack()); background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); skin = new RecyclableLazy(GetSkin); } + protected virtual Track GetVirtualTrack() + { + const double excess_length = 1000; + + var lastObject = Beatmap.HitObjects.LastOrDefault(); + + double length; + + switch (lastObject) + { + case null: + length = excess_length; + break; + + case IHasEndTime endTime: + length = endTime.EndTime + excess_length; + break; + + default: + length = lastObject.StartTime + excess_length; + break; + } + + return AudioManager.Tracks.GetVirtual(length); + } + /// /// Saves the . /// @@ -150,6 +181,7 @@ namespace osu.Game.Beatmaps public bool SkinLoaded => skin.IsResultAvailable; public Skin Skin => skin.Value; + protected virtual Skin GetSkin() => new DefaultSkin(); private readonly RecyclableLazy skin; @@ -175,7 +207,7 @@ namespace osu.Game.Beatmaps /// Eagerly dispose of the audio track associated with this (if any). /// Accessing track again will load a fresh instance. /// - public void RecycleTrack() => track.Recycle(); + public virtual void RecycleTrack() => track.Recycle(); public class RecyclableLazy { diff --git a/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs b/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs deleted file mode 100644 index 1e237a2b53..0000000000 --- a/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osu.Framework.Audio.Track; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Beatmaps -{ - public partial class WorkingBeatmap - { - /// - /// A type of which provides a valid length based on the s of an . - /// - protected class VirtualBeatmapTrack : TrackVirtual - { - private const double excess_length = 1000; - - public VirtualBeatmapTrack(IBeatmap beatmap) - { - var lastObject = beatmap.HitObjects.LastOrDefault(); - - switch (lastObject) - { - case null: - Length = excess_length; - break; - - case IHasEndTime endTime: - Length = endTime.EndTime + excess_length; - break; - - default: - Length = lastObject.StartTime + excess_length; - break; - } - } - } - } -} diff --git a/osu.Game/Configuration/DatabasedConfigManager.cs b/osu.Game/Configuration/DatabasedConfigManager.cs index 8f1780cab5..d5cdd7e4bc 100644 --- a/osu.Game/Configuration/DatabasedConfigManager.cs +++ b/osu.Game/Configuration/DatabasedConfigManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Game.Rulesets; @@ -19,6 +20,8 @@ namespace osu.Game.Configuration private readonly RulesetInfo ruleset; + private readonly bool legacySettingsExist; + protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int? variant = null) { this.settings = settings; @@ -26,6 +29,7 @@ namespace osu.Game.Configuration this.variant = variant; databasedSettings = settings.Query(ruleset?.ID, variant); + legacySettingsExist = databasedSettings.Any(s => int.TryParse(s.Key, out var _)); InitialiseDefaults(); } @@ -43,7 +47,18 @@ namespace osu.Game.Configuration { base.AddBindable(lookup, bindable); - var setting = databasedSettings.Find(s => (int)s.Key == (int)(object)lookup); + if (legacySettingsExist) + { + var legacySetting = databasedSettings.Find(s => s.Key == ((int)(object)lookup).ToString()); + + if (legacySetting != null) + { + bindable.Parse(legacySetting.Value); + settings.Delete(legacySetting); + } + } + + var setting = databasedSettings.Find(s => s.Key == lookup.ToString()); if (setting != null) { @@ -53,7 +68,7 @@ namespace osu.Game.Configuration { settings.Update(setting = new DatabasedSetting { - Key = lookup, + Key = lookup.ToString(), Value = bindable.Value, RulesetID = ruleset?.ID, Variant = variant, diff --git a/osu.Game/Configuration/DatabasedSetting.cs b/osu.Game/Configuration/DatabasedSetting.cs index d56ac49358..f5c92b3029 100644 --- a/osu.Game/Configuration/DatabasedSetting.cs +++ b/osu.Game/Configuration/DatabasedSetting.cs @@ -1,4 +1,4 @@ -// 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. using System.ComponentModel.DataAnnotations.Schema; @@ -15,12 +15,10 @@ namespace osu.Game.Configuration public int? Variant { get; set; } + public int? SkinInfoID { get; set; } + [Column("Key")] - public int IntKey - { - get => (int)Key; - private set => Key = value; - } + public string Key { get; set; } [Column("Value")] public string StringValue @@ -29,10 +27,9 @@ namespace osu.Game.Configuration set => Value = value; } - public object Key; public object Value; - public DatabasedSetting(object key, object value) + public DatabasedSetting(string key, object value) { Key = key; Value = value; diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index f15fd1f17b..f8c9bdeaf8 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -37,5 +37,11 @@ namespace osu.Game.Configuration SettingChanged?.Invoke(); } + + public void Delete(DatabasedSetting setting) + { + using (var usage = ContextFactory.GetForWrite()) + usage.Context.Remove(setting); + } } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 54dbae9ddc..1c8e722589 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; @@ -13,6 +14,7 @@ using osu.Framework.Extensions; using osu.Framework.IO.File; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Threading; using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.IPC; @@ -29,7 +31,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles + public abstract class ArchiveModelManager : ArchiveModelManager, ICanAcceptFiles where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : INamedFileInfo, new() { @@ -130,56 +132,50 @@ namespace osu.Game.Database /// This will post notifications tracking progress. /// /// One or more archive locations on disk. - public void Import(params string[] paths) + public Task Import(params string[] paths) { var notification = new ProgressNotification { State = ProgressNotificationState.Active }; PostNotification?.Invoke(notification); - Import(notification, paths); + + return Import(notification, paths); } - protected void Import(ProgressNotification notification, params string[] paths) + protected async Task Import(ProgressNotification notification, params string[] paths) { notification.Progress = 0; notification.Text = "Import is initialising..."; - var term = $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; - - List imported = new List(); - int current = 0; - foreach (string path in paths) + var imported = new List(); + + await Task.WhenAll(paths.Select(async path => { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; + notification.CancellationToken.ThrowIfCancellationRequested(); try { - var text = "Importing "; + var model = await Import(path, notification.CancellationToken); - if (path.Length > 1) - text += $"{++current} of {paths.Length} {term}s.."; - else - text += $"{term}.."; + lock (imported) + { + imported.Add(model); + current++; - // only show the filename if it isn't a temporary one (as those look ugly). - if (!path.Contains(Path.GetTempPath())) - text += $"\n{Path.GetFileName(path)}"; - - notification.Text = text; - - imported.Add(Import(path)); - - notification.Progress = (float)current / paths.Length; + notification.Text = $"Imported {current} of {paths.Length} {HumanisedModelName}s"; + notification.Progress = (float)current / paths.Length; + } + } + catch (TaskCanceledException) + { + throw; } catch (Exception e) { - e = e.InnerException ?? e; - Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); + Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})", LoggingTarget.Database); } - } + })); if (imported.Count == 0) { @@ -190,7 +186,7 @@ namespace osu.Game.Database { notification.CompletionText = imported.Count == 1 ? $"Imported {imported.First()}!" - : $"Imported {current} {term}s!"; + : $"Imported {current} {HumanisedModelName}s!"; if (imported.Count > 0 && PresentImport != null) { @@ -210,12 +206,15 @@ namespace osu.Game.Database /// Import one from the filesystem and delete the file on success. /// /// The archive location on disk. + /// An optional cancellation token. /// The imported model, if successful. - public TModel Import(string path) + public async Task Import(string path, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + TModel import; using (ArchiveReader reader = getReaderFrom(path)) - import = Import(reader); + import = await Import(reader, cancellationToken); // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with items from default storage. @@ -228,7 +227,7 @@ namespace osu.Game.Database } catch (Exception e) { - Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); + LogForModel(import, $@"Could not delete original file after import ({Path.GetFileName(path)})", e); } return import; @@ -243,23 +242,32 @@ namespace osu.Game.Database /// Import an item from an . /// /// The archive to be imported. - public TModel Import(ArchiveReader archive) + /// An optional cancellation token. + public Task Import(ArchiveReader archive, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + + TModel model = null; + try { - var model = CreateModel(archive); + model = CreateModel(archive); if (model == null) return null; model.Hash = computeHash(archive); - - return Import(model, archive); + } + catch (TaskCanceledException) + { + throw; } catch (Exception e) { - Logger.Error(e, $"Model creation of {archive.Name} failed.", LoggingTarget.Database); + LogForModel(model, $"Model creation of {archive.Name} failed.", e); return null; } + + return Import(model, archive, cancellationToken); } /// @@ -269,6 +277,16 @@ namespace osu.Game.Database /// protected abstract string[] HashableFileTypes { get; } + protected static void LogForModel(TModel model, string message, Exception e = null) + { + string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]"; + + if (e != null) + Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database); + else + Logger.Log($"{prefix} {message}", LoggingTarget.Database); + } + /// /// Create a SHA-2 hash from the provided archive based on file content of all files matching . /// @@ -288,13 +306,30 @@ namespace osu.Game.Database /// /// The model to be imported. /// An optional archive to use for model population. - public TModel Import(TModel item, ArchiveReader archive = null) + /// An optional cancellation token. + public async Task Import(TModel item, ArchiveReader archive = null, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => { + cancellationToken.ThrowIfCancellationRequested(); + delayEvents(); + void rollback() + { + if (!Delete(item)) + { + // We may have not yet added the model to the underlying table, but should still clean up files. + LogForModel(item, "Dereferencing files for incomplete import."); + Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray()); + } + } + try { - Logger.Log($"Importing {item}...", LoggingTarget.Database); + LogForModel(item, "Beginning import..."); + + item.Files = archive != null ? createFileInfos(archive, Files) : new List(); + + await Populate(item, archive, cancellationToken); using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { @@ -302,11 +337,6 @@ namespace osu.Game.Database { if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - if (archive != null) - item.Files = createFileInfos(archive, Files); - - Populate(item, archive); - var existing = CheckForExisting(item); if (existing != null) @@ -314,15 +344,17 @@ namespace osu.Game.Database if (CanUndelete(existing, item)) { Undelete(existing); - Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database); + LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); handleEvent(() => ItemAdded?.Invoke(existing, true)); + + // existing item will be used; rollback new import and exit early. + rollback(); + flushEvents(true); return existing; } - else - { - Delete(existing); - ModelStore.PurgeDeletable(s => s.ID == existing.ID); - } + + Delete(existing); + ModelStore.PurgeDeletable(s => s.ID == existing.ID); } PreImport(item); @@ -337,21 +369,21 @@ namespace osu.Game.Database } } - Logger.Log($"Import of {item} successfully completed!", LoggingTarget.Database); + LogForModel(item, "Import successfully completed!"); } catch (Exception e) { - Logger.Error(e, $"Import of {item} failed and has been rolled back.", LoggingTarget.Database); - item = null; - } - finally - { - // we only want to flush events after we've confirmed the write context didn't have any errors. - flushEvents(item != null); + if (!(e is TaskCanceledException)) + LogForModel(item, "Database import or population failed and has been rolled back.", e); + + rollback(); + flushEvents(false); + throw; } + flushEvents(true); return item; - } + }, cancellationToken, TaskCreationOptions.HideScheduler, IMPORT_SCHEDULER).Unwrap(); /// /// Perform an update of the specified item. @@ -392,7 +424,8 @@ namespace osu.Game.Database var notification = new ProgressNotification { Progress = 0, - CompletionText = $"Deleted all {typeof(TModel).Name.Replace("Info", "").ToLower()}s!", + Text = $"Preparing to delete all {HumanisedModelName}s...", + CompletionText = $"Deleted all {HumanisedModelName}s!", State = ProgressNotificationState.Active, }; @@ -409,7 +442,7 @@ namespace osu.Game.Database // user requested abort return; - notification.Text = $"Deleting ({++i} of {items.Count})"; + notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})"; Delete(b); @@ -533,7 +566,7 @@ namespace osu.Game.Database return Task.CompletedTask; } - return Task.Factory.StartNew(() => Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning); + return Task.Run(async () => await Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray())); } #endregion @@ -552,9 +585,8 @@ namespace osu.Game.Database /// /// The model to populate. /// The archive to use as a reference for population. May be null. - protected virtual void Populate(TModel model, [CanBeNull] ArchiveReader archive) - { - } + /// An optional cancellation token. + protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask; /// /// Perform any final actions before the import to database executes. @@ -582,6 +614,8 @@ namespace osu.Game.Database private DbSet queryModel() => ContextFactory.Get().Set(); + protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; + /// /// Creates an from a valid storage path. /// @@ -599,4 +633,18 @@ namespace osu.Game.Database throw new InvalidFormatException($"{path} is not a valid archive"); } } + + public abstract class ArchiveModelManager + { + private const int import_queue_request_concurrency = 1; + + /// + /// A singleton scheduler shared by all . + /// + /// + /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly. + /// It is mainly being used as a queue mechanism for large imports. + /// + protected static readonly ThreadedTaskScheduler IMPORT_SCHEDULER = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); + } } diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index f55d0c389e..b9f882468d 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; + namespace osu.Game.Database { /// @@ -12,7 +14,7 @@ namespace osu.Game.Database /// Import the specified paths. /// /// The files which should be imported. - void Import(params string[] paths); + Task Import(params string[] paths); /// /// An array of accepted file extensions (in the standard format of ".abc"). diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index e2c7693700..29113e0e2f 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -214,7 +214,6 @@ namespace osu.Game.Graphics.Backgrounds base.Draw(vertexAction); shader.Bind(); - texture.TextureGL.Bind(); Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy; @@ -231,7 +230,8 @@ namespace osu.Game.Graphics.Backgrounds ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(particle.Colour); - texture.DrawTriangle( + DrawTriangle( + texture, triangle, colourInfo, null, diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index a5b5b7af42..cda5e150de 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Containers protected void BeginConfirm() { - if (confirming || !AllowMultipleFires && fired) return; + if (confirming || (!AllowMultipleFires && fired)) return; confirming = true; diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 6dbe340efb..1f31e4cdda 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.Containers { if (AutoSizeAxes != Axes.None) { - content.RelativeSizeAxes = RelativeSizeAxes; + content.RelativeSizeAxes = (Axes.Both & ~AutoSizeAxes); content.AutoSizeAxes = AutoSizeAxes; } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 3f84f77081..f6db3102f2 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -51,10 +51,10 @@ namespace osu.Game.Graphics.Containers if (osuGame != null) OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); - samplePopIn = audio.Sample.Get(@"UI/overlay-pop-in"); - samplePopOut = audio.Sample.Get(@"UI/overlay-pop-out"); + samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in"); + samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out"); - StateChanged += onStateChanged; + State.ValueChanged += onStateChanged; } /// @@ -70,7 +70,7 @@ namespace osu.Game.Graphics.Containers { if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) { - State = Visibility.Hidden; + Hide(); return true; } @@ -82,7 +82,7 @@ namespace osu.Game.Graphics.Containers switch (action) { case GlobalAction.Back: - State = Visibility.Hidden; + Hide(); return true; case GlobalAction.Select: @@ -94,9 +94,9 @@ namespace osu.Game.Graphics.Containers public bool OnReleased(GlobalAction action) => false; - private void onStateChanged(Visibility visibility) + private void onStateChanged(ValueChangedEvent state) { - switch (visibility) + switch (state.NewValue) { case Visibility.Visible: if (OverlayActivationMode.Value != OverlayActivation.Disabled) @@ -105,7 +105,7 @@ namespace osu.Game.Graphics.Containers if (BlockScreenWideMouse && DimMainContent) osuGame?.AddBlockingOverlay(this); } else - State = Visibility.Hidden; + Hide(); break; diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index c4f85926ee..67af79c763 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -24,7 +24,13 @@ namespace osu.Game.Graphics.Containers { Enabled.ValueChanged += e => { - if (!e.NewValue) unhover(); + if (isHovered) + { + if (e.NewValue) + fadeIn(); + else + fadeOut(); + } }; } @@ -32,27 +38,28 @@ namespace osu.Game.Graphics.Containers protected override bool OnHover(HoverEvent e) { + if (isHovered) + return false; + + isHovered = true; + if (!Enabled.Value) return false; - EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint)); - isHovered = true; + fadeIn(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - unhover(); - base.OnHoverLost(e); - } - - private void unhover() - { - if (!isHovered) return; + if (!isHovered) + return; isHovered = false; - EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint)); + fadeOut(); + + base.OnHoverLost(e); } [BackgroundDependencyLoader] @@ -67,5 +74,9 @@ namespace osu.Game.Graphics.Containers base.LoadComplete(); EffectTargets.ForEach(d => d.FadeColour(IdleColour)); } + + private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint)); + + private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint)); } } diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 464682a0ad..f87909ab17 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers protected override void PopIn() { foreach (var w in wavesContainer.Children) - w.State = Visibility.Visible; + w.Show(); this.FadeIn(100, Easing.OutQuint); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); @@ -117,7 +117,7 @@ namespace osu.Game.Graphics.Containers contentContainer.MoveToY(DrawHeight * 2f, DISAPPEAR_DURATION, Easing.In); foreach (var w in wavesContainer.Children) - w.State = Visibility.Hidden; + w.Hide(); this.FadeOut(DISAPPEAR_DURATION, Easing.InQuint); } diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 92e5ba6195..b7ea1ba56a 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -29,7 +29,7 @@ namespace osu.Game.Graphics.Cursor { AddRangeInternal(new Drawable[] { - Cursor = new MenuCursor { State = Visibility.Hidden }, + Cursor = new MenuCursor { State = { Value = Visibility.Hidden } }, content = new Container { RelativeSizeAxes = Axes.Both } }); } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 53693a1e38..63ec24f84f 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -40,8 +40,10 @@ namespace osu.Game.Graphics // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = FromHex(@"eeeeff"); public readonly Color4 PurpleLight = FromHex(@"aa88ff"); + public readonly Color4 PurpleLightAlternative = FromHex(@"cba4da"); public readonly Color4 Purple = FromHex(@"8866ee"); public readonly Color4 PurpleDark = FromHex(@"6644cc"); + public readonly Color4 PurpleDarkAlternative = FromHex(@"312436"); public readonly Color4 PurpleDarker = FromHex(@"441188"); public readonly Color4 PinkLighter = FromHex(@"ffddee"); diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 24a98e6dc9..5ad5e5569a 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -51,7 +51,7 @@ namespace osu.Game.Graphics screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); - shutter = audio.Sample.Get("UI/shutter"); + shutter = audio.Samples.Get("UI/shutter"); } public bool OnPressed(GlobalAction action) diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index f5e57e5f27..d1e55fee24 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -82,6 +82,10 @@ namespace osu.Game.Graphics.UserInterface } } + public override void Hide() => State = Visibility.Hidden; + + public override void Show() => State = Visibility.Visible; + public BreadcrumbTabItem(T value) : base(value) { diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index cbbaa6d303..70d988f60e 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleClick = audio.Sample.Get($@"UI/generic-select{SampleSet.GetDescription()}"); + sampleClick = audio.Samples.Get($@"UI/generic-select{SampleSet.GetDescription()}"); } } } diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index b246092a7f..f1ac8ced6e 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleHover = audio.Sample.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); + sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); } } diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs index 6414e488e8..052e9194fa 100644 --- a/osu.Game/Graphics/UserInterface/IconButton.cs +++ b/osu.Game/Graphics/UserInterface/IconButton.cs @@ -66,6 +66,7 @@ namespace osu.Game.Graphics.UserInterface set { Content.RelativeSizeAxes = Axes.None; + Content.AutoSizeAxes = Axes.None; Content.Size = value; } } diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index a8041c79fc..1a8fea4ff9 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -19,14 +19,14 @@ namespace osu.Game.Graphics.UserInterface public class OsuAnimatedButton : OsuClickableContainer { /// - /// The colour that should be flashed when the is clicked. + /// The colour that should be flashed when the is clicked. /// protected Color4 FlashColour = Color4.White.Opacity(0.3f); private Color4 hoverColour = Color4.White.Opacity(0.1f); /// - /// The background colour of the while it is hovered. + /// The background colour of the while it is hovered. /// protected Color4 HoverColour { @@ -74,6 +74,12 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { + if (AutoSizeAxes != Axes.None) + { + content.RelativeSizeAxes = (Axes.Both & ~AutoSizeAxes); + content.AutoSizeAxes = AutoSizeAxes; + } + Enabled.BindValueChanged(enabled => this.FadeColour(enabled.NewValue ? Color4.White : colours.Gray9, 200, Easing.OutQuint), true); } diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 7a27f825f6..494d4e4262 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -17,11 +17,11 @@ namespace osu.Game.Graphics.UserInterface /// /// A button with added default sound effects. /// - public class OsuButton : Button + public abstract class OsuButton : Button { private Box hover; - public OsuButton() + protected OsuButton() { Height = 40; diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index de3d93d845..5ead5987a1 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -6,10 +6,9 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface @@ -33,27 +32,26 @@ namespace osu.Game.Graphics.UserInterface public string LabelText { - get => labelSpriteText?.Text; set { - if (labelSpriteText != null) - labelSpriteText.Text = value; + if (labelText != null) + labelText.Text = value; } } public MarginPadding LabelPadding { - get => labelSpriteText?.Padding ?? new MarginPadding(); + get => labelText?.Padding ?? new MarginPadding(); set { - if (labelSpriteText != null) - labelSpriteText.Padding = value; + if (labelText != null) + labelText.Padding = value; } } protected readonly Nub Nub; - private readonly SpriteText labelSpriteText; + private readonly OsuTextFlowContainer labelText; private SampleChannel sampleChecked; private SampleChannel sampleUnchecked; @@ -62,24 +60,28 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; + const float nub_padding = 5; + Children = new Drawable[] { - labelSpriteText = new OsuSpriteText(), + labelText = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding } + }, Nub = new Nub { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 5 }, + Margin = new MarginPadding { Right = nub_padding }, }, new HoverClickSounds() }; Nub.Current.BindTo(Current); - Current.DisabledChanged += disabled => - { - Alpha = disabled ? 0.3f : 1; - }; + Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1; } protected override void LoadComplete() @@ -112,8 +114,8 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleChecked = audio.Sample.Get(@"UI/check-on"); - sampleUnchecked = audio.Sample.Get(@"UI/check-off"); + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 32994be78a..f8234cb81f 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -71,8 +71,8 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleHover = audio.Sample.Get(@"UI/generic-hover"); - sampleClick = audio.Sample.Get(@"UI/generic-select"); + sampleHover = audio.Samples.Get(@"UI/generic-hover"); + sampleClick = audio.Samples.Get(@"UI/generic-select"); BackgroundColour = Color4.Transparent; BackgroundColourHover = OsuColour.FromHex(@"172023"); diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index c3c447ef83..5c706781e6 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -86,7 +86,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { - sample = audio.Sample.Get(@"UI/sliderbar-notch"); + sample = audio.Samples.Get(@"UI/sliderbar-notch"); AccentColour = colours.Pink; } diff --git a/osu.Game/Graphics/UserInterface/ProcessingOverlay.cs b/osu.Game/Graphics/UserInterface/ProcessingOverlay.cs index 8b50f4a97a..d75e78e2d9 100644 --- a/osu.Game/Graphics/UserInterface/ProcessingOverlay.cs +++ b/osu.Game/Graphics/UserInterface/ProcessingOverlay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, Alpha = 0.9f, }, - new LoadingAnimation { State = Visibility.Visible } + new LoadingAnimation { State = { Value = Visibility.Visible } } }; } diff --git a/osu.Game/Graphics/UserInterface/ScreenTitle.cs b/osu.Game/Graphics/UserInterface/ScreenTitle.cs index fe1936d4e8..7b39238e5e 100644 --- a/osu.Game/Graphics/UserInterface/ScreenTitle.cs +++ b/osu.Game/Graphics/UserInterface/ScreenTitle.cs @@ -76,11 +76,11 @@ namespace osu.Game.Graphics.UserInterface { titleText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 25), + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, pageText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 25), + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), } } } diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index a561523799..4ee7a19ebc 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -15,6 +15,8 @@ namespace osu.Game.IO.Archives /// public abstract Stream GetStream(string name); + public IEnumerable GetAvailableResources() => Filenames; + public abstract void Dispose(); /// diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 458f8964f9..370d6786f5 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -2,8 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -27,6 +30,13 @@ namespace osu.Game.IO Store = new StorageBackedResourceStore(Storage); } + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public IEnumerable QueryFiles(Expression> query) => ContextFactory.Get().Set().AsNoTracking().Where(f => f.ReferenceCount > 0).Where(query); + public FileInfo Add(Stream data, bool reference = true) { using (var usage = ContextFactory.GetForWrite()) diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index fc747cd446..484db932f8 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -38,7 +38,7 @@ namespace osu.Game.IPC } if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant())) - importer.Import(path); + await importer.Import(path); } } diff --git a/osu.Game/Migrations/20180125143340_Settings.cs b/osu.Game/Migrations/20180125143340_Settings.cs index 2e2768dc7c..166d3c086d 100644 --- a/osu.Game/Migrations/20180125143340_Settings.cs +++ b/osu.Game/Migrations/20180125143340_Settings.cs @@ -16,7 +16,7 @@ namespace osu.Game.Migrations { ID = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - Key = table.Column(type: "INTEGER", nullable: false), + Key = table.Column(type: "TEXT", nullable: false), RulesetID = table.Column(type: "INTEGER", nullable: true), Value = table.Column(type: "TEXT", nullable: true), Variant = table.Column(type: "INTEGER", nullable: true) diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs new file mode 100644 index 0000000000..348c42adb9 --- /dev/null +++ b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs @@ -0,0 +1,498 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190525060824_SkinSettings")] + partial class SkinSettings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs new file mode 100644 index 0000000000..99237419b7 --- /dev/null +++ b/osu.Game/Migrations/20190525060824_SkinSettings.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class SkinSettings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"create table Settings_dg_tmp + ( + ID INTEGER not null + constraint PK_Settings + primary key autoincrement, + Key TEXT not null, + RulesetID INTEGER, + Value TEXT, + Variant INTEGER, + SkinInfoID int + constraint Settings_SkinInfo_ID_fk + references SkinInfo + on delete restrict + ); + + insert into Settings_dg_tmp(ID, Key, RulesetID, Value, Variant) select ID, Key, RulesetID, Value, Variant from Settings; + + drop table Settings; + + alter table Settings_dg_tmp rename to Settings; + + create index IX_Settings_RulesetID_Variant + on Settings (RulesetID, Variant); + + create index Settings_SkinInfoID_index + on Settings (SkinInfoID); + + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Settings_SkinInfo_SkinInfoID", + table: "Settings"); + + migrationBuilder.DropIndex( + name: "IX_Settings_SkinInfoID", + table: "Settings"); + + migrationBuilder.DropColumn( + name: "SkinInfoID", + table: "Settings"); + } + } +} diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs new file mode 100644 index 0000000000..9477369aa0 --- /dev/null +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs @@ -0,0 +1,489 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190605091246_AddDateAddedColumnToBeatmapSet")] + partial class AddDateAddedColumnToBeatmapSet + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs new file mode 100644 index 0000000000..55dc18b6a3 --- /dev/null +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddDateAddedColumnToBeatmapSet : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DateAdded", + table: "BeatmapSetInfo", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DateAdded", + table: "BeatmapSetInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 8430e00e4f..11b032a941 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace osu.Game.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.2.1-servicing-10028"); + .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => { @@ -166,6 +166,8 @@ namespace osu.Game.Migrations b.Property("ID") .ValueGeneratedOnAdd(); + b.Property("DateAdded"); + b.Property("DeletePending"); b.Property("Hash"); @@ -198,11 +200,13 @@ namespace osu.Game.Migrations b.Property("ID") .ValueGeneratedOnAdd(); - b.Property("IntKey") + b.Property("Key") .HasColumnName("Key"); b.Property("RulesetID"); + b.Property("SkinInfoID"); + b.Property("StringValue") .HasColumnName("Value"); @@ -210,6 +214,8 @@ namespace osu.Game.Migrations b.HasKey("ID"); + b.HasIndex("SkinInfoID"); + b.HasIndex("RulesetID", "Variant"); b.ToTable("Settings"); @@ -442,6 +448,13 @@ namespace osu.Game.Migrations .HasForeignKey("MetadataID"); }); + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => { b.HasOne("osu.Game.IO.FileInfo", "FileInfo") diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 594bc1e3ca..12b38fab1e 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -37,7 +37,9 @@ namespace osu.Game.Online.API public Bindable LocalUser { get; } = new Bindable(createGuestUser()); - protected bool HasLogin => authentication.Token.Value != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password); + public Bindable Activity { get; } = new Bindable(); + + protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -55,6 +57,12 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; + LocalUser.BindValueChanged(u => + { + u.OldValue?.Activity.UnbindFrom(Activity); + u.NewValue.Activity.BindTo(Activity); + }, true); + var thread = new Thread(run) { Name = "APIAccess", diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 99fde10309..6c04c77dc0 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -17,6 +17,8 @@ namespace osu.Game.Online.API Id = 1001, }); + public Bindable Activity { get; } = new Bindable(); + public bool IsLoggedIn => true; public string ProvidedUsername => LocalUser.Value.Username; @@ -41,6 +43,15 @@ namespace osu.Game.Online.API } } + public DummyAPIAccess() + { + LocalUser.BindValueChanged(u => + { + u.OldValue?.Activity.UnbindFrom(Activity); + u.NewValue.Activity.BindTo(Activity); + }, true); + } + public virtual void Queue(APIRequest request) { } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 7c1f850943..0cd41aee26 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -13,6 +13,11 @@ namespace osu.Game.Online.API /// Bindable LocalUser { get; } + /// + /// The current user's activity. + /// + Bindable Activity { get; } + /// /// Returns whether the local user is logged in. /// diff --git a/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs b/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs new file mode 100644 index 0000000000..baa15c70c4 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetChangelogBuildRequest.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetChangelogBuildRequest : APIRequest + { + private readonly string name; + private readonly string version; + + public GetChangelogBuildRequest(string streamName, string buildVersion) + { + name = streamName; + version = buildVersion; + } + + protected override string Target => $@"changelog/{name}/{version}"; + } +} diff --git a/osu.Game/Online/API/Requests/GetChangelogRequest.cs b/osu.Game/Online/API/Requests/GetChangelogRequest.cs new file mode 100644 index 0000000000..97799ff66a --- /dev/null +++ b/osu.Game/Online/API/Requests/GetChangelogRequest.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetChangelogRequest : APIRequest + { + protected override string Target => @"changelog"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogBuild.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogBuild.cs new file mode 100644 index 0000000000..40f1b791f9 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogBuild.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 Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIChangelogBuild : IEquatable + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("display_version")] + public string DisplayVersion { get; set; } + + [JsonProperty("users")] + public long Users { get; set; } + + [JsonProperty("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonProperty("update_stream")] + public APIUpdateStream UpdateStream { get; set; } + + [JsonProperty("changelog_entries")] + public List ChangelogEntries { get; set; } + + [JsonProperty("versions")] + public VersionNatigation Versions { get; set; } + + public class VersionNatigation + { + [JsonProperty("next")] + public APIChangelogBuild Next { get; set; } + + [JsonProperty("previous")] + public APIChangelogBuild Previous { get; set; } + } + + public bool Equals(APIChangelogBuild other) => Id == other?.Id; + + public override string ToString() => $"{UpdateStream.DisplayName} {DisplayVersion}"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs new file mode 100644 index 0000000000..140e228acd --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.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 System; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIChangelogEntry + { + [JsonProperty("id")] + public long? Id { get; set; } + + [JsonProperty("repository")] + public string Repository { get; set; } + + [JsonProperty("github_pull_request_id")] + public long? GithubPullRequestId { get; set; } + + [JsonProperty("github_url")] + public string GithubUrl { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("category")] + public string Category { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("message_html")] + public string MessageHtml { get; set; } + + [JsonProperty("major")] + public bool Major { get; set; } + + [JsonProperty("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + [JsonProperty("github_user")] + public APIChangelogUser GithubUser { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogIndex.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogIndex.cs new file mode 100644 index 0000000000..778e8754fe --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogIndex.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIChangelogIndex + { + [JsonProperty] + public List Builds; + + [JsonProperty] + public List Streams; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs new file mode 100644 index 0000000000..5891391e83 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIChangelogUser + { + [JsonProperty("id")] + public long? Id { get; set; } + + [JsonProperty("display_name")] + public string DisplayName { get; set; } + + [JsonProperty("github_url")] + public string GithubUrl { get; set; } + + [JsonProperty("osu_username")] + public string OsuUsername { get; set; } + + [JsonProperty("user_id")] + public long? UserId { get; set; } + + [JsonProperty("user_url")] + public string UserUrl { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs new file mode 100644 index 0000000000..d9e48373bb --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs @@ -0,0 +1,60 @@ +// 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 Newtonsoft.Json; +using osu.Framework.Graphics.Colour; +using osuTK.Graphics; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIUpdateStream : IEquatable + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("is_featured")] + public bool IsFeatured { get; set; } + + [JsonProperty("display_name")] + public string DisplayName { get; set; } + + [JsonProperty("latest_build")] + public APIChangelogBuild LatestBuild { get; set; } + + public bool Equals(APIUpdateStream other) => Id == other?.Id; + + public ColourInfo Colour + { + get + { + switch (Name) + { + case "stable40": + return new Color4(102, 204, 255, 255); + + case "stable": + return new Color4(34, 153, 187, 255); + + case "beta40": + return new Color4(255, 221, 85, 255); + + case "cuttingedge": + return new Color4(238, 170, 0, 255); + + case OsuGameBase.CLIENT_STREAM_NAME: + return new Color4(237, 18, 33, 255); + + case "web": + return new Color4(136, 102, 238, 255); + + default: + return new Color4(0, 0, 0, 255); + } + } + } + } +} diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 2efc9f4968..3af11ff20f 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -81,6 +81,9 @@ namespace osu.Game.Online.Chat if (user == null) throw new ArgumentNullException(nameof(user)); + if (user.Id == api.LocalUser.Value.Id) + return; + CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id)) ?? new Channel(user); } diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index e1fc65da6c..4aaffdd161 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -69,7 +69,7 @@ namespace osu.Game.Online.Chat if (displayText.Length == 0 || linkText.Length == 0) continue; // Check for encapsulated links - if (result.Links.Find(l => l.Index <= index && l.Index + l.Length >= index + m.Length || index <= l.Index && index + m.Length >= l.Index + l.Length) == null) + if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || (index <= l.Index && index + m.Length >= l.Index + l.Length)) == null) { result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8cdddd6736..d5fbcdfee3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -132,12 +132,12 @@ namespace osu.Game public void CloseAllOverlays(bool hideToolbarElements = true) { foreach (var overlay in overlays) - overlay.State = Visibility.Hidden; + overlay.Hide(); if (hideToolbarElements) { foreach (var overlay in toolbarElements) - overlay.State = Visibility.Hidden; + overlay.Hide(); } } @@ -435,6 +435,7 @@ namespace osu.Game loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true); + var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); @@ -460,7 +461,7 @@ namespace osu.Game loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add); - chatOverlay.StateChanged += state => channelManager.HighPollRate.Value = state == Visibility.Visible; + chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); @@ -469,9 +470,9 @@ namespace osu.Game foreach (var overlay in singleDisplaySideOverlays) { - overlay.StateChanged += state => + overlay.State.ValueChanged += state => { - if (state == Visibility.Hidden) return; + if (state.NewValue == Visibility.Hidden) return; singleDisplaySideOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); }; @@ -483,26 +484,26 @@ namespace osu.Game foreach (var overlay in informationalOverlays) { - overlay.StateChanged += state => + overlay.State.ValueChanged += state => { - if (state == Visibility.Hidden) return; + if (state.NewValue == Visibility.Hidden) return; informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); }; } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay }; overlays.AddRange(singleDisplayOverlays); foreach (var overlay in singleDisplayOverlays) { - overlay.StateChanged += state => + overlay.State.ValueChanged += state => { // informational overlays should be dismissed on a show or hide of a full overlay. informationalOverlays.ForEach(o => o.Hide()); - if (state == Visibility.Hidden) return; + if (state.NewValue == Visibility.Hidden) return; singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); }; @@ -517,16 +518,16 @@ namespace osu.Game { float offset = 0; - if (settings.State == Visibility.Visible) + if (settings.State.Value == Visibility.Visible) offset += ToolbarButton.WIDTH / 2; - if (notifications.State == Visibility.Visible) + if (notifications.State.Value == Visibility.Visible) offset -= ToolbarButton.WIDTH / 2; screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint); } - settings.StateChanged += _ => updateScreenOffset(); - notifications.StateChanged += _ => updateScreenOffset(); + settings.State.ValueChanged += _ => updateScreenOffset(); + notifications.State.ValueChanged += _ => updateScreenOffset(); } public class GameIdleTracker : IdleTracker @@ -767,7 +768,7 @@ namespace osu.Game if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); else - Toolbar.State = Visibility.Visible; + Toolbar.Show(); } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0f84b17c89..4c1266c3a6 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -44,6 +45,8 @@ namespace osu.Game /// public class OsuGameBase : Framework.Game, ICanAcceptFiles { + public const string CLIENT_STREAM_NAME = "lazer"; + protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; @@ -161,7 +164,7 @@ namespace osu.Game dependencies.CacheAs(API); - var defaultBeatmap = new DummyWorkingBeatmap(this); + var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); @@ -193,9 +196,9 @@ namespace osu.Game // tracks play so loud our samples can't keep up. // this adds a global reduction of track volume for the time being. - Audio.Track.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); + Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); - beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio); + beatmap = new OsuBindableBeatmap(defaultBeatmap); dependencies.CacheAs>(beatmap); dependencies.CacheAs(beatmap); @@ -268,38 +271,23 @@ namespace osu.Game private readonly List fileImporters = new List(); - protected LargeTextureStore LargeTextureStore; - - public void Import(params string[] paths) + public async Task Import(params string[] paths) { var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); foreach (var importer in fileImporters) if (importer.HandledExtensions.Contains(extension)) - importer.Import(paths); + await importer.Import(paths); } public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); private class OsuBindableBeatmap : BindableBeatmap { - public OsuBindableBeatmap(WorkingBeatmap defaultValue, AudioManager audioManager) - : this(defaultValue) - { - RegisterAudioManager(audioManager); - } - public OsuBindableBeatmap(WorkingBeatmap defaultValue) : base(defaultValue) { } - - public override BindableBeatmap GetBoundCopy() - { - var copy = new OsuBindableBeatmap(Default); - copy.BindTo(this); - return copy; - } } private class OsuUserInputManager : UserInputManager diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 52d2917677..89d8cbde11 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.cs @@ -109,7 +109,7 @@ namespace osu.Game.Overlays break; case APIState.Online: - State = Visibility.Hidden; + Hide(); break; } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 89da0fc254..cbcf3e6160 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Text = "#1", Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold, italics: true) }, - rank = new DrawableRank(ScoreRank.F) + rank = new DrawableRank(ScoreRank.D) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 3ed398d31a..e0852a890c 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays protected override bool OnClick(ClickEvent e) { - State = Visibility.Hidden; + Hide(); return true; } diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs new file mode 100644 index 0000000000..3d145af562 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -0,0 +1,207 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using System; +using System.Linq; +using System.Text.RegularExpressions; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; +using osuTK.Graphics; +using osu.Framework.Allocation; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogBuild : FillFlowContainer + { + public const float HORIZONTAL_PADDING = 70; + + public Action SelectBuild; + + protected readonly APIChangelogBuild Build; + + public readonly FillFlowContainer ChangelogEntries; + + public ChangelogBuild(APIChangelogBuild build) + { + Build = build; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING }; + + Children = new Drawable[] + { + CreateHeader(), + ChangelogEntries = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + foreach (var categoryEntries in Build.ChangelogEntries.GroupBy(b => b.Category).OrderBy(c => c.Key)) + { + ChangelogEntries.Add(new OsuSpriteText + { + Text = categoryEntries.Key, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 24), + Margin = new MarginPadding { Top = 35, Bottom = 15 }, + }); + + var fontLarge = OsuFont.GetFont(size: 18); + var fontMedium = OsuFont.GetFont(size: 14); + var fontSmall = OsuFont.GetFont(size: 12); + + foreach (APIChangelogEntry entry in categoryEntries) + { + LinkFlowContainer title = new LinkFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Vertical = 5 }, + }; + + var entryColour = entry.Major ? colours.YellowLight : Color4.White; + + title.AddIcon(FontAwesome.Solid.Check, t => + { + t.Font = fontSmall; + t.Colour = entryColour; + t.Padding = new MarginPadding { Left = -17, Right = 5 }; + }); + + title.AddText(entry.Title, t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + + if (!string.IsNullOrEmpty(entry.Repository)) + { + title.AddText(" (", t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External, + creationParameters: t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + title.AddText(")", t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + } + + title.AddText(" by ", t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + + if (entry.GithubUser.UserId != null) + title.AddUserLink(new User + { + Username = entry.GithubUser.OsuUsername, + Id = entry.GithubUser.UserId.Value + }, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + else if (entry.GithubUser.GithubUrl != null) + title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + else + title.AddText(entry.GithubUser.DisplayName, t => + { + t.Font = fontSmall; + t.Colour = entryColour; + }); + + ChangelogEntries.Add(title); + + if (!string.IsNullOrEmpty(entry.MessageHtml)) + { + TextFlowContainer message = new TextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + // todo: use markdown parsing once API returns markdown + message.AddText(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty), t => + { + t.Font = fontSmall; + t.Colour = new Color4(235, 184, 254, 255); + }); + + ChangelogEntries.Add(message); + } + } + } + } + + protected virtual FillFlowContainer CreateHeader() => new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Top = 20 }, + Children = new Drawable[] + { + new OsuHoverContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Action = () => SelectBuild?.Invoke(Build), + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Horizontal = 40 }, + Children = new[] + { + new OsuSpriteText + { + Text = Build.UpdateStream.DisplayName, + Font = OsuFont.GetFont(weight: FontWeight.Medium, size: 19), + }, + new OsuSpriteText + { + Text = " ", + Font = OsuFont.GetFont(weight: FontWeight.Medium, size: 19), + }, + new OsuSpriteText + { + Text = Build.DisplayVersion, + Font = OsuFont.GetFont(weight: FontWeight.Light, size: 19), + Colour = Build.UpdateStream.Colour, + }, + } + } + }, + } + }; + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogContent.cs b/osu.Game/Overlays/Changelog/ChangelogContent.cs new file mode 100644 index 0000000000..f8d5bbd66c --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogContent.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using System; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogContent : FillFlowContainer + { + public Action BuildSelected; + + public void SelectBuild(APIChangelogBuild build) => BuildSelected?.Invoke(build); + + public ChangelogContent() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Padding = new MarginPadding { Bottom = 100 }; + } + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs new file mode 100644 index 0000000000..fca62fbb44 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -0,0 +1,170 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using 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.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogHeader : OverlayHeader + { + public readonly Bindable Current = new Bindable(); + + public Action ListingSelected; + + public UpdateStreamBadgeArea Streams; + + private const string listing_string = "Listing"; + + public ChangelogHeader() + { + TabControl.AddItem(listing_string); + TabControl.Current.ValueChanged += e => + { + if (e.NewValue == listing_string) + ListingSelected?.Invoke(); + }; + + Current.ValueChanged += showBuild; + + Streams.Current.ValueChanged += e => + { + if (e.NewValue?.LatestBuild != null && e.NewValue != Current.Value?.UpdateStream) + Current.Value = e.NewValue.LatestBuild; + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + TabControl.AccentColour = colours.Violet; + } + + private ChangelogHeaderTitle title; + + private void showBuild(ValueChangedEvent e) + { + if (e.OldValue != null) + TabControl.RemoveItem(e.OldValue.ToString()); + + if (e.NewValue != null) + { + TabControl.AddItem(e.NewValue.ToString()); + TabControl.Current.Value = e.NewValue.ToString(); + + Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == e.NewValue.UpdateStream.Name); + + title.Version = e.NewValue.UpdateStream.DisplayName; + } + else + { + TabControl.Current.Value = listing_string; + Streams.Current.Value = null; + title.Version = null; + } + } + + protected override Drawable CreateBackground() => new HeaderBackground(); + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + Streams = new UpdateStreamBadgeArea(), + } + }; + + protected override ScreenTitle CreateTitle() => title = new ChangelogHeaderTitle(); + + public class HeaderBackground : Sprite + { + public HeaderBackground() + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fill; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get(@"Headers/changelog"); + } + } + + private class ChangelogHeaderTitle : ScreenTitle + { + public string Version + { + set => Section = value ?? listing_string; + } + + public ChangelogHeaderTitle() + { + Title = "Changelog"; + Version = null; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.Violet; + } + + protected override Drawable CreateIcon() => new ChangelogIcon(); + + internal class ChangelogIcon : CompositeDrawable + { + private const float circle_allowance = 0.8f; + + [BackgroundDependencyLoader] + private void load(TextureStore textures, OsuColour colours) + { + Size = new Vector2(ICON_SIZE / circle_allowance); + + InternalChildren = new Drawable[] + { + new CircularContainer + { + Masking = true, + BorderColour = colours.Violet, + BorderThickness = 3, + MaskingSmoothness = 1, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(@"Icons/changelog"), + Size = new Vector2(circle_allowance), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Violet, + Alpha = 0, + AlwaysPresent = true, + }, + } + }, + }; + } + } + } + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogListing.cs b/osu.Game/Overlays/Changelog/ChangelogListing.cs new file mode 100644 index 0000000000..41d8228475 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogListing.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogListing : ChangelogContent + { + private readonly List entries; + + public ChangelogListing(List entries) + { + this.entries = entries; + } + + [BackgroundDependencyLoader] + private void load() + { + DateTime currentDate = DateTime.MinValue; + + if (entries == null) return; + + foreach (APIChangelogBuild build in entries) + { + if (build.CreatedAt.Date != currentDate) + { + if (Children.Count != 0) + { + Add(new Box + { + RelativeSizeAxes = Axes.X, + Height = 2, + Colour = new Color4(17, 17, 17, 255), + Margin = new MarginPadding { Top = 30 }, + }); + } + + Add(new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 15 }, + Text = build.CreatedAt.Date.ToString("dd MMM yyyy"), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 24), + Colour = OsuColour.FromHex(@"FD5"), + }); + + currentDate = build.CreatedAt.Date; + } + else + { + Add(new Container + { + RelativeSizeAxes = Axes.X, + Height = 1, + Padding = new MarginPadding { Horizontal = ChangelogBuild.HORIZONTAL_PADDING }, + Margin = new MarginPadding { Top = 30 }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(32, 24, 35, 255), + } + }); + } + + Add(new ChangelogBuild(build) { SelectBuild = SelectBuild }); + } + } + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs new file mode 100644 index 0000000000..36ae5a756c --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogSingleBuild : ChangelogContent + { + private APIChangelogBuild build; + + public ChangelogSingleBuild(APIChangelogBuild build) + { + this.build = build; + } + + [BackgroundDependencyLoader] + private void load(CancellationToken? cancellation, IAPIProvider api) + { + bool complete = false; + + var req = new GetChangelogBuildRequest(build.UpdateStream.Name, build.Version); + req.Success += res => + { + build = res; + complete = true; + }; + req.Failure += _ => complete = true; + + // This is done on a separate thread to support cancellation below + Task.Run(() => req.Perform(api)); + + while (!complete) + { + if (cancellation?.IsCancellationRequested == true) + { + req.Cancel(); + return; + } + + Thread.Sleep(10); + } + + if (build != null) + Child = new ChangelogBuildWithNavigation(build) { SelectBuild = SelectBuild }; + } + + public class ChangelogBuildWithNavigation : ChangelogBuild + { + public ChangelogBuildWithNavigation(APIChangelogBuild build) + : base(build) + { + } + + protected override FillFlowContainer CreateHeader() + { + var fill = base.CreateHeader(); + + foreach (var existing in fill.Children.OfType()) + { + existing.Scale = new Vector2(1.25f); + existing.Action = null; + + existing.Add(new OsuSpriteText + { + Text = Build.CreatedAt.Date.ToString("dd MMM yyyy"), + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), + Colour = OsuColour.FromHex(@"FD5"), + Anchor = Anchor.BottomCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 5 }, + }); + } + + NavigationIconButton left, right; + + fill.AddRange(new[] + { + left = new NavigationIconButton(Build.Versions?.Previous) + { + Icon = FontAwesome.Solid.ChevronLeft, + SelectBuild = b => SelectBuild(b) + }, + right = new NavigationIconButton(Build.Versions?.Next) + { + Icon = FontAwesome.Solid.ChevronRight, + SelectBuild = b => SelectBuild(b) + }, + }); + + fill.SetLayoutPosition(left, -1); + fill.SetLayoutPosition(right, 1); + + return fill; + } + } + + private class NavigationIconButton : IconButton + { + public Action SelectBuild; + + public NavigationIconButton(APIChangelogBuild build) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + if (build == null) return; + + TooltipText = build.DisplayVersion; + + Action = () => + { + SelectBuild?.Invoke(build); + Enabled.Value = false; + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + HoverColour = colours.GreyVioletLight.Opacity(0.6f); + FlashColour = colours.GreyVioletLighter; + } + } + } +} diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs new file mode 100644 index 0000000000..52b77604d9 --- /dev/null +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class UpdateStreamBadge : TabItem + { + private const float badge_height = 66.5f; + private const float badge_width = 100; + private const float transition_duration = 100; + + private readonly ExpandingBar expandingBar; + private SampleChannel sampleClick; + private SampleChannel sampleHover; + + private readonly FillFlowContainer text; + + public readonly Bindable SelectedTab = new Bindable(); + + private readonly Container fadeContainer; + + public UpdateStreamBadge(APIUpdateStream stream) + : base(stream) + { + Size = new Vector2(stream.IsFeatured ? badge_width * 2 : badge_width, badge_height); + Padding = new MarginPadding(5); + + Child = fadeContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + text = new FillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new[] + { + new OsuSpriteText + { + Text = stream.DisplayName, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), + Margin = new MarginPadding { Top = 6 }, + }, + new OsuSpriteText + { + Text = stream.LatestBuild.DisplayVersion, + Font = OsuFont.GetFont(weight: FontWeight.Light, size: 16), + }, + new OsuSpriteText + { + Text = stream.LatestBuild.Users > 0 ? $"{stream.LatestBuild.Users:N0} {"user".Pluralize(stream.LatestBuild.Users == 1)} online" : null, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 10), + Colour = new Color4(203, 164, 218, 255), + }, + } + }, + expandingBar = new ExpandingBar + { + Anchor = Anchor.TopCentre, + Colour = stream.Colour, + ExpandedSize = 4, + CollapsedSize = 2, + IsCollapsed = true + }, + } + }; + + SelectedTab.BindValueChanged(_ => updateState(), true); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleClick = audio.Samples.Get(@"UI/generic-select-soft"); + sampleHover = audio.Samples.Get(@"UI/generic-hover-soft"); + } + + protected override void OnActivated() => updateState(); + + protected override void OnDeactivated() => updateState(); + + protected override bool OnClick(ClickEvent e) + { + sampleClick?.Play(); + return base.OnClick(e); + } + + protected override bool OnHover(HoverEvent e) + { + sampleHover?.Play(); + updateState(); + + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + // Expand based on the local state + bool shouldExpand = Active.Value || IsHovered; + + // Expand based on whether no build is selected and the badge area is hovered + shouldExpand |= SelectedTab.Value == null && !externalDimRequested; + + if (shouldExpand) + { + expandingBar.Expand(); + fadeContainer.FadeTo(1, transition_duration); + } + else + { + expandingBar.Collapse(); + fadeContainer.FadeTo(0.5f, transition_duration); + } + + text.FadeTo(externalDimRequested && !IsHovered ? 0.5f : 1, transition_duration); + } + + private bool externalDimRequested; + + public void EnableDim() + { + externalDimRequested = true; + updateState(); + } + + public void DisableDim() + { + externalDimRequested = false; + updateState(); + } + } +} diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs new file mode 100644 index 0000000000..2b48811bd6 --- /dev/null +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Online.API.Requests.Responses; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class UpdateStreamBadgeArea : TabControl + { + public UpdateStreamBadgeArea() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddInternal(new Box + { + Colour = Color4.Black, + Alpha = 0.12f, + RelativeSizeAxes = Axes.Both, + }); + } + + public void Populate(List streams) + { + Current.Value = null; + + foreach (APIUpdateStream updateStream in streams) + AddItem(updateStream); + } + + protected override bool OnHover(HoverEvent e) + { + foreach (UpdateStreamBadge streamBadge in TabContainer.Children.OfType()) + streamBadge.EnableDim(); + + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + foreach (UpdateStreamBadge streamBadge in TabContainer.Children.OfType()) + streamBadge.DisableDim(); + + base.OnHoverLost(e); + } + + protected override TabFillFlowContainer CreateTabFlow() + { + var flow = base.CreateTabFlow(); + + flow.RelativeSizeAxes = Axes.X; + flow.AutoSizeAxes = Axes.Y; + flow.AllowMultiline = true; + flow.Padding = new MarginPadding + { + Vertical = 20, + Horizontal = 85, + }; + + return flow; + } + + protected override Dropdown CreateDropdown() => null; + + protected override TabItem CreateTabItem(APIUpdateStream value) => + new UpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; + } +} diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs new file mode 100644 index 0000000000..67f195580e --- /dev/null +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -0,0 +1,210 @@ +// 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.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Changelog; + +namespace osu.Game.Overlays +{ + public class ChangelogOverlay : FullscreenOverlay + { + public readonly Bindable Current = new Bindable(); + + private ChangelogHeader header; + + private Container content; + + private SampleChannel sampleBack; + + private List builds; + + private List streams; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colour) + { + Waves.FirstWaveColour = colour.GreyVioletLight; + Waves.SecondWaveColour = colour.GreyViolet; + Waves.ThirdWaveColour = colour.GreyVioletDark; + Waves.FourthWaveColour = colour.GreyVioletDarker; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour.PurpleDarkAlternative, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new ReverseChildIDFillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + header = new ChangelogHeader + { + ListingSelected = ShowListing, + }, + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }, + }, + }, + }; + + sampleBack = audio.Samples.Get(@"UI/generic-select-soft"); + + header.Current.BindTo(Current); + + Current.BindValueChanged(e => + { + if (e.NewValue != null) + loadContent(new ChangelogSingleBuild(e.NewValue)); + else + loadContent(new ChangelogListing(builds)); + }); + } + + public void ShowListing() + { + Current.Value = null; + Show(); + } + + /// + /// Fetches and shows a specific build from a specific update stream. + /// + /// Must contain at least and + /// . If and + /// are specified, the header will instantly display them. + public void ShowBuild([NotNull] APIChangelogBuild build) + { + if (build == null) throw new ArgumentNullException(nameof(build)); + + Current.Value = build; + Show(); + } + + public void ShowBuild([NotNull] string updateStream, [NotNull] string version) + { + if (updateStream == null) throw new ArgumentNullException(nameof(updateStream)); + if (version == null) throw new ArgumentNullException(nameof(version)); + + performAfterFetch(() => + { + var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream) + ?? streams.Find(s => s.Name == updateStream)?.LatestBuild; + + if (build != null) + ShowBuild(build); + }); + + Show(); + } + + public override bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + if (Current.Value == null) + { + Hide(); + } + else + { + Current.Value = null; + sampleBack?.Play(); + } + + return true; + } + + return false; + } + + protected override void PopIn() + { + base.PopIn(); + + if (initialFetchTask == null) + // fetch and refresh to show listing, if no other request was made via Show methods + performAfterFetch(() => Current.TriggerChange()); + } + + private Task initialFetchTask; + + private void performAfterFetch(Action action) => fetchListing()?.ContinueWith(_ => Schedule(action)); + + private Task fetchListing() + { + if (initialFetchTask != null) + return initialFetchTask; + + return initialFetchTask = Task.Run(async () => + { + var tcs = new TaskCompletionSource(); + + var req = new GetChangelogRequest(); + req.Success += res => + { + // remap streams to builds to ensure model equality + res.Builds.ForEach(b => b.UpdateStream = res.Streams.Find(s => s.Id == b.UpdateStream.Id)); + res.Streams.ForEach(s => s.LatestBuild.UpdateStream = res.Streams.Find(s2 => s2.Id == s.LatestBuild.UpdateStream.Id)); + + builds = res.Builds; + streams = res.Streams; + + header.Streams.Populate(res.Streams); + + tcs.SetResult(true); + }; + req.Failure += _ => initialFetchTask = null; + req.Perform(API); + + await tcs.Task; + }); + } + + private CancellationTokenSource loadContentCancellation; + + private void loadContent(ChangelogContent newContent) + { + content.FadeTo(0.2f, 300, Easing.OutQuint); + + loadContentCancellation?.Cancel(); + + LoadComponentAsync(newContent, c => + { + content.FadeIn(300, Easing.OutQuint); + + c.BuildSelected = ShowBuild; + content.Child = c; + }, (loadContentCancellation = new CancellationTokenSource()).Token); + } + } +} diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 66a6672ab1..86bbe91d35 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -14,6 +15,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Users; using osuTK; @@ -201,6 +203,9 @@ namespace osu.Game.Overlays.Chat private Action startChatAction; + [Resolved] + private IAPIProvider api { get; set; } + public MessageSender(User sender) { this.sender = sender; @@ -213,11 +218,21 @@ namespace osu.Game.Overlays.Chat startChatAction = () => chatManager?.OpenPrivateChannel(sender); } - public MenuItem[] ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems { - new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), - new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction), - }; + get + { + List items = new List + { + new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action) + }; + + if (sender.Id != api.LocalUser.Value.Id) + items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction)); + + return items.ToArray(); + } + } } private static readonly Color4[] username_colours = diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 6c30b8cc36..aec78b962f 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Chat ChatLineFlow.AddRange(displayMessages.Select(CreateChatLine)); - //if (scroll.IsScrolledToEnd(10) || !ChatLineFlow.Children.Any() || newMessages.Any(m => m is LocalMessage)) + if (scroll.IsScrolledToEnd(10) || !ChatLineFlow.Children.Any() || newMessages.Any(m => m is LocalMessage)) scrollToEnd(); var staleMessages = ChatLineFlow.Children.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index eb95fabe02..dd48a5d29e 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -56,7 +56,7 @@ namespace osu.Game.Overlays private readonly Container channelSelectionContainer; private readonly ChannelSelectionOverlay channelSelectionOverlay; - public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || channelSelectionOverlay.State == Visibility.Visible && channelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos); + public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || (channelSelectionOverlay.State.Value == Visibility.Visible && channelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos)); public ChatOverlay() { @@ -130,7 +130,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Height = 1, PlaceholderText = "type your message", - Exit = () => State = Visibility.Hidden, + Exit = Hide, OnCommit = postMessage, ReleaseFocusOnCommit = false, HoldFocus = true, @@ -163,19 +163,19 @@ namespace osu.Game.Overlays }; channelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; - channelTabControl.ChannelSelectorActive.ValueChanged += active => channelSelectionOverlay.State = active.NewValue ? Visibility.Visible : Visibility.Hidden; - channelSelectionOverlay.StateChanged += state => + channelTabControl.ChannelSelectorActive.ValueChanged += active => channelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; + channelSelectionOverlay.State.ValueChanged += state => { - if (state == Visibility.Hidden && channelManager.CurrentChannel.Value == null) + if (state.NewValue == Visibility.Hidden && channelManager.CurrentChannel.Value == null) { - channelSelectionOverlay.State = Visibility.Visible; - State = Visibility.Hidden; + channelSelectionOverlay.Show(); + Hide(); return; } - channelTabControl.ChannelSelectorActive.Value = state == Visibility.Visible; + channelTabControl.ChannelSelectorActive.Value = state.NewValue == Visibility.Visible; - if (state == Visibility.Visible) + if (state.NewValue == Visibility.Visible) { textbox.HoldFocus = false; if (1f - ChatHeight.Value < channel_selection_min_height) @@ -195,7 +195,7 @@ namespace osu.Game.Overlays { textbox.Current.Disabled = true; currentChannelContainer.Clear(false); - channelSelectionOverlay.State = Visibility.Visible; + channelSelectionOverlay.Show(); return; } @@ -253,7 +253,7 @@ namespace osu.Game.Overlays double targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; // If the channel selection screen is shown, mind its minimum height - if (channelSelectionOverlay.State == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height) + if (channelSelectionOverlay.State.Value == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height) targetChatHeight = 1f - channel_selection_min_height; ChatHeight.Value = targetChatHeight; @@ -325,7 +325,7 @@ namespace osu.Game.Overlays this.MoveToY(Height, transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine); - channelSelectionOverlay.State = Visibility.Hidden; + channelSelectionOverlay.Hide(); textbox.HoldFocus = false; base.PopOut(); diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 2cc1c20a10..aaae7bcf5c 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -37,8 +37,8 @@ namespace osu.Game.Overlays dialogContainer.Add(currentDialog); currentDialog.Show(); - currentDialog.StateChanged += state => onDialogOnStateChanged(dialog, state); - State = Visibility.Visible; + currentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue); + Show(); } protected override bool PlaySamplesOnStateChange => false; @@ -53,7 +53,7 @@ namespace osu.Game.Overlays dialog.Delay(PopupDialog.EXIT_DURATION).Expire(); if (dialog == currentDialog) - State = Visibility.Hidden; + Hide(); } protected override void PopIn() @@ -66,7 +66,7 @@ namespace osu.Game.Overlays { base.PopOut(); - if (currentDialog?.State == Visibility.Visible) + if (currentDialog?.State.Value == Visibility.Visible) { currentDialog.Hide(); return; diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 975bf4e3ca..7dcf76e41f 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -252,7 +252,7 @@ namespace osu.Game.Overlays if (!IsLoaded) return; - if (State == Visibility.Hidden) + if (State.Value == Visibility.Hidden) return; if (API == null) diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 9706f75087..0911ee84de 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -40,6 +41,19 @@ namespace osu.Game.Overlays }; } + public override void Show() + { + if (State.Value == Visibility.Visible) + { + // re-trigger the state changed so we can potentially surface to front + State.TriggerChange(); + } + else + { + base.Show(); + } + } + protected override void PopIn() { base.PopIn(); diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index fb38ddcbd1..fdc6f096bc 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; @@ -17,6 +19,11 @@ namespace osu.Game.Overlays { private Box overlay; + private readonly BindableDouble audioVolume = new BindableDouble(1); + + [Resolved] + private AudioManager audio { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -33,7 +40,19 @@ namespace osu.Game.Overlays } }; - Progress.ValueChanged += p => overlay.Alpha = (float)p.NewValue; + Progress.ValueChanged += p => + { + audioVolume.Value = 1 - p.NewValue; + overlay.Alpha = (float)p.NewValue; + }; + + audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioVolume); + } + + protected override void Dispose(bool isDisposing) + { + audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); + base.Dispose(isDisposing); } } } diff --git a/osu.Game/Overlays/KeyBindingPanel.cs b/osu.Game/Overlays/KeyBindingPanel.cs index 301c8faca2..928bd080fa 100644 --- a/osu.Game/Overlays/KeyBindingPanel.cs +++ b/osu.Game/Overlays/KeyBindingPanel.cs @@ -3,22 +3,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays.KeyBinding; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; -using osu.Game.Screens.Ranking; -using osuTK; namespace osu.Game.Overlays { - public class KeyBindingPanel : SettingsPanel + public class KeyBindingPanel : SettingsSubPanel { protected override Drawable CreateHeader() => new SettingsHeader("key configuration", "Customise your keys!"); @@ -29,84 +21,6 @@ namespace osu.Game.Overlays foreach (var ruleset in rulesets.AvailableRulesets) AddSection(new RulesetBindingsSection(ruleset)); - - AddInternal(new BackButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = Hide - }); - } - - public KeyBindingPanel() - : base(true) - { - } - - private class BackButton : OsuClickableContainer, IKeyBindingHandler - { - private AspectContainer aspect; - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(Sidebar.DEFAULT_WIDTH); - Children = new Drawable[] - { - aspect = new AspectContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = -15, - Size = new Vector2(15), - Shadow = true, - Icon = FontAwesome.Solid.ChevronLeft - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = 15, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = @"back", - }, - } - } - }; - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - aspect.ScaleTo(0.75f, 2000, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override bool OnMouseUp(MouseUpEvent e) - { - aspect.ScaleTo(1, 1000, Easing.OutElastic); - return base.OnMouseUp(e); - } - - public bool OnPressed(GlobalAction action) - { - switch (action) - { - case GlobalAction.Back: - Click(); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) => false; } } } diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 6d82db5603..1f15c773f4 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(OsuColour colours, TextureStore textures, AudioManager audio) { - getSample = audio.Sample.Get(@"MedalSplash/medal-get"); + getSample = audio.Samples.Get(@"MedalSplash/medal-get"); innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin"); disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 97769fe5aa..8e5c9588ce 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -1,40 +1,40 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; -using osuTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; -using osu.Game.Rulesets; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods.Sections; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Overlays.Mods { public class ModSelectOverlay : WaveOverlayContainer { - private const float content_width = 0.8f; - - protected Color4 LowMultiplierColour, HighMultiplierColour; - protected readonly TriangleButton DeselectAllButton; - protected readonly OsuSpriteText MultiplierLabel, UnrankedLabel; - private readonly FillFlowContainer footerContainer; + protected readonly TriangleButton CloseButton; + + protected readonly OsuSpriteText MultiplierLabel; + protected readonly OsuSpriteText UnrankedLabel; protected override bool BlockNonPositionalInput => false; @@ -46,154 +46,14 @@ namespace osu.Game.Overlays.Mods protected readonly IBindable Ruleset = new Bindable(); - [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, IBindable ruleset, AudioManager audio, Bindable> mods) - { - LowMultiplierColour = colours.Red; - HighMultiplierColour = colours.Green; - UnrankedLabel.Colour = colours.Blue; + protected Color4 LowMultiplierColour; + protected Color4 HighMultiplierColour; - Ruleset.BindTo(ruleset); - if (mods != null) SelectedMods.BindTo(mods); - - sampleOn = audio.Sample.Get(@"UI/check-on"); - sampleOff = audio.Sample.Get(@"UI/check-off"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Ruleset.BindValueChanged(rulesetChanged, true); - SelectedMods.BindValueChanged(selectedModsChanged, true); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - Ruleset.UnbindAll(); - SelectedMods.UnbindAll(); - } - - private void rulesetChanged(ValueChangedEvent e) - { - if (e.NewValue == null) return; - - var instance = e.NewValue.CreateInstance(); - - foreach (ModSection section in ModSectionsContainer.Children) - section.Mods = instance.GetModsFor(section.ModType); - - // attempt to re-select any already selected mods. - // this may be the first time we are receiving the ruleset, in which case they will still match. - selectedModsChanged(new ValueChangedEvent>(SelectedMods.Value, SelectedMods.Value)); - - // write the mods back to the SelectedMods bindable in the case a change was not applicable. - // this generally isn't required as the previous line will perform deselection; just here for safety. - refreshSelectedMods(); - } - - private void selectedModsChanged(ValueChangedEvent> e) - { - foreach (ModSection section in ModSectionsContainer.Children) - section.SelectTypes(e.NewValue.Select(m => m.GetType()).ToList()); - - updateMods(); - } - - private void updateMods() - { - double multiplier = 1.0; - bool ranked = true; - - foreach (Mod mod in SelectedMods.Value) - { - multiplier *= mod.ScoreMultiplier; - ranked &= mod.Ranked; - } - - MultiplierLabel.Text = $"{multiplier:N2}x"; - if (multiplier > 1.0) - MultiplierLabel.FadeColour(HighMultiplierColour, 200); - else if (multiplier < 1.0) - MultiplierLabel.FadeColour(LowMultiplierColour, 200); - else - MultiplierLabel.FadeColour(Color4.White, 200); - - UnrankedLabel.FadeTo(ranked ? 0 : 1, 200); - } - - protected override void PopOut() - { - base.PopOut(); - - footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - - foreach (ModSection section in ModSectionsContainer.Children) - { - section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - section.ButtonsContainer.MoveToX(100f, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - section.ButtonsContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - } - } - - protected override void PopIn() - { - base.PopIn(); - - footerContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); - footerContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); - - foreach (ModSection section in ModSectionsContainer.Children) - { - section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), WaveContainer.APPEAR_DURATION, Easing.OutQuint); - section.ButtonsContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); - section.ButtonsContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); - } - } - - public void DeselectAll() - { - foreach (ModSection section in ModSectionsContainer.Children) - section.DeselectAll(); - - refreshSelectedMods(); - } - - /// - /// Deselect one or more mods. - /// - /// The types of s which should be deselected. - /// Set to true to bypass animations and update selections immediately. - public void DeselectTypes(Type[] modTypes, bool immediate = false) - { - if (modTypes.Length == 0) return; - - foreach (ModSection section in ModSectionsContainer.Children) - section.DeselectTypes(modTypes, immediate); - } + private const float content_width = 0.8f; + private readonly FillFlowContainer footerContainer; private SampleChannel sampleOn, sampleOff; - private void modButtonPressed(Mod selectedMod) - { - if (selectedMod != null) - { - if (State == Visibility.Visible) sampleOn?.Play(); - DeselectTypes(selectedMod.IncompatibleMods, true); - } - else - { - if (State == Visibility.Visible) sampleOff?.Play(); - } - - refreshSelectedMods(); - } - - private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); - public ModSelectOverlay() { Waves.FirstWaveColour = OsuColour.FromHex(@"19b0e2"); @@ -364,6 +224,16 @@ namespace osu.Game.Overlays.Mods Right = 20 } }, + CloseButton = new TriangleButton + { + Width = 180, + Text = "Close", + Action = Hide, + Margin = new MarginPadding + { + Right = 20 + } + }, new OsuSpriteText { Text = @"Score Multiplier:", @@ -401,5 +271,171 @@ namespace osu.Game.Overlays.Mods }, }; } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, IBindable ruleset, AudioManager audio, Bindable> mods) + { + LowMultiplierColour = colours.Red; + HighMultiplierColour = colours.Green; + UnrankedLabel.Colour = colours.Blue; + + Ruleset.BindTo(ruleset); + if (mods != null) SelectedMods.BindTo(mods); + + sampleOn = audio.Samples.Get(@"UI/check-on"); + sampleOff = audio.Samples.Get(@"UI/check-off"); + } + + public void DeselectAll() + { + foreach (var section in ModSectionsContainer.Children) + section.DeselectAll(); + + refreshSelectedMods(); + } + + /// + /// Deselect one or more mods. + /// + /// The types of s which should be deselected. + /// Set to true to bypass animations and update selections immediately. + public void DeselectTypes(Type[] modTypes, bool immediate = false) + { + if (modTypes.Length == 0) return; + + foreach (var section in ModSectionsContainer.Children) + section.DeselectTypes(modTypes, immediate); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Ruleset.BindValueChanged(rulesetChanged, true); + SelectedMods.BindValueChanged(selectedModsChanged, true); + } + + protected override void PopOut() + { + base.PopOut(); + + footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + + foreach (var section in ModSectionsContainer.Children) + { + section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + section.ButtonsContainer.MoveToX(100f, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + section.ButtonsContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + } + } + + protected override void PopIn() + { + base.PopIn(); + + footerContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); + footerContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + + foreach (var section in ModSectionsContainer.Children) + { + section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), WaveContainer.APPEAR_DURATION, Easing.OutQuint); + section.ButtonsContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); + section.ButtonsContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + } + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Number1: + DeselectAllButton.Click(); + return true; + + case Key.Number2: + CloseButton.Click(); + return true; + } + + return base.OnKeyDown(e); + } + + private void rulesetChanged(ValueChangedEvent e) + { + if (e.NewValue == null) return; + + var instance = e.NewValue.CreateInstance(); + + foreach (var section in ModSectionsContainer.Children) + section.Mods = instance.GetModsFor(section.ModType); + + // attempt to re-select any already selected mods. + // this may be the first time we are receiving the ruleset, in which case they will still match. + selectedModsChanged(new ValueChangedEvent>(SelectedMods.Value, SelectedMods.Value)); + + // write the mods back to the SelectedMods bindable in the case a change was not applicable. + // this generally isn't required as the previous line will perform deselection; just here for safety. + refreshSelectedMods(); + } + + private void selectedModsChanged(ValueChangedEvent> e) + { + foreach (var section in ModSectionsContainer.Children) + section.SelectTypes(e.NewValue.Select(m => m.GetType()).ToList()); + + updateMods(); + } + + private void updateMods() + { + var multiplier = 1.0; + var ranked = true; + + foreach (var mod in SelectedMods.Value) + { + multiplier *= mod.ScoreMultiplier; + ranked &= mod.Ranked; + } + + MultiplierLabel.Text = $"{multiplier:N2}x"; + if (multiplier > 1.0) + MultiplierLabel.FadeColour(HighMultiplierColour, 200); + else if (multiplier < 1.0) + MultiplierLabel.FadeColour(LowMultiplierColour, 200); + else + MultiplierLabel.FadeColour(Color4.White, 200); + + UnrankedLabel.FadeTo(ranked ? 0 : 1, 200); + } + + private void modButtonPressed(Mod selectedMod) + { + if (selectedMod != null) + { + if (State.Value == Visibility.Visible) sampleOn?.Play(); + DeselectTypes(selectedMod.IncompatibleMods, true); + } + else + { + if (State.Value == Visibility.Visible) sampleOff?.Play(); + } + + refreshSelectedMods(); + } + + private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); + + #region Disposal + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + Ruleset.UnbindAll(); + SelectedMods.UnbindAll(); + } + + #endregion } } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 4431288a1a..ec3d708645 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Music { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - ExitRequested = () => State = Visibility.Hidden, + ExitRequested = Hide, FilterChanged = search => list.Filter(search), Padding = new MarginPadding(10), }, diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index ea3e1ca00c..85524e992c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -200,7 +200,7 @@ namespace osu.Game.Overlays beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemRemoved += handleBeatmapRemoved; - playlist.StateChanged += s => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); + playlist.State.ValueChanged += s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } private ScheduledDelegate seekDelegate; @@ -350,7 +350,7 @@ namespace osu.Game.Overlays direction = last > next ? TransformDirection.Prev : TransformDirection.Next; } - current.Track.Completed -= currentTrackCompleted; + //current.Track.Completed -= currentTrackCompleted; } current = beatmap.NewValue; @@ -449,7 +449,7 @@ namespace osu.Game.Overlays // This is here mostly as a performance fix. // If the playlist is not hidden it will update children even when the music controller is hidden (due to AlwaysPresent). - playlist.State = Visibility.Hidden; + playlist.Hide(); this.FadeOut(transition_length, Easing.OutQuint); dragContainer.ScaleTo(0.9f, transition_length, Easing.OutQuint); diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 8f75d3ebf0..2e4c504645 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -81,13 +81,13 @@ namespace osu.Game.Overlays private void updateProcessingMode() { - bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State == Visibility.Visible; + bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible; notificationsEnabler?.Cancel(); if (enabled) // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. - notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State == Visibility.Visible ? 0 : 1000); + notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 1000); else processingPosts = false; } @@ -96,7 +96,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - StateChanged += _ => updateProcessingMode(); + State.ValueChanged += _ => updateProcessingMode(); OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true); } @@ -128,7 +128,7 @@ namespace osu.Game.Overlays section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); if (notification.IsImportant) - State = Visibility.Visible; + Show(); updateCounts(); }); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 857a0bda9e..c8e081d29f 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -36,6 +37,10 @@ namespace osu.Game.Overlays.Notifications State = state; } + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + public CancellationToken CancellationToken => cancellationTokenSource.Token; + public virtual ProgressNotificationState State { get => state; @@ -62,6 +67,8 @@ namespace osu.Game.Overlays.Notifications break; case ProgressNotificationState.Cancelled: + cancellationTokenSource.Cancel(); + Light.Colour = colourCancelled; Light.Pulsate = false; progressBar.Active = false; diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 0fc1398f5d..b6b0e605d7 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -29,13 +29,11 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps protected override void ShowMore() { - base.ShowMore(); - request = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); request.Success += sets => Schedule(() => { - ShowMoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0); - ShowMoreLoading.Hide(); + MoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0); + MoreButton.IsLoading = false; if (!sets.Any() && VisiblePages == 1) { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index f2eb32c53b..6085b0bc05 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -24,13 +24,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical protected override void ShowMore() { - base.ShowMore(); - request = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); request.Success += beatmaps => Schedule(() => { - ShowMoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0); - ShowMoreLoading.Hide(); + MoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0); + MoreButton.IsLoading = false; if (!beatmaps.Any() && VisiblePages == 1) { diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs index aeea5118a7..aabfa56ee6 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs @@ -3,17 +3,15 @@ using osu.Framework.Bindables; using osuTK; -using osuTK.Graphics; -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.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Users; +using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Kudosu { @@ -24,46 +22,27 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu public KudosuInfo(Bindable user) { this.user.BindTo(user); - CountSection total; CountSection avaliable; - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 3; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 3f, - Colour = Color4.Black.Opacity(0.2f), - }; Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.2f) - }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), Children = new[] { - total = new CountSection( - "Total Kudosu Earned", - "Based on how much of a contribution the user has made to beatmap moderation. See this link for more information." - ), - avaliable = new CountSection( - "Kudosu Avaliable", - "Kudosu can be traded for kudosu stars, which will help your beatmap get more attention. This is the number of kudosu you haven't traded in yet." - ), + total = new CountTotal(), + avaliable = new CountAvailable() } } }; - this.user.ValueChanged += u => { total.Count = u.NewValue?.Kudosu.Total ?? 0; @@ -73,21 +52,43 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu protected override bool OnClick(ClickEvent e) => true; + private class CountAvailable : CountSection + { + public CountAvailable() + : base("Kudosu Avaliable") + { + DescriptionText.Text = "Kudosu can be traded for kudosu stars, which will help your beatmap get more attention. This is the number of kudosu you haven't traded in yet."; + } + } + + private class CountTotal : CountSection + { + public CountTotal() + : base("Total Kudosu Earned") + { + DescriptionText.AddText("Based on how much of a contribution the user has made to beatmap moderation. See "); + DescriptionText.AddLink("this link", "https://osu.ppy.sh/wiki/Kudosu"); + DescriptionText.AddText(" for more information."); + } + } + private class CountSection : Container { private readonly OsuSpriteText valueText; + protected readonly LinkFlowContainer DescriptionText; + private readonly Box lineBackground; public new int Count { set => valueText.Text = value.ToString(); } - public CountSection(string header, string description) + public CountSection(string header) { RelativeSizeAxes = Axes.X; Width = 0.5f; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Horizontal = 10, Top = 10, Bottom = 20 }; + Padding = new MarginPadding { Top = 10, Bottom = 20 }; Child = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -96,39 +97,42 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu Spacing = new Vector2(0, 5), Children = new Drawable[] { - new FillFlowContainer + new CircularContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Children = new Drawable[] + Masking = true, + RelativeSizeAxes = Axes.X, + Height = 5, + Child = lineBackground = new Box { - new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = header + ":", - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular, italics: true) - }, - valueText = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = "0", - Font = OsuFont.GetFont(size: 40, weight: FontWeight.Regular, italics: true), - UseFullGlyphHeight = false, - } + RelativeSizeAxes = Axes.Both, } }, - new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 19)) + new OsuSpriteText + { + Text = header, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold) + }, + valueText = new OsuSpriteText + { + Text = "0", + Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light), + UseFullGlyphHeight = false, + }, + DescriptionText = new LinkFlowContainer(t => t.Font = t.Font.With(size: 14)) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = description } } }; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + lineBackground.Colour = colours.Yellow; + DescriptionText.Colour = colours.GreySeafoamLighter; + } } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 46c65b9db7..b459afcb49 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -7,20 +7,17 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections { - public class PaginatedContainer : FillFlowContainer + public abstract class PaginatedContainer : FillFlowContainer { protected readonly FillFlowContainer ItemsContainer; - protected readonly OsuHoverContainer ShowMoreButton; - protected readonly LoadingAnimation ShowMoreLoading; + protected readonly ShowMoreButton MoreButton; protected readonly OsuSpriteText MissingText; protected int VisiblePages; @@ -32,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections protected APIRequest RetrievalRequest; protected RulesetStore Rulesets; - public PaginatedContainer(Bindable user, string header, string missing) + protected PaginatedContainer(Bindable user, string header, string missing) { User.BindTo(user); @@ -45,38 +42,26 @@ namespace osu.Game.Overlays.Profile.Sections new OsuSpriteText { Text = header, - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Regular, italics: true), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), Margin = new MarginPadding { Top = 10, Bottom = 10 }, }, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Bottom = 10 } + Spacing = new Vector2(0, 2), }, - ShowMoreButton = new OsuHoverContainer + MoreButton = new ShowMoreButton { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Alpha = 0, + Margin = new MarginPadding { Top = 10 }, Action = ShowMore, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Child = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 14), - Text = "show more", - Padding = new MarginPadding { Vertical = 10, Horizontal = 15 }, - } - }, - ShowMoreLoading = new LoadingAnimation - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(14), }, MissingText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 14), + Font = OsuFont.GetFont(size: 15), Text = missing, Alpha = 0, }, @@ -97,16 +82,11 @@ namespace osu.Game.Overlays.Profile.Sections { VisiblePages = 0; ItemsContainer.Clear(); - ShowMoreButton.Hide(); if (e.NewValue != null) ShowMore(); } - protected virtual void ShowMore() - { - ShowMoreLoading.Show(); - ShowMoreButton.Hide(); - } + protected abstract void ShowMore(); } } diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 470bed2854..a149cfa12e 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests; using osu.Game.Users; @@ -9,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Profile.Sections.Ranks { @@ -31,8 +31,6 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks protected override void ShowMore() { - base.ShowMore(); - request = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); request.Success += scores => Schedule(() => { @@ -41,8 +39,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks if (!scores.Any() && VisiblePages == 1) { - ShowMoreButton.Hide(); - ShowMoreLoading.Hide(); + MoreButton.Hide(); + MoreButton.IsLoading = false; MissingText.Show(); return; } @@ -63,8 +61,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks LoadComponentsAsync(drawableScores, s => { MissingText.Hide(); - ShowMoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0); - ShowMoreLoading.Hide(); + MoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0); + MoreButton.IsLoading = false; ItemsContainer.AddRange(s); }); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 4b4acb8fbc..b72aec7a44 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -22,13 +22,11 @@ namespace osu.Game.Overlays.Profile.Sections.Recent protected override void ShowMore() { - base.ShowMore(); - request = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); request.Success += activities => Schedule(() => { - ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); - ShowMoreLoading.Hide(); + MoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); + MoreButton.IsLoading = false; if (!activities.Any() && VisiblePages == 1) { diff --git a/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs new file mode 100644 index 0000000000..5ed546c62b --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs @@ -0,0 +1,146 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +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.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using System.Collections.Generic; + +namespace osu.Game.Overlays.Profile.Sections +{ + public class ShowMoreButton : OsuHoverContainer + { + private const float fade_duration = 200; + + private readonly Box background; + private readonly LoadingAnimation loading; + private readonly FillFlowContainer content; + + protected override IEnumerable EffectTargets => new[] { background }; + + private bool isLoading; + + public bool IsLoading + { + get => isLoading; + set + { + if (isLoading == value) + return; + + isLoading = value; + + Enabled.Value = !isLoading; + + if (value) + { + loading.FadeIn(fade_duration, Easing.OutQuint); + content.FadeOut(fade_duration, Easing.OutQuint); + } + else + { + loading.FadeOut(fade_duration, Easing.OutQuint); + content.FadeIn(fade_duration, Easing.OutQuint); + } + } + } + + public ShowMoreButton() + { + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + new CircularContainer + { + Masking = true, + Size = new Vector2(140, 30), + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + content = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7), + Children = new Drawable[] + { + new ChevronIcon(), + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = "show more".ToUpper(), + }, + new ChevronIcon(), + } + }, + loading = new LoadingAnimation + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(12) + }, + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colors) + { + IdleColour = colors.GreySeafoamDark; + HoverColour = colors.GreySeafoam; + } + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + try + { + return base.OnClick(e); + } + finally + { + // run afterwards as this will disable this button. + IsLoading = true; + } + } + + private class ChevronIcon : SpriteIcon + { + private const int bottom_margin = 2; + private const int icon_size = 8; + + public ChevronIcon() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Margin = new MarginPadding { Bottom = bottom_margin }; + Size = new Vector2(icon_size); + Icon = FontAwesome.Solid.ChevronDown; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colors) + { + Colour = colors.Yellow; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index b671d0e0fd..f063898a9f 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Debug LabelText = "Bypass caching (slow)", Bindable = config.GetBindable(DebugSetting.BypassCaching) }, + new SettingsCheckbox + { + LabelText = "Bypass front-to-back render pass", + Bindable = config.GetBindable(DebugSetting.BypassFrontToBackPass) + } }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index 2cf14f5aff..2c6b2663c6 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { new SettingsCheckbox { - LabelText = "Increase visibility of first object with \"Hidden\" mod", + LabelText = "Increase visibility of first object when visual impairment mods are enabled", Bindable = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 2f56ace24d..1454b6592d 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { new LoadingAnimation { - State = Visibility.Visible, + State = { Value = Visibility.Visible }, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, @@ -154,8 +154,9 @@ namespace osu.Game.Overlays.Settings.Sections.General }; panel.Status.BindTo(api.LocalUser.Value.Status); + panel.Activity.BindTo(api.LocalUser.Value.Activity); - dropdown.Current.ValueChanged += action => + dropdown.Current.BindValueChanged(action => { switch (action.NewValue) { @@ -178,9 +179,7 @@ namespace osu.Game.Overlays.Settings.Sections.General api.Logout(); break; } - }; - dropdown.Current.TriggerChange(); - + }, true); break; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 58d2eb1f1e..36c4fb5252 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private Bindable scalingMode; private Bindable sizeFullscreen; - private readonly BindableList windowModes = new BindableList(); + private readonly IBindableList windowModes = new BindableList(); private OsuGameBase game; private SettingsDropdown resolutionDropdown; diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index 46c23c3bbf..a554159fd7 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -10,12 +10,14 @@ namespace osu.Game.Overlays.Settings { private OsuCheckbox checkbox; + private string labelText; + protected override Drawable CreateControl() => checkbox = new OsuCheckbox(); public override string LabelText { - get => checkbox.LabelText; - set => checkbox.LabelText = value; + get => labelText; + set => checkbox.LabelText = labelText = value; } } } diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index e8c2c1ffe8..b5ee4b4f0c 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -58,15 +59,49 @@ namespace osu.Game.Overlays.Settings Text = game.Name, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - new OsuSpriteText + new BuildDisplay(game.Version, DebugUtils.IsDebug) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Font = OsuFont.GetFont(size: 14), - Text = game.Version, - Colour = DebugUtils.IsDebug ? colours.Red : Color4.White, - }, + } }; } + + private class BuildDisplay : OsuAnimatedButton + { + private readonly string version; + private readonly bool isDebug; + + [Resolved] + private OsuColour colours { get; set; } + + public BuildDisplay(string version, bool isDebug) + { + this.version = version; + this.isDebug = isDebug; + + Content.RelativeSizeAxes = Axes.Y; + Content.AutoSizeAxes = AutoSizeAxes = Axes.X; + Height = 20; + } + + [BackgroundDependencyLoader(true)] + private void load(ChangelogOverlay changelog) + { + if (!isDebug) + Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); + + Add(new OsuSpriteText + { + Font = OsuFont.GetFont(size: 16), + + Text = version, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + Colour = isDebug ? colours.Red : Color4.White, + }); + } + } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 4f3a71a1b3..bb84de5d3a 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -8,13 +8,13 @@ using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osuTK.Graphics; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; namespace osu.Game.Overlays { public class SettingsOverlay : SettingsPanel { - private readonly KeyBindingPanel keyBindingPanel; - protected override IEnumerable CreateSections() => new SettingsSection[] { new GeneralSection(), @@ -22,31 +22,39 @@ namespace osu.Game.Overlays new GameplaySection(), new AudioSection(), new SkinSection(), - new InputSection(keyBindingPanel), + new InputSection(createSubPanel(new KeyBindingPanel())), new OnlineSection(), new MaintenanceSection(), new DebugSection(), }; + private readonly List subPanels = new List(); + protected override Drawable CreateHeader() => new SettingsHeader("settings", "Change the way osu! behaves"); protected override Drawable CreateFooter() => new SettingsFooter(); public SettingsOverlay() : base(true) { - keyBindingPanel = new KeyBindingPanel - { - Depth = 1, - Anchor = Anchor.TopRight, - }; - keyBindingPanel.StateChanged += keyBindingPanelStateChanged; } - public override bool AcceptsFocus => keyBindingPanel.State != Visibility.Visible; + public override bool AcceptsFocus => subPanels.All(s => s.State.Value != Visibility.Visible); - private void keyBindingPanelStateChanged(Visibility visibility) + private T createSubPanel(T subPanel) + where T : SettingsSubPanel { - switch (visibility) + subPanel.Depth = 1; + subPanel.Anchor = Anchor.TopRight; + subPanel.State.ValueChanged += subPanelStateChanged; + + subPanels.Add(subPanel); + + return subPanel; + } + + private void subPanelStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) { case Visibility.Visible: Background.FadeTo(0.9f, 300, Easing.OutQuint); @@ -66,12 +74,13 @@ namespace osu.Game.Overlays } } - protected override float ExpandedPosition => keyBindingPanel.State == Visibility.Visible ? -WIDTH : base.ExpandedPosition; + protected override float ExpandedPosition => subPanels.Any(s => s.State.Value == Visibility.Visible) ? -WIDTH : base.ExpandedPosition; [BackgroundDependencyLoader] private void load() { - ContentContainer.Add(keyBindingPanel); + foreach (var s in subPanels) + ContentContainer.Add(s); } } } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 85b74c0fad..474f529bb1 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -28,8 +28,6 @@ namespace osu.Game.Overlays protected const float WIDTH = 400; - private const float sidebar_padding = 10; - protected Container ContentContainer; protected override Container Content => ContentContainer; diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs new file mode 100644 index 0000000000..576be71ee6 --- /dev/null +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -0,0 +1,103 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; +using osu.Game.Overlays.Settings; +using osu.Game.Screens.Ranking; +using osuTK; + +namespace osu.Game.Overlays +{ + public abstract class SettingsSubPanel : SettingsPanel + { + protected SettingsSubPanel() + : base(true) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = Hide + }); + } + + private class BackButton : OsuClickableContainer, IKeyBindingHandler + { + private AspectContainer aspect; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(Sidebar.DEFAULT_WIDTH); + Children = new Drawable[] + { + aspect = new AspectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = -15, + Size = new Vector2(15), + Shadow = true, + Icon = FontAwesome.Solid.ChevronLeft + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 15, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = @"back", + }, + } + } + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + aspect.ScaleTo(0.75f, 2000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override bool OnMouseUp(MouseUpEvent e) + { + aspect.ScaleTo(1, 1000, Easing.OutElastic); + return base.OnMouseUp(e); + } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + Click(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; + } + } +} diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 2baa614365..780a80b4fc 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -119,6 +119,7 @@ namespace osu.Game.Overlays } panel.Status.BindTo(u.Status); + panel.Activity.BindTo(u.Activity); return panel; }) }; diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index a7f2a0e8d0..982fb26b6b 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -69,6 +69,7 @@ namespace osu.Game.Overlays.Toolbar AutoSizeAxes = Axes.X, Children = new Drawable[] { + new ToolbarChangelogButton(), new ToolbarDirectButton(), new ToolbarChatButton(), new ToolbarSocialButton(), @@ -83,10 +84,10 @@ namespace osu.Game.Overlays.Toolbar } }; - StateChanged += visibility => + State.ValueChanged += visibility => { if (overlayActivationMode.Value == OverlayActivation.Disabled) - State = Visibility.Hidden; + Hide(); }; if (osuGame != null) diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs new file mode 100644 index 0000000000..84210e27a4 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -0,0 +1,22 @@ +// 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.Sprites; + +namespace osu.Game.Overlays.Toolbar +{ + public class ToolbarChangelogButton : ToolbarOverlayToggleButton + { + public ToolbarChangelogButton() + { + SetIcon(FontAwesome.Solid.Bullhorn); + } + + [BackgroundDependencyLoader(true)] + private void load(ChangelogOverlay changelog) + { + StateContainer = changelog; + } + } +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index b2ae273e31..b286cbfb1d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,6 +16,8 @@ namespace osu.Game.Overlays.Toolbar private OverlayContainer stateContainer; + private readonly Bindable overlayState = new Bindable(); + public OverlayContainer StateContainer { get => stateContainer; @@ -22,10 +25,12 @@ namespace osu.Game.Overlays.Toolbar { stateContainer = value; + overlayState.UnbindBindings(); + if (stateContainer != null) { Action = stateContainer.ToggleVisibility; - stateContainer.StateChanged += stateChanged; + overlayState.BindTo(stateContainer.State); } } } @@ -40,18 +45,13 @@ namespace osu.Game.Overlays.Toolbar Depth = 2, Alpha = 0, }); + + overlayState.ValueChanged += stateChanged; } - protected override void Dispose(bool isDisposing) + private void stateChanged(ValueChangedEvent state) { - base.Dispose(isDisposing); - if (stateContainer != null) - stateContainer.StateChanged -= stateChanged; - } - - private void stateChanged(Visibility state) - { - switch (state) + switch (state.NewValue) { case Visibility.Hidden: stateBackground.FadeOut(200); diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 84a41b6547..90412ec1d1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; Children = new[] { @@ -111,12 +112,6 @@ namespace osu.Game.Overlays.Toolbar private void disabledChanged(bool isDisabled) => this.FadeColour(isDisabled ? Color4.Gray : Color4.White, 300); - protected override void Update() - { - base.Update(); - Size = new Vector2(modeButtons.DrawSize.X, 1); - } - private void rulesetChanged(ValueChangedEvent e) { foreach (ToolbarRulesetButton m in modeButtons.Children.Cast()) diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 34b15d958d..02e0f59f26 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -100,14 +100,14 @@ namespace osu.Game.Overlays switch (action) { case GlobalAction.DecreaseVolume: - if (State == Visibility.Hidden) + if (State.Value == Visibility.Hidden) Show(); else volumeMeterMaster.Decrease(amount, isPrecise); return true; case GlobalAction.IncreaseVolume: - if (State == Visibility.Hidden) + if (State.Value == Visibility.Hidden) Show(); else volumeMeterMaster.Increase(amount, isPrecise); @@ -126,7 +126,7 @@ namespace osu.Game.Overlays public override void Show() { - if (State == Visibility.Visible) + if (State.Value == Visibility.Visible) schedulePopOut(); base.Show(); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index d808ee528e..b4b4bb9cd1 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty @@ -8,6 +9,7 @@ namespace osu.Game.Rulesets.Difficulty public class DifficultyAttributes { public Mod[] Mods; + public Skill[] Skills; public double StarRating; @@ -15,9 +17,10 @@ namespace osu.Game.Rulesets.Difficulty { } - public DifficultyAttributes(Mod[] mods, double starRating) + public DifficultyAttributes(Mod[] mods, Skill[] skills, double starRating) { Mods = mods; + Skills = skills; StarRating = starRating; } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index e8020ed185..227f2f4018 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// The peak strain for each section of the beatmap. /// - public IList StrainPeaks => strainPeaks; + public IReadOnlyList StrainPeaks => strainPeaks; /// /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. @@ -84,13 +85,12 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public double DifficultyValue() { - strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. - double difficulty = 0; double weight = 1; // Difficulty is the weighted sum of the highest strains from every section. - foreach (double strain in strainPeaks) + // We're sorting from highest to lowest strain. + foreach (double strain in strainPeaks.OrderByDescending(d => d)) { difficulty += strain * weight; weight *= DecayWeight; diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index e94604554c..0f77b8d584 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Edit /// public readonly DrawableHitObject HitObject; - protected override bool ShouldBeAlive => HitObject.IsAlive && HitObject.IsPresent || State == SelectionState.Selected; + protected override bool ShouldBeAlive => (HitObject.IsAlive && HitObject.IsPresent) || State == SelectionState.Selected; public override bool HandlePositionalInput => ShouldBeAlive; public override bool RemoveWhenNotAlive => false; diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index e174a25df3..405d21c711 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Mods shader.GetUniform("flashlightSize").UpdateValue(ref flashlightSize); shader.GetUniform("flashlightDim").UpdateValue(ref flashlightDim); - Texture.WhitePixel.DrawQuad(screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: vertexAction); + DrawQuad(Texture.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: vertexAction); shader.Unbind(); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e91100608b..ec7e6dc303 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool RemoveCompletedTransforms => false; protected override bool RequiresChildrenUpdate => true; - public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart; + public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart); public readonly Bindable State = new Bindable(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 7db24d36a5..52fba9cab3 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.UI /// /// The playfield. /// - public Playfield Playfield => playfield.Value; + public override Playfield Playfield => playfield.Value; /// /// Place to put drawables above hit objects but below UI. @@ -342,6 +342,11 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool IsPaused = new BindableBool(); + /// + /// The playfield. + /// + public abstract Playfield Playfield { get; } + /// /// The frame-stable clock which is being used for playfield display. /// diff --git a/osu.Game/Rulesets/UI/GameplayCursorContainer.cs b/osu.Game/Rulesets/UI/GameplayCursorContainer.cs index 41edfa0b68..ae5f9c6111 100644 --- a/osu.Game/Rulesets/UI/GameplayCursorContainer.cs +++ b/osu.Game/Rulesets/UI/GameplayCursorContainer.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.UI protected override void Update() { base.Update(); - LastFrameState = State; + LastFrameState = State.Value; } } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs index d2c9ce81c3..0fdbd56c92 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs @@ -136,9 +136,9 @@ namespace osu.Game.Scoring.Legacy score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (ratio300 > 0.8 && countMiss == 0 || ratio300 > 0.9) + else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) score.Rank = ScoreRank.A; - else if (ratio300 > 0.7 && countMiss == 0 || ratio300 > 0.8) + else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) score.Rank = ScoreRank.B; else if (ratio300 > 0.6) score.Rank = ScoreRank.C; @@ -159,9 +159,9 @@ namespace osu.Game.Scoring.Legacy score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (ratio300 > 0.8 && countMiss == 0 || ratio300 > 0.9) + else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) score.Rank = ScoreRank.A; - else if (ratio300 > 0.7 && countMiss == 0 || ratio300 > 0.8) + else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) score.Rank = ScoreRank.B; else if (ratio300 > 0.6) score.Rank = ScoreRank.C; diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index a93d015f1b..696d493830 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -7,10 +7,7 @@ namespace osu.Game.Scoring { public enum ScoreRank { - [Description(@"F")] - F, - - [Description(@"F")] + [Description(@"D")] D, [Description(@"C")] diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index cb01e33282..de0f3870c6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -24,6 +24,7 @@ using osu.Game.Screens.Edit.Design; using osuTK.Input; using System.Collections.Generic; using osu.Framework; +using osu.Game.Users; namespace osu.Game.Screens.Edit { @@ -47,6 +48,8 @@ namespace osu.Game.Screens.Edit private DependencyContainer dependencies; private GameHost host; + protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index 7d48f619d9..badd1e0549 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -182,9 +182,9 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleHover = audio.Sample.Get(@"Menu/button-hover"); + sampleHover = audio.Samples.Get(@"Menu/button-hover"); if (!string.IsNullOrEmpty(sampleName)) - sampleClick = audio.Sample.Get($@"Menu/{sampleName}"); + sampleClick = audio.Samples.Get($@"Menu/{sampleName}"); } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game/Screens/Menu/ButtonArea.cs b/osu.Game/Screens/Menu/ButtonArea.cs index c7650a08fa..d59996a4eb 100644 --- a/osu.Game/Screens/Menu/ButtonArea.cs +++ b/osu.Game/Screens/Menu/ButtonArea.cs @@ -56,12 +56,12 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Exit: case ButtonSystemState.Initial: case ButtonSystemState.EnteringMode: - State = Visibility.Hidden; + Hide(); break; case ButtonSystemState.TopLevel: case ButtonSystemState.Play: - State = Visibility.Visible; + Show(); break; } @@ -82,6 +82,10 @@ namespace osu.Game.Screens.Menu } } + public override void Hide() => State = Visibility.Hidden; + + public override void Show() => State = Visibility.Visible; + public event Action StateChanged; private class ButtonAreaBackground : Box, IStateful diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a098d42c83..5aa244cc06 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Menu if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - sampleBack = audio.Sample.Get(@"Menu/button-back-select"); + sampleBack = audio.Samples.Get(@"Menu/button-back-select"); } private void onMulti() @@ -176,7 +176,7 @@ namespace osu.Game.Screens.Menu private void updateIdleState(bool isIdle) { - if (isIdle && State != ButtonSystemState.Exit) + if (isIdle && State != ButtonSystemState.Exit && State != ButtonSystemState.EnteringMode) State = ButtonSystemState.Initial; } diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 2392d650a0..cf5d247482 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Menu if (setInfo == null) { // we need to import the default menu background beatmap - setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")); + setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")).Result; setInfo.Protected = true; beatmaps.Update(setInfo); @@ -76,8 +76,8 @@ namespace osu.Game.Screens.Menu introBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); track = introBeatmap.Track; - welcome = audio.Sample.Get(@"welcome"); - seeya = audio.Sample.Get(@"seeya"); + welcome = audio.Samples.Get(@"welcome"); + seeya = audio.Samples.Get(@"seeya"); } private const double delay_step_one = 2300; diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 2925689d20..c6de5857c2 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -189,7 +189,6 @@ namespace osu.Game.Screens.Menu base.Draw(vertexAction); shader.Bind(); - texture.TextureGL.Bind(); Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; @@ -224,7 +223,8 @@ namespace osu.Game.Screens.Menu Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) ); - texture.DrawQuad( + DrawQuad( + texture, rectangle, colourInfo, null, diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 4631f4e222..479b3d80b6 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -255,8 +255,8 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(TextureStore textures, AudioManager audio) { - sampleClick = audio.Sample.Get(@"Menu/osu-logo-select"); - sampleBeat = audio.Sample.Get(@"Menu/osu-logo-heartbeat"); + sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); + sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs index 2a6074882d..73994fa369 100644 --- a/osu.Game/Screens/Multi/Match/Components/Header.cs +++ b/osu.Game/Screens/Multi/Match/Components/Header.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Multi.Match.Components private class BackgroundSprite : UpdateableBeatmapBackgroundSprite { - protected override double FadeDuration => 200; + protected override double TransformDuration => 200; } } } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 155665e0d5..9e5c11e098 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -255,7 +255,7 @@ namespace osu.Game.Screens.Multi if (!track.IsRunning) { - game.Audio.AddItemToList(track); + game.Audio.AddItem(track); track.Seek(Beatmap.Value.Metadata.PreviewTime); track.Start(); } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 9d53e43b80..e2aeb41de1 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -15,6 +15,8 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Screens.Menu; using osu.Game.Overlays; +using osu.Game.Users; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; namespace osu.Game.Screens @@ -52,6 +54,30 @@ namespace osu.Game.Screens protected new OsuGameBase Game => base.Game as OsuGameBase; + /// + /// The to set the user's activity automatically to when this screen is entered + /// This will be automatically set to for this screen on entering unless + /// is manually set before. + /// + protected virtual UserActivity InitialActivity => null; + + private UserActivity activity; + + /// + /// The current for this screen. + /// + protected UserActivity Activity + { + get => activity; + set + { + if (value == activity) return; + + activity = value; + updateActivity(); + } + } + /// /// Whether to disallow changes to game-wise Beatmap/Ruleset bindables for this screen (and all children). /// @@ -90,6 +116,9 @@ namespace osu.Game.Screens [Resolved(canBeNull: true)] private OsuLogo logo { get; set; } + [Resolved(canBeNull: true)] + private IAPIProvider api { get; set; } + protected OsuScreen() { Anchor = Anchor.Centre; @@ -99,7 +128,7 @@ namespace osu.Game.Screens [BackgroundDependencyLoader(true)] private void load(OsuGame osu, AudioManager audio) { - sampleExit = audio.Sample.Get(@"UI/screen-back"); + sampleExit = audio.Samples.Get(@"UI/screen-back"); } public virtual bool OnPressed(GlobalAction action) @@ -123,12 +152,15 @@ namespace osu.Game.Screens sampleExit?.Play(); applyArrivingDefaults(true); + updateActivity(); + base.OnResuming(last); } public override void OnSuspending(IScreen next) { base.OnSuspending(next); + onSuspendingLogo(); } @@ -138,6 +170,9 @@ namespace osu.Game.Screens backgroundStack?.Push(localBackground = CreateBackground()); + if (activity == null) + Activity = InitialActivity; + base.OnEntering(last); } @@ -155,6 +190,12 @@ namespace osu.Game.Screens return false; } + private void updateActivity() + { + if (api != null) + api.Activity.Value = activity; + } + /// /// Fired when this screen was entered or resumed and the logo state is required to be adjusted. /// diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs new file mode 100644 index 0000000000..a3caffb620 --- /dev/null +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -0,0 +1,113 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Game.Rulesets.UI; +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + /// + /// Manage the animation to be applied when a player fails. + /// Single file; automatically disposed after use. + /// + public class FailAnimation : Component + { + public Action OnComplete; + + private readonly DrawableRuleset drawableRuleset; + + private readonly BindableDouble trackFreq = new BindableDouble(1); + + private Track track; + + private const float duration = 2500; + + private SampleChannel failSample; + + public FailAnimation(DrawableRuleset drawableRuleset) + { + this.drawableRuleset = drawableRuleset; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, IBindable beatmap) + { + track = beatmap.Value.Track; + failSample = audio.Samples.Get(@"Gameplay/failsound"); + } + + private bool started; + + /// + /// Start the fail animation playing. + /// + /// Thrown if started more than once. + public void Start() + { + if (started) throw new InvalidOperationException("Animation cannot be started more than once."); + + started = true; + + failSample.Play(); + + this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ => + { + OnComplete?.Invoke(); + Expire(); + }); + + track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); + + applyToPlayfield(drawableRuleset.Playfield); + drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); + drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2); + } + + protected override void Update() + { + base.Update(); + + if (!started) + return; + + applyToPlayfield(drawableRuleset.Playfield); + } + + private readonly List appliedObjects = new List(); + + private void applyToPlayfield(Playfield playfield) + { + foreach (var nested in playfield.NestedPlayfields) + applyToPlayfield(nested); + + foreach (DrawableHitObject obj in playfield.HitObjectContainer.AliveObjects) + { + if (appliedObjects.Contains(obj)) + continue; + + obj.RotateTo(RNG.NextSingle(-90, 90), duration); + obj.ScaleTo(obj.Scale * 0.5f, duration); + obj.MoveToOffset(new Vector2(0, 400), duration); + appliedObjects.Add(obj); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + } + } +} diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c151e598f7..6a03271b86 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -69,6 +69,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock(); + (sourceClock as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; @@ -87,6 +88,8 @@ namespace osu.Game.Screens.Play private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; + private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -122,6 +125,8 @@ namespace osu.Game.Screens.Play Seek(GameplayClock.CurrentTime); adjustableClock.Start(); IsPaused.Value = false; + + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } /// @@ -143,7 +148,8 @@ namespace osu.Game.Screens.Play public void Stop() { - adjustableClock.Stop(); + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); + IsPaused.Value = true; } @@ -175,5 +181,11 @@ namespace osu.Game.Screens.Play foreach (var mod in mods.OfType()) mod.ApplyToClock(sourceClock); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + (sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + } } } diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 456fb4faf9..c7e762714c 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play { RelativeSizeAxes = Axes.Both; - StateChanged += s => selectionIndex = -1; + State.ValueChanged += s => selectionIndex = -1; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index e99f6d836e..b2c3952f38 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD } }; - State = Visibility.Visible; + Show(); } protected override void PopIn() => this.FadeIn(fade_duration); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 30214d1b9c..c3e351a0ca 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -27,6 +27,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; +using osu.Game.Users; namespace osu.Game.Screens.Play { @@ -34,6 +35,8 @@ namespace osu.Game.Screens.Play { protected override bool AllowBackButton => false; // handled by HoldForMenuButton + protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); + public override float BackgroundParallaxAmount => 0.1f; public override bool HideOverlaysOnEnter => true; @@ -103,7 +106,7 @@ namespace osu.Game.Screens.Play if (working == null) return; - sampleRestart = audio.Sample.Get(@"Gameplay/restart"); + sampleRestart = audio.Samples.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); @@ -173,7 +176,8 @@ namespace osu.Game.Screens.Play fadeOut(true); Restart(); }, - } + }, + failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } }; DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true); @@ -278,7 +282,7 @@ namespace osu.Game.Screens.Play var score = CreateScore(); if (DrawableRuleset.ReplayScore == null) - scoreManager.Import(score); + scoreManager.Import(score).Wait(); this.Push(CreateResults(score)); @@ -345,24 +349,32 @@ namespace osu.Game.Screens.Play protected FailOverlay FailOverlay { get; private set; } + private FailAnimation failAnimation; + private bool onFail() { if (Mods.Value.OfType().Any(m => !m.AllowFail)) return false; - GameplayClockContainer.Stop(); - HasFailed = true; // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) // could process an extra frame after the GameplayClock is stopped. // In such cases we want the fail state to precede a user triggered pause. - if (PauseOverlay.State == Visibility.Visible) + if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); + failAnimation.Start(); + return true; + } + + // Called back when the transform finishes + private void onFailComplete() + { + GameplayClockContainer.Stop(); + FailOverlay.Retries = RestartCount; FailOverlay.Show(); - return true; } #endregion @@ -489,6 +501,13 @@ namespace osu.Game.Screens.Play // still want to block if we are within the cooldown period and not already paused. return true; + if (HasFailed && ValidForResume && !FailOverlay.IsPresent) + // ValidForResume is false when restarting + { + failAnimation.FinishTransforms(true); + return true; + } + GameplayClockContainer.ResetLocalAdjustments(); fadeOut(); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 908a95c18b..9de9f5cec8 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -42,6 +43,8 @@ namespace osu.Game.Screens.Play private bool hideOverlays; public override bool HideOverlaysOnEnter => hideOverlays; + protected override UserActivity InitialActivity => null; //shows the previous screen status + public override bool DisallowExternalBeatmapRulesetChanges => true; protected override bool PlayResumeSound => false; diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index e3c56e1c2c..38dd179f25 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play { this.startTime = startTime; - State = Visibility.Visible; + Show(); RelativePositionAxes = Axes.Both; RelativeSizeAxes = Axes.X; @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseMove(MouseMoveEvent e) { if (!e.HasAnyButtonPressed) - fadeContainer.State = Visibility.Visible; + fadeContainer.Show(); return base.OnMouseMove(e); } @@ -181,7 +181,7 @@ namespace osu.Game.Screens.Play if (!IsHovered && !IsDragged) using (BeginDelayedSequence(1000)) - scheduledHide = Schedule(() => State = Visibility.Hidden); + scheduledHide = Schedule(Hide); break; case Visibility.Hidden: @@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - State = Visibility.Visible; + Show(); } protected override bool OnMouseDown(MouseDownEvent e) @@ -207,9 +207,13 @@ namespace osu.Game.Screens.Play protected override bool OnMouseUp(MouseUpEvent e) { - State = Visibility.Visible; + Show(); return base.OnMouseUp(e); } + + public override void Hide() => State = Visibility.Hidden; + + public override void Show() => State = Visibility.Visible; } private class Button : OsuClickableContainer @@ -234,7 +238,7 @@ namespace osu.Game.Screens.Play colourNormal = colours.Yellow; colourHover = colours.YellowDark; - sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + sampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); Children = new Drawable[] { diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index d478454f00..6642efdf8b 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { - State = Visibility.Visible; + Show(); replayLoaded.ValueChanged += loaded => AllowSeeking = loaded.NewValue; replayLoaded.TriggerChange(); diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs index bebeaee00a..370c856d1d 100644 --- a/osu.Game/Screens/Ranking/Results.cs +++ b/osu.Game/Screens/Ranking/Results.cs @@ -275,7 +275,7 @@ namespace osu.Game.Screens.Ranking currentPage = page.NewValue?.CreatePage(); if (currentPage != null) - circleInner.Add(currentPage); + LoadComponentAsync(currentPage, circleInner.Add); }, true); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 63ad3b6ab2..cf21c78c7f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -152,9 +152,12 @@ namespace osu.Game.Screens.Select { Schedule(() => { + int? previouslySelectedID = null; CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); - bool hadSelection = existingSet?.State?.Value == CarouselItemState.Selected; + // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required + if (existingSet?.State?.Value == CarouselItemState.Selected) + previouslySelectedID = selectedBeatmap?.Beatmap.ID; var newSet = createCarouselSet(beatmapSet); @@ -172,8 +175,8 @@ namespace osu.Game.Screens.Select applyActiveCriteria(false, false); //check if we can/need to maintain our current selection. - if (hadSelection) - select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == selectedBeatmap?.Beatmap.ID) ?? newSet); + if (previouslySelectedID != null) + select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); itemsCache.Invalidate(); Schedule(() => BeatmapSetsChanged?.Invoke()); diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index a78ab97960..378b1b1dc6 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -360,13 +360,13 @@ namespace osu.Game.Screens.Select protected override void PopIn() { this.FadeIn(transition_duration, Easing.OutQuint); - loading.State = Visibility.Visible; + loading.Show(); } protected override void PopOut() { this.FadeOut(transition_duration, Easing.OutQuint); - loading.State = Visibility.Hidden; + loading.Hide(); } } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 1508de2730..fa9ffd0706 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select { void removeOldInfo() { - State = beatmap == null ? Visibility.Hidden : Visibility.Visible; + State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; Info?.FadeOut(250); Info?.Expire(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 5c334b126c..f1951e27ab 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -45,6 +45,9 @@ namespace osu.Game.Screens.Select.Carousel case SortMode.Author: return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.InvariantCultureIgnoreCase); + case SortMode.DateAdded: + return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); + case SortMode.Difficulty: return BeatmapSet.MaxStarDifficulty.CompareTo(otherSet.BeatmapSet.MaxStarDifficulty); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index f1d6343e72..b906bd935c 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select.Carousel } }; - sampleHover = audio.Sample.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); + sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); hoverLayer.Colour = colours.Blue.Opacity(0.1f); } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 77a8054981..4df6e6a3f3 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Screens.Play; +using osu.Game.Users; using osuTK.Input; namespace osu.Game.Screens.Select @@ -18,6 +19,8 @@ namespace osu.Game.Screens.Select public override bool AllowExternalScreenChange => true; + protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap(); + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index fed1f7a944..f9df8c3a39 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -32,6 +32,7 @@ using osuTK.Input; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; namespace osu.Game.Screens.Select @@ -242,9 +243,9 @@ namespace osu.Game.Screens.Select dialogOverlay = dialog; - sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty"); - sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand"); - SampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); + sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand"); + SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); Carousel.LoadBeatmapSetsFromManager(this.beatmaps); @@ -256,8 +257,8 @@ namespace osu.Game.Screens.Select if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable) dialogOverlay.Push(new ImportFromStablePopup(() => { - beatmaps.ImportFromStableAsync(); - skins.ImportFromStableAsync(); + Task.Run(beatmaps.ImportFromStableAsync); + Task.Run(skins.ImportFromStableAsync); })); }); } @@ -278,7 +279,7 @@ namespace osu.Game.Screens.Select protected virtual void ExitFromBack() { - if (ModSelect.State == Visibility.Visible) + if (ModSelect.State.Value == Visibility.Visible) { ModSelect.Hide(); return; @@ -520,7 +521,7 @@ namespace osu.Game.Screens.Select if (base.OnExiting(next)) return true; - beatmapInfoWedge.State = Visibility.Hidden; + beatmapInfoWedge.Hide(); this.FadeOut(100); @@ -580,9 +581,6 @@ namespace osu.Game.Screens.Select if (!track.IsRunning || restart) { - // Ensure the track is added to the TrackManager, since it is removed after the player finishes the map. - // Using AddItemToList rather than AddItem so that it doesn't attempt to register adjustment dependencies more than once. - Game.Audio.Track.AddItemToList(track); track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; track.Restart(); } @@ -597,11 +595,17 @@ namespace osu.Game.Screens.Select { bindBindables(); + // If a selection was already obtained, do not attempt to update the selected beatmap. + if (Carousel.SelectedBeatmapSet != null) + return; + + // Attempt to select the current beatmap on the carousel, if it is valid to be selected. if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) return; - if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom()) + // If the current active beatmap could not be selected, select a new random beatmap. + if (!Carousel.SelectNextRandom()) { // in the case random selection failed, we want to trigger selectionChanged // to show the dummy beatmap (we have nothing else to display). diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index ea4a777b47..7b658f86d0 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -21,7 +22,7 @@ namespace osu.Game.Skinning { protected TextureStore Textures; - protected SampleManager Samples; + protected IResourceStore Samples; public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") @@ -38,10 +39,17 @@ namespace osu.Game.Skinning else Configuration = new SkinConfiguration(); - Samples = audioManager.GetSampleManager(storage); + Samples = audioManager.GetSampleStore(storage); Textures = new TextureStore(new TextureLoaderStore(storage)); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + Textures?.Dispose(); + Samples?.Dispose(); + } + public override Drawable GetDrawableComponent(string componentName) { switch (componentName) @@ -133,6 +141,8 @@ namespace osu.Game.Skinning return path == null ? null : underlyingStore.GetStream(path); } + public IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename); + byte[] IResourceStore.Get(string name) => GetAsync(name).Result; public Task GetAsync(string name) diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 07318b473a..187ea910a7 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Configuration; using osu.Game.Database; namespace osu.Game.Skinning @@ -19,6 +20,8 @@ namespace osu.Game.Skinning public List Files { get; set; } + public List Settings { get; set; } + public bool DeletePending { get; set; } public string FullName => $"\"{Name}\" by {Creator}"; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 3a4d44f608..73cc47ea47 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -71,9 +73,9 @@ namespace osu.Game.Skinning protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; - protected override void Populate(SkinInfo model, ArchiveReader archive) + protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { - base.Populate(model, archive); + await base.Populate(model, archive, cancellationToken); Skin reference = getSkin(model); diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs index 31cadb0a24..153eeda130 100644 --- a/osu.Game/Skinning/SkinStore.cs +++ b/osu.Game/Skinning/SkinStore.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using Microsoft.EntityFrameworkCore; using osu.Framework.Platform; using osu.Game.Database; @@ -12,5 +14,9 @@ namespace osu.Game.Skinning : base(contextFactory, storage) { } + + protected override IQueryable AddIncludesForDeletion(IQueryable query) => + base.AddIncludesForDeletion(query) + .Include(s => s.Settings); // don't include FileInfo. these are handled by the FileStore itself. } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index d6f3625be8..e88e088f5e 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning { var ch = loadChannel(s, skin.GetSample); if (ch == null && allowFallback) - ch = loadChannel(s, audio.Sample.Get); + ch = loadChannel(s, audio.Samples.Get); return ch; }).Where(c => c != null).ToArray(); } @@ -58,5 +58,13 @@ namespace osu.Game.Skinning return null; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + foreach (var c in channels) + c.Dispose(); + } } } diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index c558275f62..0ef35879e3 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -1,134 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osuTK; namespace osu.Game.Tests.Beatmaps { public class TestWorkingBeatmap : WorkingBeatmap { - private readonly TrackVirtualManual track; private readonly IBeatmap beatmap; - /// - /// Create an instance which creates a for the provided ruleset when requested. - /// - /// The target ruleset. - /// A clock which should be used instead of a stopwatch for virtual time progression. - public TestWorkingBeatmap(RulesetInfo ruleset, IFrameBasedClock referenceClock) - : this(new TestBeatmap(ruleset), referenceClock) - { - } - /// /// Create an instance which provides the when requested. /// /// The beatmap - /// An optional clock which should be used instead of a stopwatch for virtual time progression. - public TestWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock = null) - : base(beatmap.BeatmapInfo) + public TestWorkingBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) { this.beatmap = beatmap; - - if (referenceClock != null) - track = new TrackVirtualManual(referenceClock); } protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => null; - protected override Track GetTrack() => track; - /// - /// A virtual track which tracks a reference clock. - /// - public class TrackVirtualManual : Track - { - private readonly IFrameBasedClock referenceClock; - - private readonly ManualClock clock = new ManualClock(); - - private bool running; - - /// - /// Local offset added to the reference clock to resolve correct time. - /// - private double offset; - - public TrackVirtualManual(IFrameBasedClock referenceClock) - { - this.referenceClock = referenceClock; - Length = double.PositiveInfinity; - } - - public override bool Seek(double seek) - { - offset = MathHelper.Clamp(seek, 0, Length); - lastReferenceTime = null; - - return offset == seek; - } - - public override void Start() - { - running = true; - } - - public override void Reset() - { - Seek(0); - base.Reset(); - } - - public override void Stop() - { - if (running) - { - running = false; - // on stopping, the current value should be transferred out of the clock, as we can no longer rely on - // the referenceClock (which will still be counting time). - offset = clock.CurrentTime; - lastReferenceTime = null; - } - } - - public override bool IsRunning => running; - - private double? lastReferenceTime; - - public override double CurrentTime => clock.CurrentTime; - - protected override void UpdateState() - { - base.UpdateState(); - - if (running) - { - double refTime = referenceClock.CurrentTime; - - if (!lastReferenceTime.HasValue) - { - // if the clock just started running, the current value should be transferred to the offset - // (to zero the progression of time). - offset -= refTime; - } - - lastReferenceTime = refTime; - } - - clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length); - - if (CurrentTime >= Length) - { - Stop(); - RaiseCompleted(); - } - } - } + protected override Track GetTrack() => null; } } diff --git a/osu.Game/Tests/Visual/AllPlayersTestScene.cs b/osu.Game/Tests/Visual/AllPlayersTestScene.cs index 454fbe1222..b7d1979b0d 100644 --- a/osu.Game/Tests/Visual/AllPlayersTestScene.cs +++ b/osu.Game/Tests/Visual/AllPlayersTestScene.cs @@ -4,13 +4,10 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; -using osu.Framework.Timing; -using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { @@ -50,26 +47,20 @@ namespace osu.Game.Tests.Visual protected abstract void AddCheckSteps(); - protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); - - protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock clock) => - new TestWorkingBeatmap(beatmap, Clock); - - private Player loadPlayerFor(RulesetInfo ri) + private Player loadPlayerFor(RulesetInfo rulesetInfo) { - Ruleset.Value = ri; - var r = ri.CreateInstance(); + Ruleset.Value = rulesetInfo; + var ruleset = rulesetInfo.CreateInstance(); - var beatmap = CreateBeatmap(r); - var working = CreateWorkingBeatmap(beatmap, Clock); + var working = CreateWorkingBeatmap(rulesetInfo); Beatmap.Value = working; - Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) }; + Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; Player?.Exit(); Player = null; - Player = CreatePlayer(r); + Player = CreatePlayer(ruleset); LoadScreen(Player); diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 14c0f0950f..75bbb3e110 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Rulesets; using osu.Game.Screens.Edit; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { @@ -24,7 +23,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo, null); + Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo); LoadScreen(new Editor()); } diff --git a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs index a7a7f88ff7..86191609a4 100644 --- a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs @@ -3,8 +3,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Testing.Input; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Tests.Visual { @@ -15,21 +20,92 @@ namespace osu.Game.Tests.Visual protected readonly ManualInputManager InputManager; + private readonly TriangleButton buttonTest; + private readonly TriangleButton buttonLocal; + protected ManualInputManagerTestScene() { - base.Content.Add(InputManager = new ManualInputManager + base.Content.AddRange(new Drawable[] { - UseParentInput = true, - Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, + InputManager = new ManualInputManager + { + UseParentInput = true, + Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Margin = new MarginPadding(5), + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Margin = new MarginPadding(5), + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Input Priority" + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(5), + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + + Children = new Drawable[] + { + buttonLocal = new TriangleButton + { + Text = "local", + Size = new Vector2(50, 30), + Action = returnUserInput + }, + buttonTest = new TriangleButton + { + Text = "test", + Size = new Vector2(50, 30), + Action = returnTestInput + }, + } + }, + } + }, + } + }, }); } - /// - /// Returns input back to the user. - /// - protected void ReturnUserInput() + protected override void Update() { - AddStep("Return user input", () => InputManager.UseParentInput = true); + base.Update(); + + buttonTest.Enabled.Value = InputManager.UseParentInput; + buttonLocal.Enabled.Value = !InputManager.UseParentInput; } + + private void returnUserInput() => + InputManager.UseParentInput = true; + + private void returnTestInput() => + InputManager.UseParentInput = false; } } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 47b377587b..c8798448ae 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -3,15 +3,22 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics.Textures; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Beatmaps; +using osuTK; namespace osu.Game.Tests.Visual { @@ -19,7 +26,7 @@ namespace osu.Game.Tests.Visual { [Cached(typeof(Bindable))] [Cached(typeof(IBindable))] - private readonly OsuTestBeatmap beatmap = new OsuTestBeatmap(new DummyWorkingBeatmap()); + private OsuTestBeatmap beatmap; protected BindableBeatmap Beatmap => beatmap; @@ -39,7 +46,12 @@ namespace osu.Game.Tests.Visual protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { // This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures - beatmap.Default = new DummyWorkingBeatmap(parent.Get()); + var working = new DummyWorkingBeatmap(parent.Get(), parent.Get()); + + beatmap = new OsuTestBeatmap(working) + { + Default = working + }; return Dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); } @@ -49,19 +61,29 @@ namespace osu.Game.Tests.Visual localStorage = new Lazy(() => new NativeStorage($"{GetType().Name}-{Guid.NewGuid()}")); } - [BackgroundDependencyLoader] - private void load(AudioManager audioManager, RulesetStore rulesets) - { - beatmap.SetAudioManager(audioManager); + [Resolved] + private AudioManager audio { get; set; } - Ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(); + protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); + + protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) => + CreateWorkingBeatmap(CreateBeatmap(ruleset)); + + protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) => + new ClockBackedTestWorkingBeatmap(beatmap, Clock, audio); + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + Ruleset.Value = rulesets.AvailableRulesets.First(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - beatmap?.Value.Track.Stop(); + if (beatmap?.Value.TrackLoaded == true) + beatmap.Value.Track.Stop(); if (localStorage.IsValueCreated) { @@ -78,6 +100,164 @@ namespace osu.Game.Tests.Visual protected override ITestSceneTestRunner CreateRunner() => new OsuTestSceneTestRunner(); + public class ClockBackedTestWorkingBeatmap : TestWorkingBeatmap + { + private readonly Track track; + + private readonly TrackVirtualStore store; + + /// + /// Create an instance which creates a for the provided ruleset when requested. + /// + /// The target ruleset. + /// A clock which should be used instead of a stopwatch for virtual time progression. + /// Audio manager. Required if a reference clock isn't provided. + public ClockBackedTestWorkingBeatmap(RulesetInfo ruleset, IFrameBasedClock referenceClock, AudioManager audio) + : this(new TestBeatmap(ruleset), referenceClock, audio) + { + } + + /// + /// Create an instance which provides the when requested. + /// + /// The beatmap + /// An optional clock which should be used instead of a stopwatch for virtual time progression. + /// Audio manager. Required if a reference clock isn't provided. + /// The length of the returned virtual track. + public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000) + : base(beatmap) + { + if (referenceClock != null) + { + store = new TrackVirtualStore(referenceClock); + audio.AddItem(store); + track = store.GetVirtual(length); + } + else + track = audio?.Tracks.GetVirtual(length); + } + + public override void Dispose() + { + base.Dispose(); + store?.Dispose(); + } + + protected override Track GetTrack() => track; + + public class TrackVirtualStore : AudioCollectionManager, ITrackStore + { + private readonly IFrameBasedClock referenceClock; + + public TrackVirtualStore(IFrameBasedClock referenceClock) + { + this.referenceClock = referenceClock; + } + + public Track Get(string name) => throw new NotImplementedException(); + + public Task GetAsync(string name) => throw new NotImplementedException(); + + public Stream GetStream(string name) => throw new NotImplementedException(); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public Track GetVirtual(double length = Double.PositiveInfinity) + { + var track = new TrackVirtualManual(referenceClock) { Length = length }; + AddItem(track); + return track; + } + } + + /// + /// A virtual track which tracks a reference clock. + /// + public class TrackVirtualManual : Track + { + private readonly IFrameBasedClock referenceClock; + + private readonly ManualClock clock = new ManualClock(); + + private bool running; + + /// + /// Local offset added to the reference clock to resolve correct time. + /// + private double offset; + + public TrackVirtualManual(IFrameBasedClock referenceClock) + { + this.referenceClock = referenceClock; + Length = double.PositiveInfinity; + } + + public override bool Seek(double seek) + { + offset = MathHelper.Clamp(seek, 0, Length); + lastReferenceTime = null; + + return offset == seek; + } + + public override void Start() + { + running = true; + } + + public override void Reset() + { + Seek(0); + base.Reset(); + } + + public override void Stop() + { + if (running) + { + running = false; + // on stopping, the current value should be transferred out of the clock, as we can no longer rely on + // the referenceClock (which will still be counting time). + offset = clock.CurrentTime; + lastReferenceTime = null; + } + } + + public override bool IsRunning => running; + + private double? lastReferenceTime; + + public override double CurrentTime => clock.CurrentTime; + + protected override void UpdateState() + { + base.UpdateState(); + + if (running) + { + double refTime = referenceClock.CurrentTime; + + if (!lastReferenceTime.HasValue) + { + // if the clock just started running, the current value should be transferred to the offset + // (to zero the progression of time). + offset -= refTime; + } + + lastReferenceTime = refTime; + } + + clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length); + + if (CurrentTime >= Length) + { + Stop(); + RaiseCompleted(); + } + } + } + } + public class OsuTestSceneTestRunner : OsuGameBase, ITestSceneTestRunner { private TestSceneTestRunner.TestRunner runner; @@ -99,15 +279,6 @@ namespace osu.Game.Tests.Visual : base(defaultValue) { } - - public void SetAudioManager(AudioManager audioManager) => RegisterAudioManager(audioManager); - - public override BindableBeatmap GetBoundCopy() - { - var copy = new OsuTestBeatmap(Default); - copy.BindTo(this); - return copy; - } } } } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index c1561ffea1..2b177e264f 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -15,19 +15,18 @@ namespace osu.Game.Tests.Visual [Cached(Type = typeof(IPlacementHandler))] public abstract class PlacementBlueprintTestScene : OsuTestScene, IPlacementHandler { - protected readonly Container HitObjectContainer; + protected Container HitObjectContainer; private PlacementBlueprint currentBlueprint; protected PlacementBlueprintTestScene() { - Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2; - Add(HitObjectContainer = CreateHitObjectContainer()); } [BackgroundDependencyLoader] private void load() { + Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2; Add(currentBlueprint = CreateBlueprint()); } diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0c39194088..03e17a819c 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -4,12 +4,10 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Testing; -using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { @@ -39,15 +37,13 @@ namespace osu.Game.Tests.Visual AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } - protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); - protected virtual bool AllowFail => false; private void loadPlayer() { - var beatmap = CreateBeatmap(ruleset); + var beatmap = CreateBeatmap(ruleset.RulesetInfo); - Beatmap.Value = new TestWorkingBeatmap(beatmap, Clock); + Beatmap.Value = CreateWorkingBeatmap(beatmap); if (!AllowFail) Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 314684069a..c3ecd62e10 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -25,6 +25,8 @@ namespace osu.Game.Users public Bindable Status = new Bindable(); + public IBindable Activity = new Bindable(); + //public Team Team; [JsonProperty(@"profile_colour")] diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs new file mode 100644 index 0000000000..918c547978 --- /dev/null +++ b/osu.Game/Users/UserActivity.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Users +{ + public abstract class UserActivity + { + public abstract string Status { get; } + public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; + + public class Modding : UserActivity + { + public override string Status => "Modding a map"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; + } + + public class ChoosingBeatmap : UserActivity + { + public override string Status => "Choosing a beatmap"; + } + + public class MultiplayerGame : UserActivity + { + public override string Status => "Playing with others"; + } + + public class Editing : UserActivity + { + public BeatmapInfo Beatmap { get; } + + public Editing(BeatmapInfo info) + { + Beatmap = info; + } + + public override string Status => @"Editing a beatmap"; + } + + public class SoloGame : UserActivity + { + public BeatmapInfo Beatmap { get; } + + public Rulesets.RulesetInfo Ruleset { get; } + + public SoloGame(BeatmapInfo info, Rulesets.RulesetInfo ruleset) + { + Beatmap = info; + Ruleset = ruleset; + } + + public override string Status => @"Playing alone"; + } + + public class Spectating : UserActivity + { + public override string Status => @"Spectating a game"; + } + + public class InLobby : UserActivity + { + public override string Status => @"In a Multiplayer Lobby"; + } + } +} diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs index dbc132995a..e583acac9f 100644 --- a/osu.Game/Users/UserCoverBackground.cs +++ b/osu.Game/Users/UserCoverBackground.cs @@ -21,31 +21,45 @@ namespace osu.Game.Users set => Model = value; } - [Resolved] - private LargeTextureStore textures { get; set; } + protected override Drawable CreateDrawable(User user) => new Cover(user); - protected override Drawable CreateDrawable(User user) + private class Cover : CompositeDrawable { - if (user == null) + private readonly User user; + + public Cover(User user) { - return new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.75f)) - }; + this.user = user; + + RelativeSizeAxes = Axes.Both; } - else + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) { - var sprite = new Sprite + if (user == null) { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get(user.CoverUrl), - FillMode = FillMode.Fill, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }; - sprite.OnLoadComplete += d => d.FadeInFromZero(400); - return sprite; + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.75f)) + }; + } + else + InternalChild = new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(user.CoverUrl), + FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeInFromZero(400); } } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 47571b673d..4b2029e6fd 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -30,6 +30,9 @@ namespace osu.Game.Users private const float content_padding = 10; private const float status_height = 30; + [Resolved(canBeNull: true)] + private OsuColour colours { get; set; } + private Container statusBar; private Box statusBg; private OsuSpriteText statusMessage; @@ -39,6 +42,8 @@ namespace osu.Game.Users public readonly Bindable Status = new Bindable(); + public readonly IBindable Activity = new Bindable(); + public new Action Action; protected Action ViewProfile; @@ -54,15 +59,13 @@ namespace osu.Game.Users } [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, UserProfileOverlay profile) + private void load(UserProfileOverlay profile) { if (colours == null) throw new ArgumentNullException(nameof(colours)); FillFlowContainer infoContainer; - UserCoverBackground coverBackground; - AddInternal(content = new Container { RelativeSizeAxes = Axes.Both, @@ -77,13 +80,16 @@ namespace osu.Game.Users Children = new Drawable[] { - new DelayedLoadWrapper(coverBackground = new UserCoverBackground + new DelayedLoadUnloadWrapper(() => new UserCoverBackground { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, User = user, - }, 300) { RelativeSizeAxes = Axes.Both }, + }, 300, 5000) + { + RelativeSizeAxes = Axes.Both, + }, new Box { RelativeSizeAxes = Axes.Both, @@ -184,8 +190,6 @@ namespace osu.Game.Users } }); - coverBackground.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out); - if (user.IsSupporter) { infoContainer.Add(new SupporterIcon @@ -195,8 +199,8 @@ namespace osu.Game.Users }); } - Status.ValueChanged += status => displayStatus(status.NewValue); - Status.ValueChanged += status => statusBg.FadeColour(status.NewValue?.GetAppropriateColour(colours) ?? colours.Gray5, 500, Easing.OutQuint); + Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); + Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); base.Action = ViewProfile = () => { @@ -211,7 +215,7 @@ namespace osu.Game.Users Status.TriggerChange(); } - private void displayStatus(UserStatus status) + private void displayStatus(UserStatus status, UserActivity activity = null) { const float transition_duration = 500; @@ -226,8 +230,17 @@ namespace osu.Game.Users statusBar.ResizeHeightTo(status_height, transition_duration, Easing.OutQuint); statusBar.FadeIn(transition_duration, Easing.OutQuint); this.ResizeHeightTo(height, transition_duration, Easing.OutQuint); + } - statusMessage.Text = status.Message; + if (status is UserStatusOnline && activity != null) + { + statusMessage.Text = activity.Status; + statusBg.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); + } + else + { + statusMessage.Text = status?.Message; + statusBg.FadeColour(status?.GetAppropriateColour(colours) ?? colours.Gray5, 500, Easing.OutQuint); } } diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index 14b4538a00..cf372560af 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -29,33 +29,7 @@ namespace osu.Game.Users public override Color4 GetAppropriateColour(OsuColour colours) => colours.Gray7; } - public class UserStatusSpectating : UserStatusOnline - { - public override string Message => @"Spectating a game"; - } - - public class UserStatusInLobby : UserStatusOnline - { - public override string Message => @"in Multiplayer Lobby"; - } - - public class UserStatusSoloGame : UserStatusBusy - { - public override string Message => @"Solo Game"; - } - - public class UserStatusMultiplayerGame : UserStatusBusy - { - public override string Message => @"Multiplaying"; - } - - public class UserStatusModding : UserStatusOnline - { - public override string Message => @"Modding a map"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; - } - - public class UserStatusDoNotDisturb : UserStatusBusy + public class UserStatusDoNotDisturb : UserStatus { public override string Message => @"Do not disturb"; public override Color4 GetAppropriateColour(OsuColour colours) => colours.RedDark; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b77c724d1b..75a464d0b8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/osu.iOS.props b/osu.iOS.props index fc047aa5f0..5e151f916b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -104,9 +104,9 @@ - - - + + +