diff --git a/osu.Android.props b/osu.Android.props index 13e4457104..83b17f0d87 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index 1248409b2a..09362929d2 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -4,10 +4,8 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; @@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneCatchModHidden : ModTestScene { - [BackgroundDependencyLoader] - private void load() - { - LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false); - } - [Test] public void TestJuiceStream() { diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 7bad4c79cb..f9e106f097 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Mods } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) - { - } + => ApplyNormalVisibilityState(hitObject, state); protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 8aa0c85433..962a13ebea 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { isLegacySkin = new Lazy(() => FindProvider(s => s.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null) != null); hasKeyTexture = new Lazy(() => FindProvider(s => s.GetAnimation( - this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value + s.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1", true, true) != null) != null); } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 4dfadbb835..c282a919ea 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio; private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio; - private static readonly Vector2 playfield_middle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); + private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2; private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; @@ -74,22 +75,8 @@ namespace osu.Game.Rulesets.Osu.Mods // update end position as it may have changed as a result of the position update. current.EndPositionRandomised = current.PositionRandomised; - switch (hitObject) - { - case Slider slider: - // Shift nested objects the same distance as the slider got shifted in the randomisation process - // so that moveSliderIntoPlayfield() can determine their relative distances to slider.Position and thus minMargin - shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal)); - - var oldPos = new Vector2(slider.Position.X, slider.Position.Y); - - moveSliderIntoPlayfield(slider, current); - - // Shift them again to move them to their final position after the slider got moved into the playfield - shiftNestedObjects(slider, Vector2.Subtract(slider.Position, oldPos)); - - break; - } + if (hitObject is Slider slider) + moveSliderIntoPlayfield(slider, current); previous = current; } @@ -131,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Mods current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); - var position = Vector2.Add(previous.EndPositionRandomised, posRelativeToPrev); + var position = previous.EndPositionRandomised + posRelativeToPrev; // Move hit objects back into the playfield if they are outside of it, // which would sometimes happen during big jumps otherwise. @@ -146,34 +133,41 @@ namespace osu.Game.Rulesets.Osu.Mods /// private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo) { - // Min. distances from the slider's position to the playfield border - var minMargin = new MarginPadding(); + var minMargin = getMinSliderMargin(slider); - foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderEndCircle)) - { - if (!(hitObject is OsuHitObject osuHitObject)) - continue; - - var relativePos = Vector2.Subtract(osuHitObject.Position, slider.Position); - - minMargin.Left = Math.Max(minMargin.Left, -relativePos.X); - minMargin.Right = Math.Max(minMargin.Right, relativePos.X); - minMargin.Top = Math.Max(minMargin.Top, -relativePos.Y); - minMargin.Bottom = Math.Max(minMargin.Bottom, relativePos.Y); - } - - if (slider.Position.X < minMargin.Left) - slider.Position = new Vector2(minMargin.Left, slider.Position.Y); - else if (slider.Position.X + minMargin.Right > OsuPlayfield.BASE_SIZE.X) - slider.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - minMargin.Right, slider.Position.Y); - - if (slider.Position.Y < minMargin.Top) - slider.Position = new Vector2(slider.Position.X, minMargin.Top); - else if (slider.Position.Y + minMargin.Bottom > OsuPlayfield.BASE_SIZE.Y) - slider.Position = new Vector2(slider.Position.X, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom); + slider.Position = new Vector2( + Math.Clamp(slider.Position.X, minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right), + Math.Clamp(slider.Position.Y, minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom) + ); currentObjectInfo.PositionRandomised = slider.Position; currentObjectInfo.EndPositionRandomised = slider.EndPosition; + + shiftNestedObjects(slider, currentObjectInfo.PositionRandomised - currentObjectInfo.PositionOriginal); + } + + /// + /// Calculates the min. distances from the 's position to the playfield border for the slider to be fully inside of the playfield. + /// + private MarginPadding getMinSliderMargin(Slider slider) + { + var pathPositions = new List(); + slider.Path.GetPathToProgress(pathPositions, 0, 1); + + var minMargin = new MarginPadding(); + + foreach (var pos in pathPositions) + { + minMargin.Left = Math.Max(minMargin.Left, -pos.X); + minMargin.Right = Math.Max(minMargin.Right, pos.X); + minMargin.Top = Math.Max(minMargin.Top, -pos.Y); + minMargin.Bottom = Math.Max(minMargin.Bottom, pos.Y); + } + + minMargin.Left = Math.Min(minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right); + minMargin.Top = Math.Min(minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom); + + return minMargin; } /// @@ -188,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(hitObject is OsuHitObject osuHitObject)) continue; - osuHitObject.Position = Vector2.Add(osuHitObject.Position, shift); + osuHitObject.Position += shift; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs index ae8c03dad1..df33bf52be 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs @@ -128,5 +128,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index b52dc749f0..fece3494e6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public class MainCirclePiece : CompositeDrawable { + public override bool RemoveCompletedTransforms => false; + private readonly CirclePiece circle; private readonly RingPiece ring; private readonly FlashPiece flash; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 822dad8523..e5200ac248 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacyMainCirclePiece : CompositeDrawable { + public override bool RemoveCompletedTransforms => false; + private readonly string priorityLookup; private readonly bool hasNumber; diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 09fe9b3767..f7d42a2ee6 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,6 +12,7 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; +using osu.Game.Skinning; using osu.Game.Users; namespace osu.Game.Tests.Visual.Background @@ -23,6 +25,9 @@ namespace osu.Game.Tests.Visual.Background private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType().FirstOrDefault(); + [Resolved] + private SkinManager skins { get; set; } + [Resolved] private OsuConfigManager config { get; set; } @@ -35,7 +40,7 @@ namespace osu.Game.Tests.Visual.Background } [Test] - public void TestTogglingStoryboardSwitchesBackgroundType() + public void TestBackgroundTypeSwitch() { setSupporter(true); @@ -44,6 +49,12 @@ namespace osu.Game.Tests.Visual.Background setSourceMode(BackgroundSource.BeatmapWithStoryboard); AddUntilStep("is storyboard background", () => getCurrentBackground() is BeatmapBackgroundWithStoryboard); + + setSourceMode(BackgroundSource.Skin); + AddUntilStep("is default background", () => getCurrentBackground().GetType() == typeof(Graphics.Backgrounds.Background)); + + setCustomSkin(); + AddUntilStep("is skin background", () => getCurrentBackground() is SkinBackground); } [Test] @@ -61,15 +72,19 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground); } - [Test] - public void TestBeatmapDoesntReloadOnNoChange() + [TestCase(BackgroundSource.Beatmap, typeof(BeatmapBackground))] + [TestCase(BackgroundSource.BeatmapWithStoryboard, typeof(BeatmapBackgroundWithStoryboard))] + [TestCase(BackgroundSource.Skin, typeof(SkinBackground))] + public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType) { - BeatmapBackground last = null; + Graphics.Backgrounds.Background last = null; - setSourceMode(BackgroundSource.Beatmap); + setSourceMode(source); setSupporter(true); + if (source == BackgroundSource.Skin) + setCustomSkin(); - AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground() as BeatmapBackground) != null); + AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType); AddAssert("next doesn't load new background", () => screen.Next() == false); // doesn't really need to be checked but might as well. @@ -77,8 +92,25 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("ensure same background instance", () => last == getCurrentBackground()); } + [Test] + public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter) + { + Graphics.Backgrounds.Background last = null; + + setSourceMode(BackgroundSource.Skin); + setSupporter(supporter); + setDefaultSkin(); + + AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background)); + AddAssert("next cycles background", () => screen.Next()); + + // doesn't really need to be checked but might as well. + AddWaitStep("wait a bit", 5); + AddUntilStep("ensure different background instance", () => last != getCurrentBackground()); + } + private void setSourceMode(BackgroundSource source) => - AddStep("set background mode to beatmap", () => config.SetValue(OsuSetting.MenuBackgroundSource, source)); + AddStep($"set background mode to {source}", () => config.SetValue(OsuSetting.MenuBackgroundSource, source)); private void setSupporter(bool isSupporter) => AddStep($"set supporter {isSupporter}", () => ((DummyAPIAccess)API).LocalUser.Value = new User @@ -86,5 +118,16 @@ namespace osu.Game.Tests.Visual.Background IsSupporter = isSupporter, Id = API.LocalUser.Value.Id + 1, }); + + private void setCustomSkin() + { + // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. + AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 }); + } + + private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); + + [TearDownSteps] + public void TearDown() => setDefaultSkin(); } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 0308d74aa4..52401d32e5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -9,16 +9,19 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Visual.Multiplayer; using osuTK; using osuTK.Input; @@ -152,6 +155,14 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying); } + [Test] + public void TestPushSongSelectAndPressBackButtonImmediately() + { + AddStep("push song select", () => Game.ScreenStack.Push(new TestPlaySongSelect())); + AddStep("press back button", () => Game.ChildrenOfType().First().Action()); + AddWaitStep("wait two frames", 2); + } + [Test] public void TestExitSongSelectWithClick() { @@ -298,6 +309,18 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); } + [Test] + public void TestPushMatchSubScreenAndPressBackButtonImmediately() + { + TestMultiplayer multiplayer = null; + + PushAndConfirm(() => multiplayer = new TestMultiplayer()); + + AddStep("open room", () => multiplayer.OpenNewRoom()); + AddStep("press back button", () => Game.ChildrenOfType().First().Action()); + AddWaitStep("wait two frames", 2); + } + private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); @@ -322,5 +345,18 @@ namespace osu.Game.Tests.Visual.Navigation protected override bool DisplayStableImportPrompt => false; } + + private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer + { + [Cached(typeof(MultiplayerClient))] + public readonly TestMultiplayerClient Client; + + public TestMultiplayer() + { + Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager); + } + + protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); + } } } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 086cc573d5..b53cc659f7 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -264,14 +264,18 @@ namespace osu.Game.Collections using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write))) { sw.Write(database_version); - sw.Write(Collections.Count); - foreach (var c in Collections) + var collectionsCopy = Collections.ToArray(); + sw.Write(collectionsCopy.Length); + + foreach (var c in collectionsCopy) { sw.Write(c.Name.Value); - sw.Write(c.Beatmaps.Count); - foreach (var b in c.Beatmaps) + var beatmapsCopy = c.Beatmaps.ToArray(); + sw.Write(beatmapsCopy.Length); + + foreach (var b in beatmapsCopy) sw.Write(b.MD5Hash); } } diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index c90b1e0e98..cfc1eb1806 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.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.Graphics; using osu.Framework.Graphics.Containers; @@ -14,7 +15,7 @@ namespace osu.Game.Graphics.Backgrounds /// /// A background which offers blurring via a on demand. /// - public class Background : CompositeDrawable + public class Background : CompositeDrawable, IEquatable { private const float blur_scale = 0.5f; @@ -71,5 +72,14 @@ namespace osu.Game.Graphics.Backgrounds bufferedContainer?.BlurTo(newBlurSigma * blur_scale, duration, easing); } + + public virtual bool Equals(Background other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return other.GetType() == GetType() + && other.textureName == textureName; + } } } diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs index 058d2ed0f9..e0c15dd52a 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs @@ -24,5 +24,14 @@ namespace osu.Game.Graphics.Backgrounds { Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName); } + + public override bool Equals(Background other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return other.GetType() == GetType() + && ((BeatmapBackground)other).Beatmap == Beatmap; + } } } diff --git a/osu.Game/Graphics/Backgrounds/SkinBackground.cs b/osu.Game/Graphics/Backgrounds/SkinBackground.cs new file mode 100644 index 0000000000..9266e7b17b --- /dev/null +++ b/osu.Game/Graphics/Backgrounds/SkinBackground.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Skinning; + +namespace osu.Game.Graphics.Backgrounds +{ + internal class SkinBackground : Background + { + private readonly Skin skin; + + public SkinBackground(Skin skin, string fallbackTextureName) + : base(fallbackTextureName) + { + this.skin = skin; + } + + [BackgroundDependencyLoader] + private void load() + { + Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture; + } + + public override bool Equals(Background other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return other.GetType() == GetType() + && ((SkinBackground)other).skin.SkinInfo.Equals(skin.SkinInfo); + } + } +} diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 67cee883c8..269360c492 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -57,12 +57,6 @@ namespace osu.Game.Graphics.Backgrounds } } - /// - /// Whether we want to expire triangles as they exit our draw area completely. - /// - [Obsolete("Unused.")] // Can be removed 20210518 - protected virtual bool ExpireOffScreenTriangles => true; - /// /// Whether we should create new triangles as others expire. /// diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 3d3c07a5ad..1b394185fd 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -21,7 +21,12 @@ namespace osu.Game.Online.API.Requests.Responses { var ruleset = rulesets.GetRuleset(OnlineRulesetID); - var mods = Mods != null ? ruleset.CreateInstance().GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty(); + var rulesetInstance = ruleset.CreateInstance(); + + var mods = Mods != null ? rulesetInstance.GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty(); + + // all API scores provided by this class are considered to be legacy. + mods = mods.Append(rulesetInstance.GetAllMods().OfType().Single()).ToArray(); var scoreInfo = new ScoreInfo { @@ -38,7 +43,6 @@ namespace osu.Game.Online.API.Requests.Responses Rank = Rank, Ruleset = ruleset, Mods = mods, - IsLegacyScore = true }; if (Statistics != null) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c51624341e..2dca91cbf3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -651,9 +651,10 @@ namespace osu.Game Origin = Anchor.BottomLeft, Action = () => { - var currentScreen = ScreenStack.CurrentScreen as IOsuScreen; + if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) + return; - if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton()) + if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton())) ScreenStack.Exit(); } }, diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index feeafb7151..8a57b4af91 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -32,18 +31,6 @@ namespace osu.Game.Rulesets.Judgements private readonly Container aboveHitObjectsContent; - /// - /// Duration of initial fade in. - /// - [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")] - protected virtual double FadeInDuration => 100; - - /// - /// Duration to wait until fade out begins. Defaults to . - /// - [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")] - protected virtual double FadeOutDelay => FadeInDuration; - /// /// Creates a drawable which visualises a . /// diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index be69db5ca8..fd576e9b9f 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -69,14 +68,6 @@ namespace osu.Game.Rulesets.Judgements /// public double MaxHealthIncrease => HealthIncreaseFor(MaxResult); - /// - /// Retrieves the numeric score representation of a . - /// - /// The to find the numeric score representation for. - /// The numeric score representation of . - [Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be made non-virtual 20210328 - protected virtual int NumericResultFor(HitResult result) => ToNumericResult(result); - /// /// Retrieves the numeric score representation of a . /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 6c688c1625..5fd2b2493e 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Applies a hit object to be represented by this . /// - [Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] + [Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] // Can be removed 20211021. public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry) { if (lifetimeEntry != null) @@ -409,11 +409,6 @@ namespace osu.Game.Rulesets.Objects.Drawables using (BeginAbsoluteSequence(StateUpdateTime, true)) UpdateStartTimeStateTransforms(); -#pragma warning disable 618 - using (BeginAbsoluteSequence(StateUpdateTime + (Result?.TimeOffset ?? 0), true)) - UpdateStateTransforms(newState); -#pragma warning restore 618 - using (BeginAbsoluteSequence(HitStateUpdateTime, true)) UpdateHitStateTransforms(newState); @@ -447,7 +442,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// By default, this will fade in the object from zero with no duration. /// /// - /// This is called once before every . This is to ensure a good state in the case + /// This is called once before every . This is to ensure a good state in the case /// the was negative and potentially altered the pre-hit transforms. /// protected virtual void UpdateInitialTransforms() @@ -455,16 +450,6 @@ namespace osu.Game.Rulesets.Objects.Drawables this.FadeInFromZero(); } - /// - /// Apply transforms based on the current . Previous states are automatically cleared. - /// In the case of a non-idle , and if was not set during this call, will be invoked. - /// - /// The new armed state. - [Obsolete("Use UpdateStartTimeStateTransforms and UpdateHitStateTransforms instead")] // Can be removed 20210504 - protected virtual void UpdateStateTransforms(ArmedState state) - { - } - /// /// Apply passive transforms at the 's StartTime. /// This is called each time changes. @@ -520,23 +505,6 @@ namespace osu.Game.Rulesets.Objects.Drawables AccentColour.Value = combo.GetComboColour(comboColours); } - /// - /// Called to retrieve the combo colour. Automatically assigned to . - /// Defaults to using to decide on a colour. - /// - /// - /// This will only be called if the implements . - /// - /// A list of combo colours provided by the beatmap or skin. Can be null if not available. - [Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527 - protected virtual Color4 GetComboColour(IReadOnlyList comboColours) - { - if (!(HitObject is IHasComboInformation combo)) - throw new InvalidOperationException($"{nameof(HitObject)} must implement {nameof(IHasComboInformation)}"); - - return comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White; - } - /// /// Called when a change is made to the skin. /// @@ -630,7 +598,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// The time at which state transforms should be applied that line up to 's StartTime. - /// This is used to offset calls to . + /// This is used to offset calls to . /// public double StateUpdateTime => HitObject.StartTime; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 97cb5ca7ab..2f17167297 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -65,6 +65,10 @@ namespace osu.Game.Scoring.Legacy scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); + // lazer replays get a really high version number. + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.GetAllMods().OfType().Single()).ToArray(); + currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f8dd6953ad..288552879c 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -15,7 +15,16 @@ namespace osu.Game.Scoring.Legacy { public class LegacyScoreEncoder { - public const int LATEST_VERSION = 128; + /// + /// Database version in stable-compatible YYYYMMDD format. + /// Should be incremented if any changes are made to the format/usage. + /// + public const int LATEST_VERSION = FIRST_LAZER_VERSION; + + /// + /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. + /// + public const int FIRST_LAZER_VERSION = 30000000; private readonly Score score; private readonly IBeatmap beatmap; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a6faaf6379..3944c1d3de 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -76,9 +76,6 @@ namespace osu.Game.Scoring else if (localAPIMods != null) scoreMods = apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - if (IsLegacyScore) - scoreMods = scoreMods.Append(rulesetInstance.GetAllMods().OfType().Single()).ToArray(); - return scoreMods; } set @@ -201,33 +198,12 @@ namespace osu.Game.Scoring [JsonProperty("position")] public int? Position { get; set; } - private bool isLegacyScore; - /// /// Whether this represents a legacy (osu!stable) score. /// [JsonIgnore] [NotMapped] - public bool IsLegacyScore - { - get - { - if (isLegacyScore) - return true; - - // The above check will catch legacy online scores that have an appropriate UserString + UserId. - // For non-online scores such as those imported in, a heuristic is used based on the following table: - // - // Mode | UserString | UserId - // --------------- | ---------- | --------- - // stable | | 1 - // lazer | | - // lazer (offline) | Guest | 1 - - return ID > 0 && UserID == 1 && UserString != "Guest"; - } - set => isLegacyScore = value; - } + public bool IsLegacyScore => Mods.OfType().Any(); public IEnumerable GetStatisticsForDisplay() { diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 6bcfaac907..f0c90cc409 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -112,17 +112,24 @@ namespace osu.Game.Screens.Backgrounds newBackground = new BeatmapBackgroundWithStoryboard(beatmap.Value, getBackgroundTextureName()); newBackground ??= new BeatmapBackground(beatmap.Value, getBackgroundTextureName()); - // this method is called in many cases where the beatmap hasn't changed (ie. on screen transitions). - // if a background is already displayed for the requested beatmap, we don't want to load it again. - if (background?.GetType() == newBackground.GetType() && - (background as BeatmapBackground)?.Beatmap == beatmap.Value) - return background; - break; } + + case BackgroundSource.Skin: + // default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them. + if (skin.Value is DefaultSkin || skin.Value is DefaultLegacySkin) + break; + + newBackground = new SkinBackground(skin.Value, getBackgroundTextureName()); + break; } } + // this method is called in many cases where the background might not necessarily need to change. + // if an equivalent background is currently being shown, we don't want to load it again. + if (newBackground?.Equals(background) == true) + return background; + newBackground ??= new Background(getBackgroundTextureName()); newBackground.Depth = currentDisplay; @@ -140,22 +147,5 @@ namespace osu.Game.Screens.Backgrounds return $@"Menu/menu-background-{currentDisplay % background_count + 1}"; } } - - private class SkinnedBackground : Background - { - private readonly Skin skin; - - public SkinnedBackground(Skin skin, string fallbackTextureName) - : base(fallbackTextureName) - { - this.skin = skin; - } - - [BackgroundDependencyLoader] - private void load() - { - Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture; - } - } } } diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index cc8778d9ae..0434135547 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -67,8 +67,11 @@ namespace osu.Game.Screens /// Invoked when the back button has been pressed to close any overlays before exiting this . /// /// + /// If this has not yet finished loading, the exit will occur immediately without this method being invoked. + /// /// Return true to block this from being exited after closing an overlay. /// Return false if this should continue exiting. + /// /// bool OnBackButton(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 277aa5d772..983daac909 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -22,6 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // Isolates beatmap/ruleset to this screen. public override bool DisallowExternalBeatmapRulesetChanges => true; + // We are managing our own adjustments. For now, this happens inside the Player instances themselves. + public override bool AllowRateAdjustments => false; + /// /// Whether all spectating players have finished loading. /// diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 90e499c67f..e418d36d40 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -253,7 +253,10 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnBackButton() { - if ((screenStack.CurrentScreen as IOnlinePlaySubScreen)?.OnBackButton() == true) + if (!(screenStack.CurrentScreen is IOnlinePlaySubScreen onlineSubScreen)) + return false; + + if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowBackButton && onlineSubScreen.OnBackButton()) return true; if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 33e8c137f4..3fcca74fb8 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -70,22 +71,48 @@ namespace osu.Game.Skinning updateSample(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + CurrentSkin.SourceChanged += skinChangedImmediate; + } + + private void skinChangedImmediate() + { + // Clean up the previous sample immediately on a source change. + // This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled). + clearPreviousSamples(); + } + protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); updateSample(); } + /// + /// Whether this sample was playing before a skin source change. + /// + private bool wasPlaying; + + private void clearPreviousSamples() + { + // only run if the samples aren't already cleared. + // this ensures the "wasPlaying" state is stored correctly even if multiple clear calls are executed. + if (!sampleContainer.Any()) return; + + wasPlaying = Playing; + + sampleContainer.Clear(); + Sample = null; + } + private void updateSample() { if (sampleInfo == null) return; - bool wasPlaying = Playing; - - sampleContainer.Clear(); - Sample = null; - var sample = CurrentSkin.GetSample(sampleInfo); if (sample == null) @@ -146,6 +173,14 @@ namespace osu.Game.Skinning } } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (CurrentSkin != null) + CurrentSkin.SourceChanged -= skinChangedImmediate; + } + #region Re-expose AudioContainer public BindableNumber Volume => sampleContainer.Volume; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7eca28a023..ae3644d5cb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -34,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 096f497acb..6b9aaeafa9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +