diff --git a/global.json b/global.json index 6858d4044d..0223dc7330 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.24" + "Microsoft.Build.Traversal": "2.0.32" } } \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index b147fdd05b..6db4220fad 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5880a227c2..4d9dbbbc5f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -72,10 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { halfCatcherWidth = catcher.CatchWidth * 0.5f; - halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } return new Skill[] { diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8fa9c61b6f..13935e036b 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -37,10 +37,15 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherAnimationState CurrentState { get; private set; } + /// + /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. + /// + private const float allowed_catch_range = 0.8f; + /// /// Width of the area that can be used to attempt catches during gameplay. /// - internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range; protected bool Dashing { diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs new file mode 100644 index 0000000000..231a55a7e2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -0,0 +1,41 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + /// + /// A which is placed somewhere within a . + /// + public class LegacyManiaColumnElement : CompositeDrawable + { + [Resolved(CanBeNull = true)] + [CanBeNull] + protected ManiaStage Stage { get; private set; } + + [Resolved] + protected Column Column { get; private set; } + + /// + /// The column index to use for texture lookups, in the case of no user-provided configuration. + /// + protected int FallbackColumnIndex { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + if (Stage == null) + FallbackColumnIndex = Column.Index % 2 + 1; + else + { + int dist = Math.Min(Column.Index, Stage.Columns.Count - Column.Index - 1); + FallbackColumnIndex = dist % 2 + 1; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 6286c80d7c..9b0759d9d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Mods void handleHitCircle(DrawableHitCircle circle) { - if (!circle.IsHovered) + if (!circle.HitArea.IsHovered) return; Debug.Assert(circle.HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 7e530ca047..8bb324d02e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// A single follow point positioned between two adjacent s. /// - public class FollowPoint : Container + public class FollowPoint : Container, IAnimationTimeReference { private const float width = 8; @@ -45,5 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } }, confineMode: ConfineMode.NoScaling); } + + public double AnimationStartTime { get; set; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index d0935e46f7..6f09bbcd57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -116,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections int point = 0; + ClearInternal(); + for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) { float fraction = (float)d / distance; @@ -126,13 +128,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections FollowPoint fp; - if (InternalChildren.Count > point) - { - fp = (FollowPoint)InternalChildren[point]; - fp.ClearTransforms(); - } - else - AddInternal(fp = new FollowPoint()); + AddInternal(fp = new FollowPoint()); fp.Position = pointStartPosition; fp.Rotation = rotation; @@ -142,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections if (firstTransformStartTime == null) firstTransformStartTime = fadeInTime; + fp.AnimationStartTime = fadeInTime; + using (fp.BeginAbsoluteSequence(fadeInTime)) { fp.FadeIn(osuEnd.TimeFadeIn); diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 38ba4c5974..e7486ef9b0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + + if (!overlayAboveNumber) + ChangeInternalChildDepth(hitCircleText, -float.MaxValue); + state.BindTo(drawableObject.State); state.BindValueChanged(updateState, true); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 5d99960f10..c6920bd03e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderPathRadius, AllowSliderBallTint, CursorExpand, - CursorRotate + CursorRotate, + HitCircleOverlayAboveNumber } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index abb3f8ac42..6542866936 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -87,7 +87,7 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); @@ -103,7 +103,19 @@ namespace osu.Game.Beatmaps validateOnlineIds(beatmapSet); - return updateQueue.UpdateAsync(beatmapSet, cancellationToken); + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); + + await updateQueue.UpdateAsync(beatmapSet, cancellationToken); + + // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. + if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + { + if (beatmapSet.OnlineBeatmapSetID != null) + { + beatmapSet.OnlineBeatmapSetID = null; + LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); + } + } } protected override void PreImport(BeatmapSetInfo beatmapSet) @@ -447,12 +459,15 @@ namespace osu.Game.Beatmaps var res = req.Result; - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + if (res != null) + { + beatmap.Status = res.Status; + beatmap.BeatmapSet.Status = res.BeatmapSet.Status; + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + } } catch (Exception e) { diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 30c1018c1e..6a6c7b72a8 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -12,11 +12,11 @@ namespace osu.Game.Online.API /// An API request with a well-defined response type. /// /// Type of the response (used for deserialisation). - public abstract class APIRequest : APIRequest + public abstract class APIRequest : APIRequest where T : class { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result => ((OsuJsonWebRequest)WebRequest).ResponseObject; + public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject; protected APIRequest() { diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index 941691c4c1..ddc3298ca7 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public abstract class GetRankingsRequest : APIRequest + public abstract class GetRankingsRequest : APIRequest where TModel : class { private readonly RulesetInfo ruleset; private readonly int page; diff --git a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs index 52e12f04ee..bddc34a0dc 100644 --- a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs +++ b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs @@ -6,7 +6,7 @@ using osu.Framework.IO.Network; namespace osu.Game.Online.API.Requests { - public abstract class PaginatedAPIRequest : APIRequest + public abstract class PaginatedAPIRequest : APIRequest where T : class { private readonly int page; private readonly int itemsPerPage; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c356dd246d..a4a560c8e4 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy { var score = new Score { - ScoreInfo = new ScoreInfo(), Replay = new Replay() }; + WorkingBeatmap workingBeatmap; + using (SerializationReader sr = new SerializationReader(stream)) { currentRuleset = GetRuleset(sr.ReadByte()); @@ -41,7 +42,7 @@ namespace osu.Game.Scoring.Legacy var version = sr.ReadInt32(); - var workingBeatmap = GetBeatmap(sr.ReadString()); + workingBeatmap = GetBeatmap(sr.ReadString()); if (workingBeatmap is DummyWorkingBeatmap) throw new BeatmapNotFoundException(); @@ -113,6 +114,10 @@ namespace osu.Game.Scoring.Legacy CalculateAccuracy(score.ScoreInfo); + // before returning for database import, we must restore the database-sourced BeatmapInfo. + // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. + score.ScoreInfo.Beatmap = workingBeatmap.BeatmapInfo; + return score; } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 74c853340d..0d2ddb7b01 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Screens; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { @@ -29,6 +30,8 @@ namespace osu.Game.Screens.Play this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); } + protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false); + protected override ScoreInfo CreateScore() => score.ScoreInfo; } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index d063d8749f..cfba1e6e3e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -33,16 +33,21 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; + private readonly bool allowRetry; + private Drawable bottomPanel; - public ResultsScreen(ScoreInfo score) + public ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; + this.allowRetry = allowRetry; } [BackgroundDependencyLoader] private void load() { + FillFlowContainer buttons; + InternalChildren = new[] { new ResultsScrollContainer @@ -68,7 +73,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new FillFlowContainer + buttons = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -78,15 +83,16 @@ namespace osu.Game.Screens.Ranking Children = new Drawable[] { new ReplayDownloadButton(Score) { Width = 300 }, - new RetryButton { Width = 300 }, } } } } }; - if (player != null) + if (player != null && allowRetry) { + buttons.Add(new RetryButton { Width = 300 }); + AddInternal(new HotkeyRetryOverlay { Action = () => diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs new file mode 100644 index 0000000000..bcff10a24b --- /dev/null +++ b/osu.Game/Skinning/IAnimationTimeReference.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.Allocation; +using osu.Framework.Timing; + +namespace osu.Game.Skinning +{ + /// + /// Denotes an object which provides a reference time to start animations from. + /// + [Cached] + public interface IAnimationTimeReference + { + /// + /// The reference clock. + /// + IFrameBasedClock Clock { get; } + + /// + /// The time which animations should be started from, relative to . + /// + double AnimationStartTime { get; } + } +} diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 52328d43b2..8765b161d4 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Timing; namespace osu.Game.Skinning { @@ -22,7 +24,7 @@ namespace osu.Game.Skinning if (textures.Length > 0) { - var animation = new TextureAnimation + var animation = new SkinnableTextureAnimation { DefaultFrameLength = getFrameLength(source, applyConfigFrameRate, textures), Repeat = looping, @@ -53,6 +55,25 @@ namespace osu.Game.Skinning } } + public class SkinnableTextureAnimation : TextureAnimation + { + [Resolved(canBeNull: true)] + private IAnimationTimeReference timeReference { get; set; } + + public SkinnableTextureAnimation() + : base(false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (timeReference != null) + Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime }; + } + } + private const double default_frame_time = 1000 / 60d; private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index bc6e01a729..c4d796e30b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Storyboards.Drawables AddInternal(Content = new Container { - Size = new Vector2(640, 480), + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index def4eed2ca..2ada83c3b4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardLayer : LifetimeManagementContainer + public class DrawableStoryboardLayer : CompositeDrawable { public StoryboardLayer Layer { get; } public bool Enabled; @@ -23,17 +23,34 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre; Enabled = layer.VisibleWhenPassing; Masking = layer.Masking; + + InternalChild = new LayerElementContainer(layer); } - [BackgroundDependencyLoader] - private void load(CancellationToken? cancellationToken) + private class LayerElementContainer : LifetimeManagementContainer { - foreach (var element in Layer.Elements) - { - cancellationToken?.ThrowIfCancellationRequested(); + private readonly StoryboardLayer storyboardLayer; - if (element.IsDrawable) - AddInternal(element.CreateDrawable()); + public LayerElementContainer(StoryboardLayer layer) + { + storyboardLayer = layer; + + Width = 640; + Height = 480; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(CancellationToken? cancellationToken) + { + foreach (var element in storyboardLayer.Elements) + { + cancellationToken?.ThrowIfCancellationRequested(); + + if (element.IsDrawable) + AddInternal(element.CreateDrawable()); + } } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 781c566b5f..4163044273 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a2c6106931..17430e4b25 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - +