diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs new file mode 100644 index 0000000000..bbe543e73e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + public class TestSceneCatchModNoScope : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Test] + public void TestVisibleDuringBreak() + { + CreateModTest(new ModTestData + { + Mod = new CatchModNoScope + { + HiddenComboCount = { Value = 0 }, + }, + Autoplay = true, + PassCondition = () => true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Fruit + { + X = CatchPlayfield.CENTER_X, + StartTime = 1000, + }, + new Fruit + { + X = CatchPlayfield.CENTER_X, + StartTime = 5000, + } + }, + Breaks = new List + { + new BreakPeriod(2000, 4000), + } + } + }); + + AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0)); + AddUntilStep("wait for start of break", isBreak); + AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1)); + AddUntilStep("wait for end of break", () => !isBreak()); + AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0)); + } + + [Test] + public void TestVisibleAfterComboBreak() + { + CreateModTest(new ModTestData + { + Mod = new CatchModNoScope + { + HiddenComboCount = { Value = 2 }, + }, + Autoplay = true, + PassCondition = () => true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Fruit + { + X = 0, + StartTime = 1000, + }, + new Fruit + { + X = CatchPlayfield.CENTER_X, + StartTime = 3000, + }, + new Fruit + { + X = CatchPlayfield.WIDTH, + StartTime = 5000, + }, + } + } + }); + + AddAssert("catcher must start visible", () => catcherAlphaAlmostEquals(1)); + AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2); + AddAssert("catcher must dim after combo", () => !catcherAlphaAlmostEquals(1)); + AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0); + AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1)); + } + + private bool isBreak() => Player.IsBreakTime.Value; + + private bool catcherAlphaAlmostEquals(float alpha) + { + var playfield = (CatchPlayfield)Player.DrawableRuleset.Playfield; + return Precision.AlmostEquals(playfield.CatcherArea.Alpha, alpha); + } + } +} diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9fee6b2bc1..c256172177 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -133,6 +133,7 @@ namespace osu.Game.Rulesets.Catch new MultiMod(new ModWindUp(), new ModWindDown()), new CatchModFloatingFruits(), new CatchModMuted(), + new CatchModNoScope(), }; default: diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs new file mode 100644 index 0000000000..a24a6227fe --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Mods; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield + { + public override string Description => "Where's the catcher?"; + + [SettingSource( + "Hidden at combo", + "The combo count at which the catcher becomes completely hidden", + SettingControlType = typeof(SettingsSlider) + )] + public override BindableInt HiddenComboCount { get; } = new BindableInt + { + Default = 10, + Value = 10, + MinValue = 0, + MaxValue = 50, + }; + + public void Update(Playfield playfield) + { + var catchPlayfield = (CatchPlayfield)playfield; + bool shouldAlwaysShowCatcher = IsBreakTime.Value; + float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha; + catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs index 501c0a55bd..8e377ea632 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs @@ -4,52 +4,29 @@ using System; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Screens.Play; using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor, IApplicableToPlayer, IApplicableToBeatmap + public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap { - /// - /// Slightly higher than the cutoff for . - /// - private const float min_alpha = 0.0002f; - - private const float transition_duration = 100; - - public override string Name => "No Scope"; - public override string Acronym => "NS"; - public override ModType Type => ModType.Fun; - public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; public override string Description => "Where's the cursor?"; - public override double ScoreMultiplier => 1; - private BindableNumber currentCombo; - private IBindable isBreakTime; private PeriodTracker spinnerPeriods; - private float comboBasedAlpha; - [SettingSource( "Hidden at combo", "The combo count at which the cursor becomes completely hidden", SettingControlType = typeof(SettingsSlider) )] - public BindableInt HiddenComboCount { get; } = new BindableInt + public override BindableInt HiddenComboCount { get; } = new BindableInt { Default = 10, Value = 10, @@ -57,39 +34,16 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 50, }; - public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; - - public void ApplyToPlayer(Player player) - { - isBreakTime = player.IsBreakTime.GetBoundCopy(); - } - public void ApplyToBeatmap(IBeatmap beatmap) { - spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType().Select(b => new Period(b.StartTime - transition_duration, b.EndTime))); + spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType().Select(b => new Period(b.StartTime - TRANSITION_DURATION, b.EndTime))); } - public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + public void Update(Playfield playfield) { - if (HiddenComboCount.Value == 0) return; - - currentCombo = scoreProcessor.Combo.GetBoundCopy(); - currentCombo.BindValueChanged(combo => - { - comboBasedAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value); - }, true); + bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime); + float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha; + playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1)); } - - public virtual void Update(Playfield playfield) - { - bool shouldAlwaysShowCursor = isBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime); - float targetAlpha = shouldAlwaysShowCursor ? 1 : comboBasedAlpha; - playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1)); - } - } - - public class HiddenComboSlider : OsuSliderBar - { - public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText; } } diff --git a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs index 2918dde2db..05bfae7e63 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Editing.Checks { // While this is a problem, it is out of scope for this check and is caught by a different one. beatmap.Metadata.BackgroundFile = string.Empty; - var context = getContext(null, System.Array.Empty()); + var context = getContext(null, new MemoryStream(System.Array.Empty())); Assert.That(check.Run(context), Is.Empty); } @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestTooUncompressed() { - var context = getContext(new Texture(1920, 1080), new byte[1024 * 1024 * 3]); + var context = getContext(new Texture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3])); var issues = check.Run(context).ToList(); @@ -111,19 +111,32 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed); } - private BeatmapVerifierContext getContext(Texture background, [CanBeNull] byte[] fileBytes = null) + [Test] + public void TestStreamClosed() { - return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, fileBytes).Object); + var background = new Texture(1920, 1080); + var stream = new Mock(new byte[1024 * 1024]); + + var context = getContext(background, stream.Object); + + Assert.That(check.Run(context), Is.Empty); + + stream.Verify(x => x.Close(), Times.Once()); + } + + private BeatmapVerifierContext getContext(Texture background, [CanBeNull] Stream stream = null) + { + return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, stream).Object); } /// - /// Returns the mock of the working beatmap with the given background and filesize. + /// Returns the mock of the working beatmap with the given background and its file stream. /// /// The texture of the background. - /// The bytes that represent the background file. - private Mock getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null) + /// The stream representing the background file. + private Mock getMockWorkingBeatmap(Texture background, [CanBeNull] Stream stream = null) { - var stream = new MemoryStream(fileBytes ?? new byte[1024 * 1024]); + stream ??= new MemoryStream(new byte[1024 * 1024]); var mock = new Mock(); mock.SetupGet(w => w.Beatmap).Returns(beatmap); diff --git a/osu.Game.Tests/Models/DisplayStringTest.cs b/osu.Game.Tests/Models/DisplayStringTest.cs new file mode 100644 index 0000000000..cac5dd1aaa --- /dev/null +++ b/osu.Game.Tests/Models/DisplayStringTest.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Tests.Models +{ + [TestFixture] + public class DisplayStringTest + { + [Test] + public void TestBeatmapSet() + { + var mock = new Mock(); + + mock.Setup(m => m.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Metadata.Title).Returns("title"); + mock.Setup(m => m.Metadata.Author.Username).Returns("author"); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author)")); + } + + [Test] + public void TestBeatmapSetWithNoAuthor() + { + var mock = new Mock(); + + mock.Setup(m => m.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Metadata.Title).Returns("title"); + mock.Setup(m => m.Metadata.Author.Username).Returns(string.Empty); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title")); + } + + [Test] + public void TestBeatmapSetWithNoMetadata() + { + var mock = new Mock(); + + mock.Setup(m => m.Metadata).Returns(new BeatmapMetadata()); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("unknown artist - unknown title")); + } + + [Test] + public void TestBeatmap() + { + var mock = new Mock(); + + mock.Setup(m => m.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Metadata.Title).Returns("title"); + mock.Setup(m => m.Metadata.Author.Username).Returns("author"); + mock.Setup(m => m.DifficultyName).Returns("difficulty"); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author) [difficulty]")); + } + + [Test] + public void TestMetadata() + { + var mock = new Mock(); + + mock.Setup(m => m.Artist).Returns("artist"); + mock.Setup(m => m.Title).Returns("title"); + mock.Setup(m => m.Author.Username).Returns("author"); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author)")); + } + + [Test] + public void TestScore() + { + var mock = new Mock(); + + mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary. + mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist"); + mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title"); + mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author"); + mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty"); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]")); + } + + [Test] + public void TestRuleset() + { + var mock = new Mock(); + + mock.Setup(m => m.Name).Returns("ruleset"); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("ruleset")); + } + + [Test] + public void TestUser() + { + var mock = new Mock(); + + mock.Setup(m => m.Username).Returns("user"); + + Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user")); + } + + [Test] + public void TestFallback() + { + var fallback = new Fallback(); + + Assert.That(fallback.GetDisplayString(), Is.EqualTo("fallback")); + } + + private class Fallback + { + public override string ToString() => "fallback"; + } + } +} diff --git a/osu.Game.Tests/Online/TestSceneBeatmapManager.cs b/osu.Game.Tests/Online/TestSceneBeatmapManager.cs index 0ae0186770..4d5bee13f2 100644 --- a/osu.Game.Tests/Online/TestSceneBeatmapManager.cs +++ b/osu.Game.Tests/Online/TestSceneBeatmapManager.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Tests.Visual; @@ -16,7 +17,30 @@ namespace osu.Game.Tests.Online private BeatmapManager beatmaps; private ProgressNotification recentNotification; - private static readonly BeatmapSetInfo test_model = new BeatmapSetInfo { OnlineBeatmapSetID = 1 }; + private static readonly BeatmapSetInfo test_db_model = new BeatmapSetInfo + { + OnlineBeatmapSetID = 1, + Metadata = new BeatmapMetadata + { + Artist = "test author", + Title = "test title", + Author = new APIUser + { + Username = "mapper" + } + } + }; + + private static readonly APIBeatmapSet test_online_model = new APIBeatmapSet + { + OnlineID = 2, + Artist = "test author", + Title = "test title", + Author = new APIUser + { + Username = "mapper" + } + }; [BackgroundDependencyLoader] private void load(BeatmapManager beatmaps) @@ -26,25 +50,41 @@ namespace osu.Game.Tests.Online beatmaps.PostNotification = n => recentNotification = n as ProgressNotification; } + private static readonly object[][] notification_test_cases = + { + new object[] { test_db_model }, + new object[] { test_online_model } + }; + + [TestCaseSource(nameof(notification_test_cases))] + public void TestNotificationMessage(IBeatmapSetInfo model) + { + AddStep("clear recent notification", () => recentNotification = null); + AddStep("download beatmap", () => beatmaps.Download(model)); + + AddUntilStep("wait for notification", () => recentNotification != null); + AddUntilStep("notification text correct", () => recentNotification.Text.ToString() == "Downloading test author - test title (mapper)"); + } + [Test] public void TestCancelDownloadFromRequest() { - AddStep("download beatmap", () => beatmaps.Download(test_model)); + AddStep("download beatmap", () => beatmaps.Download(test_db_model)); - AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_model).Cancel()); + AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model).Cancel()); - AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_model) == null); + AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null); AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled); } [Test] public void TestCancelDownloadFromNotification() { - AddStep("download beatmap", () => beatmaps.Download(test_model)); + AddStep("download beatmap", () => beatmaps.Download(test_db_model)); AddStep("cancel download from notification", () => recentNotification.Close()); - AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_model) == null); + AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null); AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index d4036fefc0..f47fae33ca 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Gameplay Id = 39828, Username = @"WubWoofWolf", } - }.CreateScoreInfo(rulesets); + }.CreateScoreInfo(rulesets, CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo); } private class TestReplayDownloadButton : ReplayDownloadButton diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index ad92886bab..1efa8d6810 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -127,9 +127,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); + AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam == null)); + AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + + AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType().All(d => d.DisplayedTeam != null)); } private void createRoom(Func room) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 707b588063..eab66b9857 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps /// /// A user-presentable display title representing this beatmap. /// - public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata} {getVersionString(beatmapInfo)}".Trim(); + public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata.GetDisplayTitle()} {getVersionString(beatmapInfo)}".Trim(); /// /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index 27cd7f8d9a..7aab6a7a9b 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -27,8 +27,12 @@ namespace osu.Game.Beatmaps /// public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo) { - string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})"; - return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim(); + string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $" ({metadataInfo.Author.Username})"; + + string artist = string.IsNullOrEmpty(metadataInfo.Artist) ? "unknown artist" : metadataInfo.Artist; + string title = string.IsNullOrEmpty(metadataInfo.Title) ? "unknown title" : metadataInfo.Title; + + return $"{artist} - {title}{author}".Trim(); } /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 019749760f..320f108886 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -15,6 +15,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; +using osu.Game.Extensions; using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.IPC; @@ -192,7 +193,7 @@ namespace osu.Game.Database else { notification.CompletionText = imported.Count == 1 - ? $"Imported {imported.First().Value}!" + ? $"Imported {imported.First().Value.GetDisplayString()}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; if (imported.Count > 0 && PostImport != null) diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index 3c1f181f24..43ba62dfe0 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Humanizer; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; @@ -50,7 +51,7 @@ namespace osu.Game.Database DownloadNotification notification = new DownloadNotification { - Text = $"Downloading {request.Model}", + Text = $"Downloading {request.Model.GetDisplayString()}", }; request.DownloadProgressed += progress => diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs new file mode 100644 index 0000000000..5c96add076 --- /dev/null +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -0,0 +1,61 @@ +// 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.Rulesets; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Extensions +{ + public static class ModelExtensions + { + /// + /// Returns a user-facing string representing the . + /// + /// + /// + /// Non-interface types without special handling will fall back to . + /// + /// + /// Warning: This method is _purposefully_ not called GetDisplayTitle() like the others, because otherwise + /// extension method type inference rules cause this method to call itself and cause a stack overflow. + /// + /// + public static string GetDisplayString(this object model) + { + string result = null; + + switch (model) + { + case IBeatmapSetInfo beatmapSetInfo: + result = beatmapSetInfo.Metadata?.GetDisplayTitle(); + break; + + case IBeatmapInfo beatmapInfo: + result = beatmapInfo.GetDisplayTitle(); + break; + + case IBeatmapMetadataInfo metadataInfo: + result = metadataInfo.GetDisplayTitle(); + break; + + case IScoreInfo scoreInfo: + result = scoreInfo.GetDisplayTitle(); + break; + + case IRulesetInfo rulesetInfo: + result = rulesetInfo.Name; + break; + + case IUser user: + result = user.Username; + break; + } + + // fallback in case none of the above happens to match. + result ??= model.ToString(); + return result; + } + } +} diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index c44b88ad29..5b74bff817 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -23,9 +23,16 @@ namespace osu.Game.Overlays.Notifications { private const float loading_spinner_size = 22; + private LocalisableString text; + public LocalisableString Text { - set => Schedule(() => textDrawable.Text = value); + get => text; + set + { + text = value; + Schedule(() => textDrawable.Text = text); + } } public string CompletionText { get; set; } = "Task has completed!"; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index 8fa79e2ee8..7ce2ee802e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.IO; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks @@ -48,10 +49,14 @@ namespace osu.Game.Rulesets.Edit.Checks yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height); string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile); - double filesizeMb = context.WorkingBeatmap.GetStream(storagePath).Length / (1024d * 1024d); - if (filesizeMb > max_filesize_mb) - yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb); + using (Stream stream = context.WorkingBeatmap.GetStream(storagePath)) + { + double filesizeMb = stream.Length / (1024d * 1024d); + + if (filesizeMb > max_filesize_mb) + yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb); + } } public class IssueTemplateTooHighResolution : IssueTemplate diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs new file mode 100644 index 0000000000..7a935eb38f --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModNoScope.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModNoScope : Mod, IApplicableToScoreProcessor, IApplicableToPlayer + { + public override string Name => "No Scope"; + public override string Acronym => "NS"; + public override ModType Type => ModType.Fun; + public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; + public override double ScoreMultiplier => 1; + + /// + /// Slightly higher than the cutoff for . + /// + protected const float MIN_ALPHA = 0.0002f; + + protected const float TRANSITION_DURATION = 100; + + protected BindableNumber CurrentCombo; + + protected IBindable IsBreakTime; + + protected float ComboBasedAlpha; + + public abstract BindableInt HiddenComboCount { get; } + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + + public void ApplyToPlayer(Player player) + { + IsBreakTime = player.IsBreakTime.GetBoundCopy(); + } + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + if (HiddenComboCount.Value == 0) return; + + CurrentCombo = scoreProcessor.Combo.GetBoundCopy(); + CurrentCombo.BindValueChanged(combo => + { + ComboBasedAlpha = Math.Max(MIN_ALPHA, 1 - (float)combo.NewValue / HiddenComboCount.Value); + }, true); + } + } + + public class HiddenComboSlider : OsuSliderBar + { + public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText; + } +} diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index e5b050fc01..736a939a59 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -225,7 +225,7 @@ namespace osu.Game.Scoring return clone; } - public override string ToString() => $"{User} playing {BeatmapInfo}"; + public override string ToString() => this.GetDisplayTitle(); public bool Equals(ScoreInfo other) { diff --git a/osu.Game/Scoring/ScoreInfoExtensions.cs b/osu.Game/Scoring/ScoreInfoExtensions.cs new file mode 100644 index 0000000000..2279337fef --- /dev/null +++ b/osu.Game/Scoring/ScoreInfoExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; + +namespace osu.Game.Scoring +{ + public static class ScoreInfoExtensions + { + /// + /// A user-presentable display title representing this score. + /// + public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}"; + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 221b31f855..3da740b85d 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.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; @@ -214,10 +215,16 @@ namespace osu.Game.Screens.Menu } else if (!api.IsLoggedIn) { - logo.Action += displayLogin; + // copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop. + var previousAction = logo.Action; + + // we want to hook into logo.Action to display the login overlay, but also preserve the return value of the old action. + // therefore pass the old action to displayLogin, so that it can return that value. + // this ensures that the OsuLogo sample does not play when it is not desired. + logo.Action = () => displayLogin(previousAction); } - bool displayLogin() + bool displayLogin(Func originalAction) { if (!loginDisplayed.Value) { @@ -225,7 +232,7 @@ namespace osu.Game.Screens.Menu loginDisplayed.Value = true; } - return true; + return originalAction.Invoke(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs index 833fbd6605..1bf62241f2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs @@ -29,52 +29,51 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants [Resolved] private OsuColour colours { get; set; } + private OsuClickableContainer clickableContent; + public TeamDisplay(MultiplayerRoomUser user) { this.user = user; RelativeSizeAxes = Axes.Y; - Width = 15; + + AutoSizeAxes = Axes.X; Margin = new MarginPadding { Horizontal = 3 }; - - Alpha = 0; - Scale = new Vector2(0, 1); } [BackgroundDependencyLoader] private void load(AudioManager audio) { - box = new Container + InternalChild = clickableContent = new OsuClickableContainer { - RelativeSizeAxes = Axes.Both, - CornerRadius = 5, - Masking = true, + Width = 15, + Alpha = 0, Scale = new Vector2(0, 1), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = new Box + RelativeSizeAxes = Axes.Y, + Action = changeTeam, + Child = box = new Container { - Colour = Color4.White, RelativeSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Scale = new Vector2(0, 1), Anchor = Anchor.Centre, Origin = Anchor.Centre, + Child = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } } }; if (Client.LocalUser?.Equals(user) == true) { - InternalChild = new OsuClickableContainer - { - RelativeSizeAxes = Axes.Both, - TooltipText = "Change team", - Action = changeTeam, - Child = box - }; - } - else - { - InternalChild = box; + clickableContent.Action = changeTeam; + clickableContent.TooltipText = "Change team"; } sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap"); @@ -88,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants }); } - private int? displayedTeam; + public int? DisplayedTeam { get; private set; } protected override void OnRoomUpdated() { @@ -102,28 +101,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID; - if (newTeam == displayedTeam) + if (newTeam == DisplayedTeam) return; // only play the sample if an already valid team changes to another valid team. // this avoids playing a sound for each user if the match type is changed to/from a team mode. - if (newTeam != null && displayedTeam != null) + if (newTeam != null && DisplayedTeam != null) sampleTeamSwap?.Play(); - displayedTeam = newTeam; + DisplayedTeam = newTeam; - if (displayedTeam != null) + if (DisplayedTeam != null) { - box.FadeColour(getColourForTeam(displayedTeam.Value), duration, Easing.OutQuint); + box.FadeColour(getColourForTeam(DisplayedTeam.Value), duration, Easing.OutQuint); box.ScaleTo(new Vector2(box.Scale.X < 0 ? 1 : -1, 1), duration, Easing.OutQuint); - this.ScaleTo(Vector2.One, duration, Easing.OutQuint); - this.FadeIn(duration); + clickableContent.ScaleTo(Vector2.One, duration, Easing.OutQuint); + clickableContent.FadeIn(duration); } else { - this.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint); - this.FadeOut(duration); + clickableContent.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint); + clickableContent.FadeOut(duration); } }