1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-17 00:03:22 +08:00

Merge branch 'master' into reuse-framework-exclusuion-function

This commit is contained in:
Dan Balasescu 2019-11-05 16:18:16 +09:00 committed by GitHub
commit 0758025b6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1281 additions and 399 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here.

View File

@ -1,7 +0,0 @@
---
name: Missing for Live
about: Features which are available in osu!stable but not yet in osu!lazer.
---
**Describe the missing feature:**
**Proposal designs of the feature:**

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -85,6 +86,93 @@ namespace osu.Game.Rulesets.Osu.Tests
checkPositions(); checkPositions();
} }
[Test]
public void TestSingleControlPointSelection()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, true);
checkControlPointSelected(1, false);
}
[Test]
public void TestSingleControlPointDeselectionViaOtherControlPoint()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, true);
}
[Test]
public void TestSingleControlPointDeselectionViaClickOutside()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
[Test]
public void TestMultipleControlPointSelection()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
checkControlPointSelected(0, true);
checkControlPointSelected(1, true);
}
[Test]
public void TestMultipleControlPointDeselectionViaOtherControlPoint()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
moveMouseToControlPoint(2);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
[Test]
public void TestMultipleControlPointDeselectionViaClickOutside()
{
moveMouseToControlPoint(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(1);
AddStep("ctrl + click", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkControlPointSelected(0, false);
checkControlPointSelected(1, false);
}
private void moveHitObject() private void moveHitObject()
{ {
AddStep("move hitobject", () => AddStep("move hitobject", () =>
@ -104,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); () => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
} }
private void moveMouseToControlPoint(int index)
{
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index];
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
private void checkControlPointSelected(int index, bool selected)
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
private class TestSliderBlueprint : SliderSelectionBlueprint private class TestSliderBlueprint : SliderSelectionBlueprint
{ {
public new SliderBodyPiece BodyPiece => base.BodyPiece; public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint; public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint; public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(DrawableSlider slider) public TestSliderBlueprint(DrawableSlider slider)
: base(slider) : base(slider)

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Lines;
@ -11,18 +12,22 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public class PathControlPointPiece : BlueprintPiece<Slider> public class PathControlPointPiece : BlueprintPiece<Slider>
{ {
public Action<int> RequestSelection;
public Action<Vector2[]> ControlPointsChanged; public Action<Vector2[]> ControlPointsChanged;
private readonly Slider slider; public readonly BindableBool IsSelected = new BindableBool();
private readonly int index; public readonly int Index;
private readonly Slider slider;
private readonly Path path; private readonly Path path;
private readonly CircularContainer marker; private readonly Container marker;
private readonly Drawable markerRing;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -30,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointPiece(Slider slider, int index) public PathControlPointPiece(Slider slider, int index)
{ {
this.slider = slider; this.slider = slider;
this.index = index; Index = index;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -42,13 +47,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
PathRadius = 1 PathRadius = 1
}, },
marker = new CircularContainer marker = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(10), AutoSizeAxes = Axes.Both,
Masking = true, Children = new[]
Child = new Box { RelativeSizeAxes = Axes.Both } {
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(14),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
} }
}; };
} }
@ -57,30 +85,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
base.Update(); base.Update();
Position = slider.StackedPosition + slider.Path.ControlPoints[index]; Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow; updateMarkerDisplay();
updateConnectingPath();
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
if (IsHovered || IsSelected.Value)
colour = Color4.White;
marker.Colour = colour;
}
/// <summary>
/// Updates the path connecting this control point to the previous one.
/// </summary>
private void updateConnectingPath()
{
path.ClearVertices(); path.ClearVertices();
if (index != slider.Path.ControlPoints.Length - 1) if (Index != slider.Path.ControlPoints.Length - 1)
{ {
path.AddVertex(Vector2.Zero); path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]); path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
} }
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
} }
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection != null)
{
RequestSelection.Invoke(Index);
return true;
}
return false;
}
protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) => true; protected override bool OnDragStart(DragStartEvent e) => true;
protected override bool OnDrag(DragEvent e) protected override bool OnDrag(DragEvent e)
{ {
var newControlPoints = slider.Path.ControlPoints.ToArray(); var newControlPoints = slider.Path.ControlPoints.ToArray();
if (index == 0) if (Index == 0)
{ {
// Special handling for the head - only the position of the slider changes // Special handling for the head - only the position of the slider changes
slider.Position += e.Delta; slider.Position += e.Delta;
@ -90,13 +154,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
newControlPoints[i] -= e.Delta; newControlPoints[i] -= e.Delta;
} }
else else
newControlPoints[index] += e.Delta; newControlPoints[Index] += e.Delta;
if (isSegmentSeparatorWithNext) if (isSegmentSeparatorWithNext)
newControlPoints[index + 1] = newControlPoints[index]; newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious) if (isSegmentSeparatorWithPrevious)
newControlPoints[index - 1] = newControlPoints[index]; newControlPoints[Index - 1] = newControlPoints[Index];
ControlPointsChanged?.Invoke(newControlPoints); ControlPointsChanged?.Invoke(newControlPoints);
@ -107,8 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious; private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index]; private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index]; private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
} }
} }

View File

@ -4,6 +4,8 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
@ -13,25 +15,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public Action<Vector2[]> ControlPointsChanged; public Action<Vector2[]> ControlPointsChanged;
internal readonly Container<PathControlPointPiece> Pieces;
private readonly Slider slider; private readonly Slider slider;
private readonly bool allowSelection;
private readonly Container<PathControlPointPiece> pieces; private InputManager inputManager;
public PathControlPointVisualiser(Slider slider) public PathControlPointVisualiser(Slider slider, bool allowSelection)
{ {
this.slider = slider; this.slider = slider;
this.allowSelection = allowSelection;
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }; RelativeSizeAxes = Axes.Both;
InternalChild = Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
while (slider.Path.ControlPoints.Length > pieces.Count) while (slider.Path.ControlPoints.Length > Pieces.Count)
pieces.Add(new PathControlPointPiece(slider, pieces.Count) { ControlPointsChanged = c => ControlPointsChanged?.Invoke(c) }); {
while (slider.Path.ControlPoints.Length < pieces.Count) var piece = new PathControlPointPiece(slider, Pieces.Count)
pieces.Remove(pieces[pieces.Count - 1]); {
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
};
if (allowSelection)
piece.RequestSelection = selectPiece;
Pieces.Add(piece);
}
while (slider.Path.ControlPoints.Length < Pieces.Count)
Pieces.Remove(Pieces[Pieces.Count - 1]);
}
protected override bool OnClick(ClickEvent e)
{
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return false;
}
private void selectPiece(int index)
{
if (inputManager.CurrentState.Keyboard.ControlPressed)
Pieces[index].IsSelected.Toggle();
else
{
foreach (var piece in Pieces)
piece.IsSelected.Value = piece.Index == index;
}
} }
} }
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(), bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(), headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(),
new PathControlPointVisualiser(HitObject) { ControlPointsChanged = _ => updateSlider() }, new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
}; };
setState(PlacementState.Initial); setState(PlacementState.Initial);

View File

@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected readonly SliderBodyPiece BodyPiece; protected readonly SliderBodyPiece BodyPiece;
protected readonly SliderCircleSelectionBlueprint HeadBlueprint; protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
protected readonly SliderCircleSelectionBlueprint TailBlueprint; protected readonly SliderCircleSelectionBlueprint TailBlueprint;
protected readonly PathControlPointVisualiser ControlPointVisualiser;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; } private HitObjectComposer composer { get; set; }
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece = new SliderBodyPiece(), BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
new PathControlPointVisualiser(sliderObject) { ControlPointsChanged = onNewControlPoints }, ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
}; };
} }
@ -49,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance; var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance); HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
UpdateHitObject();
} }
public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint; public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint;

View File

@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
PathBindable.Value = value; PathBindable.Value = value;
endPositionCache.Invalidate(); endPositionCache.Invalidate();
updateNestedPositions();
} }
} }
@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
set set
{ {
base.Position = value; base.Position = value;
endPositionCache.Invalidate(); endPositionCache.Invalidate();
if (HeadCircle != null) updateNestedPositions();
HeadCircle.Position = value;
if (TailCircle != null)
TailCircle.Position = EndPosition;
} }
} }
@ -197,6 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects
} }
} }
private void updateNestedPositions()
{
if (HeadCircle != null)
HeadCircle.Position = Position;
if (TailCircle != null)
TailCircle.Position = EndPosition;
}
private List<HitSampleInfo> getNodeSamples(int nodeIndex) => private List<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;

View File

@ -38,9 +38,10 @@ namespace osu.Game.Rulesets.Osu.UI
}); });
} }
public override void Show() protected override void PopIn()
{ {
base.Show(); base.PopIn();
GameplayCursor.ActiveCursor.Hide(); GameplayCursor.ActiveCursor.Hide();
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position); cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
clickToResumeCursor.Appear(); clickToResumeCursor.Appear();
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.UI
} }
} }
public override void Hide() protected override void PopOut()
{ {
base.PopOut();
localCursorContainer?.Expire(); localCursorContainer?.Expire();
localCursorContainer = null; localCursorContainer = null;
GameplayCursor.ActiveCursor.Show(); GameplayCursor?.ActiveCursor?.Show();
base.Hide();
} }
protected override bool OnHover(HoverEvent e) => true; protected override bool OnHover(HoverEvent e) => true;

View File

@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(true); confirmClockRunning(true);
} }
[Test]
public void TestPauseWithResumeOverlay()
{
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
pauseAndConfirm();
resume();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
pauseAndConfirm();
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
confirmPaused();
}
[Test] [Test]
public void TestResumeWithResumeOverlaySkipped() public void TestResumeWithResumeOverlaySkipped()
{ {

View File

@ -7,11 +7,10 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
using osuTK;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Screens.Ranking.Pages;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(80, 40),
}; };
}); });
} }

View File

@ -3,11 +3,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Pages; using osu.Game.Screens.Ranking.Pages;
@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(ScoreInfo),
typeof(Results), typeof(Results),
typeof(ResultsPage), typeof(ResultsPage),
typeof(ScoreResultsPage), typeof(ScoreResultsPage),
typeof(LocalLeaderboardPage) typeof(RetryButton),
typeof(ReplayDownloadButton),
typeof(LocalLeaderboardPage),
typeof(TestPlayer)
}; };
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
if (beatmapInfo != null) if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}
LoadScreen(new SoloResults(new ScoreInfo private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
{
TotalScore = 2845370,
Accuracy = 0.98,
MaxCombo = 123,
Rank = ScoreRank.A,
Date = DateTimeOffset.Now,
Statistics = new Dictionary<HitResult, int>
{ {
TotalScore = 2845370, { HitResult.Great, 50 },
Accuracy = 0.98, { HitResult.Good, 20 },
MaxCombo = 123, { HitResult.Meh, 50 },
Rank = ScoreRank.A, { HitResult.Miss, 1 }
Date = DateTimeOffset.Now, },
Statistics = new Dictionary<HitResult, int> User = new User
{
Username = "peppy",
}
});
[Test]
public void ResultsWithoutPlayer()
{
TestSoloResults screen = null;
AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
{
RelativeSizeAxes = Axes.Both
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
}
[Test]
public void ResultsWithPlayer()
{
TestSoloResults screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
private readonly Player player = new TestPlayer();
public TestResultsContainer(IScreen screen)
{
RelativeSizeAxes = Axes.Both;
InternalChild = new OsuScreenStack(screen)
{ {
{ HitResult.Great, 50 }, RelativeSizeAxes = Axes.Both,
{ HitResult.Good, 20 }, };
{ HitResult.Meh, 50 }, }
{ HitResult.Miss, 1 } }
},
User = new User private class TestSoloResults : SoloResults
{ {
Username = "peppy", public HotkeyRetryOverlay RetryOverlay;
}
})); public TestSoloResults(ScoreInfo score)
: base(score)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
}
} }
} }
} }

View File

@ -0,0 +1,102 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapRulesetSelector : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(BeatmapRulesetSelector),
typeof(BeatmapRulesetTabItem),
};
private readonly TestRulesetSelector selector;
public TestSceneBeatmapRulesetSelector()
{
Add(selector = new TestRulesetSelector());
}
[Resolved]
private RulesetStore rulesets { get; set; }
[Test]
public void TestMultipleRulesetsBeatmapSet()
{
var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2);
AddStep("load multiple rulesets beatmapset", () =>
{
selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = enabledRulesets.Select(r => new BeatmapInfo { Ruleset = r }).ToList()
};
});
var tabItems = selector.TabContainer.TabItems;
AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value));
AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value);
}
[Test]
public void TestSingleRulesetBeatmapSet()
{
var enabledRuleset = rulesets.AvailableRulesets.Last();
AddStep("load single ruleset beatmapset", () =>
{
selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = enabledRuleset
}
}
};
});
AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset));
}
[Test]
public void TestEmptyBeatmapSet()
{
AddStep("load empty beatmapset", () => selector.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>()
});
AddAssert("no ruleset selected", () => selector.SelectedTab == null);
AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
}
[Test]
public void TestNullBeatmapSet()
{
AddStep("load null beatmapset", () => selector.BeatmapSet = null);
AddAssert("no ruleset selected", () => selector.SelectedTab == null);
AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
}
private class TestRulesetSelector : BeatmapRulesetSelector
{
public new TabItem<RulesetInfo> SelectedTab => base.SelectedTab;
public new TabFillFlowContainer TabContainer => base.TabContainer;
}
}
}

View File

@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
typeof(PreviewButton), typeof(PreviewButton),
typeof(SuccessRate), typeof(SuccessRate),
typeof(BeatmapAvailability), typeof(BeatmapAvailability),
typeof(BeatmapRulesetSelector),
typeof(BeatmapRulesetTabItem),
}; };
protected override bool UseOnlineAPI => true; protected override bool UseOnlineAPI => true;
private RulesetInfo taikoRuleset;
private RulesetInfo maniaRuleset;
public TestSceneBeatmapSetOverlay() public TestSceneBeatmapSetOverlay()
{ {
Add(overlay = new TestBeatmapSetOverlay()); Add(overlay = new TestBeatmapSetOverlay());
} }
[BackgroundDependencyLoader] [Resolved]
private void load(RulesetStore rulesets) private RulesetStore rulesets { get; set; }
{
taikoRuleset = rulesets.GetRuleset(1);
maniaRuleset = rulesets.GetRuleset(3);
}
[Test] [Test]
public void TestLoading() public void TestLoading()
@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 9.99, StarDifficulty = 9.99,
Version = @"TEST", Version = @"TEST",
Length = 456000, Length = 456000,
Ruleset = maniaRuleset, Ruleset = rulesets.GetRuleset(3),
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 1, CircleSize = 1,
@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 5.67, StarDifficulty = 5.67,
Version = @"ANOTHER TEST", Version = @"ANOTHER TEST",
Length = 123000, Length = 123000,
Ruleset = taikoRuleset, Ruleset = rulesets.GetRuleset(1),
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 9, CircleSize = 9,
@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
downloadAssert(false); downloadAssert(false);
} }
[Test]
public void TestMultipleRulesets()
{
AddStep("show multiple rulesets beatmap", () =>
{
var beatmaps = new List<BeatmapInfo>();
foreach (var ruleset in rulesets.AvailableRulesets.Skip(1))
{
beatmaps.Add(new BeatmapInfo
{
Version = ruleset.Name,
Ruleset = ruleset,
BaseDifficulty = new BeatmapDifficulty(),
OnlineInfo = new BeatmapOnlineInfo(),
Metrics = new BeatmapMetrics
{
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
});
}
overlay.ShowBeatmapSet(new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"multiple rulesets beatmap",
Artist = @"none",
Author = new User
{
Username = "BanchoBot",
Id = 3,
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers(),
},
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = beatmaps
});
});
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
}
[Test] [Test]
public void TestHide() public void TestHide()
{ {
@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
private void downloadAssert(bool shown) private void downloadAssert(bool shown)
{ {
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown); AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown);
} }
private class TestBeatmapSetOverlay : BeatmapSetOverlay private class TestBeatmapSetOverlay : BeatmapSetOverlay
{ {
public bool DownloadButtonsVisible => Header.DownloadButtonsVisible; public new Header Header => base.Header;
} }
} }
} }

View File

@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); }; req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
// intentionally blocking to limit web request concurrency try
req.Perform(api); {
// intentionally blocking to limit web request concurrency
req.Perform(api);
}
catch (Exception e)
{
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
}
} }
} }
} }

View File

@ -86,16 +86,7 @@ namespace osu.Game.Database
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
}; };
request.Failure += error => request.Failure += triggerFailure;
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
};
notification.CancelRequested += () => notification.CancelRequested += () =>
{ {
@ -108,11 +99,31 @@ namespace osu.Game.Database
currentDownloads.Add(request); currentDownloads.Add(request);
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() =>
{
try
{
request.Perform(api);
}
catch (Exception error)
{
triggerFailure(error);
}
}, TaskCreationOptions.LongRunning);
DownloadBegan?.Invoke(request); DownloadBegan?.Invoke(request);
return true; return true;
void triggerFailure(Exception error)
{
DownloadFailed?.Invoke(request);
if (error is OperationCanceledException) return;
notification.State = ProgressNotificationState.Cancelled;
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
currentDownloads.Remove(request);
}
} }
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));

View File

@ -8,9 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
} }
private OsuGame game; private OsuGame game;
private ChannelManager channelManager;
private Action showNotImplementedError;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager) private void load(OsuGame game)
{ {
// will be null in tests // will be null in tests
this.game = game; this.game = game;
this.channelManager = channelManager;
showNotImplementedError = () => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
});
} }
public void AddLinks(string text, List<Link> links) public void AddLinks(string text, List<Link> links)
@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links) foreach (var link in links)
{ {
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd)); AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument); AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
previousLinkEnd = link.Index + link.Length; previousLinkEnd = link.Index + link.Length;
} }
AddText(text.Substring(previousLinkEnd)); AddText(text.Substring(previousLinkEnd));
} }
public IEnumerable<Drawable> AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
=> createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText); createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url);
public IEnumerable<Drawable> AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null) public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action); => createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
public IEnumerable<Drawable> AddLink(IEnumerable<SpriteText> text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null) public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
public void AddLink(IEnumerable<SpriteText> text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{ {
foreach (var t in text) foreach (var t in text)
AddArbitraryDrawable(t); AddArbitraryDrawable(t);
return createLink(text, null, url, linkType, linkArgument, tooltipText); createLink(text, new LinkDetails(action, linkArgument), tooltipText);
} }
public IEnumerable<Drawable> AddUserLink(User user, Action<SpriteText> creationParameters = null) public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
=> createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile"); => createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "View Profile");
private IEnumerable<Drawable> createLink(IEnumerable<Drawable> drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null) private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null)
{ {
AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList()) AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
TooltipText = tooltipText ?? (url != text ? url : string.Empty), TooltipText = tooltipText,
Action = action ?? (() => Action = () =>
{ {
switch (linkType) if (action != null)
{ action();
case LinkAction.OpenBeatmap: else
// TODO: proper query params handling game.HandleLink(link);
if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId)) },
game?.ShowBeatmap(beatmapId);
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(linkArgument, out int setId))
game?.ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
try
{
channelManager?.OpenChannel(linkArgument);
}
catch (ChannelNotFoundException)
{
Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
}
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
showNotImplementedError?.Invoke();
break;
case LinkAction.External:
game?.OpenUrlExternally(url);
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(linkArgument, out long userId))
game?.ShowUser(userId);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
}
}),
}); });
return drawables;
} }
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.

View File

@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
//since we just changed the line display text, offset any already processed links. //since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0); result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = getLinkDetails(linkText); var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument)); result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group. //adjust the offset for processing the current matches group.
@ -98,7 +98,7 @@ namespace osu.Game.Online.Chat
var linkText = m.Groups["link"].Value; var linkText = m.Groups["link"].Value;
var indexLength = linkText.Length; var indexLength = linkText.Length;
var details = getLinkDetails(linkText); var details = GetLinkDetails(linkText);
var link = new Link(linkText, index, indexLength, details.Action, details.Argument); var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
// sometimes an already-processed formatted link can reduce to a simple URL, too // sometimes an already-processed formatted link can reduce to a simple URL, too
@ -109,7 +109,7 @@ namespace osu.Game.Online.Chat
} }
} }
private static LinkDetails getLinkDetails(string url) public static LinkDetails GetLinkDetails(string url)
{ {
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':'); args[0] = args[0].TrimEnd(':');
@ -255,17 +255,17 @@ namespace osu.Game.Online.Chat
OriginalText = Text = text; OriginalText = Text = text;
} }
} }
}
public class LinkDetails public class LinkDetails
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{ {
public LinkAction Action; Action = action;
public string Argument; Argument = argument;
public LinkDetails(LinkAction action, string argument)
{
Action = action;
Argument = argument;
}
} }
} }
@ -279,6 +279,7 @@ namespace osu.Game.Online.Chat
JoinMultiplayerMatch, JoinMultiplayerMatch,
Spectate, Spectate,
OpenUserProfile, OpenUserProfile,
Custom
} }
public class Link : IComparable<Link> public class Link : IComparable<Link>

View File

@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Humanizer; using Humanizer;
using osu.Game.Online.API;
namespace osu.Game.Online.Leaderboards namespace osu.Game.Online.Leaderboards
{ {
@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly int rank; private readonly int rank;
private readonly bool allowHighlight;
private Box background; private Box background;
private Container content; private Container content;
@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
private List<ScoreComponentLabel> statisticsLabels; private List<ScoreComponentLabel> statisticsLabels;
public LeaderboardScore(ScoreInfo score, int rank) public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
{ {
this.score = score; this.score = score;
this.rank = rank; this.rank = rank;
this.allowHighlight = allowHighlight;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = HEIGHT; Height = HEIGHT;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(IAPIProvider api, OsuColour colour)
{ {
var user = score.User; var user = score.User;
@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha, Alpha = background_alpha,
}, },
}, },

View File

@ -215,31 +215,102 @@ namespace osu.Game
private ExternalLinkOpener externalLinkOpener; private ExternalLinkOpener externalLinkOpener;
public void OpenUrlExternally(string url) /// <summary>
/// Handle an arbitrary URL. Displays via in-game overlays where possible.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="url">The URL to load.</param>
public void HandleLink(string url) => HandleLink(MessageFormatter.GetLinkDetails(url));
/// <summary>
/// Handle a specific <see cref="LinkDetails"/>.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="link">The link to load.</param>
public void HandleLink(LinkDetails link) => Schedule(() =>
{
switch (link.Action)
{
case LinkAction.OpenBeatmap:
// TODO: proper query params handling
if (link.Argument != null && int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
ShowBeatmap(beatmapId);
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(link.Argument, out int setId))
ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
ShowChannel(link.Argument);
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
}));
break;
case LinkAction.External:
OpenUrlExternally(link.Argument);
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(link.Argument, out long userId))
ShowUser(userId);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
}
});
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{ {
if (url.StartsWith("/")) if (url.StartsWith("/"))
url = $"{API.Endpoint}{url}"; url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url); externalLinkOpener.OpenUrlExternally(url);
} });
/// <summary>
/// Open a specific channel in chat.
/// </summary>
/// <param name="channel">The channel to display.</param>
public void ShowChannel(string channel) => waitForReady(() => channelManager, _ =>
{
try
{
channelManager.OpenChannel(channel);
}
catch (ChannelNotFoundException)
{
Logger.Log($"The requested channel \"{channel}\" does not exist");
}
});
/// <summary> /// <summary>
/// Show a beatmap set as an overlay. /// Show a beatmap set as an overlay.
/// </summary> /// </summary>
/// <param name="setId">The set to display.</param> /// <param name="setId">The set to display.</param>
public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId); public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId));
/// <summary> /// <summary>
/// Show a user's profile as an overlay. /// Show a user's profile as an overlay.
/// </summary> /// </summary>
/// <param name="userId">The user to display.</param> /// <param name="userId">The user to display.</param>
public void ShowUser(long userId) => userProfile.ShowUser(userId); public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
/// <summary> /// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap. /// Show a beatmap's set as an overlay, displaying the given beatmap.
/// </summary> /// </summary>
/// <param name="beatmapId">The beatmap to show.</param> /// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId); public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
/// <summary> /// <summary>
/// Present a beatmap at song select immediately. /// Present a beatmap at song select immediately.
@ -397,6 +468,23 @@ namespace osu.Game
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName)); performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
} }
/// <summary>
/// Wait for the game (and target component) to become loaded and then run an action.
/// </summary>
/// <param name="retrieveInstance">A function to retrieve a (potentially not-yet-constructed) target instance.</param>
/// <param name="action">The action to perform on the instance when load is confirmed.</param>
/// <typeparam name="T">The type of the target instance.</typeparam>
private void waitForReady<T>(Func<T> retrieveInstance, Action<T> action)
where T : Drawable
{
var instance = retrieveInstance();
if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true)
Schedule(() => waitForReady(retrieveInstance, action));
else
action(instance);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_icon_padding = 7; private const float tile_icon_padding = 7;
private const float tile_spacing = 2; private const float tile_spacing = 2;
private readonly DifficultiesContainer difficulties;
private readonly OsuSpriteText version, starRating; private readonly OsuSpriteText version, starRating;
private readonly Statistic plays, favourites; private readonly Statistic plays, favourites;
public readonly DifficultiesContainer Difficulties;
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>(); public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
private BeatmapSetInfo beatmapSet; private BeatmapSetInfo beatmapSet;
@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return; if (value == beatmapSet) return;
beatmapSet = value; beatmapSet = value;
updateDisplay(); updateDisplay();
} }
} }
private void updateDisplay()
{
difficulties.Clear();
if (BeatmapSet != null)
{
difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
starRating.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRating.FadeOut(100);
Beatmap.Value = BeatmapSet?.Beatmaps.FirstOrDefault();
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
updateDifficultyButtons();
}
public BeatmapPicker() public BeatmapPicker()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
difficulties = new DifficultiesContainer Difficulties = new DifficultiesContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
}; };
} }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
base.LoadComplete(); base.LoadComplete();
ruleset.ValueChanged += r => updateDisplay();
// done here so everything can bind in intialization and get the first trigger // done here so everything can bind in intialization and get the first trigger
Beatmap.TriggerChange(); Beatmap.TriggerChange();
} }
private void updateDisplay()
{
Difficulties.Clear();
if (BeatmapSet != null)
{
Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Where(b => b.Ruleset.Equals(ruleset.Value)).OrderBy(b => b.StarDifficulty).Select(b => new DifficultySelectorButton(b)
{
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
starRating.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRating.FadeOut(100);
Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap;
plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
updateDifficultyButtons();
}
private void showBeatmap(BeatmapInfo beatmap) private void showBeatmap(BeatmapInfo beatmap)
{ {
version.Text = beatmap?.Version; version.Text = beatmap?.Version;
@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDifficultyButtons() private void updateDifficultyButtons()
{ {
difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
} }
private class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton> public class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
{ {
public Action OnLostHover; public Action OnLostHover;
@ -183,7 +189,7 @@ namespace osu.Game.Overlays.BeatmapSet
} }
} }
private class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState> public class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
{ {
private const float transition_duration = 100; private const float transition_duration = 100;
private const float size = 52; private const float size = 52;
@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
} }
} }
private enum DifficultySelectorState public enum DifficultySelectorState
{ {
Selected, Selected,
NotSelected, NotSelected,

View File

@ -0,0 +1,48 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osuTK;
using System.Linq;
namespace osu.Game.Overlays.BeatmapSet
{
public class BeatmapRulesetSelector : RulesetSelector
{
private readonly Bindable<BeatmapSetInfo> beatmapSet = new Bindable<BeatmapSetInfo>();
public BeatmapSetInfo BeatmapSet
{
get => beatmapSet.Value;
set
{
// propagate value to tab items first to enable only available rulesets.
beatmapSet.Value = value;
SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value));
}
}
public BeatmapRulesetSelector()
{
AutoSizeAxes = Axes.Both;
}
protected override TabItem<RulesetInfo> CreateTabItem(RulesetInfo value) => new BeatmapRulesetTabItem(value)
{
BeatmapSet = { BindTarget = beatmapSet }
};
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
};
}
}

View File

@ -0,0 +1,145 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
using System.Linq;
namespace osu.Game.Overlays.BeatmapSet
{
public class BeatmapRulesetTabItem : TabItem<RulesetInfo>
{
private readonly OsuSpriteText name, count;
private readonly Box bar;
public readonly Bindable<BeatmapSetInfo> BeatmapSet = new Bindable<BeatmapSetInfo>();
public override bool PropagatePositionalInputSubTree => Enabled.Value && !Active.Value && base.PropagatePositionalInputSubTree;
public BeatmapRulesetTabItem(RulesetInfo value)
: base(value)
{
AutoSizeAxes = Axes.Both;
FillFlowContainer nameContainer;
Children = new Drawable[]
{
nameContainer = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Bottom = 7.5f },
Spacing = new Vector2(2.5f),
Children = new Drawable[]
{
name = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = value.Name,
Font = OsuFont.Default.With(size: 18),
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4f,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
},
count = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 5f },
Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
}
}
}
}
},
bar = new Box
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
},
new HoverClickSounds(),
};
BeatmapSet.BindValueChanged(setInfo =>
{
var beatmapsCount = setInfo.NewValue?.Beatmaps.Count(b => b.Ruleset.Equals(Value)) ?? 0;
count.Text = beatmapsCount.ToString();
count.Alpha = beatmapsCount > 0 ? 1f : 0f;
Enabled.Value = beatmapsCount > 0;
}, true);
Enabled.BindValueChanged(v => nameContainer.Alpha = v.NewValue ? 1f : 0.5f, true);
}
[Resolved]
private OsuColour colour { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
count.Colour = colour.Gray9;
bar.Colour = colour.Blue;
updateState();
}
private void updateState()
{
var isHoveredOrActive = IsHovered || Active.Value;
bar.ResizeHeightTo(isHoveredOrActive ? 4 : 0, 200, Easing.OutQuint);
name.Colour = isHoveredOrActive ? colour.GrayE : colour.GrayC;
name.Font = name.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
}
#region Hovering and activation logic
protected override void OnActivated() => updateState();
protected override void OnDeactivated() => updateState();
protected override bool OnHover(HoverEvent e)
{
updateState();
return false;
}
protected override void OnHoverLost(HoverLostEvent e) => updateState();
#endregion
}
}

View File

@ -3,6 +3,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Overlays.Direct; using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
public readonly BeatmapRulesetSelector RulesetSelector;
public readonly BeatmapPicker Picker; public readonly BeatmapPicker Picker;
private readonly FavouriteButton favouriteButton; private readonly FavouriteButton favouriteButton;
@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly LoadingAnimation loading; private readonly LoadingAnimation loading;
[Cached(typeof(IBindable<RulesetInfo>))]
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
public Header() public Header()
{ {
ExternalLinkButton externalLink; ExternalLinkButton externalLink;
@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = tabs_height, Height = tabs_height,
Children = new[] Children = new Drawable[]
{ {
tabsBg = new Box tabsBg = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
RulesetSelector = new BeatmapRulesetSelector
{
Current = ruleset,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
}, },
}, },
new Container new Container
@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.BindValueChanged(setInfo => BeatmapSet.BindValueChanged(setInfo =>
{ {
Picker.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.BeatmapSet = setInfo.NewValue; cover.BeatmapSet = setInfo.NewValue;
if (setInfo.NewValue == null) if (setInfo.NewValue == null)

View File

@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
t.Font = fontLarge; t.Font = fontLarge;
t.Colour = entryColour; t.Colour = entryColour;
}); });
title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External, title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
creationParameters: t => creationParameters: t =>
{ {
t.Font = fontLarge; t.Font = fontLarge;
@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour; t.Colour = entryColour;
}); });
else if (entry.GithubUser.GithubUrl != null) else if (entry.GithubUser.GithubUrl != null)
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t => title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
{ {
t.Font = fontMedium; t.Font = fontMedium;
t.Colour = entryColour; t.Colour = entryColour;

View File

@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
req.Failure += _ => complete = true; req.Failure += _ => complete = true;
// This is done on a separate thread to support cancellation below // This is done on a separate thread to support cancellation below
Task.Run(() => req.Perform(api)); Task.Run(() =>
{
try
{
req.Perform(api);
}
catch
{
complete = true;
}
});
while (!complete) while (!complete)
{ {

View File

@ -170,6 +170,7 @@ namespace osu.Game.Overlays
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
var req = new GetChangelogRequest(); var req = new GetChangelogRequest();
req.Success += res => Schedule(() => req.Success += res => Schedule(() =>
{ {
// remap streams to builds to ensure model equality // remap streams to builds to ensure model equality
@ -183,8 +184,22 @@ namespace osu.Game.Overlays
tcs.SetResult(true); tcs.SetResult(true);
}); });
req.Failure += _ => initialFetchTask = null;
req.Perform(API); req.Failure += _ =>
{
initialFetchTask = null;
tcs.SetResult(false);
};
try
{
req.Perform(API);
}
catch
{
initialFetchTask = null;
tcs.SetResult(false);
}
await tcs.Task; await tcs.Task;
}); });

View File

@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap); EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject; EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject; EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += updateHitObject; EditorBeatmap.StartTimeChanged += UpdateHitObject;
var dependencies = new DependencyContainer(parent); var dependencies = new DependencyContainer(parent);
dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap); dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap);
@ -225,11 +225,7 @@ namespace osu.Game.Rulesets.Edit
private ScheduledDelegate scheduledUpdate; private ScheduledDelegate scheduledUpdate;
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject); public override void UpdateHitObject(HitObject hitObject)
private void removeHitObject(HitObject hitObject) => updateHitObject(null);
private void updateHitObject([CanBeNull] HitObject hitObject)
{ {
scheduledUpdate?.Cancel(); scheduledUpdate?.Cancel();
scheduledUpdate = Schedule(() => scheduledUpdate = Schedule(() =>
@ -240,6 +236,10 @@ namespace osu.Game.Rulesets.Edit
}); });
} }
private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject);
private void removeHitObject(HitObject hitObject) => UpdateHitObject(null);
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
@ -351,11 +351,22 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull] [CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null; protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable<HitObject> selectedHitObjects) => null;
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public abstract void UpdateHitObject([CanBeNull] HitObject hitObject);
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float GetBeatSnapDistanceAt(double referenceTime);
public abstract float DurationToDistance(double referenceTime, double duration); public abstract float DurationToDistance(double referenceTime, double duration);
public abstract double DistanceToDuration(double referenceTime, float distance); public abstract double DistanceToDuration(double referenceTime, float distance);
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
} }
} }

View File

@ -3,10 +3,12 @@
using System; using System;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
@ -36,6 +38,9 @@ namespace osu.Game.Rulesets.Edit
public override bool HandlePositionalInput => ShouldBeAlive; public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
protected SelectionBlueprint(DrawableHitObject drawableObject) protected SelectionBlueprint(DrawableHitObject drawableObject)
{ {
DrawableObject = drawableObject; DrawableObject = drawableObject;
@ -89,6 +94,11 @@ namespace osu.Game.Rulesets.Edit
public bool IsSelected => State == SelectionState.Selected; public bool IsSelected => State == SelectionState.Selected;
/// <summary>
/// Updates the <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
protected void UpdateHitObject() => composer?.UpdateHitObject(DrawableObject.HitObject);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
/// <summary> /// <summary>

View File

@ -239,6 +239,12 @@ namespace osu.Game.Rulesets.UI
continueResume(); continueResume();
} }
public override void CancelResume()
{
// called if the user pauses while the resume overlay is open
ResumeOverlay?.Hide();
}
/// <summary> /// <summary>
/// Creates and adds the visual representation of a <see cref="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>. /// Creates and adds the visual representation of a <see cref="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>.
/// </summary> /// </summary>
@ -453,6 +459,11 @@ namespace osu.Game.Rulesets.UI
/// <param name="continueResume">The action to run when resuming is to be completed.</param> /// <param name="continueResume">The action to run when resuming is to be completed.</param>
public abstract void RequestResume(Action continueResume); public abstract void RequestResume(Action continueResume);
/// <summary>
/// Invoked when the user requests to pause while the resume overlay is active.
/// </summary>
public abstract void CancelResume();
/// <summary> /// <summary>
/// Create a <see cref="ScoreProcessor"/> for the associated ruleset and link with this /// Create a <see cref="ScoreProcessor"/> for the associated ruleset and link with this
/// <see cref="DrawableRuleset"/>. /// <see cref="DrawableRuleset"/>.

View File

@ -13,12 +13,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
public class ScrollingHitObjectContainer : HitObjectContainer public class ScrollingHitObjectContainer : HitObjectContainer
{ {
/// <summary>
/// A multiplier applied to the length of the scrolling area to determine a safe default lifetime end for hitobjects.
/// This is only used to limit the lifetime end within reason, as proper lifetime management should be implemented on hitobjects themselves.
/// </summary>
private const float safe_lifetime_end_multiplier = 2;
private readonly IBindable<double> timeRange = new BindableDouble(); private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
@ -123,28 +117,22 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (cached.IsValid) if (cached.IsValid)
return; return;
double endTime = hitObject.HitObject.StartTime;
if (hitObject.HitObject is IHasEndTime e) if (hitObject.HitObject is IHasEndTime e)
{ {
endTime = e.EndTime;
switch (direction.Value) switch (direction.Value)
{ {
case ScrollingDirection.Up: case ScrollingDirection.Up:
case ScrollingDirection.Down: case ScrollingDirection.Down:
hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength); hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
break; break;
case ScrollingDirection.Left: case ScrollingDirection.Left:
case ScrollingDirection.Right: case ScrollingDirection.Right:
hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength); hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
break; break;
} }
} }
hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
foreach (var obj in hitObject.NestedHitObjects) foreach (var obj in hitObject.NestedHitObjects)
{ {
computeInitialStateRecursive(obj); computeInitialStateRecursive(obj);

View File

@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.Components
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)), Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
Font = OsuFont.GetFont(size: TextSize), Font = OsuFont.GetFont(size: TextSize),
} }
}, null, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap"); }, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
} }
} }
} }

View File

@ -167,14 +167,17 @@ namespace osu.Game.Screens.Multi
public void APIStateChanged(IAPIProvider api, APIState state) public void APIStateChanged(IAPIProvider api, APIState state)
{ {
if (state != APIState.Online) if (state != APIState.Online)
forcefullyExit(); Schedule(forcefullyExit);
} }
private void forcefullyExit() private void forcefullyExit()
{ {
// This is temporary since we don't currently have a way to force screens to be exited // This is temporary since we don't currently have a way to force screens to be exited
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
this.Exit(); {
while (this.IsCurrentScreen())
this.Exit();
}
else else
{ {
this.MakeCurrent(); this.MakeCurrent();
@ -212,6 +215,8 @@ namespace osu.Game.Screens.Multi
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
roomManager.PartRoom();
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
{ {
screenStack.Exit(); screenStack.Exit();

View File

@ -87,9 +87,8 @@ namespace osu.Game.Screens.Multi
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{ {
currentJoinRoomRequest?.Cancel(); currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = null;
currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value); currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value);
currentJoinRoomRequest.Success += () => currentJoinRoomRequest.Success += () =>
{ {
joinedRoom = room; joinedRoom = room;
@ -98,7 +97,8 @@ namespace osu.Game.Screens.Multi
currentJoinRoomRequest.Failure += exception => currentJoinRoomRequest.Failure += exception =>
{ {
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); if (!(exception is OperationCanceledException))
Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
onError?.Invoke(exception.ToString()); onError?.Invoke(exception.ToString());
}; };
@ -107,6 +107,8 @@ namespace osu.Game.Screens.Multi
public void PartRoom() public void PartRoom()
{ {
currentJoinRoomRequest?.Cancel();
if (joinedRoom == null) if (joinedRoom == null)
return; return;

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
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;
@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track). /// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
/// </summary> /// </summary>
private readonly IAdjustableClock sourceClock; private IAdjustableClock sourceClock;
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool();
@ -153,6 +154,18 @@ namespace osu.Game.Screens.Play
IsPaused.Value = true; IsPaused.Value = true;
} }
/// <summary>
/// Changes the backing clock to avoid using the originally provided beatmap's track.
/// </summary>
public void StopUsingBeatmapClock()
{
if (sourceClock != beatmap.Track)
return;
sourceClock = new TrackVirtual(beatmap.Track.Length);
adjustableClock.ChangeSource(sourceClock);
}
public void ResetLocalAdjustments() public void ResetLocalAdjustments()
{ {
// In the case of replays, we may have changed the playback rate. // In the case of replays, we may have changed the playback rate.

View File

@ -30,6 +30,7 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
[Cached]
public class Player : ScreenWithBeatmapBackground public class Player : ScreenWithBeatmapBackground
{ {
public override bool AllowBackButton => false; // handled by HoldForMenuButton public override bool AllowBackButton => false; // handled by HoldForMenuButton
@ -311,14 +312,19 @@ namespace osu.Game.Screens.Play
this.Exit(); this.Exit();
} }
/// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
/// </summary>
public void Restart() public void Restart()
{ {
if (!this.IsCurrentScreen()) return;
sampleRestart?.Play(); sampleRestart?.Play();
RestartRequested?.Invoke(); RestartRequested?.Invoke();
performImmediateExit();
if (this.IsCurrentScreen())
performImmediateExit();
else
this.MakeCurrent();
} }
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
@ -443,7 +449,12 @@ namespace osu.Game.Screens.Play
{ {
if (!canPause) return; if (!canPause) return;
IsResuming = false; if (IsResuming)
{
DrawableRuleset.CancelResume();
IsResuming = false;
}
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();
PauseOverlay.Show(); PauseOverlay.Show();
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
@ -527,6 +538,10 @@ namespace osu.Game.Screens.Play
GameplayClockContainer.ResetLocalAdjustments(); GameplayClockContainer.ResetLocalAdjustments();
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
GameplayClockContainer.StopUsingBeatmapClock();
fadeOut(); fadeOut();
return base.OnExiting(next); return base.OnExiting(next);
} }

View File

@ -4,12 +4,13 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Scoring;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Ranking.Pages
{ {
public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager> public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager>
{ {
@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
public ReplayDownloadButton(ScoreInfo score) public ReplayDownloadButton(ScoreInfo score)
: base(score) : base(score)
{ {
Size = new Vector2(50, 30);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -0,0 +1,54 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Screens.Ranking.Pages
{
public class RetryButton : OsuAnimatedButton
{
private readonly Box background;
[Resolved(canBeNull: true)]
private Player player { get; set; }
public RetryButton()
{
Size = new Vector2(50, 30);
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(13),
Icon = FontAwesome.Solid.ArrowCircleLeft,
},
};
TooltipText = "Retry";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Green;
if (player != null)
Action = () => player.Restart();
}
}
}

View File

@ -169,12 +169,19 @@ namespace osu.Game.Screens.Ranking.Pages
}, },
}, },
}, },
new ReplayDownloadButton(score) new FillFlowContainer
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 10 }, Margin = new MarginPadding { Bottom = 10 },
Size = new Vector2(50, 30), Spacing = new Vector2(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new ReplayDownloadButton(score),
new RetryButton()
}
}, },
}; };

View File

@ -19,6 +19,7 @@ using osu.Game.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
@ -34,6 +35,9 @@ namespace osu.Game.Screens.Ranking
private ResultModeTabControl modeChangeButtons; private ResultModeTabControl modeChangeButtons;
[Resolved(canBeNull: true)]
private Player player { get; set; }
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
protected readonly ScoreInfo Score; protected readonly ScoreInfo Score;
@ -100,10 +104,7 @@ namespace osu.Game.Screens.Ranking
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
allCircles.ForEach(c => allCircles.ForEach(c => c.ScaleTo(0, transition_time, Easing.OutSine));
{
c.ScaleTo(0, transition_time, Easing.OutSine);
});
Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint); Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint);
@ -115,147 +116,157 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
InternalChildren = new Drawable[] InternalChild = new AspectContainer
{ {
new AspectContainer RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = overscan,
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Y, circleOuterBackground = new CircularContainer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = overscan,
Children = new Drawable[]
{ {
circleOuterBackground = new CircularContainer RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new Box
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{ {
new Box Alpha = 0.2f,
{ RelativeSizeAxes = Axes.Both,
Alpha = 0.2f, Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
}
} }
}, }
circleOuter = new CircularContainer },
circleOuter = new CircularContainer
{
Size = new Vector2(circle_outer_scale),
EdgeEffect = new EdgeEffectParameters
{ {
Size = new Vector2(circle_outer_scale), Colour = Color4.Black.Opacity(0.4f),
EdgeEffect = new EdgeEffectParameters Type = EdgeEffectType.Shadow,
Radius = 15,
},
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{
new Box
{ {
Colour = Color4.Black.Opacity(0.4f), RelativeSizeAxes = Axes.Both,
Type = EdgeEffectType.Shadow, Colour = Color4.White,
Radius = 15,
}, },
RelativeSizeAxes = Axes.Both, backgroundParallax = new ParallaxContainer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{ {
new Box RelativeSizeAxes = Axes.Both,
ParallaxAmount = 0.01f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new Sprite
Colour = Color4.White,
},
backgroundParallax = new ParallaxContainer
{
RelativeSizeAxes = Axes.Both,
ParallaxAmount = 0.01f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{ {
new Sprite RelativeSizeAxes = Axes.Both,
{ Alpha = 0.2f,
RelativeSizeAxes = Axes.Both, Texture = Beatmap.Value.Background,
Alpha = 0.2f, Anchor = Anchor.Centre,
Texture = Beatmap.Value.Background, Origin = Anchor.Centre,
Anchor = Anchor.Centre, FillMode = FillMode.Fill
Origin = Anchor.Centre,
FillMode = FillMode.Fill
}
} }
}, }
modeChangeButtons = new ResultModeTabControl
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = 50,
Margin = new MarginPadding { Bottom = 110 },
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomCentre,
Text = $"{Score.MaxCombo}x",
RelativePositionAxes = Axes.X,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
X = 0.1f,
Colour = colours.BlueDarker,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre,
Text = "max combo",
Font = OsuFont.GetFont(size: 20),
RelativePositionAxes = Axes.X,
X = 0.1f,
Colour = colours.Gray6,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomCentre,
Text = $"{Score.Accuracy:P2}",
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
RelativePositionAxes = Axes.X,
X = 0.9f,
Colour = colours.BlueDarker,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre,
Text = "accuracy",
Font = OsuFont.GetFont(size: 20),
RelativePositionAxes = Axes.X,
X = 0.9f,
Colour = colours.Gray6,
},
}
},
circleInner = new CircularContainer
{
Size = new Vector2(0.6f),
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.4f),
Type = EdgeEffectType.Shadow,
Radius = 15,
}, },
RelativeSizeAxes = Axes.Both, modeChangeButtons = new ResultModeTabControl
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{ {
new Box Anchor = Anchor.BottomCentre,
{ Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
Colour = Color4.White, Height = 50,
}, Margin = new MarginPadding { Bottom = 110 },
} },
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomCentre,
Text = $"{Score.MaxCombo}x",
RelativePositionAxes = Axes.X,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
X = 0.1f,
Colour = colours.BlueDarker,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre,
Text = "max combo",
Font = OsuFont.GetFont(size: 20),
RelativePositionAxes = Axes.X,
X = 0.1f,
Colour = colours.Gray6,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomCentre,
Text = $"{Score.Accuracy:P2}",
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
RelativePositionAxes = Axes.X,
X = 0.9f,
Colour = colours.BlueDarker,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre,
Text = "accuracy",
Font = OsuFont.GetFont(size: 20),
RelativePositionAxes = Axes.X,
X = 0.9f,
Colour = colours.Gray6,
},
}
},
circleInner = new CircularContainer
{
Size = new Vector2(0.6f),
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.4f),
Type = EdgeEffectType.Shadow,
Radius = 15,
},
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
},
} }
} }
} }
}; };
if (player != null)
{
AddInternal(new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
player?.Restart();
},
});
}
var pages = CreateResultPages(); var pages = CreateResultPages();
foreach (var p in pages) foreach (var p in pages)

View File

@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return req; return req;
} }
protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index) protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope)
{ {
Action = () => ScoreSelected?.Invoke(model) Action = () => ScoreSelected?.Invoke(model)
}; };

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Leaderboards
if (newScore == null) if (newScore == null)
return; return;
LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position) LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position, false)
{ {
Action = () => ScoreSelected?.Invoke(newScore.Score) Action = () => ScoreSelected?.Invoke(newScore.Score)
}, drawableScore => }, drawableScore =>

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
public abstract class SelectionBlueprintTestScene : OsuTestScene public abstract class SelectionBlueprintTestScene : ManualInputManagerTestScene
{ {
protected override Container<Drawable> Content => content ?? base.Content; protected override Container<Drawable> Content => content ?? base.Content;
private readonly Container content; private readonly Container content;

View File

@ -4,7 +4,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Foundation; using Foundation;
using osu.Framework.iOS; using osu.Framework.iOS;
using osu.Game; using osu.Framework.Threading;
using UIKit; using UIKit;
namespace osu.iOS namespace osu.iOS
@ -16,9 +16,12 @@ namespace osu.iOS
protected override Framework.Game CreateGame() => game = new OsuGameIOS(); protected override Framework.Game CreateGame() => game = new OsuGameIOS();
public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation) public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{ {
Task.Run(() => game.Import(url.Path)); if (url.IsFileUrl)
Task.Run(() => game.Import(url.Path));
else
Task.Run(() => game.HandleLink(url.AbsoluteString));
return true; return true;
} }
} }

View File

@ -14,6 +14,8 @@
<string>0.1.0</string> <string>0.1.0</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>10.0</string> <string>10.0</string>
<key>UIDeviceFamily</key> <key>UIDeviceFamily</key>
@ -32,9 +34,9 @@
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>We don't really use the camera.</string> <string>We don&apos;t really use the camera.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>We don't really use the microphone.</string> <string>We don&apos;t really use the microphone.</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@ -109,5 +111,17 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>osu</string>
<string>osump</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
</dict> </dict>
</plist> </plist>