diff --git a/osu.Android.props b/osu.Android.props
index 376cc73592..9067ae1cd9 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 47cd39dc5a..58d67c11d9 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
+ Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@@ -141,7 +141,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ .ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
new file mode 100644
index 0000000000..d80fbfe309
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -0,0 +1,121 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Online.Solo;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Ranking;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestScenePlayerScoreSubmission : OsuPlayerTestScene
+ {
+ protected override bool AllowFail => allowFail;
+
+ private bool allowFail;
+
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ protected override bool HasCustomSteps => true;
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+
+ [Test]
+ public void TestNoSubmissionOnResultsWithNoToken()
+ {
+ prepareTokenResponse(false);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnResults()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnExitWithNoToken()
+ {
+ prepareTokenResponse(false);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnFail()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = true);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("exit", () => Player.Exit());
+
+ AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ }
+
+ [Test]
+ public void TestSubmissionOnExit()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ }
+
+ private void prepareTokenResponse(bool validToken)
+ {
+ AddStep("Prepare test API", () =>
+ {
+ dummyAPI.HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case CreateSoloScoreRequest tokenRequest:
+ if (validToken)
+ tokenRequest.TriggerSuccess(new APIScoreToken { ID = 1234 });
+ else
+ tokenRequest.TriggerFailure(new APIException("something went wrong!", null));
+ return true;
+ }
+
+ return false;
+ };
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
index 4f2ca34fb0..0e036e8868 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -65,18 +66,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- OnReadyClick = async () =>
+ OnReadyClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
- if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ Task.Run(async () =>
{
- await Client.StartMatch();
- return;
- }
+ if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ {
+ await Client.StartMatch();
+ return;
+ }
- await Client.ToggleReady();
- readyClickOperation.Dispose();
+ await Client.ToggleReady();
+
+ readyClickOperation.Dispose();
+ });
}
});
});
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
index 070158f552..4966dfbe50 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -69,11 +70,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- OnSpectateClick = async () =>
+ OnSpectateClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
- await Client.ToggleSpectate();
- readyClickOperation.Dispose();
+
+ Task.Run(async () =>
+ {
+ await Client.ToggleSpectate();
+ readyClickOperation.Dispose();
+ });
}
},
readyButton = new MultiplayerReadyButton
@@ -81,18 +86,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- OnReadyClick = async () =>
+ OnReadyClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
- if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ Task.Run(async () =>
{
- await Client.StartMatch();
- return;
- }
+ if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ {
+ await Client.StartMatch();
+ return;
+ }
- await Client.ToggleReady();
- readyClickOperation.Dispose();
+ await Client.ToggleReady();
+
+ readyClickOperation.Dispose();
+ });
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index b6dce2c398..af2e4fc91a 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -9,6 +10,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
@@ -102,7 +106,7 @@ needs_cleanup: true
{
AddStep("Add absolute image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)";
});
}
@@ -112,8 +116,7 @@ needs_cleanup: true
{
AddStep("Add relative image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
- markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Interface/";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = "![intro](img/intro-screen.jpg)";
});
}
@@ -123,8 +126,7 @@ needs_cleanup: true
{
AddStep("Add paragraph with block image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
- markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Interface/";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image
![play menu](img/play-menu.jpg ""Main Menu in osu!"")
@@ -138,7 +140,7 @@ Line after image";
{
AddStep("Add inline image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!";
});
}
@@ -148,7 +150,7 @@ Line after image";
{
AddStep("Add Table", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = @"
| Image | Name | Effect |
| :-: | :-: | :-- |
@@ -162,15 +164,33 @@ Line after image";
});
}
+ [Test]
+ public void TestWideImageNotExceedContainer()
+ {
+ AddStep("Add image", () =>
+ {
+ markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
+ markdownContainer.Text = "![](img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
+ });
+
+ AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType().First().DelayedLoadCompleted);
+
+ AddStep("Change container width", () =>
+ {
+ markdownContainer.Width = 0.5f;
+ });
+
+ AddAssert("Image not exceed container width", () =>
+ {
+ var spriteImage = markdownContainer.ChildrenOfType().First();
+ return Precision.DefinitelyBigger(markdownContainer.DrawWidth, spriteImage.DrawWidth);
+ });
+ }
+
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,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs
new file mode 100644
index 0000000000..fa9179443d
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs
@@ -0,0 +1,91 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneColourPicker : OsuTestScene
+ {
+ private readonly Bindable colour = new Bindable(Colour4.Aquamarine);
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create pickers", () => Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = @"No OverlayColourProvider",
+ Font = OsuFont.Default.With(size: 40)
+ },
+ new OsuColourPicker
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Current = { BindTarget = colour },
+ }
+ }
+ },
+ new ColourProvidingContainer(OverlayColourScheme.Blue)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = @"With blue OverlayColourProvider",
+ Font = OsuFont.Default.With(size: 40)
+ },
+ new OsuColourPicker
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Current = { BindTarget = colour },
+ }
+ }
+ }
+ }
+ }
+ });
+
+ AddStep("set green", () => colour.Value = Colour4.LimeGreen);
+ AddStep("set white", () => colour.Value = Colour4.White);
+ AddStep("set red", () => colour.Value = Colour4.Red);
+ }
+
+ private class ColourProvidingContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider provider { get; }
+
+ public ColourProvidingContainer(OverlayColourScheme colourScheme)
+ {
+ provider = new OverlayColourProvider(colourScheme);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs
new file mode 100644
index 0000000000..5394e5d0aa
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.UserInterface;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public class OsuColourPicker : ColourPicker
+ {
+ public OsuColourPicker()
+ {
+ CornerRadius = 10;
+ Masking = true;
+ }
+
+ protected override HSVColourPicker CreateHSVColourPicker() => new OsuHSVColourPicker();
+ protected override HexColourPicker CreateHexColourPicker() => new OsuHexColourPicker();
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs
new file mode 100644
index 0000000000..06056f239b
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs
@@ -0,0 +1,129 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public class OsuHSVColourPicker : HSVColourPicker
+ {
+ private const float spacing = 10;
+ private const float corner_radius = 10;
+ private const float control_border_thickness = 3;
+
+ protected override HueSelector CreateHueSelector() => new OsuHueSelector();
+ protected override SaturationValueSelector CreateSaturationValueSelector() => new OsuSaturationValueSelector();
+
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour osuColour)
+ {
+ Background.Colour = colourProvider?.Dark5 ?? osuColour.GreySeafoamDark;
+
+ Content.Padding = new MarginPadding(spacing);
+ Content.Spacing = new Vector2(0, spacing);
+ }
+
+ private static EdgeEffectParameters createShadowParameters() => new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Offset = new Vector2(0, 1),
+ Radius = 3,
+ Colour = Colour4.Black.Opacity(0.3f)
+ };
+
+ private class OsuHueSelector : HueSelector
+ {
+ public OsuHueSelector()
+ {
+ SliderBar.CornerRadius = corner_radius;
+ SliderBar.Masking = true;
+ }
+
+ protected override Drawable CreateSliderNub() => new SliderNub(this);
+
+ private class SliderNub : CompositeDrawable
+ {
+ private readonly Bindable hue;
+ private readonly Box fill;
+
+ public SliderNub(OsuHueSelector osuHueSelector)
+ {
+ hue = osuHueSelector.Hue.GetBoundCopy();
+
+ InternalChild = new CircularContainer
+ {
+ Height = 35,
+ Width = 10,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Masking = true,
+ BorderColour = Colour4.White,
+ BorderThickness = control_border_thickness,
+ EdgeEffect = createShadowParameters(),
+ Child = fill = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ hue.BindValueChanged(h => fill.Colour = Colour4.FromHSV(h.NewValue, 1, 1), true);
+ }
+ }
+ }
+
+ private class OsuSaturationValueSelector : SaturationValueSelector
+ {
+ public OsuSaturationValueSelector()
+ {
+ SelectionArea.CornerRadius = corner_radius;
+ SelectionArea.Masking = true;
+ // purposefully use hard non-AA'd masking to avoid edge artifacts.
+ SelectionArea.MaskingSmoothness = 0;
+ }
+
+ protected override Marker CreateMarker() => new OsuMarker();
+
+ private class OsuMarker : Marker
+ {
+ private readonly Box previewBox;
+
+ public OsuMarker()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChild = new CircularContainer
+ {
+ Size = new Vector2(20),
+ Masking = true,
+ BorderColour = Colour4.White,
+ BorderThickness = control_border_thickness,
+ EdgeEffect = createShadowParameters(),
+ Child = previewBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(colour => previewBox.Colour = colour.NewValue, true);
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs
new file mode 100644
index 0000000000..331a1b67c9
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public class OsuHexColourPicker : HexColourPicker
+ {
+ public OsuHexColourPicker()
+ {
+ Padding = new MarginPadding(20);
+ Spacing = 20;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour osuColour)
+ {
+ Background.Colour = overlayColourProvider?.Dark6 ?? osuColour.GreySeafoamDarker;
+ }
+
+ protected override TextBox CreateHexCodeTextBox() => new OsuTextBox();
+ protected override ColourPreview CreateColourPreview() => new OsuColourPreview();
+
+ private class OsuColourPreview : ColourPreview
+ {
+ private readonly Box preview;
+
+ public OsuColourPreview()
+ {
+ InternalChild = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Child = preview = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(colour => preview.Colour = colour.NewValue, true);
+ }
+ }
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 14309e2296..dcd2d68b43 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -491,6 +491,10 @@ namespace osu.Game
public override Task Import(params ImportTask[] imports)
{
// encapsulate task as we don't want to begin the import process until in a ready state.
+
+ // ReSharper disable once AsyncVoidLambda
+ // TODO: This is bad because `new Task` doesn't have a Func override.
+ // Only used for android imports and a bit of a mess. Probably needs rethinking overall.
var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
waitForReady(() => this, _ => importTask.Start());
diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs
index 179762103a..1a4f6087c7 100644
--- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs
+++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
+using osu.Framework.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Wiki.Markdown
@@ -32,11 +33,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
{
Children = new Drawable[]
{
- new WikiMarkdownImage(linkInline)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- },
+ new BlockMarkdownImage(linkInline),
parentTextComponent.CreateSpriteText().With(t =>
{
t.Text = linkInline.Title;
@@ -45,5 +42,50 @@ namespace osu.Game.Overlays.Wiki.Markdown
}),
};
}
+
+ private class BlockMarkdownImage : WikiMarkdownImage
+ {
+ public BlockMarkdownImage(LinkInline linkInline)
+ : base(linkInline)
+ {
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+ }
+
+ protected override ImageContainer CreateImageContainer(string url) => new BlockImageContainer(url);
+
+ private class BlockImageContainer : ImageContainer
+ {
+ public BlockImageContainer(string url)
+ : base(url)
+ {
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+ }
+
+ protected override Sprite CreateImageSprite() => new ImageSprite();
+
+ private class ImageSprite : Sprite
+ {
+ public ImageSprite()
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (Width > Parent.DrawWidth)
+ {
+ float ratio = Height / Width;
+ Width = Parent.DrawWidth;
+ Height = ratio * Width;
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 40c3761768..6f00bb6c75 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool UserPlayable => true;
- [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to true.")] // Can be removed 20211009
+ [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
public virtual bool Ranked => false;
///
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 58f60d14cf..97854ee12f 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -768,6 +768,7 @@ namespace osu.Game.Screens.Play
return false;
HasFailed = true;
+ Score.ScoreInfo.Passed = false;
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
// could process an extra frame after the GameplayClock is stopped.
@@ -950,6 +951,10 @@ namespace osu.Game.Screens.Play
{
screenSuspension?.Expire();
+ // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
+ if (prepareScoreForDisplayTask == null)
+ Score.ScoreInfo.Passed = false;
+
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits.
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index d0ef4131dc..ef1087dd62 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -12,6 +12,16 @@ namespace osu.Game.Screens.Play
{
public class SoloPlayer : SubmittingPlayer
{
+ public SoloPlayer()
+ : this(null)
+ {
+ }
+
+ protected SoloPlayer(PlayerConfiguration configuration = null)
+ : base(configuration)
+ {
+ }
+
protected override APIRequest CreateTokenRequest()
{
if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
@@ -27,9 +37,11 @@ namespace osu.Game.Screens.Play
protected override APIRequest CreateSubmissionRequest(Score score, long token)
{
- Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null);
+ var beatmap = score.ScoreInfo.Beatmap;
- int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value;
+ Debug.Assert(beatmap.OnlineBeatmapID != null);
+
+ int beatmapId = beatmap.OnlineBeatmapID.Value;
return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
}
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index b843915a7c..7c5a06707d 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Screens.Play
[Resolved]
private IAPIProvider api { get; set; }
+ private TaskCompletionSource scoreSubmissionSource;
+
protected SubmittingPlayer(PlayerConfiguration configuration = null)
: base(configuration)
{
@@ -106,27 +108,16 @@ namespace osu.Game.Screens.Play
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
- // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
- if (token == null)
- return;
+ await submitScore(score).ConfigureAwait(false);
+ }
- var tcs = new TaskCompletionSource();
- var request = CreateSubmissionRequest(score, token.Value);
+ public override bool OnExiting(IScreen next)
+ {
+ var exiting = base.OnExiting(next);
- request.Success += s =>
- {
- score.ScoreInfo.OnlineScoreID = s.ID;
- tcs.SetResult(true);
- };
+ submitScore(Score);
- request.Failure += e =>
- {
- Logger.Error(e, "Failed to submit score");
- tcs.SetResult(false);
- };
-
- api.Queue(request);
- await tcs.Task.ConfigureAwait(false);
+ return exiting;
}
///
@@ -143,5 +134,33 @@ namespace osu.Game.Screens.Play
/// The score to be submitted.
/// The submission token.
protected abstract APIRequest CreateSubmissionRequest(Score score, long token);
+
+ private Task submitScore(Score score)
+ {
+ // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
+ if (token == null)
+ return Task.CompletedTask;
+
+ if (scoreSubmissionSource != null)
+ return scoreSubmissionSource.Task;
+
+ scoreSubmissionSource = new TaskCompletionSource();
+ var request = CreateSubmissionRequest(score, token.Value);
+
+ request.Success += s =>
+ {
+ score.ScoreInfo.OnlineScoreID = s.ID;
+ scoreSubmissionSource.SetResult(true);
+ };
+
+ request.Failure += e =>
+ {
+ Logger.Error(e, "Failed to submit score");
+ scoreSubmissionSource.SetResult(false);
+ };
+
+ api.Queue(request);
+ return scoreSubmissionSource.Task;
+ }
}
}
diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs
index 23f36ffe5b..7a35c8600d 100644
--- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs
+++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs
@@ -127,6 +127,7 @@ namespace osu.Game.Skinning.Editor
public override bool HandleFlip(Direction direction)
{
var selectionQuad = getSelectionQuad();
+ Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1);
foreach (var b in SelectedBlueprints)
{
@@ -136,10 +137,8 @@ namespace osu.Game.Skinning.Editor
updateDrawablePosition(drawableItem, flippedPosition);
- drawableItem.Scale *= new Vector2(
- direction == Direction.Horizontal ? -1 : 1,
- direction == Direction.Vertical ? -1 : 1
- );
+ drawableItem.Scale *= scaleFactor;
+ drawableItem.Rotation -= drawableItem.Rotation % 180 * 2;
}
return true;
diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs
index 09da4db952..5e5f20b307 100644
--- a/osu.Game/Tests/Visual/TestPlayer.cs
+++ b/osu.Game/Tests/Visual/TestPlayer.cs
@@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
@@ -16,7 +20,7 @@ namespace osu.Game.Tests.Visual
///
/// A player that exposes many components that would otherwise not be available, for testing purposes.
///
- public class TestPlayer : Player
+ public class TestPlayer : SoloPlayer
{
protected override bool PauseOnFocusLost { get; }
@@ -35,6 +39,10 @@ namespace osu.Game.Tests.Visual
public new HealthProcessor HealthProcessor => base.HealthProcessor;
+ public bool TokenCreationRequested { get; private set; }
+
+ public Score SubmittedScore { get; private set; }
+
public new bool PauseCooldownActive => base.PauseCooldownActive;
public readonly List Results = new List();
@@ -49,6 +57,20 @@ namespace osu.Game.Tests.Visual
PauseOnFocusLost = pauseOnFocusLost;
}
+ protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
+
+ protected override APIRequest CreateTokenRequest()
+ {
+ TokenCreationRequested = true;
+ return base.CreateTokenRequest();
+ }
+
+ protected override APIRequest CreateSubmissionRequest(Score score, long token)
+ {
+ SubmittedScore = score;
+ return base.CreateSubmissionRequest(score, token);
+ }
+
protected override void PrepareReplay()
{
// Generally, replay generation is handled by whatever is constructing the player.
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 55f6ff88d7..357aa89329 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 1a5351eef1..e339e49187 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 62751cebb1..d2c5b1223c 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -308,6 +308,7 @@
GL
GLSL
HID
+ HSV
HTML
HUD
ID