diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index b94e9f38c6..08eea35425 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase("basic")] [TestCase("colinear-perfect-curve")] [TestCase("slider-ticks")] + [TestCase("slider-ticks-edge-case")] [TestCase("repeat-slider")] [TestCase("uneven-repeat-slider")] [TestCase("old-stacking")] diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 8bf19f030f..e05dbd8ea6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -16,6 +16,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -166,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocityMultiplier; + Velocity = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, OsuRuleset.SHORT_NAME); + // WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier` + // for backwards compatibility reasons (intentionally introducing floating point errors to match stable). + double scoringDistance = Velocity * timingPoint.BeatLength; - Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json new file mode 100644 index 0000000000..6d97b643b1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case-expected-conversion.json @@ -0,0 +1,39 @@ +{ + "Mappings": [ + { + "StartTime": 7493.0, + "Objects": [ + { + "StartTime": 7493.0, + "EndTime": 7493.0, + "X": 130.0, + "Y": 232.0, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 7817.0, + "EndTime": 7817.0, + "X": 30.9946651, + "Y": 208.5157, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + }, + { + "StartTime": 7843.0, + "EndTime": 7843.0, + "X": 33.7820168, + "Y": 208.9957, + "StackOffset": { + "X": 0.0, + "Y": 0.0 + } + } + ] + } + ] +} diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu new file mode 100644 index 0000000000..daf35e1d2b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/slider-ticks-edge-case.osu @@ -0,0 +1,31 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 + +[Difficulty] +HPDrainRate:3 +CircleSize:3.4 +OverallDifficulty:4 +ApproachRate:5.5 +SliderMultiplier:1.1 +SliderTickRate:1 + +[Events] +//Background and Video events +0,0,"aa.jpg",0,0 +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +6967,350.877192982456,6,2,1,55,1,0 +6967,-100,3,2,1,55,0,0 +7493,-111.111111111111,3,2,1,55,0,0 + +[HitObjects] +130,232,7493,6,0,P|78:218|28:208,1,101.82149697876,0|0,3:2|2:0,2:0:0:0: diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 47f4410b07..1baa737a9c 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -32,8 +32,26 @@ namespace osu.Game.Tests.Chat Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." }); Assert.AreEqual(result.Content, result.DisplayContent); - Assert.AreEqual(1, result.Links.Count); - Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url); + Assert.AreEqual(0, result.Links.Count); + } + + [Test] + public void TestFakeProtocolLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a osunotarealprotocol://completely-made-up-protocol we don't support." }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(0, result.Links.Count); + } + + [Test] + public void TestSupportedProtocolLinkParsing() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "forgotspacehttps://dev.ppy.sh joinmyosump://12345 jointheosu://chan/#english" }); + + Assert.AreEqual("https://dev.ppy.sh", result.Links[0].Url); + Assert.AreEqual("osump://12345", result.Links[1].Url); + Assert.AreEqual("osu://chan/#english", result.Links[2].Url); } [Test] diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index d4018be7fc..fed26d8acb 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc)); } + [Test] + public void TestNano() + { + createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo)); + } + [Test] public void TestNormal() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index e86302bbd1..63fc4e47f9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay private Player loadPlayerFor(RulesetInfo rulesetInfo) { + // if a player screen is present already, we must exit that before loading another one, + // otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already. + if (Stack.CurrentScreen is Player) + Stack.Exit(); + Ruleset.Value = rulesetInfo; var ruleset = rulesetInfo.CreateInstance(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index ecf9b68297..7616b9b83c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("test!"); addMessageWithChecks("dev.ppy.sh!"); addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External); addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp); addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki); addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); @@ -84,9 +87,11 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("feels important", 0, true, true); addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 94b2956b4e..a16f6d5689 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -89,6 +89,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards { switch (size) { + case BeatmapCardSize.Nano: + return new BeatmapCardNano(beatmapSet); + case BeatmapCardSize.Normal: return new BeatmapCardNormal(beatmapSet, allowExpansion); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs new file mode 100644 index 0000000000..ba2142d28f --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -0,0 +1,166 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public partial class BeatmapCardNano : BeatmapCard + { + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + + private const float height = 60; + private const float width = 300; + private const float cover_width = 80; + + [Cached] + private readonly BeatmapCardContent content; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardNano(APIBeatmapSet beatmapSet) + : base(beatmapSet, false) + { + content = new BeatmapCardContent(height); + } + + [BackgroundDependencyLoader] + private void load() + { + Width = width; + Height = height; + + Child = content.With(c => + { + c.MainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + thumbnail = new BeatmapCardThumbnail(BeatmapSet) + { + Name = @"Left (icon) area", + Size = new Vector2(cover_width, height), + Padding = new MarginPadding { Right = CORNER_RADIUS }, + }, + buttonContainer = new CollapsibleButtonContainer(BeatmapSet) + { + X = cover_width - CORNER_RADIUS, + Width = width - cover_width + CORNER_RADIUS, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = CORNER_RADIUS, + ButtonsExpandedWidth = 30, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), + Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + new TruncatingSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + AlwaysPresent = true, + Children = new Drawable[] + { + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(BeatmapSet.Author); + }), + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } + } + } + } + } + } + } + }; + c.ExpandedContent = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Child = new BeatmapCardDifficultyList(BeatmapSet) + }; + c.Expanded.BindTarget = Expanded; + }); + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + protected override void UpdateState() + { + base.UpdateState(); + + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs index 098265506d..0b5acc4a05 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs @@ -8,6 +8,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards /// public enum BeatmapCardSize { + Nano, Normal, Extra } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 8089d789c1..e2e541811d 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -36,9 +36,10 @@ namespace osu.Game.Beatmaps BeatmapSet = new BeatmapSetInfo(), Difficulty = new BeatmapDifficulty { - DrainRate = 0, CircleSize = 0, + DrainRate = 0, OverallDifficulty = 0, + ApproachRate = 0, }, Ruleset = new DummyRuleset().RulesetInfo }, audio) diff --git a/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs b/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs index 37ce1e508e..d8e1199e93 100644 --- a/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs +++ b/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs @@ -19,7 +19,7 @@ namespace osu.Game.IO.Archives this.stream = stream; } - public override Stream GetStream(string name) => new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length); + public override Stream GetStream(string name) => new MemoryStream(stream.ToArray(), 0, (int)stream.Length); public override void Dispose() { diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 10a005d71a..667175117f 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.Chat // http[s]://.[:port][/path][?query][#fragment] private static readonly Regex advanced_link_regex = new Regex( // protocol - @"(?[a-z]*?:\/\/" + + @"(?(https?|osu(mp)?):\/\/" + // domain + tld @"(?(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" + // port (optional) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs index feb0c27ee7..9cd0031e3d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs @@ -22,8 +22,12 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapListingCardSizeTabControl() { AutoSizeAxes = Axes.Both; + + Items = new[] { BeatmapCardSize.Normal, BeatmapCardSize.Extra }; } + protected override bool AddEnumEntriesAutomatically => false; + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0392e3ae52..ee184c1f35 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -79,6 +80,11 @@ namespace osu.Game.Tests.Visual protected void LoadPlayer(Mod[] mods) { + // if a player screen is present already, we must exit that before loading another one, + // otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already. + if (Stack.CurrentScreen is Player) + Stack.Exit(); + var ruleset = CreatePlayerRuleset(); Ruleset.Value = ruleset.RulesetInfo;