mirror of
https://github.com/ppy/osu.git
synced 2025-01-17 00:12:55 +08:00
Merge branch 'master' into reuse-framework-exclusuion-function
This commit is contained in:
commit
0758025b6b
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
@ -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:**
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
@ -85,6 +86,93 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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()
|
||||
{
|
||||
AddStep("move hitobject", () =>
|
||||
@ -104,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
() => 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
|
||||
{
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
|
||||
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
|
||||
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(DrawableSlider slider)
|
||||
: base(slider)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
@ -11,18 +12,22 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
public class PathControlPointPiece : BlueprintPiece<Slider>
|
||||
{
|
||||
public Action<int> RequestSelection;
|
||||
public Action<Vector2[]> ControlPointsChanged;
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly int index;
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly int Index;
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly Path path;
|
||||
private readonly CircularContainer marker;
|
||||
private readonly Container marker;
|
||||
private readonly Drawable markerRing;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
@ -30,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public PathControlPointPiece(Slider slider, int index)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.index = index;
|
||||
Index = index;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -42,13 +47,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
Anchor = Anchor.Centre,
|
||||
PathRadius = 1
|
||||
},
|
||||
marker = new CircularContainer
|
||||
marker = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(10),
|
||||
Masking = true,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
if (index != slider.Path.ControlPoints.Length - 1)
|
||||
if (Index != slider.Path.ControlPoints.Length - 1)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// The connecting path is excluded from positional input
|
||||
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 OnDrag(DragEvent e)
|
||||
{
|
||||
var newControlPoints = slider.Path.ControlPoints.ToArray();
|
||||
|
||||
if (index == 0)
|
||||
if (Index == 0)
|
||||
{
|
||||
// Special handling for the head - only the position of the slider changes
|
||||
slider.Position += e.Delta;
|
||||
@ -90,13 +154,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
newControlPoints[i] -= e.Delta;
|
||||
}
|
||||
else
|
||||
newControlPoints[index] += e.Delta;
|
||||
newControlPoints[Index] += e.Delta;
|
||||
|
||||
if (isSegmentSeparatorWithNext)
|
||||
newControlPoints[index + 1] = newControlPoints[index];
|
||||
newControlPoints[Index + 1] = newControlPoints[Index];
|
||||
|
||||
if (isSegmentSeparatorWithPrevious)
|
||||
newControlPoints[index - 1] = newControlPoints[index];
|
||||
newControlPoints[Index - 1] = newControlPoints[Index];
|
||||
|
||||
ControlPointsChanged?.Invoke(newControlPoints);
|
||||
|
||||
@ -107,8 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@ -13,25 +15,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
public Action<Vector2[]> ControlPointsChanged;
|
||||
|
||||
internal readonly Container<PathControlPointPiece> Pieces;
|
||||
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.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()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
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)
|
||||
pieces.Remove(pieces[pieces.Count - 1]);
|
||||
while (slider.Path.ControlPoints.Length > Pieces.Count)
|
||||
{
|
||||
var piece = new PathControlPointPiece(slider, Pieces.Count)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bodyPiece = new SliderBodyPiece(),
|
||||
headCirclePiece = new HitCirclePiece(),
|
||||
tailCirclePiece = new HitCirclePiece(),
|
||||
new PathControlPointVisualiser(HitObject) { ControlPointsChanged = _ => updateSlider() },
|
||||
new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
|
||||
};
|
||||
|
||||
setState(PlacementState.Initial);
|
||||
|
@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected readonly SliderBodyPiece BodyPiece;
|
||||
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
|
||||
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
|
||||
protected readonly PathControlPointVisualiser ControlPointVisualiser;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
BodyPiece = new SliderBodyPiece(),
|
||||
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
|
||||
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;
|
||||
|
||||
HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
|
||||
|
||||
UpdateHitObject();
|
||||
}
|
||||
|
||||
public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint;
|
||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
PathBindable.Value = value;
|
||||
endPositionCache.Invalidate();
|
||||
|
||||
updateNestedPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
base.Position = value;
|
||||
|
||||
endPositionCache.Invalidate();
|
||||
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.Position = value;
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
updateNestedPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) =>
|
||||
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
|
||||
|
||||
|
@ -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();
|
||||
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
|
||||
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 = null;
|
||||
GameplayCursor.ActiveCursor.Show();
|
||||
|
||||
base.Hide();
|
||||
GameplayCursor?.ActiveCursor?.Show();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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]
|
||||
public void TestResumeWithResumeOverlaySkipped()
|
||||
{
|
||||
|
@ -7,11 +7,10 @@ using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Screens.Ranking.Pages;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(80, 40),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Ranking.Pages;
|
||||
@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(ScoreInfo),
|
||||
typeof(Results),
|
||||
typeof(ResultsPage),
|
||||
typeof(ScoreResultsPage),
|
||||
typeof(LocalLeaderboardPage)
|
||||
typeof(RetryButton),
|
||||
typeof(ReplayDownloadButton),
|
||||
typeof(LocalLeaderboardPage),
|
||||
typeof(TestPlayer)
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
|
||||
if (beatmapInfo != null)
|
||||
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,
|
||||
Accuracy = 0.98,
|
||||
MaxCombo = 123,
|
||||
Rank = ScoreRank.A,
|
||||
Date = DateTimeOffset.Now,
|
||||
Statistics = new Dictionary<HitResult, int>
|
||||
{ HitResult.Great, 50 },
|
||||
{ HitResult.Good, 20 },
|
||||
{ HitResult.Meh, 50 },
|
||||
{ HitResult.Miss, 1 }
|
||||
},
|
||||
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 },
|
||||
{ HitResult.Good, 20 },
|
||||
{ HitResult.Meh, 50 },
|
||||
{ HitResult.Miss, 1 }
|
||||
},
|
||||
User = new User
|
||||
{
|
||||
Username = "peppy",
|
||||
}
|
||||
}));
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSoloResults : SoloResults
|
||||
{
|
||||
public HotkeyRetryOverlay RetryOverlay;
|
||||
|
||||
public TestSoloResults(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
Normal file
102
osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
typeof(PreviewButton),
|
||||
typeof(SuccessRate),
|
||||
typeof(BeatmapAvailability),
|
||||
typeof(BeatmapRulesetSelector),
|
||||
typeof(BeatmapRulesetTabItem),
|
||||
};
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
private RulesetInfo taikoRuleset;
|
||||
private RulesetInfo maniaRuleset;
|
||||
|
||||
public TestSceneBeatmapSetOverlay()
|
||||
{
|
||||
Add(overlay = new TestBeatmapSetOverlay());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
taikoRuleset = rulesets.GetRuleset(1);
|
||||
maniaRuleset = rulesets.GetRuleset(3);
|
||||
}
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestLoading()
|
||||
@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
StarDifficulty = 9.99,
|
||||
Version = @"TEST",
|
||||
Length = 456000,
|
||||
Ruleset = maniaRuleset,
|
||||
Ruleset = rulesets.GetRuleset(3),
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 1,
|
||||
@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
StarDifficulty = 5.67,
|
||||
Version = @"ANOTHER TEST",
|
||||
Length = 123000,
|
||||
Ruleset = taikoRuleset,
|
||||
Ruleset = rulesets.GetRuleset(1),
|
||||
BaseDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 9,
|
||||
@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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]
|
||||
public void TestHide()
|
||||
{
|
||||
@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
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
|
||||
{
|
||||
public bool DownloadButtonsVisible => Header.DownloadButtonsVisible;
|
||||
public new Header Header => base.Header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
|
||||
|
||||
// intentionally blocking to limit web request concurrency
|
||||
req.Perform(api);
|
||||
try
|
||||
{
|
||||
// intentionally blocking to limit web request concurrency
|
||||
req.Perform(api);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,16 +86,7 @@ namespace osu.Game.Database
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
};
|
||||
|
||||
request.Failure += error =>
|
||||
{
|
||||
DownloadFailed?.Invoke(request);
|
||||
|
||||
if (error is OperationCanceledException) return;
|
||||
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
|
||||
currentDownloads.Remove(request);
|
||||
};
|
||||
request.Failure += triggerFailure;
|
||||
|
||||
notification.CancelRequested += () =>
|
||||
{
|
||||
@ -108,11 +99,31 @@ namespace osu.Game.Database
|
||||
currentDownloads.Add(request);
|
||||
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);
|
||||
|
||||
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));
|
||||
|
@ -8,9 +8,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
|
||||
private OsuGame game;
|
||||
private ChannelManager channelManager;
|
||||
private Action showNotImplementedError;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
|
||||
private void load(OsuGame game)
|
||||
{
|
||||
// will be null in tests
|
||||
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)
|
||||
@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
|
||||
foreach (var link in links)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
=> createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
|
||||
public void AddLink(string text, string url, Action<SpriteText> creationParameters = null) =>
|
||||
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)
|
||||
=> createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
|
||||
public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||
=> 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)
|
||||
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)
|
||||
=> createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile");
|
||||
public void AddUserLink(User user, Action<SpriteText> creationParameters = null)
|
||||
=> 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())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
TooltipText = tooltipText ?? (url != text ? url : string.Empty),
|
||||
Action = action ?? (() =>
|
||||
TooltipText = tooltipText,
|
||||
Action = () =>
|
||||
{
|
||||
switch (linkType)
|
||||
{
|
||||
case LinkAction.OpenBeatmap:
|
||||
// TODO: proper query params handling
|
||||
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.");
|
||||
}
|
||||
}),
|
||||
if (action != null)
|
||||
action();
|
||||
else
|
||||
game.HandleLink(link);
|
||||
},
|
||||
});
|
||||
|
||||
return drawables;
|
||||
}
|
||||
|
||||
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
|
||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
|
||||
//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);
|
||||
|
||||
var details = getLinkDetails(linkText);
|
||||
var details = GetLinkDetails(linkText);
|
||||
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
|
||||
|
||||
//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 indexLength = linkText.Length;
|
||||
|
||||
var details = getLinkDetails(linkText);
|
||||
var details = GetLinkDetails(linkText);
|
||||
var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
|
||||
|
||||
// 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);
|
||||
args[0] = args[0].TrimEnd(':');
|
||||
@ -255,17 +255,17 @@ namespace osu.Game.Online.Chat
|
||||
OriginalText = Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LinkDetails
|
||||
public class LinkDetails
|
||||
{
|
||||
public LinkAction Action;
|
||||
public string Argument;
|
||||
|
||||
public LinkDetails(LinkAction action, string argument)
|
||||
{
|
||||
public LinkAction Action;
|
||||
public string Argument;
|
||||
|
||||
public LinkDetails(LinkAction action, string argument)
|
||||
{
|
||||
Action = action;
|
||||
Argument = argument;
|
||||
}
|
||||
Action = action;
|
||||
Argument = argument;
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,6 +279,7 @@ namespace osu.Game.Online.Chat
|
||||
JoinMultiplayerMatch,
|
||||
Spectate,
|
||||
OpenUserProfile,
|
||||
Custom
|
||||
}
|
||||
|
||||
public class Link : IComparable<Link>
|
||||
|
@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Humanizer;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
private readonly int rank;
|
||||
private readonly bool allowHighlight;
|
||||
|
||||
private Box background;
|
||||
private Container content;
|
||||
@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private List<ScoreComponentLabel> statisticsLabels;
|
||||
|
||||
public LeaderboardScore(ScoreInfo score, int rank)
|
||||
public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
|
||||
{
|
||||
this.score = score;
|
||||
this.rank = rank;
|
||||
this.allowHighlight = allowHighlight;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = HEIGHT;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(IAPIProvider api, OsuColour colour)
|
||||
{
|
||||
var user = score.User;
|
||||
|
||||
@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
|
||||
Alpha = background_alpha,
|
||||
},
|
||||
},
|
||||
|
@ -215,31 +215,102 @@ namespace osu.Game
|
||||
|
||||
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("/"))
|
||||
url = $"{API.Endpoint}{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>
|
||||
/// Show a beatmap set as an overlay.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Show a user's profile as an overlay.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Show a beatmap's set as an overlay, displaying the given beatmap.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Present a beatmap at song select immediately.
|
||||
@ -397,6 +468,23 @@ namespace osu.Game
|
||||
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)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
private const float tile_icon_padding = 7;
|
||||
private const float tile_spacing = 2;
|
||||
|
||||
private readonly DifficultiesContainer difficulties;
|
||||
private readonly OsuSpriteText version, starRating;
|
||||
private readonly Statistic plays, favourites;
|
||||
|
||||
public readonly DifficultiesContainer Difficulties;
|
||||
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
private BeatmapSetInfo beatmapSet;
|
||||
@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
if (value == beatmapSet) return;
|
||||
|
||||
beatmapSet = value;
|
||||
|
||||
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()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
difficulties = new DifficultiesContainer
|
||||
Difficulties = new DifficultiesContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.ValueChanged += r => updateDisplay();
|
||||
|
||||
// done here so everything can bind in intialization and get the first trigger
|
||||
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)
|
||||
{
|
||||
version.Text = beatmap?.Version;
|
||||
@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
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;
|
||||
|
||||
@ -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 size = 52;
|
||||
@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
}
|
||||
}
|
||||
|
||||
private enum DifficultySelectorState
|
||||
public enum DifficultySelectorState
|
||||
{
|
||||
Selected,
|
||||
NotSelected,
|
||||
|
48
osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs
Normal file
48
osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
145
osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs
Normal file
145
osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs
Normal 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
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Overlays.BeatmapSet.Buttons;
|
||||
using osu.Game.Overlays.Direct;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
|
||||
|
||||
public readonly BeatmapRulesetSelector RulesetSelector;
|
||||
public readonly BeatmapPicker Picker;
|
||||
|
||||
private readonly FavouriteButton favouriteButton;
|
||||
@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
private readonly LoadingAnimation loading;
|
||||
|
||||
[Cached(typeof(IBindable<RulesetInfo>))]
|
||||
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||
|
||||
public Header()
|
||||
{
|
||||
ExternalLinkButton externalLink;
|
||||
@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = tabs_height,
|
||||
Children = new[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
tabsBg = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
RulesetSelector = new BeatmapRulesetSelector
|
||||
{
|
||||
Current = ruleset,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
}
|
||||
},
|
||||
},
|
||||
new Container
|
||||
@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
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;
|
||||
|
||||
if (setInfo.NewValue == null)
|
||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
t.Font = fontLarge;
|
||||
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 =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
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.Colour = entryColour;
|
||||
|
@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
|
||||
req.Failure += _ => complete = true;
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
@ -170,6 +170,7 @@ namespace osu.Game.Overlays
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
var req = new GetChangelogRequest();
|
||||
|
||||
req.Success += res => Schedule(() =>
|
||||
{
|
||||
// remap streams to builds to ensure model equality
|
||||
@ -183,8 +184,22 @@ namespace osu.Game.Overlays
|
||||
|
||||
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;
|
||||
});
|
||||
|
@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
|
||||
EditorBeatmap.HitObjectAdded += addHitObject;
|
||||
EditorBeatmap.HitObjectRemoved += removeHitObject;
|
||||
EditorBeatmap.StartTimeChanged += updateHitObject;
|
||||
EditorBeatmap.StartTimeChanged += UpdateHitObject;
|
||||
|
||||
var dependencies = new DependencyContainer(parent);
|
||||
dependencies.CacheAs<IEditorBeatmap>(EditorBeatmap);
|
||||
@ -225,11 +225,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
private ScheduledDelegate scheduledUpdate;
|
||||
|
||||
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
|
||||
|
||||
private void removeHitObject(HitObject hitObject) => updateHitObject(null);
|
||||
|
||||
private void updateHitObject([CanBeNull] HitObject hitObject)
|
||||
public override void UpdateHitObject(HitObject hitObject)
|
||||
{
|
||||
scheduledUpdate?.Cancel();
|
||||
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 bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
|
||||
|
||||
@ -351,11 +351,22 @@ namespace osu.Game.Rulesets.Edit
|
||||
[CanBeNull]
|
||||
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 float GetBeatSnapDistanceAt(double referenceTime);
|
||||
|
||||
public abstract float DurationToDistance(double referenceTime, double duration);
|
||||
|
||||
public abstract double DistanceToDuration(double referenceTime, float distance);
|
||||
|
||||
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
|
||||
|
||||
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
@ -36,6 +38,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
public override bool HandlePositionalInput => ShouldBeAlive;
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
protected SelectionBlueprint(DrawableHitObject drawableObject)
|
||||
{
|
||||
DrawableObject = drawableObject;
|
||||
@ -89,6 +94,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
|
@ -239,6 +239,12 @@ namespace osu.Game.Rulesets.UI
|
||||
continueResume();
|
||||
}
|
||||
|
||||
public override void CancelResume()
|
||||
{
|
||||
// called if the user pauses while the resume overlay is open
|
||||
ResumeOverlay?.Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds the visual representation of a <see cref="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>.
|
||||
/// </summary>
|
||||
@ -453,6 +459,11 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <param name="continueResume">The action to run when resuming is to be completed.</param>
|
||||
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>
|
||||
/// Create a <see cref="ScoreProcessor"/> for the associated ruleset and link with this
|
||||
/// <see cref="DrawableRuleset"/>.
|
||||
|
@ -13,12 +13,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
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<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
@ -123,28 +117,22 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
if (cached.IsValid)
|
||||
return;
|
||||
|
||||
double endTime = hitObject.HitObject.StartTime;
|
||||
|
||||
if (hitObject.HitObject is IHasEndTime e)
|
||||
{
|
||||
endTime = e.EndTime;
|
||||
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
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;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
{
|
||||
computeInitialStateRecursive(obj);
|
||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.Components
|
||||
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
|
||||
Font = OsuFont.GetFont(size: TextSize),
|
||||
}
|
||||
}, null, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
|
||||
}, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,14 +167,17 @@ namespace osu.Game.Screens.Multi
|
||||
public void APIStateChanged(IAPIProvider api, APIState state)
|
||||
{
|
||||
if (state != APIState.Online)
|
||||
forcefullyExit();
|
||||
Schedule(forcefullyExit);
|
||||
}
|
||||
|
||||
private void forcefullyExit()
|
||||
{
|
||||
// This is temporary since we don't currently have a way to force screens to be exited
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
{
|
||||
while (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.MakeCurrent();
|
||||
@ -212,6 +215,8 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
roomManager.PartRoom();
|
||||
|
||||
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
|
||||
{
|
||||
screenStack.Exit();
|
||||
|
@ -87,9 +87,8 @@ namespace osu.Game.Screens.Multi
|
||||
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||
{
|
||||
currentJoinRoomRequest?.Cancel();
|
||||
currentJoinRoomRequest = null;
|
||||
|
||||
currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value);
|
||||
|
||||
currentJoinRoomRequest.Success += () =>
|
||||
{
|
||||
joinedRoom = room;
|
||||
@ -98,7 +97,8 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
@ -107,6 +107,8 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
public void PartRoom()
|
||||
{
|
||||
currentJoinRoomRequest?.Cancel();
|
||||
|
||||
if (joinedRoom == null)
|
||||
return;
|
||||
|
||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
|
||||
/// </summary>
|
||||
private readonly IAdjustableClock sourceClock;
|
||||
private IAdjustableClock sourceClock;
|
||||
|
||||
public readonly BindableBool IsPaused = new BindableBool();
|
||||
|
||||
@ -153,6 +154,18 @@ namespace osu.Game.Screens.Play
|
||||
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()
|
||||
{
|
||||
// In the case of replays, we may have changed the playback rate.
|
||||
|
@ -30,6 +30,7 @@ using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
[Cached]
|
||||
public class Player : ScreenWithBeatmapBackground
|
||||
{
|
||||
public override bool AllowBackButton => false; // handled by HoldForMenuButton
|
||||
@ -311,14 +312,19 @@ namespace osu.Game.Screens.Play
|
||||
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()
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
sampleRestart?.Play();
|
||||
|
||||
RestartRequested?.Invoke();
|
||||
performImmediateExit();
|
||||
|
||||
if (this.IsCurrentScreen())
|
||||
performImmediateExit();
|
||||
else
|
||||
this.MakeCurrent();
|
||||
}
|
||||
|
||||
private ScheduledDelegate completionProgressDelegate;
|
||||
@ -443,7 +449,12 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
if (!canPause) return;
|
||||
|
||||
IsResuming = false;
|
||||
if (IsResuming)
|
||||
{
|
||||
DrawableRuleset.CancelResume();
|
||||
IsResuming = false;
|
||||
}
|
||||
|
||||
GameplayClockContainer.Stop();
|
||||
PauseOverlay.Show();
|
||||
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
|
||||
@ -527,6 +538,10 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
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();
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
@ -4,12 +4,13 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
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.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>
|
||||
{
|
||||
@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
|
||||
public ReplayDownloadButton(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
Size = new Vector2(50, 30);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
54
osu.Game/Screens/Ranking/Pages/RetryButton.cs
Normal file
54
osu.Game/Screens/Ranking/Pages/RetryButton.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -169,12 +169,19 @@ namespace osu.Game.Screens.Ranking.Pages
|
||||
},
|
||||
},
|
||||
},
|
||||
new ReplayDownloadButton(score)
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
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()
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
@ -34,6 +35,9 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
private ResultModeTabControl modeChangeButtons;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private Player player { get; set; }
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
protected readonly ScoreInfo Score;
|
||||
@ -100,10 +104,7 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
allCircles.ForEach(c =>
|
||||
{
|
||||
c.ScaleTo(0, transition_time, Easing.OutSine);
|
||||
});
|
||||
allCircles.ForEach(c => c.ScaleTo(0, transition_time, Easing.OutSine));
|
||||
|
||||
Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint);
|
||||
|
||||
@ -115,147 +116,157 @@ namespace osu.Game.Screens.Ranking
|
||||
[BackgroundDependencyLoader]
|
||||
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,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Height = overscan,
|
||||
Children = new Drawable[]
|
||||
circleOuterBackground = new CircularContainer
|
||||
{
|
||||
circleOuterBackground = new CircularContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
new Box
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Alpha = 0.2f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
}
|
||||
Alpha = 0.2f,
|
||||
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),
|
||||
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
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.4f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 15,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
backgroundParallax = new ParallaxContainer
|
||||
{
|
||||
new Box
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ParallaxAmount = 0.01f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
},
|
||||
backgroundParallax = new ParallaxContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ParallaxAmount = 0.01f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
new Sprite
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.2f,
|
||||
Texture = Beatmap.Value.Background,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill
|
||||
}
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.2f,
|
||||
Texture = Beatmap.Value.Background,
|
||||
Anchor = Anchor.Centre,
|
||||
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,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
modeChangeButtons = new ResultModeTabControl
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
},
|
||||
}
|
||||
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,
|
||||
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();
|
||||
|
||||
foreach (var p in pages)
|
||||
|
@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
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)
|
||||
};
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
if (newScore == null)
|
||||
return;
|
||||
|
||||
LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position)
|
||||
LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position, false)
|
||||
{
|
||||
Action = () => ScoreSelected?.Invoke(newScore.Score)
|
||||
}, drawableScore =>
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public abstract class SelectionBlueprintTestScene : OsuTestScene
|
||||
public abstract class SelectionBlueprintTestScene : ManualInputManagerTestScene
|
||||
{
|
||||
protected override Container<Drawable> Content => content ?? base.Content;
|
||||
private readonly Container content;
|
||||
|
@ -4,7 +4,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Foundation;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game;
|
||||
using osu.Framework.Threading;
|
||||
using UIKit;
|
||||
|
||||
namespace osu.iOS
|
||||
@ -16,9 +16,12 @@ namespace osu.iOS
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@
|
||||
<string>0.1.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>10.0</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
@ -32,9 +34,9 @@
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We don't really use the camera.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We don't really use the microphone.</string>
|
||||
<string>We don't really use the camera.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We don't really use the microphone.</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
@ -109,5 +111,17 @@
|
||||
</array>
|
||||
</dict>
|
||||
</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>
|
||||
</plist>
|
||||
|
Loading…
Reference in New Issue
Block a user