Merge branch 'master' into fix-slow-load-test-failure

This commit is contained in:
Dan Balasescu
2021-05-27 23:15:42 +09:00
committed by GitHub
18 changed files with 530 additions and 101 deletions

View File

@ -1,7 +0,0 @@
---
name: Feature Request
about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
**Proposal designs of the feature:**

View File

@ -1,5 +1,12 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues - name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here. about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.

View File

@ -7,13 +7,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking, IHasMainCirclePiece public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, IHasMainCirclePiece
{ {
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@ -111,7 +112,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
} }
public void UpdateSnakingPosition(Vector2 start, Vector2 end) => protected override void OnApply()
Position = HitObject.RepeatIndex % 2 == 0 ? end : start; {
base.OnApply();
if (Slider != null)
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
}
} }
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(new MemoryStream()); await importCollectionsFromStream(osu, new MemoryStream());
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero); Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
} }
@ -43,7 +43,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Collections.IO
ms.Seek(0, SeekOrigin.Begin); ms.Seek(0, SeekOrigin.Begin);
await osu.CollectionManager.Import(ms); await importCollectionsFromStream(osu, ms);
} }
Assert.That(host.UpdateThread.Running, Is.True); Assert.That(host.UpdateThread.Running, Is.True);
@ -134,7 +134,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first. // Move first beatmap from second collection into the first.
osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]); osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
@ -169,5 +169,12 @@ namespace osu.Game.Tests.Collections.IO
} }
} }
} }
private static async Task importCollectionsFromStream(TestOsuGameBase osu, Stream stream)
{
// intentionally spin this up on a separate task to avoid disposal deadlocks.
// see https://github.com/EventStore/EventStore/issues/1179
await Task.Run(() => osu.CollectionManager.Import(stream).Wait());
}
} }
} }

View File

@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(null); CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space)); AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
AddUntilStep("time less than storyboard duration", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < currentStoryboardDuration);
} }
[Test] [Test]

View File

@ -73,8 +73,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
for (int i = 0; i < users; i++) for (int i = 0; i < users; i++)
spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
Client.CurrentMatchPlayingUserIds.Clear(); spectatorClient.Schedule(() =>
Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers); {
Client.CurrentMatchPlayingUserIds.Clear();
Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
});
Children = new Drawable[] Children = new Drawable[]
{ {
@ -91,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for load", () => leaderboard.IsLoaded); AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0);
} }
[Test] [Test]

View File

@ -0,0 +1,175 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneWikiMarkdownContainer : OsuTestScene
{
private TestMarkdownContainer markdownContainer;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = markdownContainer = new TestMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
};
});
[Test]
public void TestLink()
{
AddStep("set current path", () => markdownContainer.CurrentPath = "Article_styling_criteria/");
AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page");
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing");
AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting");
}
[Test]
public void TestOutdatedNoticeBox()
{
AddStep("Add outdated yaml header", () =>
{
markdownContainer.Text = @"---
outdated: true
---";
});
}
[Test]
public void TestNeedsCleanupNoticeBox()
{
AddStep("Add needs cleanup yaml header", () =>
{
markdownContainer.Text = @"---
needs_cleanup: true
---";
});
}
[Test]
public void TestOnlyShowOutdatedNoticeBox()
{
AddStep("Add outdated and needs cleanup yaml", () =>
{
markdownContainer.Text = @"---
outdated: true
needs_cleanup: true
---";
});
}
[Test]
public void TestAbsoluteImage()
{
AddStep("Add absolute image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestRelativeImage()
{
AddStep("Add relative image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.CurrentPath = "Interface/";
markdownContainer.Text = "![intro](img/intro-screen.jpg)";
});
}
[Test]
public void TestBlockImage()
{
AddStep("Add paragraph with block image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.CurrentPath = "Interface/";
markdownContainer.Text = @"Line before image
![play menu](img/play-menu.jpg ""Main Menu in osu!"")
Line after image";
});
}
[Test]
public void TestInlineImage()
{
AddStep("Add inline image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!";
});
}
private class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
public new string DocumentUrl
{
set => base.DocumentUrl = value;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
{
UrlAdded = link => Link = link,
};
private class TestMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
public Action<LinkInline> UrlAdded;
protected override void AddLinkText(string text, LinkInline linkInline)
{
base.AddLinkText(text, linkInline);
UrlAdded?.Invoke(linkInline);
}
protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline));
}
}
}
}

View File

@ -786,9 +786,12 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
private void checkVisibleItemCount(bool diff, int count) => private void checkVisibleItemCount(bool diff, int count)
AddAssert($"{count} {(diff ? "diffs" : "sets")} visible", () => {
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count);
}
private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null); private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null);

View File

@ -58,8 +58,13 @@ namespace osu.Game.Collections
if (storage.Exists(database_name)) if (storage.Exists(database_name))
{ {
List<BeatmapCollection> beatmapCollections;
using (var stream = storage.GetStream(database_name)) using (var stream = storage.GetStream(database_name))
importCollections(readCollections(stream)); beatmapCollections = readCollections(stream);
// intentionally fire-and-forget async.
importCollections(beatmapCollections);
} }
} }

View File

@ -2,14 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
@ -20,26 +19,16 @@ namespace osu.Game.Overlays.News.Displays
/// </summary> /// </summary>
public class ArticleListing : CompositeDrawable public class ArticleListing : CompositeDrawable
{ {
public Action<APINewsSidebar> SidebarMetadataUpdated; private readonly Action fetchMorePosts;
[Resolved]
private IAPIProvider api { get; set; }
private FillFlowContainer content; private FillFlowContainer content;
private ShowMoreButton showMore; private ShowMoreButton showMore;
private GetNewsRequest request; private CancellationTokenSource cancellationToken;
private Cursor lastCursor;
private readonly int? year; public ArticleListing(Action fetchMorePosts)
/// <summary>
/// Instantiate a listing for the specified year.
/// </summary>
/// <param name="year">The year to load articles from. If null, will show the most recent articles.</param>
public ArticleListing(int? year = null)
{ {
this.year = year; this.fetchMorePosts = fetchMorePosts;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -47,6 +36,7 @@ namespace osu.Game.Overlays.News.Displays
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = 20, Vertical = 20,
@ -75,53 +65,25 @@ namespace osu.Game.Overlays.News.Displays
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Margin = new MarginPadding Margin = new MarginPadding { Top = 15 },
{ Action = fetchMorePosts,
Top = 15
},
Action = performFetch,
Alpha = 0 Alpha = 0
} }
} }
}; };
performFetch();
} }
private void performFetch() public void AddPosts(IEnumerable<APINewsPost> posts, bool morePostsAvailable) => Schedule(() =>
{ LoadComponentsAsync(posts.Select(p => new NewsCard(p)).ToList(), loaded =>
request?.Cancel();
request = new GetNewsRequest(year, lastCursor);
request.Success += response => Schedule(() => onSuccess(response));
api.PerformAsync(request);
}
private CancellationTokenSource cancellationToken;
private void onSuccess(GetNewsResponse response)
{
cancellationToken?.Cancel();
// only needs to be updated on the initial load, as the content won't change during pagination.
if (lastCursor == null)
SidebarMetadataUpdated?.Invoke(response.SidebarMetadata);
// store cursor for next pagination request.
lastCursor = response.Cursor;
LoadComponentsAsync(response.NewsPosts.Select(p => new NewsCard(p)).ToList(), loaded =>
{ {
content.AddRange(loaded); content.AddRange(loaded);
showMore.IsLoading = false; showMore.IsLoading = false;
showMore.Alpha = response.Cursor != null ? 1 : 0; showMore.Alpha = morePostsAvailable ? 1 : 0;
}, (cancellationToken = new CancellationTokenSource()).Token); }, (cancellationToken = new CancellationTokenSource()).Token)
} );
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -6,6 +6,7 @@ using System.Threading;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.News; using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays; using osu.Game.Overlays.News.Displays;
using osu.Game.Overlays.News.Sidebar; using osu.Game.Overlays.News.Sidebar;
@ -14,13 +15,21 @@ namespace osu.Game.Overlays
{ {
public class NewsOverlay : OnlineOverlay<NewsHeader> public class NewsOverlay : OnlineOverlay<NewsHeader>
{ {
private readonly Bindable<string> article = new Bindable<string>(null); private readonly Bindable<string> article = new Bindable<string>();
private readonly Container sidebarContainer; private readonly Container sidebarContainer;
private readonly NewsSidebar sidebar; private readonly NewsSidebar sidebar;
private readonly Container content; private readonly Container content;
private GetNewsRequest request;
private Cursor lastCursor;
/// <summary>
/// The year currently being displayed. If null, the main listing is being displayed.
/// </summary>
private int? displayedYear;
private CancellationTokenSource cancellationToken; private CancellationTokenSource cancellationToken;
private bool displayUpdateRequired = true; private bool displayUpdateRequired = true;
@ -65,7 +74,13 @@ namespace osu.Game.Overlays
base.LoadComplete(); base.LoadComplete();
// should not be run until first pop-in to avoid requesting data before user views. // should not be run until first pop-in to avoid requesting data before user views.
article.BindValueChanged(onArticleChanged); article.BindValueChanged(a =>
{
if (a.NewValue == null)
loadListing();
else
loadArticle(a.NewValue);
});
} }
protected override NewsHeader CreateHeader() => new NewsHeader { ShowFrontPage = ShowFrontPage }; protected override NewsHeader CreateHeader() => new NewsHeader { ShowFrontPage = ShowFrontPage };
@ -95,7 +110,7 @@ namespace osu.Game.Overlays
public void ShowYear(int year) public void ShowYear(int year)
{ {
loadFrontPage(year); loadListing(year);
Show(); Show();
} }
@ -108,7 +123,11 @@ namespace osu.Game.Overlays
protected void LoadDisplay(Drawable display) protected void LoadDisplay(Drawable display)
{ {
ScrollFlow.ScrollToStart(); ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded => content.Child = loaded, (cancellationToken = new CancellationTokenSource()).Token); LoadComponentAsync(display, loaded =>
{
content.Child = loaded;
Loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
@ -118,48 +137,65 @@ namespace osu.Game.Overlays
sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
} }
private void onArticleChanged(ValueChangedEvent<string> article) private void loadListing(int? year = null)
{ {
if (article.NewValue == null)
loadFrontPage();
else
loadArticle(article.NewValue);
}
private void loadFrontPage(int? year = null)
{
beginLoading();
Header.SetFrontPage(); Header.SetFrontPage();
var page = new ArticleListing(year); displayedYear = year;
page.SidebarMetadataUpdated += metadata => Schedule(() => lastCursor = null;
beginLoading(true);
request = new GetNewsRequest(displayedYear);
request.Success += response => Schedule(() =>
{ {
sidebar.Metadata.Value = metadata; lastCursor = response.Cursor;
Loading.Hide(); sidebar.Metadata.Value = response.SidebarMetadata;
var listing = new ArticleListing(getMorePosts);
listing.AddPosts(response.NewsPosts, response.Cursor != null);
LoadDisplay(listing);
}); });
LoadDisplay(page);
API.PerformAsync(request);
}
private void getMorePosts()
{
beginLoading(false);
request = new GetNewsRequest(displayedYear, lastCursor);
request.Success += response => Schedule(() =>
{
lastCursor = response.Cursor;
if (content.Child is ArticleListing listing)
listing.AddPosts(response.NewsPosts, response.Cursor != null);
});
API.PerformAsync(request);
} }
private void loadArticle(string article) private void loadArticle(string article)
{ {
beginLoading(); // This is not yet implemented nor called from anywhere.
beginLoading(true);
Header.SetArticle(article); Header.SetArticle(article);
// Temporary, should be handled by ArticleDisplay later
LoadDisplay(Empty()); LoadDisplay(Empty());
Loading.Hide();
} }
private void beginLoading() private void beginLoading(bool showLoadingOverlay)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
Loading.Show();
if (showLoadingOverlay)
Loading.Show();
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownContainer : OsuMarkdownContainer
{
public string CurrentPath
{
set => DocumentUrl = $"{DocumentUrl}wiki/{value}";
}
protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level)
{
switch (markdownObject)
{
case YamlFrontMatterBlock yamlFrontMatterBlock:
container.Add(new WikiNoticeContainer(yamlFrontMatterBlock));
break;
case ParagraphBlock paragraphBlock:
// Check if paragraph only contains an image
if (paragraphBlock.Inline.Count() == 1 && paragraphBlock.Inline.FirstChild is LinkInline { IsImage: true } linkInline)
{
container.Add(new WikiMarkdownImageBlock(linkInline));
return;
}
break;
}
base.AddMarkdownComponent(markdownObject, container, level);
}
public override MarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer();
private class WikiMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline));
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownImage : MarkdownImage, IHasTooltip
{
public string TooltipText { get; }
public WikiMarkdownImage(LinkInline linkInline)
: base(linkInline.Url)
{
TooltipText = linkInline.Title;
}
protected override ImageContainer CreateImageContainer(string url)
{
// The idea is replace "https://website.url/wiki/{path-to-image}" to "https://website.url/wiki/images/{path-to-image}"
// "/wiki/images/*" is route to fetch wiki image from osu!web server (see: https://github.com/ppy/osu-web/blob/4205eb66a4da86bdee7835045e4bf28c35456e04/routes/web.php#L289)
url = url.Replace("/wiki/", "/wiki/images/");
return base.CreateImageContainer(url);
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osuTK;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownImageBlock : FillFlowContainer
{
[Resolved]
private IMarkdownTextComponent parentTextComponent { get; set; }
private readonly LinkInline linkInline;
public WikiMarkdownImageBlock(LinkInline linkInline)
{
this.linkInline = linkInline;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(0, 3);
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new WikiMarkdownImage(linkInline)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
parentTextComponent.CreateSpriteText().With(t =>
{
t.Text = linkInline.Title;
t.Anchor = Anchor.TopCentre;
t.Origin = Anchor.TopCentre;
}),
};
}
}
}

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Extensions.Yaml;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiNoticeContainer : FillFlowContainer
{
private readonly bool isOutdated;
private readonly bool needsCleanup;
public WikiNoticeContainer(YamlFrontMatterBlock yamlFrontMatterBlock)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
foreach (var line in yamlFrontMatterBlock.Lines)
{
switch (line.ToString())
{
case "outdated: true":
isOutdated = true;
break;
case "needs_cleanup: true":
needsCleanup = true;
break;
}
}
}
[BackgroundDependencyLoader]
private void load()
{
// Reference : https://github.com/ppy/osu-web/blob/master/resources/views/wiki/_notice.blade.php and https://github.com/ppy/osu-web/blob/master/resources/lang/en/wiki.php
// TODO : add notice box for fallback translation, legal translation and outdated translation after implement wiki locale in the future.
if (isOutdated)
{
Add(new NoticeBox
{
Text = "The content on this page is incomplete or outdated. If you are able to help out, please consider updating the article!",
});
}
else if (needsCleanup)
{
Add(new NoticeBox
{
Text = "This page does not meet the standards of the osu! wiki and needs to be cleaned up or rewritten. If you are able to help out, please consider updating the article!",
});
}
}
private class NoticeBox : Container
{
[Resolved]
private IMarkdownTextFlowComponent parentFlowComponent { get; set; }
public string Text { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colour)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
MarkdownTextFlowContainer textFlow;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
textFlow = parentFlowComponent.CreateTextFlow().With(t =>
{
t.Colour = colour.Orange1;
t.Padding = new MarginPadding
{
Vertical = 10,
Horizontal = 15,
};
})
};
textFlow.AddText(Text);
}
}
}
}

View File

@ -311,6 +311,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>. /// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>.
/// This is also fired after any changes which occurred via an <see cref="osu.Game.Rulesets.Objects.HitObject.ApplyDefaults"/> call.
/// </summary> /// </summary>
protected virtual void OnApply() protected virtual void OnApply()
{ {
@ -318,6 +319,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>. /// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>.
/// This is also fired after any changes which occurred via an <see cref="osu.Game.Rulesets.Objects.HitObject.ApplyDefaults"/> call.
/// </summary> /// </summary>
protected virtual void OnFree() protected virtual void OnFree()
{ {

View File

@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; } private ManageCollectionsDialog manageCollectionsDialog { get; set; }
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty<DrawableCarouselItem>(); public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
[CanBeNull] [CanBeNull]
private Container<DrawableCarouselItem> beatmapContainer; private Container<DrawableCarouselItem> beatmapContainer;

View File

@ -3,6 +3,7 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -53,6 +54,8 @@ namespace osu.Game.Tests.Visual.Spectator
}); });
} }
public new void Schedule(Action action) => base.Schedule(action);
/// <summary> /// <summary>
/// Sends frames for an arbitrary user. /// Sends frames for an arbitrary user.
/// </summary> /// </summary>