diff --git a/Directory.Build.props b/Directory.Build.props index 2d3478f256..551cb75077 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,9 +16,9 @@ - + - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset diff --git a/global.json b/global.json index a9a531f59c..10b61047ac 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.1.1" + "Microsoft.Build.Traversal": "2.2.3" } } \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index a4bcbd289d..97812402a3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json index d49ffa01c5..6f1d45ad8c 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json @@ -6,20 +6,20 @@ "EndTime": 2750.0, "Column": 1, "NodeSamples": [ - ["normal-hitnormal"], - ["soft-hitnormal"], - ["drum-hitnormal"] + ["Gameplay/normal-hitnormal"], + ["Gameplay/soft-hitnormal"], + ["Gameplay/drum-hitnormal"] ], - "Samples": ["-hitnormal"] + "Samples": ["Gameplay/-hitnormal"] }, { "StartTime": 1875.0, "EndTime": 2750.0, "Column": 0, "NodeSamples": [ - ["soft-hitnormal"], - ["drum-hitnormal"] + ["Gameplay/soft-hitnormal"], + ["Gameplay/drum-hitnormal"] ], - "Samples": ["-hitnormal"] + "Samples": ["Gameplay/-hitnormal"] }] }, { "StartTime": 3750.0, @@ -27,7 +27,7 @@ "StartTime": 3750.0, "EndTime": 3750.0, "Column": 3, - "Samples": ["normal-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }] }] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json index 1aca75a796..fd0c0cad60 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json @@ -6,10 +6,10 @@ "EndTime": 1500.0, "Column": 0, "NodeSamples": [ - ["normal-hitnormal"], + ["Gameplay/normal-hitnormal"], [] ], - "Samples": ["normal-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }] }, { "StartTime": 2000.0, @@ -18,10 +18,10 @@ "EndTime": 3000.0, "Column": 2, "NodeSamples": [ - ["drum-hitnormal"], + ["Gameplay/drum-hitnormal"], [] ], - "Samples": ["drum-hitnormal"] + "Samples": ["Gameplay/drum-hitnormal"] }] }] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json index e3768a90d7..e07bd3c47c 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json @@ -5,17 +5,17 @@ "StartTime": 8470.0, "EndTime": 8470.0, "Column": 0, - "Samples": ["normal-hitnormal", "normal-hitclap"] + "Samples": ["Gameplay/normal-hitnormal", "Gameplay/normal-hitclap"] }, { "StartTime": 8626.470587768974, "EndTime": 8626.470587768974, "Column": 1, - "Samples": ["normal-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }, { "StartTime": 8782.941175537948, "EndTime": 8782.941175537948, "Column": 2, - "Samples": ["normal-hitnormal", "normal-hitclap"] + "Samples": ["Gameplay/normal-hitnormal", "Gameplay/normal-hitclap"] }] }] } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index 937473e824..6841ecd23c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -64,8 +64,8 @@ namespace osu.Game.Rulesets.Osu.Mods /// private const float target_clamp = 1; - private readonly float targetBreakMultiplier = 0; - private readonly float easing = 1; + private readonly float targetBreakMultiplier; + private readonly float easing; private readonly CompositeDrawable restrictTo; @@ -86,6 +86,9 @@ namespace osu.Game.Rulesets.Osu.Mods { this.restrictTo = restrictTo; this.beatmap = beatmap; + + targetBreakMultiplier = 0; + easing = 1; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index fcf7c529f5..4a3dc58604 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -2,6 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; @@ -18,47 +22,83 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); + private IBindableList samplePoints; + public DrumSampleContainer(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; - - IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; - - for (int i = 0; i < samplePoints.Count; i++) - { - var samplePoint = samplePoints[i]; - - var centre = samplePoint.GetSampleInfo(); - var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP); - - var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue; - var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue; - - mappings[samplePoint.Time] = new DrumSample - { - Centre = addSound(centre, lifetimeStart, lifetimeEnd), - Rim = addSound(rim, lifetimeStart, lifetimeEnd) - }; - } } - private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) + [BackgroundDependencyLoader] + private void load() { - var drawable = new PausableSkinnableSound(hitSampleInfo) + samplePoints = controlPoints.SamplePoints.GetBoundCopy(); + samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true); + } + + private void recreateMappings() + { + mappings.Clear(); + ClearInternal(); + + SampleControlPoint[] points = samplePoints.Count == 0 + ? new[] { controlPoints.SamplePointAt(double.MinValue) } + : samplePoints.ToArray(); + + for (int i = 0; i < points.Length; i++) { - LifetimeStart = lifetimeStart, - LifetimeEnd = lifetimeEnd - }; - AddInternal(drawable); - return drawable; + var samplePoint = points[i]; + + var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue; + var lifetimeEnd = i + 1 < points.Length ? points[i + 1].Time : double.MaxValue; + + AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint) + { + LifetimeStart = lifetimeStart, + LifetimeEnd = lifetimeEnd + }); + } } public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; - public class DrumSample + public class DrumSample : CompositeDrawable { - public PausableSkinnableSound Centre; - public PausableSkinnableSound Rim; + public override bool RemoveWhenNotAlive => false; + + public PausableSkinnableSound Centre { get; private set; } + public PausableSkinnableSound Rim { get; private set; } + + private readonly SampleControlPoint samplePoint; + + private Bindable sampleBank; + private BindableNumber sampleVolume; + + public DrumSample(SampleControlPoint samplePoint) + { + this.samplePoint = samplePoint; + } + + [BackgroundDependencyLoader] + private void load() + { + sampleBank = samplePoint.SampleBankBindable.GetBoundCopy(); + sampleBank.BindValueChanged(_ => recreate()); + + sampleVolume = samplePoint.SampleVolumeBindable.GetBoundCopy(); + sampleVolume.BindValueChanged(_ => recreate()); + + recreate(); + } + + private void recreate() + { + InternalChildren = new Drawable[] + { + Centre = new PausableSkinnableSound(samplePoint.GetSampleInfo()), + Rim = new PausableSkinnableSound(samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP)) + }; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index a804ea5f82..c88480d18f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning get { foreach (var name in source.LookupNames) - yield return $"taiko-{name}"; + yield return name.Insert(name.LastIndexOf('/') + 1, "taiko-"); foreach (var name in source.LookupNames) yield return name; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index b6e1af57fd..4b9e9dd88c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -410,13 +410,13 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); // The control point at the end time of the slider should be applied - Assert.AreEqual("soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); + Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); } static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); @@ -432,9 +432,9 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); } static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); @@ -452,7 +452,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume); } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 80fbda8e1d..b941313103 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -821,15 +821,13 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - await manager.Import(temp); - - var imported = manager.GetAllUsableBeatmapSets(); + var importedSet = await manager.Import(temp); ensureLoaded(osu); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return imported.LastOrDefault(); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); } private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 58cc324233..de46f9d1cf 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -81,8 +81,8 @@ namespace osu.Game.Tests.Gameplay private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation { - public bool NewCombo { get; set; } = false; - public int ComboOffset { get; } = 0; + public bool NewCombo { get; set; } + public int ComboOffset => 0; public Bindable IndexInCurrentComboBindable { get; } = new Bindable(); diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index 8f5990aeb1..fba0d92d4b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -149,18 +149,14 @@ namespace osu.Game.Tests.Visual.Background => AddStep($"set seasonal mode to {mode}", () => config.Set(OsuSetting.SeasonalBackgroundMode, mode)); private void createLoader() - { - AddStep("create loader", () => + => AddStep("create loader", () => { if (backgroundLoader != null) Remove(backgroundLoader); - LoadComponentAsync(backgroundLoader = new SeasonalBackgroundLoader(), Add); + Add(backgroundLoader = new SeasonalBackgroundLoader()); }); - AddUntilStep("wait for loaded", () => backgroundLoader.IsLoaded); - } - private void loadNextBackground() { SeasonalBackground background = null; diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 528689e67c..9ef9649f77 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -19,7 +19,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens; @@ -193,9 +192,9 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { - Ruleset = new OsuRuleset().RulesetInfo, User = new User { Username = "osu!" }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, + Ruleset = Ruleset.Value, }))); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 864e88d023..fc0cda2c1f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay skinSource = new TestSkinSourceContainer { RelativeSizeAxes = Axes.Both, - Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) + Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide")) }, }; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index fdc20dc477..07ff56b5c3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public Bindable InitialRoomsReceived { get; } = new Bindable(true); - public IBindableList Rooms { get; } = null; + public IBindableList Rooms => null; public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 1e87893f39..2af15923a0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking } } }, - new AccuracyCircle(score) + new AccuracyCircle(score, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index e02ebf3be1..0b2c0ce63b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -197,8 +197,8 @@ namespace osu.Game.Tests.Visual.SongSelect private class TestHitObject : ConvertHitObject, IHasPosition { - public float X { get; } = 0; - public float Y { get; } = 0; + public float X => 0; + public float Y => 0; public Vector2 Position { get; } = Vector2.Zero; } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8b1f5a366a..8efaeb3795 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -50,9 +50,9 @@ namespace osu.Game.Audio get { if (!string.IsNullOrEmpty(Suffix)) - yield return $"{Bank}-{Name}{Suffix}"; + yield return $"Gameplay/{Bank}-{Name}{Suffix}"; - yield return $"{Bank}-{Name}"; + yield return $"Gameplay/{Bank}-{Name}"; } } diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 2406b0bef2..240d70c418 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -10,14 +10,14 @@ namespace osu.Game.Audio /// public class SampleInfo : ISampleInfo { - private readonly string sampleName; + private readonly string[] sampleNames; - public SampleInfo(string sampleName) + public SampleInfo(params string[] sampleNames) { - this.sampleName = sampleName; + this.sampleNames = sampleNames; } - public IEnumerable LookupNames => new[] { sampleName }; + public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index c1f4c07833..9e83738e70 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -17,6 +17,7 @@ using osu.Framework.Logging; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -238,7 +239,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo); + return difficultyCache[key] = new StarDifficulty(attributes); } catch (BeatmapInvalidForRulesetException e) { @@ -346,17 +347,46 @@ namespace osu.Game.Beatmaps public readonly struct StarDifficulty { + /// + /// The star difficulty rating for the given beatmap. + /// public readonly double Stars; + + /// + /// The maximum combo achievable on the given beatmap. + /// public readonly int MaxCombo; - public StarDifficulty(double stars, int maxCombo) - { - Stars = stars; - MaxCombo = maxCombo; + /// + /// The difficulty attributes computed for the given beatmap. + /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. + /// + [CanBeNull] + public readonly DifficultyAttributes Attributes; + /// + /// Creates a structure based on computed + /// by a . + /// + public StarDifficulty([NotNull] DifficultyAttributes attributes) + { + Stars = attributes.StarRating; + MaxCombo = attributes.MaxCombo; + Attributes = attributes; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } + /// + /// Creates a structure with a pre-populated star difficulty and max combo + /// in scenarios where computing is not feasible (i.e. when working with online sources). + /// + public StarDifficulty(double starDifficulty, int maxCombo) + { + Stars = starDifficulty; + MaxCombo = maxCombo; + Attributes = null; + } + public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 22314f28c7..b843aad950 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps.ControlPoints /// All sound points. /// [JsonProperty] - public IReadOnlyList SamplePoints => samplePoints; + public IBindableList SamplePoints => samplePoints; - private readonly SortedList samplePoints = new SortedList(Comparer.Default); + private readonly BindableList samplePoints = new BindableList(); /// /// All effect points. diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 80a4d6dea4..80fd6c22bb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -417,7 +417,7 @@ namespace osu.Game.Beatmaps.Formats string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; int volume = samples.FirstOrDefault()?.Volume ?? 100; - sb.Append(":"); + sb.Append(':'); sb.Append(FormattableString.Invariant($"{customSampleBank}:")); sb.Append(FormattableString.Invariant($"{volume}:")); sb.Append(FormattableString.Invariant($"{sampleFilename}")); diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index e2550d1ca4..8d8ca523d5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -347,7 +347,7 @@ namespace osu.Game.Beatmaps.Formats /// The line which may contains variables. private void decodeVariables(ref string line) { - while (line.IndexOf('$') >= 0) + while (line.Contains('$')) { string origLine = line; diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index 1fd2f23d50..ddafd77066 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -26,7 +26,7 @@ namespace osu.Game.Database /// Whether this write usage will commit a transaction on completion. /// If false, there is a parent usage responsible for transaction commit. /// - public bool IsTransactionLeader = false; + public bool IsTransactionLeader; protected void Dispose(bool disposing) { diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 99f3a8a6e8..a48da37804 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -15,7 +15,6 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Graphics.Backgrounds { - [LongRunningLoad] public class SeasonalBackgroundLoader : Component { /// diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index ed5c73bee6..b9122d254d 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers /// Allows controlling the scroll bar from any position in the container using the right mouse button. /// Uses the value of to smoothly scroll to the dragged location. /// - public bool RightMouseScrollbar = false; + public bool RightMouseScrollbar; /// /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index a30f961daf..f74574e60c 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives return null; byte[] buffer = new byte[input.Length]; - await input.ReadAsync(buffer, 0, buffer.Length); + await input.ReadAsync(buffer); return buffer; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 481c94e6f3..cb170ad298 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Spectator public event Action OnUserBeganPlaying; /// - /// Called whenever a user starts a play session. + /// Called whenever a user finishes a play session. /// public event Action OnUserFinishedPlaying; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7364cf04b0..4bc54e7e83 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -229,6 +229,10 @@ namespace osu.Game dependencies.Cache(DifficultyManager = new BeatmapDifficultyManager()); AddInternal(DifficultyManager); + var scorePerformanceManager = new ScorePerformanceManager(); + dependencies.Cache(scorePerformanceManager); + AddInternal(scorePerformanceManager); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 282de3a8e1..e8b051b4d9 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")), - clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")), - kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")), - finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")), + hatSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")), + clapSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-clap")), + kickSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-kick")), + finishSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-finish")), }; } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index b43324bcfa..0b41ca31ea 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Replays /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. /// Disabling this can make replay playback smoother (useful for autoplay, currently). /// - public bool FrameAccuratePlayback = false; + public bool FrameAccuratePlayback; public bool HasFrames => Frames.Count > 0; diff --git a/osu.Game/Rulesets/UI/IFrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs index c1d8733d26..569ef5e06c 100644 --- a/osu.Game/Rulesets/UI/IFrameStableClock.cs +++ b/osu.Game/Rulesets/UI/IFrameStableClock.cs @@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.UI { IBindable IsCatchingUp { get; } + /// + /// Whether the frame stable clock is waiting on new frames to arrive to be able to progress time. + /// IBindable WaitingOnFrames { get; } } } diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs new file mode 100644 index 0000000000..ddda1b99af --- /dev/null +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; + +namespace osu.Game.Scoring +{ + /// + /// A global component which calculates and caches results of performance calculations for locally databased scores. + /// + public class ScorePerformanceManager : Component + { + // this cache will grow indefinitely per session and should be considered temporary. + // this whole component should likely be replaced with database persistence. + private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + + /// + /// Calculates performance for the given . + /// + /// The score to do the calculation on. + /// An optional to cancel the operation. + public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) + { + var lookupKey = new PerformanceCacheLookup(score); + + if (performanceCache.TryGetValue(lookupKey, out double performance)) + return Task.FromResult((double?)performance); + + return computePerformanceAsync(score, lookupKey, token); + } + + private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) + { + var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); + + // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. + if (attributes.Attributes == null) + return null; + + token.ThrowIfCancellationRequested(); + + var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Attributes, score); + var total = calculator?.Calculate(); + + if (total.HasValue) + performanceCache[lookupKey] = total.Value; + + return total; + } + + public readonly struct PerformanceCacheLookup + { + public readonly string ScoreHash; + public readonly int LocalScoreID; + + public PerformanceCacheLookup(ScoreInfo info) + { + ScoreHash = info.Hash; + LocalScoreID = info.ID; + } + + public override int GetHashCode() + { + var hash = new HashCode(); + + hash.Add(ScoreHash); + hash.Add(LocalScoreID); + + return hash.ToHashCode(); + } + } + } +} diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 3f6210310f..8beb955824 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Backgrounds mode = config.GetBindable(OsuSetting.MenuBackgroundSource); introSequence = config.GetBindable(OsuSetting.IntroSequence); + AddInternal(seasonalBackgroundLoader); + user.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next(); @@ -53,7 +55,6 @@ namespace osu.Game.Screens.Backgrounds currentDisplay = RNG.Next(0, background_count); - LoadComponentAsync(seasonalBackgroundLoader); Next(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5ac360d029..fa98358dbe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -210,10 +210,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } if (DragBox.State == Visibility.Visible) - { DragBox.Hide(); - SelectionHandler.UpdateVisibility(); - } } protected override bool OnKeyDown(KeyDownEvent e) @@ -352,11 +349,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Selects all s. /// - private void selectAll() - { - SelectionBlueprints.ToList().ForEach(m => m.Select()); - SelectionHandler.UpdateVisibility(); - } + private void selectAll() => SelectionBlueprints.ToList().ForEach(m => m.Select()); /// /// Deselects all selected s. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 01e23bafc5..e346630235 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -201,8 +201,6 @@ namespace osu.Game.Screens.Edit.Compose.Components // there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once. if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); - - UpdateVisibility(); } /// @@ -214,8 +212,6 @@ namespace osu.Game.Screens.Edit.Compose.Components selectedBlueprints.Remove(blueprint); EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); - - UpdateVisibility(); } /// @@ -226,13 +222,21 @@ namespace osu.Game.Screens.Edit.Compose.Components internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) - EditorBeatmap.Remove(blueprint.HitObject); + handleQuickDeletion(blueprint); else if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) blueprint.ToggleSelection(); else ensureSelected(blueprint); } + private void handleQuickDeletion(SelectionBlueprint blueprint) + { + if (!blueprint.IsSelected) + EditorBeatmap.Remove(blueprint.HitObject); + else + deleteSelected(); + } + private void ensureSelected(SelectionBlueprint blueprint) { if (blueprint.IsSelected) @@ -254,23 +258,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Updates whether this is visible. /// - internal void UpdateVisibility() + private void updateVisibility() { int count = selectedBlueprints.Count; selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; - if (count > 0) - { - Show(); - OnSelectionChanged(); - } - else - Hide(); + this.FadeTo(count > 0 ? 1 : 0); + OnSelectionChanged(); } /// - /// Triggered whenever more than one object is selected, on each change. + /// Triggered whenever the set of selected objects changes. /// Should update the selection box's state to match supported operations. /// protected virtual void OnSelectionChanged() @@ -421,7 +420,11 @@ namespace osu.Game.Screens.Edit.Compose.Components // bring in updates from selection changes EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); - EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates(); + EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => + { + Scheduler.AddOnce(updateVisibility); + UpdateTernaryStates(); + }; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 84328466c3..10913a8bb9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -9,10 +9,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; -using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -137,8 +137,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private class TimelineDragBox : DragBox { - private Vector2 lastMouseDown; - private float localMouseDown; + // the following values hold the start and end X positions of the drag box in the timeline's local space, + // but with zoom unapplied in order to be able to compensate for positional changes + // while the timeline is being zoomed in/out. + private float? selectionStart; + private float selectionEnd; + + [Resolved] + private Timeline timeline { get; set; } public TimelineDragBox(Action performSelect) : base(performSelect) @@ -153,21 +159,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override bool HandleDrag(MouseButtonEvent e) { - // store the original position of the mouse down, as we may be scrolled during selection. - if (lastMouseDown != e.ScreenSpaceMouseDownPosition) - { - lastMouseDown = e.ScreenSpaceMouseDownPosition; - localMouseDown = e.MouseDownPosition.X; - } + selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom; - float selection1 = localMouseDown; - float selection2 = e.MousePosition.X; + // only calculate end when a transition is not in progress to avoid bouncing. + if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom)) + selectionEnd = e.MousePosition.X / timeline.CurrentZoom; - Box.X = Math.Min(selection1, selection2); - Box.Width = Math.Abs(selection1 - selection2); + updateDragBoxPosition(); + return true; + } + + private void updateDragBoxPosition() + { + if (selectionStart == null) + return; + + float rescaledStart = selectionStart.Value * timeline.CurrentZoom; + float rescaledEnd = selectionEnd * timeline.CurrentZoom; + + Box.X = Math.Min(rescaledStart, rescaledEnd); + Box.Width = Math.Abs(rescaledStart - rescaledEnd); PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat); - return true; + } + + public override void Hide() + { + base.Hide(); + selectionStart = null; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 227eecf9c7..f90658e99c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -29,9 +29,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Container zoomedContent; protected override Container Content => zoomedContent; - private float currentZoom = 1; + /// + /// The current zoom level of . + /// It may differ from during transitions. + /// + public float CurrentZoom => currentZoom; + [Resolved(canBeNull: true)] private IFrameBasedClock editorClock { get; set; } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 60c6aa1d8a..c7c37cbc0d 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); if (!string.IsNullOrEmpty(criteria.SearchString)) - matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); + matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); r.MatchingFilter = matchingFilter; } diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 5bcda50399..831b2f593c 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("combobreak")); + InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("Gameplay/combobreak")); alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6154ec67b8..2c83161614 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -63,6 +63,14 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; + /// + /// Creates a new . + /// + /// The beatmap being played. + /// The suggested time to start gameplay at. + /// + /// Whether should be used regardless of when storyboard events and hitobjects are supposed to start. + /// public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) { this.beatmap = beatmap; diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 65f34aba3e..8778cff535 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop")) + AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("Gameplay/pause-loop")) { Looping = true, Volume = { Value = 0 } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 2f98647d3e..fbed84b820 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -54,25 +54,6 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(score); } - private void userBeganPlaying(int userId, SpectatorState state) - { - if (userId == score.ScoreInfo.UserID) - { - Schedule(() => - { - if (this.IsCurrentScreen()) this.Exit(); - }); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (spectatorStreaming != null) - spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; - } - protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. @@ -83,5 +64,29 @@ namespace osu.Game.Screens.Play return new GameplayClockContainer(beatmap, firstFrameTime.Value, true); } + + public override bool OnExiting(IScreen next) + { + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + return base.OnExiting(next); + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId != score.ScoreInfo.UserID) return; + + Schedule(() => + { + if (this.IsCurrentScreen()) this.Exit(); + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorStreaming != null) + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 45da23f1f9..bca3a07fa6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -4,14 +4,17 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Accuracy @@ -73,18 +76,23 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly ScoreInfo score; + private readonly bool withFlair; + private SmoothCircularProgress accuracyCircle; private SmoothCircularProgress innerMask; private Container badges; private RankText rankText; - public AccuracyCircle(ScoreInfo score) + private SkinnableSound applauseSound; + + public AccuracyCircle(ScoreInfo score, bool withFlair) { this.score = score; + this.withFlair = withFlair; } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { InternalChildren = new Drawable[] { @@ -203,6 +211,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }, rankText = new RankText(score.Rank) }; + + if (withFlair) + { + AddInternal(applauseSound = score.Rank >= ScoreRank.A + ? new SkinnableSound(new SampleInfo("Results/rankpass", "applause")) + : new SkinnableSound(new SampleInfo("Results/rankfail"))); + } } private ScoreRank getRank(ScoreRank rank) @@ -234,11 +249,16 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy continue; using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) + { badge.Appear(); + } } using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true)) + { + this.Delay(-1440).Schedule(() => applauseSound?.Play()); rankText.Appear(); + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index cb4560802b..f9b7625913 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Expanded { new AccuracyStatistic(score.Accuracy), new ComboStatistic(score.MaxCombo, !score.Statistics.TryGetValue(HitResult.Miss, out var missCount) || missCount == 0), - new CounterStatistic("pp", (int)(score.PP ?? 0)), + new PerformanceStatistic(score), }; var bottomStatistics = new List(); @@ -120,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded Margin = new MarginPadding { Top = 40 }, RelativeSizeAxes = Axes.X, Height = 230, - Child = new AccuracyCircle(score) + Child = new AccuracyCircle(score, withFlair) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index fc01f5e9c4..d37f6c5e5f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Ranking.Expanded.Accuracy; using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics @@ -46,7 +45,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Child = counter = new Counter + Child = counter = new StatisticCounter { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre @@ -67,18 +66,5 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics return container; } - - private class Counter : RollingCounter - { - protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; - - protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; - - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => - { - s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); - s.Spacing = new Vector2(-2, 0); - }); - } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs new file mode 100644 index 0000000000..cd9d8005c6 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.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 System; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class PerformanceStatistic : StatisticDisplay + { + private readonly ScoreInfo score; + + private readonly Bindable performance = new Bindable(); + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + private RollingCounter counter; + + public PerformanceStatistic(ScoreInfo score) + : base("PP") + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load(ScorePerformanceManager performanceManager) + { + if (score.PP.HasValue) + { + setPerformanceValue(score.PP.Value); + } + else + { + performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.Result)), cancellationTokenSource.Token); + } + } + + private void setPerformanceValue(double? pp) + { + if (pp.HasValue) + performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + } + + public override void Appear() + { + base.Appear(); + counter.Current.BindTo(performance); + } + + protected override void Dispose(bool isDisposing) + { + cancellationTokenSource?.Cancel(); + base.Dispose(isDisposing); + } + + protected override Drawable CreateContent() => counter = new StatisticCounter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs new file mode 100644 index 0000000000..bbcfc43dc8 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.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.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class StatisticCounter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + s.Spacing = new Vector2(-2, 0); + }); + } +} diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 0ee52f3e48..71f78c5c95 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -300,6 +300,7 @@ namespace osu.Game.Screens.Select public MetadataSection(string title) { + Alpha = 0; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index dce4028f17..1aab50037a 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Select.Carousel var terms = Beatmap.SearchableTerms; foreach (var criteriaTerm in criteria.SearchTerms) - match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); + match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. // this should be done after text matching so we can prioritise matching numbers in metadata. diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index f34f8f6505..7bddb3e51b 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Select if (string.IsNullOrEmpty(value)) return false; - return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; + return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); } public string SearchTerm; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 19769f487d..ee8825640c 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -32,11 +32,7 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load(OsuColour colours) { - BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => - { - ValidForResume = false; - Edit(); - }); + BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 94b09684d3..fb020f4e39 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -417,10 +417,14 @@ namespace osu.Game.Skinning public override SampleChannel GetSample(ISampleInfo sampleInfo) { - var lookupNames = sampleInfo.LookupNames; + IEnumerable lookupNames; if (sampleInfo is HitSampleInfo hitSample) lookupNames = getLegacyLookupNames(hitSample); + else + { + lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackNames); + } foreach (var lookup in lookupNames) { @@ -433,6 +437,27 @@ namespace osu.Game.Skinning return null; } + private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) + { + var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames); + + if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) + { + // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. + // using .EndsWith() is intentional as it ensures parity in all edge cases + // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). + lookupNames = lookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); + } + + foreach (var l in lookupNames) + yield return l; + + // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. + // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, + // which is why this is done locally here. + yield return hitSample.Name; + } + private IEnumerable getFallbackNames(string componentName) { // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin. @@ -442,23 +467,5 @@ namespace osu.Game.Skinning string lastPiece = componentName.Split('/').Last(); yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece; } - - private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) - { - var lookupNames = hitSample.LookupNames; - - if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) - // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. - // using .EndsWith() is intentional as it ensures parity in all edge cases - // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). - lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); - - // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. - // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, - // which is why this is done locally here. - lookupNames = lookupNames.Append(hitSample.Name); - - return lookupNames; - } } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f6e91811dd..ffa0a963ce 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -88,7 +88,7 @@ namespace osu.Game.Skinning { foreach (var lookup in s.LookupNames) { - if ((ch = samples.Get($"Gameplay/{lookup}")) != null) + if ((ch = samples.Get(lookup)) != null) break; } } diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index aa3bd2e4b7..088e997de9 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual /// /// Whether custom test steps are provided. Custom tests should invoke to create the test steps. /// - protected virtual bool HasCustomSteps { get; } = false; + protected virtual bool HasCustomSteps => false; protected TestPlayer Player; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9be933c74a..e57807e989 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -27,7 +27,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index e26f8cc8b4..76c496cd2d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - +