mirror of
https://github.com/ppy/osu.git
synced 2026-05-14 18:22:54 +08:00
Compare commits
153 Commits
@@ -19,6 +19,11 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
|
||||
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
|
||||
#license header
|
||||
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.908.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.930.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
private Drawable noteAnimation = null!;
|
||||
|
||||
private float? minimumColumnWidth;
|
||||
private float? widthForNoteHeightScale;
|
||||
|
||||
public LegacyNotePiece()
|
||||
{
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||
{
|
||||
minimumColumnWidth = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
|
||||
widthForNoteHeightScale = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale))?.Value;
|
||||
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
@@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
// The height is scaled to the minimum column width, if provided.
|
||||
float minimumWidth = minimumColumnWidth ?? DrawWidth;
|
||||
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth);
|
||||
float noteHeight = widthForNoteHeightScale ?? DrawWidth;
|
||||
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, noteHeight), texture.DisplayWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestRewind()
|
||||
{
|
||||
AddStep("set manual clock", () => manualClock = new ManualClock
|
||||
|
||||
+13
-2
@@ -469,9 +469,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
|
||||
Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
// Snapping inherited B-spline control points to nearby objects would be unintuitive, because snapping them does not equate to snapping the interpolated slider path.
|
||||
bool shouldSnapToNearbyObjects = dragPathTypes[draggedControlPointIndex] is not null ||
|
||||
dragPathTypes[..draggedControlPointIndex].LastOrDefault(t => t is not null)?.Type != SplineType.BSpline;
|
||||
|
||||
SnapResult result = null;
|
||||
if (shouldSnapToNearbyObjects)
|
||||
result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime);
|
||||
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(newControlPointPosition, oldStartTime);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
|
||||
for (int i = 0; i < controlPoints.Count; ++i)
|
||||
{
|
||||
|
||||
@@ -626,10 +626,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
|
||||
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
|
||||
|
||||
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
|
||||
{
|
||||
protected override Vector2[] ScreenSpaceAdditionalNodes => getScreenSpaceControlPointNodes().Prepend(
|
||||
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
|
||||
};
|
||||
).ToArray();
|
||||
|
||||
private IEnumerable<Vector2> getScreenSpaceControlPointNodes()
|
||||
{
|
||||
// Returns the positions of control points that produce visible kinks on the slider's path
|
||||
// This excludes inherited control points from Bezier, B-Spline, Perfect, and Catmull curves
|
||||
if (DrawableObject.SliderBody == null)
|
||||
yield break;
|
||||
|
||||
PathType? currentPathType = null;
|
||||
|
||||
// Skip the last control point because its always either not on the slider path or exactly on the slider end
|
||||
for (int i = 0; i < DrawableObject.HitObject.Path.ControlPoints.Count - 1; i++)
|
||||
{
|
||||
var controlPoint = DrawableObject.HitObject.Path.ControlPoints[i];
|
||||
|
||||
if (controlPoint.Type is not null)
|
||||
currentPathType = controlPoint.Type;
|
||||
|
||||
// Skip the first control point because it is already covered by the slider head
|
||||
if (i == 0)
|
||||
continue;
|
||||
|
||||
if (controlPoint.Type is null && currentPathType != PathType.LINEAR)
|
||||
continue;
|
||||
|
||||
var screenSpacePosition = DrawableObject.SliderBody.ToScreenSpace(DrawableObject.SliderBody.PathOffset + controlPoint.Position);
|
||||
yield return screenSpacePosition;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
|
||||
@@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects, true);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
|
||||
@@ -81,12 +81,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
|
||||
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
|
||||
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||
OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
originalConvexHull = GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
||||
}
|
||||
|
||||
@@ -312,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
{
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys, true);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
|
||||
@@ -46,9 +46,12 @@ namespace osu.Game.Tests.Extensions
|
||||
|
||||
[Test]
|
||||
[SetCulture("fr-FR")]
|
||||
public void TestCultureInsensitivity()
|
||||
[TestCase(0.4, true, 2, ExpectedResult = "40%")]
|
||||
[TestCase(1e-6, false, 6, ExpectedResult = "0.000001")]
|
||||
[TestCase(0.48333, true, 4, ExpectedResult = "48.33%")]
|
||||
public string TestCultureInsensitivity(double input, bool percent, int decimalDigits)
|
||||
{
|
||||
Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%"));
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,25 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
progress.SetInProgress(incrementingProgress += RNG.NextSingle(0.08f));
|
||||
}, 0, true);
|
||||
});
|
||||
AddStep("increase progress slowly then fail", () =>
|
||||
{
|
||||
incrementingProgress = 0;
|
||||
|
||||
ScheduledDelegate? task = null;
|
||||
|
||||
task = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (incrementingProgress >= 1)
|
||||
{
|
||||
progress.SetFailed("nope");
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
task?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
progress.SetInProgress(incrementingProgress += RNG.NextSingle(0.001f));
|
||||
}, 0, true);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for completed", () => incrementingProgress >= 1);
|
||||
AddStep("completed", () => progress.SetCompleted());
|
||||
|
||||
@@ -183,6 +183,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Last().Alpha == 1);
|
||||
AddToggleStep("toggle wireframe display", t => counterDisplay.WireframeOpacity.Value = t ? 0.3f : 0);
|
||||
AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical);
|
||||
AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal);
|
||||
}
|
||||
|
||||
private int hiddenCount()
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene
|
||||
{
|
||||
private MultiplayerPlaylistItem[] items = null!;
|
||||
|
||||
private BeatmapSelectGrid grid = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var beatmaps = beatmapManager.GetAllUsableBeatmapSets()
|
||||
.SelectMany(it => it.Beatmaps)
|
||||
.Take(50)
|
||||
.ToArray();
|
||||
|
||||
if (beatmaps.Length > 0)
|
||||
{
|
||||
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = beatmaps[i % beatmaps.Length].OnlineID,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("add grid", () => Child = grid = new BeatmapSelectGrid
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f),
|
||||
});
|
||||
|
||||
AddStep("add items", () =>
|
||||
{
|
||||
foreach (var item in items)
|
||||
grid.AddItem(item);
|
||||
});
|
||||
|
||||
AddWaitStep("wait for panels", 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("do nothing", () =>
|
||||
{
|
||||
// test scene is weird.
|
||||
});
|
||||
|
||||
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser
|
||||
{
|
||||
Id = 6411631,
|
||||
Username = "Maarvin",
|
||||
}, isOwnUser: true));
|
||||
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
}));
|
||||
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser
|
||||
{
|
||||
Id = 1040328,
|
||||
Username = "smoogipoo",
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCompleteRollAnimation()
|
||||
{
|
||||
AddStep("play animation", () =>
|
||||
{
|
||||
var (candidateItems, finalItem) = pickRandomItems(5);
|
||||
|
||||
grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRollAnimation()
|
||||
{
|
||||
AddStep("play animation", () =>
|
||||
{
|
||||
var (candidateItems, finalItem) = pickRandomItems(5);
|
||||
|
||||
grid.TransferCandidatePanelsToRollContainer(candidateItems, duration: 0);
|
||||
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
|
||||
|
||||
Scheduler.AddDelayed(() => grid.PlayRollAnimation(finalItem), 500);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPresentRolledBeatmap()
|
||||
{
|
||||
AddStep("present beatmap", () =>
|
||||
{
|
||||
var (candidateItems, finalItem) = pickRandomItems(5);
|
||||
|
||||
grid.TransferCandidatePanelsToRollContainer(candidateItems, duration: 0);
|
||||
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
|
||||
grid.PlayRollAnimation(finalItem, duration: 0);
|
||||
|
||||
Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem), 500);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPresentUnanimouslyChosenBeatmap()
|
||||
{
|
||||
AddStep("present beatmap", () =>
|
||||
{
|
||||
var (candidateItems, finalItem) = pickRandomItems(5);
|
||||
|
||||
grid.TransferCandidatePanelsToRollContainer(candidateItems, duration: 0);
|
||||
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
|
||||
grid.PlayRollAnimation(finalItem, duration: 0);
|
||||
|
||||
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem), 500);
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
[TestCase(3)]
|
||||
[TestCase(4)]
|
||||
[TestCase(5)]
|
||||
[TestCase(6)]
|
||||
[TestCase(7)]
|
||||
[TestCase(8)]
|
||||
public void TestPanelArrangement(int count)
|
||||
{
|
||||
AddStep("arrange panels", () =>
|
||||
{
|
||||
var (candidateItems, _) = pickRandomItems(count);
|
||||
|
||||
grid.TransferCandidatePanelsToRollContainer(candidateItems);
|
||||
grid.Delay(BeatmapSelectGrid.ARRANGE_DELAY)
|
||||
.Schedule(() => grid.ArrangeItemsForRollAnimation());
|
||||
});
|
||||
|
||||
AddWaitStep("wait for movement", 5);
|
||||
|
||||
AddStep("display roll order", () =>
|
||||
{
|
||||
var panels = grid.ChildrenOfType<BeatmapSelectPanel>().ToArray();
|
||||
|
||||
for (int i = 0; i < panels.Length; i++)
|
||||
{
|
||||
var panel = panels[i];
|
||||
|
||||
panel.Add(new OsuSpriteText
|
||||
{
|
||||
Text = (i + 1).ToString(),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Default.With(size: 50, weight: FontWeight.SemiBold),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private (long[] candidateItems, long finalItem) pickRandomItems(int count)
|
||||
{
|
||||
long[] candidateItems = items.Select(it => it.ID).ToArray();
|
||||
Random.Shared.Shuffle(candidateItems);
|
||||
candidateItems = candidateItems.Take(count).ToArray();
|
||||
|
||||
long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)];
|
||||
|
||||
return (candidateItems, finalItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapPanel()
|
||||
{
|
||||
BeatmapSelectPanel? panel = null;
|
||||
|
||||
AddStep("add panel", () => Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddStep("add maarvin", () => panel!.AddUser(new APIUser
|
||||
{
|
||||
Id = 6411631,
|
||||
Username = "Maarvin",
|
||||
}, isOwnUser: true));
|
||||
AddStep("add peppy", () => panel!.AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
}));
|
||||
AddStep("add smogipoo", () => panel!.AddUser(new APIUser
|
||||
{
|
||||
Id = 1040328,
|
||||
Username = "smoogipoo",
|
||||
}));
|
||||
AddStep("remove smogipoo", () => panel!.RemoveUser(new APIUser { Id = 1040328 }));
|
||||
AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 }));
|
||||
AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 }));
|
||||
|
||||
AddToggleStep("allow selection", value =>
|
||||
{
|
||||
if (panel != null)
|
||||
panel.AllowSelection = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneIdleScreen : MultiplayerTestScene
|
||||
{
|
||||
private const int user_count = 8;
|
||||
|
||||
private (MultiplayerRoomUser user, int score)[] userScores = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add list", () =>
|
||||
{
|
||||
userScores = Enumerable.Range(1, user_count).Select(i =>
|
||||
{
|
||||
var user = new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"Player {i}"
|
||||
}
|
||||
};
|
||||
|
||||
return (user, 0);
|
||||
}).ToArray();
|
||||
|
||||
Child = new ScreenStack(new SubScreenRoundWarmup())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.8f)
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
foreach (var (user, _) in userScores)
|
||||
MultiplayerClient.AddUser(user);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomChanges()
|
||||
{
|
||||
AddStep("apply random changes", () =>
|
||||
{
|
||||
int[] deltas = Enumerable.Range(1, userScores.Length).ToArray();
|
||||
new Random().Shuffle(deltas);
|
||||
|
||||
for (int i = 0; i < userScores.Length; i++)
|
||||
userScores[i] = (userScores[i].user, userScores[i].score + deltas[i]);
|
||||
userScores = userScores.OrderByDescending(u => u.score).ToArray();
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Users =
|
||||
{
|
||||
UserDictionary = userScores.Select((tuple, i) => new MatchmakingUser
|
||||
{
|
||||
UserId = tuple.user.UserID,
|
||||
Points = tuple.score,
|
||||
Placement = i + 1
|
||||
}).ToDictionary(s => s.UserId)
|
||||
}
|
||||
}).WaitSafely();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Queue;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingCloud : OsuTestScene
|
||||
{
|
||||
private CloudVisualisation cloud = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Child = cloud = new CloudVisualisation
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("refresh users", () =>
|
||||
{
|
||||
var testUsers = Enumerable.Range(0, 50).Select(_ => new APIUser
|
||||
{
|
||||
Username = "peppy",
|
||||
Statistics = new UserStatistics { GlobalRank = 1234 },
|
||||
Id = RNG.Next(2, 30000000),
|
||||
}).ToArray();
|
||||
|
||||
cloud.Users = testUsers;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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.Graphics;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Queue;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingPoolSelector : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("add selector", () => Child = new PoolSelector
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AvailablePools =
|
||||
{
|
||||
Value =
|
||||
[
|
||||
new MatchmakingPool { Id = 0, RulesetId = 0 },
|
||||
new MatchmakingPool { Id = 1, RulesetId = 1 },
|
||||
new MatchmakingPool { Id = 2, RulesetId = 2 },
|
||||
new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4 },
|
||||
new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7 },
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Intro;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Queue;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingQueueScreen : MultiplayerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly QueueController controller = new QueueController();
|
||||
|
||||
private ScreenQueue? queueScreen => Stack.CurrentScreen as ScreenQueue;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("load screen", () => LoadScreen(new IntroScreen()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddUntilStep("wait for queue screen", () => queueScreen?.IsLoaded == true);
|
||||
|
||||
AddStep("set users", () =>
|
||||
{
|
||||
queueScreen!.Users = Enumerable.Range(0, 10).Select(_ => new APIUser
|
||||
{
|
||||
Username = "peppy",
|
||||
Statistics = new UserStatistics { GlobalRank = 1234 },
|
||||
Id = RNG.Next(2, 30000000),
|
||||
}).ToArray();
|
||||
});
|
||||
|
||||
AddStep("change state to idle", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.Idle));
|
||||
|
||||
AddStep("change state to queueing", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.Queueing));
|
||||
|
||||
AddStep("change state to found match", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.PendingAccept));
|
||||
|
||||
AddStep("change state to waiting for room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.AcceptedWaitingForRoom));
|
||||
|
||||
AddStep("change state to in room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.InRoom));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingScreen : MultiplayerTestScene
|
||||
{
|
||||
private const int user_count = 8;
|
||||
private const int beatmap_count = 50;
|
||||
|
||||
private MultiplayerRoomUser[] users = null!;
|
||||
private ScreenMatchmaking screen = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom(MatchType.Matchmaking);
|
||||
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
})).ToArray();
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {i}"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setupRequestHandler();
|
||||
|
||||
AddStep("load match", () =>
|
||||
{
|
||||
users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"Player {i}"
|
||||
}
|
||||
}).ToArray();
|
||||
|
||||
var beatmaps = Enumerable.Range(1, beatmap_count).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0
|
||||
}).ToArray();
|
||||
|
||||
LoadScreen(screen = new ScreenMatchmaking(new MultiplayerRoom(0)
|
||||
{
|
||||
Users = users,
|
||||
Playlist = beatmaps
|
||||
}));
|
||||
});
|
||||
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayFlow()
|
||||
{
|
||||
for (int round = 1; round <= 3; round++)
|
||||
{
|
||||
AddLabel($"Round {round}");
|
||||
|
||||
int r = round;
|
||||
changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r);
|
||||
changeStage(MatchmakingStage.UserBeatmapSelect);
|
||||
changeStage(MatchmakingStage.ServerBeatmapFinalised, state =>
|
||||
{
|
||||
MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
|
||||
state.CandidateItems = beatmaps.Select(b => b.ID).ToArray();
|
||||
state.CandidateItem = beatmaps[0].ID;
|
||||
}, waitTime: 35);
|
||||
|
||||
changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload);
|
||||
changeStage(MatchmakingStage.GameplayWarmupTime);
|
||||
changeStage(MatchmakingStage.Gameplay);
|
||||
changeStage(MatchmakingStage.ResultsDisplaying);
|
||||
}
|
||||
|
||||
changeStage(MatchmakingStage.Ended, state =>
|
||||
{
|
||||
int localUserId = API.LocalUser.Value.OnlineID;
|
||||
|
||||
state.Users[localUserId].Placement = 1;
|
||||
state.Users[localUserId].Rounds[1].Placement = 1;
|
||||
state.Users[localUserId].Rounds[1].TotalScore = 1;
|
||||
state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
|
||||
});
|
||||
}
|
||||
|
||||
private void changeStage(MatchmakingStage stage, Action<MatchmakingRoomState>? prepare = null, int waitTime = 5)
|
||||
{
|
||||
AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely());
|
||||
AddWaitStep("wait", waitTime);
|
||||
}
|
||||
|
||||
private void setupRequestHandler()
|
||||
{
|
||||
AddStep("setup request handler", () =>
|
||||
{
|
||||
Func<APIRequest, bool>? defaultRequestHandler = null;
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapsRequest getBeatmaps:
|
||||
getBeatmaps.TriggerSuccess(new GetBeatmapsResponse
|
||||
{
|
||||
Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap
|
||||
{
|
||||
OnlineID = id,
|
||||
StarRating = id,
|
||||
DifficultyName = $"Beatmap {id}",
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = $"Title {id}",
|
||||
Artist = $"Artist {id}",
|
||||
AuthorString = $"Author {id}"
|
||||
}
|
||||
}).ToList()
|
||||
});
|
||||
return true;
|
||||
|
||||
case IndexPlaylistScoresRequest index:
|
||||
var result = new IndexedMultiplayerScores();
|
||||
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
result.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = i,
|
||||
Accuracy = 1 - (float)i / 16,
|
||||
Position = i + 1,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH),
|
||||
MaxCombo = 1000 - i,
|
||||
TotalScore = (long)(1_000_000 * (1 - (float)i / 16)),
|
||||
User = new APIUser { Username = $"user {i}" },
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
});
|
||||
}
|
||||
|
||||
index.TriggerSuccess(result);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return defaultRequestHandler?.Invoke(request) ?? false;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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.Graphics;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestScenePanelRoomAward : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("add statistic", () => Child = new PanelRoomAward("Statistic description", 1)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestScenePickScreen : MultiplayerTestScene
|
||||
{
|
||||
private readonly IReadOnlyList<APIUser> users = new[]
|
||||
{
|
||||
new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 1040328,
|
||||
Username = "smoogipoo",
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 6573093,
|
||||
Username = "OliBomby",
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 7782553,
|
||||
Username = "aesth",
|
||||
},
|
||||
new APIUser
|
||||
{
|
||||
Id = 6411631,
|
||||
Username = "Maarvin",
|
||||
}
|
||||
};
|
||||
|
||||
private readonly PlaylistItem[] items = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
})).ToArray();
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom(MatchType.Matchmaking);
|
||||
room.Playlist = items;
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add users", () =>
|
||||
{
|
||||
foreach (var user in users)
|
||||
MultiplayerClient.AddUser(user);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScreen()
|
||||
{
|
||||
var selectedItems = new List<long>();
|
||||
|
||||
SubScreenBeatmapSelect screen = null!;
|
||||
|
||||
AddStep("add screen", () => Child = new ScreenStack(screen = new SubScreenBeatmapSelect()));
|
||||
|
||||
AddStep("select maps", () =>
|
||||
{
|
||||
selectedItems.Clear();
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
var item = items[Random.Shared.Next(items.Length)];
|
||||
selectedItems.Add(item.ID);
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
MultiplayerClient.MatchmakingToggleUserSelection(user.Id, item.ID).FireAndForget();
|
||||
}, RNG.NextDouble(10, 1000));
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("show final map", () =>
|
||||
{
|
||||
long[] candidateItems = selectedItems.ToArray();
|
||||
long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)];
|
||||
|
||||
screen.RollFinalBeatmap(candidateItems, finalItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestScenePlayerPanel : MultiplayerTestScene
|
||||
{
|
||||
private PlayerPanel panel = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
|
||||
}
|
||||
})
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIncreasePlacement()
|
||||
{
|
||||
int rank = 0;
|
||||
|
||||
AddStep("increase placement", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Users =
|
||||
{
|
||||
UserDictionary =
|
||||
{
|
||||
{
|
||||
2, new MatchmakingUser
|
||||
{
|
||||
UserId = 2,
|
||||
Placement = ++rank
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).WaitSafely());
|
||||
|
||||
AddToggleStep("toggle horizontal", h => panel.Horizontal = h);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIncreasePoints()
|
||||
{
|
||||
int points = 0;
|
||||
|
||||
AddStep("increase points", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Users =
|
||||
{
|
||||
UserDictionary =
|
||||
{
|
||||
{
|
||||
1, new MatchmakingUser
|
||||
{
|
||||
UserId = 1,
|
||||
Placement = 1,
|
||||
Points = ++points
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).WaitSafely());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestJump()
|
||||
{
|
||||
AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneResultsScreen : MultiplayerTestScene
|
||||
{
|
||||
private const int invalid_user_id = 1;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add results screen", () =>
|
||||
{
|
||||
Child = new ScreenStack(new SubScreenResults())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.8f)
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(invalid_user_id)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Id = invalid_user_id,
|
||||
Username = "Invalid user"
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestResults()
|
||||
{
|
||||
AddStep("set results stage", () =>
|
||||
{
|
||||
var state = new MatchmakingRoomState
|
||||
{
|
||||
CurrentRound = 6,
|
||||
Stage = MatchmakingStage.Ended
|
||||
};
|
||||
|
||||
int localUserId = API.LocalUser.Value.OnlineID;
|
||||
|
||||
// Overall state.
|
||||
state.Users[localUserId].Placement = 1;
|
||||
state.Users[localUserId].Points = 8;
|
||||
state.Users[invalid_user_id].Placement = 2;
|
||||
state.Users[invalid_user_id].Points = 7;
|
||||
for (int round = 1; round <= state.CurrentRound; round++)
|
||||
state.Users[localUserId].Rounds[round].Placement = round;
|
||||
|
||||
// Highest score.
|
||||
state.Users[localUserId].Rounds[1].TotalScore = 1000;
|
||||
state.Users[invalid_user_id].Rounds[1].TotalScore = 990;
|
||||
|
||||
// Highest accuracy.
|
||||
state.Users[localUserId].Rounds[2].Accuracy = 0.9995;
|
||||
state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5;
|
||||
|
||||
// Highest combo.
|
||||
state.Users[localUserId].Rounds[3].MaxCombo = 100;
|
||||
state.Users[invalid_user_id].Rounds[3].MaxCombo = 10;
|
||||
|
||||
// Most bonus score.
|
||||
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50;
|
||||
state.Users[invalid_user_id].Rounds[4].Statistics[HitResult.LargeBonus] = 25;
|
||||
|
||||
// Smallest score difference.
|
||||
state.Users[localUserId].Rounds[5].TotalScore = 1000;
|
||||
state.Users[invalid_user_id].Rounds[5].TotalScore = 999;
|
||||
|
||||
// Largest score difference.
|
||||
state.Users[localUserId].Rounds[6].TotalScore = 1000;
|
||||
state.Users[invalid_user_id].Rounds[6].TotalScore = 0;
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneRoundResultsScreen : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
setupRequestHandler();
|
||||
|
||||
AddStep("load screen", () =>
|
||||
{
|
||||
Child = new ScreenStack(new SubScreenRoundResults())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.8f)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void setupRequestHandler()
|
||||
{
|
||||
AddStep("setup request handler", () =>
|
||||
{
|
||||
Func<APIRequest, bool>? defaultRequestHandler = null;
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapsRequest getBeatmaps:
|
||||
getBeatmaps.TriggerSuccess(new GetBeatmapsResponse
|
||||
{
|
||||
Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap
|
||||
{
|
||||
OnlineID = id,
|
||||
StarRating = id,
|
||||
DifficultyName = $"Beatmap {id}",
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = $"Title {id}",
|
||||
Artist = $"Artist {id}",
|
||||
AuthorString = $"Author {id}"
|
||||
}
|
||||
}).ToList()
|
||||
});
|
||||
return true;
|
||||
|
||||
case IndexPlaylistScoresRequest index:
|
||||
var result = new IndexedMultiplayerScores();
|
||||
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
result.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = i,
|
||||
Accuracy = 1 - (float)i / 16,
|
||||
Position = i + 1,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH),
|
||||
MaxCombo = 1000 - i,
|
||||
TotalScore = (long)(1_000_000 * (1 - (float)i / 16)),
|
||||
User = new APIUser { Username = $"user {i}" },
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
});
|
||||
}
|
||||
|
||||
index.TriggerSuccess(result);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return defaultRequestHandler?.Invoke(request) ?? false;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneStageDisplay : MultiplayerTestScene
|
||||
{
|
||||
[Cached]
|
||||
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add display", () => Child = new StageDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeStage()
|
||||
{
|
||||
addStage(MatchmakingStage.WaitingForClientsJoin);
|
||||
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
addStage(MatchmakingStage.RoundWarmupTime);
|
||||
addStage(MatchmakingStage.UserBeatmapSelect);
|
||||
addStage(MatchmakingStage.ServerBeatmapFinalised);
|
||||
addStage(MatchmakingStage.WaitingForClientsBeatmapDownload);
|
||||
addStage(MatchmakingStage.GameplayWarmupTime);
|
||||
addStage(MatchmakingStage.Gameplay);
|
||||
addStage(MatchmakingStage.ResultsDisplaying);
|
||||
}
|
||||
|
||||
addStage(MatchmakingStage.Ended);
|
||||
}
|
||||
|
||||
private void addStage(MatchmakingStage stage)
|
||||
{
|
||||
AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely());
|
||||
AddWaitStep("wait a bit", 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneStageSegment : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add bubble", () => Child = new StageDisplay.StageSegment(null, MatchmakingStage.RoundWarmupTime, "Next Round")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStartStopCountdown()
|
||||
{
|
||||
MultiplayerCountdown countdown = null!;
|
||||
|
||||
AddStep("start countdown", () => MultiplayerClient.StartCountdown(countdown = new MatchmakingStageCountdown
|
||||
{
|
||||
Stage = MatchmakingStage.RoundWarmupTime,
|
||||
TimeRemaining = TimeSpan.FromSeconds(5)
|
||||
}).WaitSafely());
|
||||
|
||||
AddWaitStep("wait a bit", 10);
|
||||
|
||||
AddStep("stop countdown", () => MultiplayerClient.StopCountdown(countdown).WaitSafely());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneStatusText : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create display", () => Child = new StageDisplay.StatusText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeStage()
|
||||
{
|
||||
foreach (var stage in Enum.GetValues<MatchmakingStage>())
|
||||
{
|
||||
AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely());
|
||||
AddWaitStep("wait a bit", 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneUserPanelOverlay : MultiplayerTestScene
|
||||
{
|
||||
private PlayerPanelOverlay list = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add list", () => Child = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.8f),
|
||||
Child = list = new PlayerPanelOverlay()
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeDisplayMode()
|
||||
{
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {i}"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split);
|
||||
AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid);
|
||||
AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddPanelsGrid()
|
||||
{
|
||||
AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid);
|
||||
|
||||
int userId = 0;
|
||||
|
||||
AddRepeatStep("join user", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(userId)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {userId}"
|
||||
}
|
||||
});
|
||||
|
||||
userId++;
|
||||
}, 8);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddPanelsSplit()
|
||||
{
|
||||
AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split);
|
||||
|
||||
int userId = 0;
|
||||
|
||||
AddRepeatStep("join user", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(userId)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {userId}"
|
||||
}
|
||||
});
|
||||
|
||||
userId++;
|
||||
}, 8);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemovePanels()
|
||||
{
|
||||
AddStep("join another user", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(1)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = "User 1"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("two panels displayed", () => this.ChildrenOfType<UserPanel>().Count(), () => Is.EqualTo(2));
|
||||
|
||||
AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
|
||||
AddUntilStep("one panel displayed", () => this.ChildrenOfType<UserPanel>().Count(), () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChangeRankings()
|
||||
{
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {i}"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("set random placements", () =>
|
||||
{
|
||||
MultiplayerRoom room = MultiplayerClient.ServerRoom!;
|
||||
|
||||
int[] placements = Enumerable.Range(1, room.Users.Count).ToArray();
|
||||
Random.Shared.Shuffle(placements);
|
||||
|
||||
MatchmakingRoomState state = new MatchmakingRoomState();
|
||||
|
||||
for (int i = 0; i < room.Users.Count; i++)
|
||||
state.Users[room.Users[i].UserID].Placement = placements[i];
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NUnit.Framework;
|
||||
@@ -9,10 +10,12 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Login;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@@ -54,7 +57,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoginSuccess()
|
||||
public void TestLoginSuccess_EmailVerification()
|
||||
{
|
||||
AddStep("logout", () => API.Logout());
|
||||
assertAPIState(APIState.Offline);
|
||||
@@ -94,6 +97,152 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
assertDropdownState(UserAction.DoNotDisturb);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoginSuccess_TOTPVerification()
|
||||
{
|
||||
AddStep("logout", () => API.Logout());
|
||||
assertAPIState(APIState.Offline);
|
||||
AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword);
|
||||
|
||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
|
||||
|
||||
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case VerifySessionRequest verifySessionRequest:
|
||||
if (verifySessionRequest.VerificationKey == "012345")
|
||||
verifySessionRequest.TriggerSuccess();
|
||||
else
|
||||
verifySessionRequest.TriggerFailure(new WebException());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "012345");
|
||||
assertAPIState(APIState.Online);
|
||||
assertDropdownState(UserAction.Online);
|
||||
|
||||
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
|
||||
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
|
||||
|
||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||
|
||||
assertDropdownState(UserAction.Online);
|
||||
AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb));
|
||||
assertDropdownState(UserAction.DoNotDisturb);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoginSuccess_TOTPVerification_FallbackToEmail()
|
||||
{
|
||||
AddStep("logout", () => API.Logout());
|
||||
assertAPIState(APIState.Offline);
|
||||
AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword);
|
||||
|
||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
|
||||
|
||||
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case VerifySessionRequest verifySessionRequest:
|
||||
if (verifySessionRequest.VerificationKey == "deadbeef")
|
||||
verifySessionRequest.TriggerSuccess();
|
||||
else
|
||||
verifySessionRequest.TriggerFailure(new WebException());
|
||||
return true;
|
||||
|
||||
case VerificationMailFallbackRequest verificationMailFallbackRequest:
|
||||
verificationMailFallbackRequest.TriggerSuccess();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("request fallback to email", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<OsuSpriteText>().Single(t => t.Text.ToString().Contains("email", StringComparison.InvariantCultureIgnoreCase)));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "deadbeef");
|
||||
assertAPIState(APIState.Online);
|
||||
assertDropdownState(UserAction.Online);
|
||||
|
||||
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
|
||||
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
|
||||
|
||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||
|
||||
assertDropdownState(UserAction.Online);
|
||||
AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb));
|
||||
assertDropdownState(UserAction.DoNotDisturb);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoginSuccess_TOTPVerification_TurnedOffMidwayThrough()
|
||||
{
|
||||
bool firstAttemptHandled = false;
|
||||
|
||||
AddStep("logout", () => API.Logout());
|
||||
assertAPIState(APIState.Offline);
|
||||
AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword);
|
||||
|
||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
|
||||
|
||||
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case VerifySessionRequest verifySessionRequest:
|
||||
verifySessionRequest.RequiredVerificationMethod = SessionVerificationMethod.EmailMessage;
|
||||
verifySessionRequest.TriggerFailure(new WebException());
|
||||
firstAttemptHandled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "123456");
|
||||
AddUntilStep("first verification attempt handled", () => firstAttemptHandled);
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
|
||||
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case VerifySessionRequest verifySessionRequest:
|
||||
if (verifySessionRequest.VerificationKey == "deadbeef")
|
||||
verifySessionRequest.TriggerSuccess();
|
||||
else
|
||||
verifySessionRequest.TriggerFailure(new WebException());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "deadbeef");
|
||||
assertAPIState(APIState.Online);
|
||||
assertDropdownState(UserAction.Online);
|
||||
}
|
||||
|
||||
private void assertDropdownState(UserAction state)
|
||||
{
|
||||
AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType<UserDropdown>().First().Current.Value, () => Is.EqualTo(state));
|
||||
|
||||
@@ -278,6 +278,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
advanceToSongSelect();
|
||||
openSkinEditor();
|
||||
AddUntilStep("skin editor visible", () => skinEditor.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
@@ -290,8 +292,9 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
|
||||
|
||||
toggleSkinEditor();
|
||||
AddUntilStep("skin editor hidden", () => skinEditor.State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1)));
|
||||
AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(2)));
|
||||
AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0));
|
||||
|
||||
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@@ -217,6 +218,26 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("leaderboard matches gameplay beatmap", () => Game.ChildrenOfType<LeaderboardManager>().Single().CurrentCriteria?.Beatmap, () => Is.EqualTo(beatmap().BeatmapInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnterKeyProgressesToGameplayEvenIfCarouselFilteredOut()
|
||||
{
|
||||
PushAndConfirm(() => new SoloSongSelect());
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("filter out active beatmap", () => this.ChildrenOfType<SearchTextBox>().First().Text = "abacadabadaeba");
|
||||
AddUntilStep("wait for filter", () => this.ChildrenOfType<BeatmapCarousel>().Single().IsFiltering, () => Is.False);
|
||||
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("player entered", () =>
|
||||
{
|
||||
DismissAnyNotifications();
|
||||
return Game.ScreenStack.CurrentScreen is Player;
|
||||
});
|
||||
}
|
||||
|
||||
private Func<Player> playToResults()
|
||||
{
|
||||
var player = playToCompletion();
|
||||
|
||||
@@ -86,11 +86,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
|
||||
|
||||
AddAssert("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
AddUntilStep("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
|
||||
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
|
||||
|
||||
AddAssert("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
AddUntilStep("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -185,11 +185,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
var groupModel = (GroupDefinition)groupItem.Model;
|
||||
|
||||
Assert.That(groupModel.Title, Is.EqualTo(expectedTitle));
|
||||
Assert.That(groupModel.Title.ToString(), Is.EqualTo(expectedTitle));
|
||||
Assert.That(itemsInGroup.Select(i => i.Model).OfType<GroupedBeatmap>().Select(gb => gb.Beatmap), Is.EquivalentTo(expectedBeatmaps));
|
||||
|
||||
totalItems += itemsInGroup.Count() + 1;
|
||||
|
||||
@@ -249,5 +249,39 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupDoesNotExpandAgainOnRefilterIfManuallyCollapsed()
|
||||
{
|
||||
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
|
||||
CheckDisplayedGroupsCount(1);
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
CheckHasSelection();
|
||||
|
||||
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
|
||||
CheckDisplayedGroupsCount(5);
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
|
||||
ToggleGroupCollapse();
|
||||
|
||||
ApplyToFilterAndWaitForFilter("apply no-op filter", c => c.AllowConvertedBeatmaps = !c.AllowConvertedBeatmaps);
|
||||
AddAssert("group didn't re-expand", () => Carousel.ExpandedGroup, () => Is.Null);
|
||||
|
||||
ToggleGroupCollapse();
|
||||
AddAssert("beatmap set re-expanded correctly", () => Carousel.ExpandedBeatmapSet?.BeatmapSet, () => Is.EqualTo(BeatmapSets[2]));
|
||||
|
||||
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[1].Metadata.Title);
|
||||
|
||||
CheckDisplayedGroupsCount(1);
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
|
||||
CheckHasSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,5 +337,31 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddAssert("expanded group is still first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpandedGroupDoesNotExpandAgainOnRefilterIfManuallyCollapsed()
|
||||
{
|
||||
SelectPrevSet();
|
||||
|
||||
WaitForBeatmapSelection(2, 9);
|
||||
AddAssert("expanded group is last", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(6));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
WaitForBeatmapSelection(2, 9);
|
||||
AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0));
|
||||
|
||||
ToggleGroupCollapse();
|
||||
|
||||
// doesn't actually filter anything away, but triggers a filter.
|
||||
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Some");
|
||||
AddAssert("group didn't re-expand", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.Null);
|
||||
|
||||
ToggleGroupCollapse();
|
||||
|
||||
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Som");
|
||||
AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
|
||||
|
||||
AddAssert("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
AddUntilStep("check count 5", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
|
||||
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
|
||||
|
||||
AddAssert("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
AddUntilStep("check count 2", () => dropdown.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -186,11 +186,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.MinusSquare);
|
||||
|
||||
addClickAddOrRemoveButtonStep(1);
|
||||
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||
assertFirstButtonIs(FontAwesome.Solid.PlusSquare);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
@@ -86,6 +87,64 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRanks()
|
||||
{
|
||||
for (int i = -1; i <= 7; i++)
|
||||
{
|
||||
ScoreRank rank = (ScoreRank)i;
|
||||
|
||||
AddStep($"display rank {rank}", () =>
|
||||
{
|
||||
ContentContainer.Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||
},
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new[]
|
||||
{
|
||||
new PanelGroupRankDisplay
|
||||
{
|
||||
Item = new CarouselItem(new RankDisplayGroupDefinition(rank))
|
||||
},
|
||||
new PanelGroupRankDisplay
|
||||
{
|
||||
Item = new CarouselItem(new RankDisplayGroupDefinition(rank)),
|
||||
KeyboardSelected = { Value = true },
|
||||
},
|
||||
new PanelGroupRankDisplay
|
||||
{
|
||||
Item = new CarouselItem(new RankDisplayGroupDefinition(rank)),
|
||||
Expanded = { Value = true },
|
||||
},
|
||||
new PanelGroupRankDisplay
|
||||
{
|
||||
Item = new CarouselItem(new RankDisplayGroupDefinition(rank)),
|
||||
Expanded = { Value = true },
|
||||
KeyboardSelected = { Value = true },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
return new OsuContextMenuContainer
|
||||
|
||||
+11
-7
@@ -55,22 +55,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
waitForFiltering(6);
|
||||
|
||||
BeatmapInfo? initiallySelected = null;
|
||||
AddAssert("selected is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
AddAssert("carousel beatmap is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
AddUntilStep("global beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
|
||||
ChangeRuleset(0);
|
||||
waitForFiltering(7);
|
||||
AddAssert("selected is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0));
|
||||
AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet));
|
||||
AddAssert("carousel beatmap is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0));
|
||||
AddUntilStep("global beatmap is osu", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(0));
|
||||
AddAssert("carousel beatmap is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet));
|
||||
|
||||
ChangeRuleset(1);
|
||||
waitForFiltering(8);
|
||||
AddAssert("selected is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet));
|
||||
AddAssert("carousel beatmap is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
AddUntilStep("global beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
|
||||
AddAssert("carousel beatmap is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet));
|
||||
|
||||
ChangeRuleset(2);
|
||||
waitForFiltering(9);
|
||||
AddAssert("selected is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2));
|
||||
AddAssert("selected is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet));
|
||||
AddAssert("carousel beatmap is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2));
|
||||
AddUntilStep("global beatmap is catch", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(2));
|
||||
AddAssert("carousel beatmap is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -245,7 +245,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
GroupBy(GroupMode.RankAchieved);
|
||||
WaitForFiltering();
|
||||
|
||||
assertGroupPresent("S+", () => new[] { beatmapSets[0] });
|
||||
assertGroupPresent("Silver S", () => new[] { beatmapSets[0] });
|
||||
assertGroupPresent("A", () => new[] { beatmapSets[1] });
|
||||
assertGroupPresent("C", () => new[] { beatmapSets[2] });
|
||||
assertGroupPresent("Unplayed", () => new[] { beatmapSets[3], beatmapSets[4] });
|
||||
@@ -316,7 +316,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddAssert($"\"{name}\" present", () =>
|
||||
{
|
||||
var group = grouping.GroupItems.Single(g => g.Key.Title == name);
|
||||
var group = grouping.GroupItems.Single(g => g.Key.Title.ToString() == name);
|
||||
var actualBeatmaps = group.Value.Select(i => i.Model).OfType<GroupedBeatmap>().Select(gb => gb.Beatmap).OrderBy(b => b.ID);
|
||||
var expectedBeatmaps = getBeatmaps().SelectMany(s => s.Beatmaps).OrderBy(b => b.ID);
|
||||
return actualBeatmaps.SequenceEqual(expectedBeatmaps);
|
||||
|
||||
@@ -68,14 +68,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
|
||||
[TestCase(Key.P, Key.P)]
|
||||
[TestCase(Key.M, Key.P)]
|
||||
[TestCase(Key.L, Key.P)]
|
||||
[TestCase(Key.B, Key.E)]
|
||||
[TestCase(Key.S, Key.E)]
|
||||
[TestCase(Key.D, null)]
|
||||
[TestCase(Key.Q, null)]
|
||||
[TestCase(Key.O, null)]
|
||||
public void TestShortcutKeys(Key key, Key? subMenuEnterKey)
|
||||
[TestCase(Key.M, Key.M, Key.L)]
|
||||
[TestCase(Key.M, Key.M, Key.M)]
|
||||
[TestCase(Key.L, Key.L)]
|
||||
[TestCase(Key.B, Key.E, Key.B)]
|
||||
[TestCase(Key.S, Key.E, Key.S)]
|
||||
[TestCase(Key.D)]
|
||||
[TestCase(Key.Q)]
|
||||
[TestCase(Key.O)]
|
||||
public void TestShortcutKeys(params Key[] keys)
|
||||
{
|
||||
int activationCount = -1;
|
||||
AddStep("set up action", () =>
|
||||
@@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
activationCount = 0;
|
||||
void action() => activationCount++;
|
||||
|
||||
switch (key)
|
||||
switch (keys.First())
|
||||
{
|
||||
case Key.P:
|
||||
buttons.OnSolo = action;
|
||||
@@ -119,16 +120,19 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
});
|
||||
|
||||
AddStep($"press {key}", () => InputManager.Key(key));
|
||||
// trigger out of idle state
|
||||
AddStep($"press {keys.First()}", () => InputManager.Key(keys.First()));
|
||||
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
|
||||
|
||||
if (subMenuEnterKey != null)
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value));
|
||||
AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel);
|
||||
var key = keys[i];
|
||||
AddStep($"press {key}", () => InputManager.Key(key));
|
||||
|
||||
if (i > 0)
|
||||
AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel);
|
||||
}
|
||||
|
||||
AddStep($"press {key}", () => InputManager.Key(key));
|
||||
AddAssert("action triggered", () => activationCount == 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using Color4 = osuTK.Graphics.Color4;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
public const float TRANSITION_DURATION = 340;
|
||||
public const float CORNER_RADIUS = 8;
|
||||
|
||||
protected const float WIDTH = 345;
|
||||
public const float WIDTH = 345;
|
||||
|
||||
public IBindable<bool> Expanded { get; }
|
||||
|
||||
@@ -77,25 +77,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
|
||||
containingInputManager = GetContainingInputManager();
|
||||
|
||||
Action = () =>
|
||||
{
|
||||
if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true)
|
||||
{
|
||||
switch (DownloadTracker.State.Value)
|
||||
{
|
||||
case DownloadState.NotDownloaded:
|
||||
if (!BeatmapSet.Availability.DownloadDisabled)
|
||||
beatmaps?.Download(BeatmapSet, preferNoVideo.Value);
|
||||
break;
|
||||
if (Action == null)
|
||||
throw new InvalidOperationException($"An action should be assigned to this {nameof(BeatmapCard)}. To use the default, assign {nameof(DefaultAction)}.");
|
||||
}
|
||||
|
||||
case DownloadState.LocallyAvailable:
|
||||
game?.PresentBeatmap(BeatmapSet);
|
||||
break;
|
||||
}
|
||||
protected void DefaultAction()
|
||||
{
|
||||
if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true)
|
||||
{
|
||||
switch (DownloadTracker.State.Value)
|
||||
{
|
||||
case DownloadState.NotDownloaded:
|
||||
if (!BeatmapSet.Availability.DownloadDisabled) beatmaps?.Download(BeatmapSet, preferNoVideo.Value);
|
||||
break;
|
||||
|
||||
case DownloadState.LocallyAvailable:
|
||||
game?.PresentBeatmap(BeatmapSet);
|
||||
break;
|
||||
}
|
||||
else
|
||||
beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
|
||||
};
|
||||
}
|
||||
else
|
||||
beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo)
|
||||
public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), 500, 500)
|
||||
cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.Transparent
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
: base(beatmapSet, allowExpansion)
|
||||
{
|
||||
content = new BeatmapCardContent(height);
|
||||
|
||||
Action = DefaultAction;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
|
||||
@@ -54,6 +54,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
: base(beatmapSet, false)
|
||||
{
|
||||
content = new BeatmapCardContent(height);
|
||||
|
||||
Action = DefaultAction;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
: base(beatmapSet, allowExpansion)
|
||||
{
|
||||
content = new BeatmapCardContent(HEIGHT);
|
||||
|
||||
Action = DefaultAction;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -35,11 +35,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo)
|
||||
public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
|
||||
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List, keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OnlineInfo = onlineInfo
|
||||
|
||||
@@ -16,10 +16,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
|
||||
|
||||
private readonly APIBeatmapSet beatmapSet;
|
||||
private readonly bool allowNavigationToBeatmap;
|
||||
|
||||
public GoToBeatmapButton(APIBeatmapSet beatmapSet)
|
||||
public GoToBeatmapButton(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap)
|
||||
{
|
||||
this.beatmapSet = beatmapSet;
|
||||
this.allowNavigationToBeatmap = allowNavigationToBeatmap;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
@@ -27,7 +29,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
{
|
||||
Action = () => game?.PresentBeatmap(beatmapSet);
|
||||
Icon.Icon = FontAwesome.Solid.AngleDoubleRight;
|
||||
TooltipText = "Go to beatmap";
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -40,8 +41,31 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Enabled.Value = state.Value == DownloadState.LocallyAvailable;
|
||||
this.FadeTo(Enabled.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
||||
bool available = state.Value == DownloadState.LocallyAvailable;
|
||||
Enabled.Value = allowNavigationToBeatmap && available;
|
||||
|
||||
float alpha;
|
||||
|
||||
if (available && allowNavigationToBeatmap)
|
||||
{
|
||||
TooltipText = "Go to beatmap";
|
||||
Enabled.Value = true;
|
||||
alpha = 1f;
|
||||
}
|
||||
else if (available)
|
||||
{
|
||||
TooltipText = string.Empty;
|
||||
Enabled.Value = false;
|
||||
alpha = 0.3f;
|
||||
}
|
||||
else
|
||||
{
|
||||
TooltipText = string.Empty;
|
||||
Enabled.Value = false;
|
||||
alpha = 0;
|
||||
}
|
||||
|
||||
this.FadeTo(alpha, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
set
|
||||
{
|
||||
buttonsExpandedWidth = value;
|
||||
buttonArea.Width = value;
|
||||
if (IsLoaded)
|
||||
updateState();
|
||||
}
|
||||
@@ -67,7 +66,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public CollapsibleButtonContainer(APIBeatmapSet beatmapSet)
|
||||
public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true, bool keepBackgroundLoaded = false)
|
||||
{
|
||||
downloadTracker = new BeatmapDownloadTracker(beatmapSet);
|
||||
|
||||
@@ -116,14 +115,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
},
|
||||
new GoToBeatmapButton(beatmapSet)
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
State = { BindTarget = downloadTracker.State },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -135,7 +126,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new BeatmapCardContentBackground(beatmapSet)
|
||||
new BeatmapCardContentBackground(beatmapSet, keepBackgroundLoaded)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Dimmed = { BindTarget = ShowDetails }
|
||||
@@ -152,6 +143,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
buttons.Add(new GoToBeatmapButton(beatmapSet, allowNavigationToBeatmap)
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
State = { BindTarget = downloadTracker.State },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -165,6 +165,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
buttonArea.Width = buttonsExpandedWidth;
|
||||
|
||||
float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth;
|
||||
float mainAreaWidth = Width - buttonAreaWidth;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Extensions
|
||||
|
||||
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
|
||||
|
||||
return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}");
|
||||
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -453,8 +453,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
// matching with exact modifier consideration (so Ctrl+Enter would be ignored).
|
||||
case Key.Enter:
|
||||
case Key.KeypadEnter:
|
||||
activateSelection();
|
||||
return true;
|
||||
return activateSelection();
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
@@ -465,8 +464,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Select:
|
||||
activateSelection();
|
||||
return true;
|
||||
return activateSelection();
|
||||
|
||||
// the selection traversal handlers below are scheduled to avoid an issue
|
||||
// wherein if the update frame rate is low, keeping one of the actions below pressed leads to selection moving back to the start / end.
|
||||
@@ -560,10 +558,15 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
}
|
||||
|
||||
private void activateSelection()
|
||||
private bool activateSelection()
|
||||
{
|
||||
if (currentKeyboardSelection.CarouselItem != null)
|
||||
{
|
||||
Activate(currentKeyboardSelection.CarouselItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void traverseKeyboardSelection(int direction)
|
||||
@@ -761,10 +764,10 @@ namespace osu.Game.Graphics.Carousel
|
||||
updateItemYPosition(item, ref lastVisible, ref yPos);
|
||||
|
||||
if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!))
|
||||
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i);
|
||||
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i);
|
||||
|
||||
if (CheckModelEquality(item.Model, currentSelection.Model!))
|
||||
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i);
|
||||
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i);
|
||||
}
|
||||
|
||||
// Update the total height of all items (to make the scroll container scrollable through the full height even though
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private HoverSounds samples = null!;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
|
||||
base.ReceivePositionalInputAt(screenSpacePos)
|
||||
@@ -33,6 +35,14 @@ namespace osu.Game.Graphics.Containers
|
||||
this.sampleSet = sampleSet;
|
||||
}
|
||||
|
||||
public void TriggerClickWithSound()
|
||||
{
|
||||
TriggerClick();
|
||||
|
||||
// TriggerClick doesn't recursively fire the event so we need to manually do this.
|
||||
(samples as HoverClickSounds)?.PlayClickSample();
|
||||
}
|
||||
|
||||
public virtual LocalisableString TooltipText { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -46,7 +56,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
CreateHoverSounds(sampleSet),
|
||||
samples = CreateHoverSounds(sampleSet),
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,77 +16,19 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
public static class OsuIcon
|
||||
{
|
||||
#region Legacy spritesheet-based icons
|
||||
|
||||
private static IconUsage get(int icon) => new IconUsage((char)icon, @"osuFont");
|
||||
|
||||
// ruleset icons in circles
|
||||
public static IconUsage RulesetOsu => get(0xe000);
|
||||
public static IconUsage RulesetMania => get(0xe001);
|
||||
public static IconUsage RulesetCatch => get(0xe002);
|
||||
public static IconUsage RulesetTaiko => get(0xe003);
|
||||
|
||||
// ruleset icons without circles
|
||||
public static IconUsage FilledCircle => get(0xe004);
|
||||
public static IconUsage Logo => get(0xe006);
|
||||
public static IconUsage ChevronDownCircle => get(0xe007);
|
||||
public static IconUsage EditCircle => get(0xe033);
|
||||
public static IconUsage LeftCircle => get(0xe034);
|
||||
public static IconUsage RightCircle => get(0xe035);
|
||||
public static IconUsage Charts => get(0xe036);
|
||||
public static IconUsage Solo => get(0xe037);
|
||||
public static IconUsage Multi => get(0xe038);
|
||||
public static IconUsage Gear => get(0xe039);
|
||||
|
||||
// misc icons
|
||||
public static IconUsage Bat => get(0xe008);
|
||||
public static IconUsage Bubble => get(0xe009);
|
||||
public static IconUsage BubblePop => get(0xe02e);
|
||||
public static IconUsage Dice => get(0xe011);
|
||||
public static IconUsage HeartBreak => get(0xe030);
|
||||
public static IconUsage Hot => get(0xe031);
|
||||
public static IconUsage ListSearch => get(0xe032);
|
||||
|
||||
//osu! playstyles
|
||||
public static IconUsage PlayStyleTablet => get(0xe02a);
|
||||
public static IconUsage PlayStyleMouse => get(0xe029);
|
||||
public static IconUsage PlayStyleKeyboard => get(0xe02b);
|
||||
public static IconUsage PlayStyleTouch => get(0xe02c);
|
||||
|
||||
// osu! difficulties
|
||||
public static IconUsage EasyOsu => get(0xe015);
|
||||
public static IconUsage NormalOsu => get(0xe016);
|
||||
public static IconUsage HardOsu => get(0xe017);
|
||||
public static IconUsage InsaneOsu => get(0xe018);
|
||||
public static IconUsage ExpertOsu => get(0xe019);
|
||||
|
||||
// taiko difficulties
|
||||
public static IconUsage EasyTaiko => get(0xe01a);
|
||||
public static IconUsage NormalTaiko => get(0xe01b);
|
||||
public static IconUsage HardTaiko => get(0xe01c);
|
||||
public static IconUsage InsaneTaiko => get(0xe01d);
|
||||
public static IconUsage ExpertTaiko => get(0xe01e);
|
||||
|
||||
// fruits difficulties
|
||||
public static IconUsage EasyFruits => get(0xe01f);
|
||||
public static IconUsage NormalFruits => get(0xe020);
|
||||
public static IconUsage HardFruits => get(0xe021);
|
||||
public static IconUsage InsaneFruits => get(0xe022);
|
||||
public static IconUsage ExpertFruits => get(0xe023);
|
||||
|
||||
// mania difficulties
|
||||
public static IconUsage EasyMania => get(0xe024);
|
||||
public static IconUsage NormalMania => get(0xe025);
|
||||
public static IconUsage HardMania => get(0xe026);
|
||||
public static IconUsage InsaneMania => get(0xe027);
|
||||
public static IconUsage ExpertMania => get(0xe028);
|
||||
|
||||
#endregion
|
||||
|
||||
#region New single-file-based icons
|
||||
|
||||
public const string FONT_NAME = @"Icons";
|
||||
|
||||
// ruleset icons
|
||||
public static IconUsage RulesetOsu => get(OsuIconMapping.RulesetOsu);
|
||||
public static IconUsage RulesetMania => get(OsuIconMapping.RulesetMania);
|
||||
public static IconUsage RulesetCatch => get(OsuIconMapping.RulesetCatch);
|
||||
public static IconUsage RulesetTaiko => get(OsuIconMapping.RulesetTaiko);
|
||||
|
||||
public static IconUsage Logo => get(OsuIconMapping.Logo);
|
||||
public static IconUsage EditCircle => get(OsuIconMapping.EditCircle);
|
||||
public static IconUsage LeftCircle => get(OsuIconMapping.LeftCircle);
|
||||
public static IconUsage RightCircle => get(OsuIconMapping.RightCircle);
|
||||
|
||||
public static IconUsage Audio => get(OsuIconMapping.Audio);
|
||||
public static IconUsage Beatmap => get(OsuIconMapping.Beatmap);
|
||||
public static IconUsage Calendar => get(OsuIconMapping.Calendar);
|
||||
@@ -246,6 +188,30 @@ namespace osu.Game.Graphics
|
||||
|
||||
private enum OsuIconMapping
|
||||
{
|
||||
[Description(@"Logo")]
|
||||
Logo,
|
||||
|
||||
[Description(@"RulesetOsu")]
|
||||
RulesetOsu,
|
||||
|
||||
[Description(@"RulesetMania")]
|
||||
RulesetMania,
|
||||
|
||||
[Description(@"RulesetCatch")]
|
||||
RulesetCatch,
|
||||
|
||||
[Description(@"RulesetTaiko")]
|
||||
RulesetTaiko,
|
||||
|
||||
[Description(@"EditCircle")]
|
||||
EditCircle,
|
||||
|
||||
[Description(@"LeftCircle")]
|
||||
LeftCircle,
|
||||
|
||||
[Description(@"RightCircle")]
|
||||
RightCircle,
|
||||
|
||||
[Description(@"audio")]
|
||||
Audio,
|
||||
|
||||
@@ -737,7 +703,5 @@ namespace osu.Game.Graphics
|
||||
textures.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,18 +42,20 @@ namespace osu.Game.Graphics.UserInterface
|
||||
this.buttons = buttons ?? new[] { MouseButton.Left };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select")
|
||||
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
|
||||
|
||||
sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled")
|
||||
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled");
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (buttons.Contains(e.Button))
|
||||
{
|
||||
var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel();
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
|
||||
channel.Play();
|
||||
}
|
||||
}
|
||||
PlayClickSample();
|
||||
|
||||
return base.OnClick(e);
|
||||
}
|
||||
@@ -66,14 +68,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.PlayHoverSample();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
public void PlayClickSample()
|
||||
{
|
||||
sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select")
|
||||
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
|
||||
var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel();
|
||||
|
||||
sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled")
|
||||
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled");
|
||||
if (channel != null)
|
||||
{
|
||||
channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
|
||||
channel.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ExceptionExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -52,6 +53,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
public string ProvidedUsername { get; private set; }
|
||||
|
||||
public SessionVerificationMethod? SessionVerificationMethod { get; set; }
|
||||
|
||||
public string SecondFactorCode { get; private set; }
|
||||
|
||||
private string password;
|
||||
@@ -292,7 +295,17 @@ namespace osu.Game.Online.API
|
||||
verificationRequest.Failure += ex =>
|
||||
{
|
||||
state.Value = APIState.RequiresSecondFactorAuth;
|
||||
LastLoginError = ex;
|
||||
|
||||
if (verificationRequest.RequiredVerificationMethod != null)
|
||||
{
|
||||
SessionVerificationMethod = verificationRequest.RequiredVerificationMethod;
|
||||
LastLoginError = new APIException($"Must use {SessionVerificationMethod.GetDescription().ToLowerInvariant()} to complete verification.", ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
LastLoginError = ex;
|
||||
}
|
||||
|
||||
SecondFactorCode = null;
|
||||
};
|
||||
|
||||
@@ -337,7 +350,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
localUser.Value = me;
|
||||
configSupporter.Value = me.IsSupporter;
|
||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||
SessionVerificationMethod = me.SessionVerificationMethod;
|
||||
state.Value = SessionVerificationMethod == null ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||
failureCount = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests;
|
||||
@@ -62,7 +63,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
private bool shouldFailNextLogin;
|
||||
private bool stayConnectingNextLogin;
|
||||
private bool requiredSecondFactorAuth = true;
|
||||
|
||||
public SessionVerificationMethod? SessionVerificationMethod { get; set; } = Requests.Responses.SessionVerificationMethod.EmailMessage;
|
||||
|
||||
/// <summary>
|
||||
/// The current connectivity state of the API.
|
||||
@@ -130,14 +132,14 @@ namespace osu.Game.Online.API
|
||||
Id = DUMMY_USER_ID,
|
||||
};
|
||||
|
||||
if (requiredSecondFactorAuth)
|
||||
if (SessionVerificationMethod != null)
|
||||
{
|
||||
state.Value = APIState.RequiresSecondFactorAuth;
|
||||
}
|
||||
else
|
||||
{
|
||||
onSuccessfulLogin();
|
||||
requiredSecondFactorAuth = true;
|
||||
SessionVerificationMethod = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +149,16 @@ namespace osu.Game.Online.API
|
||||
request.Failure += e =>
|
||||
{
|
||||
state.Value = APIState.RequiresSecondFactorAuth;
|
||||
LastLoginError = e;
|
||||
|
||||
if (request.RequiredVerificationMethod != null)
|
||||
{
|
||||
SessionVerificationMethod = request.RequiredVerificationMethod;
|
||||
LastLoginError = new APIException($"Must use {SessionVerificationMethod.GetDescription().ToLowerInvariant()} to complete verification.", e);
|
||||
}
|
||||
else
|
||||
{
|
||||
LastLoginError = e;
|
||||
}
|
||||
};
|
||||
|
||||
state.Value = APIState.Connecting;
|
||||
@@ -204,7 +215,7 @@ namespace osu.Game.Online.API
|
||||
/// <summary>
|
||||
/// Skip 2FA requirement for next login.
|
||||
/// </summary>
|
||||
public void SkipSecondFactor() => requiredSecondFactorAuth = false;
|
||||
public void SkipSecondFactor() => SessionVerificationMethod = null;
|
||||
|
||||
/// <summary>
|
||||
/// During the next simulated login, the process will fail immediately.
|
||||
|
||||
@@ -107,10 +107,15 @@ namespace osu.Game.Online.API
|
||||
/// <param name="password">The user's password.</param>
|
||||
void Login(string username, string password);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SessionVerificationMethod"/> requested by the server to complete verification.
|
||||
/// </summary>
|
||||
SessionVerificationMethod? SessionVerificationMethod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provide a second-factor authentication code for authentication.
|
||||
/// </summary>
|
||||
/// <param name="code">The 2FA code.</param>
|
||||
/// <paramref name="code">The 2FA code.</paramref>
|
||||
void AuthenticateSecondFactor(string code);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APIMe : APIUser
|
||||
{
|
||||
[JsonProperty("session_verified")]
|
||||
public bool SessionVerified { get; set; }
|
||||
[JsonProperty("session_verification_method")]
|
||||
public SessionVerificationMethod? SessionVerificationMethod { get; set; }
|
||||
}
|
||||
|
||||
public enum SessionVerificationMethod
|
||||
{
|
||||
[Description("Timed one-time password")]
|
||||
[EnumMember(Value = "totp")]
|
||||
TimedOneTimePassword,
|
||||
|
||||
[Description("E-mail")]
|
||||
[EnumMember(Value = "mail")]
|
||||
EmailMessage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class VerificationMailFallbackRequest : APIRequest
|
||||
{
|
||||
protected override string Target => @"session/verify/mail-fallback";
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
return req;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
@@ -13,6 +15,16 @@ namespace osu.Game.Online.API.Requests
|
||||
public VerifySessionRequest(string verificationKey)
|
||||
{
|
||||
VerificationKey = verificationKey;
|
||||
|
||||
Failure += _ =>
|
||||
{
|
||||
string? response = WebRequest?.GetResponseString();
|
||||
if (string.IsNullOrEmpty(response))
|
||||
return;
|
||||
|
||||
var responseObject = JsonConvert.DeserializeObject<VerificationFailureResponse>(response);
|
||||
RequiredVerificationMethod = responseObject?.RequiredSessionVerificationMethod;
|
||||
};
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
@@ -26,5 +38,13 @@ namespace osu.Game.Online.API.Requests
|
||||
}
|
||||
|
||||
protected override string Target => @"session/verify";
|
||||
|
||||
public SessionVerificationMethod? RequiredVerificationMethod { get; internal set; }
|
||||
|
||||
private class VerificationFailureResponse
|
||||
{
|
||||
[JsonProperty("method")]
|
||||
public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@@ -53,9 +52,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
Origin = Anchor.Centre,
|
||||
Spacing = new Vector2(-3, 0),
|
||||
Padding = new MarginPadding { Top = 5 },
|
||||
Colour = GetRankNameColour(rank),
|
||||
Colour = GetRankLetterColour(rank),
|
||||
Font = OsuFont.Numeric.With(size: 25),
|
||||
Text = GetRankName(rank),
|
||||
Text = GetRankLetter(rank),
|
||||
ShadowColour = Color4.Black.Opacity(0.3f),
|
||||
ShadowOffset = new Vector2(0, 0.08f),
|
||||
Shadow = true,
|
||||
@@ -65,12 +64,29 @@ namespace osu.Game.Online.Leaderboards
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetRankName(ScoreRank rank) => rank.GetDescription().TrimEnd('+');
|
||||
/// <summary>
|
||||
/// Returns letters to be shown in places where ranks are shown on a badge or similar to the user.
|
||||
/// </summary>
|
||||
public static string GetRankLetter(ScoreRank rank)
|
||||
{
|
||||
switch (rank)
|
||||
{
|
||||
case ScoreRank.SH:
|
||||
return @"S";
|
||||
|
||||
case ScoreRank.X:
|
||||
case ScoreRank.XH:
|
||||
return @"SS";
|
||||
|
||||
default:
|
||||
return rank.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the grade text colour.
|
||||
/// </summary>
|
||||
public static ColourInfo GetRankNameColour(ScoreRank rank)
|
||||
public static ColourInfo GetRankLetterColour(ScoreRank rank)
|
||||
{
|
||||
switch (rank)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Online.Matchmaking.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// An action performed on a user's avatar in a matchmaking room.
|
||||
/// </summary>
|
||||
public enum MatchmakingAvatarAction
|
||||
{
|
||||
Jump
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
|
||||
namespace osu.Game.Online.Matchmaking.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// An action performed by a user in a matchmaking room.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class MatchmakingAvatarActionEvent : MatchServerEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The user performing the action.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public int UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The action.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public MatchmakingAvatarAction Action { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
|
||||
namespace osu.Game.Online.Matchmaking.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Requests to perform an action on a user's avatar in a matchmaking room.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
public class MatchmakingAvatarActionRequest : MatchUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The action.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public MatchmakingAvatarAction Action { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
@@ -15,6 +16,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||
[Union(0, typeof(CountdownStartedEvent))]
|
||||
[Union(1, typeof(CountdownStoppedEvent))]
|
||||
[Union(2, typeof(MatchmakingAvatarActionEvent))]
|
||||
public abstract class MatchServerEvent
|
||||
{
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
|
||||
@@ -17,6 +18,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Union(0, typeof(ChangeTeamRequest))]
|
||||
[Union(1, typeof(StartMatchCountdownRequest))]
|
||||
[Union(2, typeof(StopCountdownRequest))]
|
||||
[Union(3, typeof(MatchmakingAvatarActionRequest))]
|
||||
public abstract class MatchUserRequest
|
||||
{
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@@ -26,7 +27,7 @@ using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
|
||||
public abstract partial class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer, IMatchmakingServer, IMatchmakingClient
|
||||
{
|
||||
public Action<Notification>? PostNotification { protected get; set; }
|
||||
|
||||
@@ -112,6 +113,24 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
public event Action<MultiplayerCountdown>? CountdownStarted;
|
||||
|
||||
public event Action<MultiplayerCountdown>? CountdownStopped;
|
||||
|
||||
public event Action<MatchServerEvent>? MatchEvent;
|
||||
|
||||
public event Action<MultiplayerRoomUser, MultiplayerUserState>? UserStateChanged;
|
||||
|
||||
public event Action? MatchmakingQueueJoined;
|
||||
public event Action? MatchmakingQueueLeft;
|
||||
public event Action? MatchmakingRoomInvited;
|
||||
public event Action<long, string>? MatchmakingRoomReady;
|
||||
public event Action<MatchmakingLobbyStatus>? MatchmakingLobbyStatusChanged;
|
||||
public event Action<MatchmakingQueueStatus>? MatchmakingQueueStatusChanged;
|
||||
public event Action<int, long>? MatchmakingItemSelected;
|
||||
public event Action<int, long>? MatchmakingItemDeselected;
|
||||
public event Action<MatchRoomState>? MatchRoomStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="MultiplayerClient"/> is currently connected.
|
||||
/// This is NOT thread safe and usage should be scheduled.
|
||||
@@ -179,9 +198,13 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
IsConnected.BindValueChanged(connected => Scheduler.Add(() =>
|
||||
{
|
||||
// clean up local room state on server disconnect.
|
||||
if (!connected.NewValue && Room != null)
|
||||
LeaveRoom();
|
||||
if (!connected.NewValue)
|
||||
{
|
||||
if (Room != null)
|
||||
LeaveRoom();
|
||||
|
||||
MatchmakingQueueLeft?.Invoke();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -254,6 +277,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room = joinedRoom;
|
||||
APIRoom = apiRoom;
|
||||
|
||||
while (pendingRequests.TryDequeue(out Action? action))
|
||||
action();
|
||||
|
||||
APIRoom.RoomID = joinedRoom.RoomID;
|
||||
APIRoom.ChannelId = joinedRoom.ChannelID;
|
||||
APIRoom.Host = joinedRoom.Host?.User;
|
||||
@@ -640,6 +666,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
user.State = state;
|
||||
updateUserPlayingState(userId, state);
|
||||
|
||||
UserStateChanged?.Invoke(user, state);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
@@ -672,13 +699,14 @@ namespace osu.Game.Online.Multiplayer
|
||||
Debug.Assert(Room != null);
|
||||
|
||||
Room.MatchState = state;
|
||||
MatchRoomStateChanged?.Invoke(state);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task MatchEvent(MatchServerEvent e)
|
||||
Task IMultiplayerClient.MatchEvent(MatchServerEvent e)
|
||||
{
|
||||
handleRoomRequest(() =>
|
||||
{
|
||||
@@ -688,6 +716,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
case CountdownStartedEvent countdownStartedEvent:
|
||||
Room.ActiveCountdowns.Add(countdownStartedEvent.Countdown);
|
||||
CountdownStarted?.Invoke(countdownStartedEvent.Countdown);
|
||||
|
||||
switch (countdownStartedEvent.Countdown)
|
||||
{
|
||||
@@ -700,11 +729,17 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
case CountdownStoppedEvent countdownStoppedEvent:
|
||||
MultiplayerCountdown? countdown = Room.ActiveCountdowns.FirstOrDefault(countdown => countdown.ID == countdownStoppedEvent.ID);
|
||||
|
||||
if (countdown != null)
|
||||
{
|
||||
Room.ActiveCountdowns.Remove(countdown);
|
||||
CountdownStopped?.Invoke(countdown);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
MatchEvent?.Invoke(e);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
@@ -1001,6 +1036,82 @@ namespace osu.Game.Online.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingQueueJoined()
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingQueueJoined?.Invoke());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingQueueLeft()
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingQueueLeft?.Invoke());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingRoomInvited()
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingRoomInvited?.Invoke());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingRoomReady(long roomId, string password)
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingRoomReady?.Invoke(roomId, password));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status)
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingLobbyStatusChanged?.Invoke(status));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingQueueStatusChanged(MatchmakingQueueStatus status)
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingQueueStatusChanged?.Invoke(status));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingItemSelected(int userId, long playlistItemId)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
MatchmakingItemSelected?.Invoke(userId, playlistItemId);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMatchmakingClient.MatchmakingItemDeselected(int userId, long playlistItemId)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
MatchmakingItemDeselected?.Invoke(userId, playlistItemId);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public abstract Task<MatchmakingPool[]> GetMatchmakingPools();
|
||||
|
||||
public abstract Task MatchmakingJoinLobby();
|
||||
|
||||
public abstract Task MatchmakingLeaveLobby();
|
||||
|
||||
public abstract Task MatchmakingJoinQueue(int poolId);
|
||||
|
||||
public abstract Task MatchmakingLeaveQueue();
|
||||
|
||||
public abstract Task MatchmakingAcceptInvitation();
|
||||
|
||||
public abstract Task MatchmakingDeclineInvitation();
|
||||
|
||||
public abstract Task MatchmakingToggleSelection(long playlistItemId);
|
||||
|
||||
public abstract Task MatchmakingSkipToNextStage();
|
||||
|
||||
private partial class MultiplayerInvitationNotification : UserAvatarNotification
|
||||
{
|
||||
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
@@ -70,6 +71,15 @@ namespace osu.Game.Online.Multiplayer
|
||||
connection.On<long>(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved);
|
||||
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged);
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested);
|
||||
|
||||
connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined);
|
||||
connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft);
|
||||
connection.On(nameof(IMatchmakingClient.MatchmakingRoomInvited), ((IMatchmakingClient)this).MatchmakingRoomInvited);
|
||||
connection.On<long, string>(nameof(IMatchmakingClient.MatchmakingRoomReady), ((IMatchmakingClient)this).MatchmakingRoomReady);
|
||||
connection.On<MatchmakingLobbyStatus>(nameof(IMatchmakingClient.MatchmakingLobbyStatusChanged), ((IMatchmakingClient)this).MatchmakingLobbyStatusChanged);
|
||||
connection.On<MatchmakingQueueStatus>(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged);
|
||||
connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected);
|
||||
connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected);
|
||||
};
|
||||
|
||||
IsConnected.BindTo(connector.IsConnected);
|
||||
@@ -310,6 +320,87 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connector.Disconnect();
|
||||
}
|
||||
|
||||
public override Task<MatchmakingPool[]> GetMatchmakingPools()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.FromResult(Array.Empty<MatchmakingPool>());
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync<MatchmakingPool[]>(nameof(IMatchmakingServer.GetMatchmakingPools));
|
||||
}
|
||||
|
||||
public override Task MatchmakingJoinLobby()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinLobby));
|
||||
}
|
||||
|
||||
public override Task MatchmakingLeaveLobby()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveLobby));
|
||||
}
|
||||
|
||||
public override Task MatchmakingJoinQueue(int poolId)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), poolId);
|
||||
}
|
||||
|
||||
public override Task MatchmakingLeaveQueue()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingLeaveQueue));
|
||||
}
|
||||
|
||||
public override Task MatchmakingAcceptInvitation()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingAcceptInvitation));
|
||||
}
|
||||
|
||||
public override Task MatchmakingDeclineInvitation()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingDeclineInvitation));
|
||||
}
|
||||
|
||||
public override Task MatchmakingToggleSelection(long playlistItemId)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingToggleSelection), playlistItemId);
|
||||
}
|
||||
|
||||
public override Task MatchmakingSkipToNextStage()
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingSkipToNextStage));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -358,6 +358,7 @@ namespace osu.Game.Online.Rooms
|
||||
public Room(MultiplayerRoom room)
|
||||
{
|
||||
RoomID = room.RoomID;
|
||||
ChannelId = room.ChannelID;
|
||||
Name = room.Settings.Name;
|
||||
Password = room.Settings.Password;
|
||||
Type = room.Settings.MatchType;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
@@ -53,7 +54,9 @@ namespace osu.Game.Online
|
||||
(typeof(MatchmakingQueueStatus.MatchFound), typeof(MatchmakingQueueStatus)),
|
||||
(typeof(MatchmakingQueueStatus.JoiningMatch), typeof(MatchmakingQueueStatus)),
|
||||
(typeof(MatchmakingRoomState), typeof(MatchRoomState)),
|
||||
(typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown))
|
||||
(typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)),
|
||||
(typeof(MatchmakingAvatarActionRequest), typeof(MatchUserRequest)),
|
||||
(typeof(MatchmakingAvatarActionEvent), typeof(MatchServerEvent)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Queue;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
@@ -79,6 +80,7 @@ using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Sentry;
|
||||
using IntroScreen = osu.Game.Screens.Menu.IntroScreen;
|
||||
using MatchType = osu.Game.Online.Rooms.MatchType;
|
||||
|
||||
namespace osu.Game
|
||||
@@ -1270,6 +1272,7 @@ namespace osu.Game
|
||||
|
||||
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
|
||||
loadComponentSingleFile<BeatmapStore>(detachedBeatmapStore = new RealmDetachedBeatmapStore(), Add, true);
|
||||
loadComponentSingleFile(new QueueController(), Add, true);
|
||||
|
||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||
Add(new MusicKeyBindingHandler());
|
||||
|
||||
@@ -467,8 +467,6 @@ namespace osu.Game
|
||||
|
||||
protected virtual void InitialiseFonts()
|
||||
{
|
||||
AddFont(Resources, @"Fonts/osuFont");
|
||||
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Light");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
@@ -21,11 +22,11 @@ namespace osu.Game.Overlays.Login
|
||||
{
|
||||
public partial class SecondFactorAuthForm : Container
|
||||
{
|
||||
private OsuTextBox codeTextBox = null!;
|
||||
private LinkFlowContainer explainText = null!;
|
||||
private ErrorTextFlowContainer errorText = null!;
|
||||
|
||||
private LoadingLayer loading = null!;
|
||||
private FillFlowContainer contentFlow = null!;
|
||||
private OsuTextBox codeTextBox = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
@@ -36,6 +37,8 @@ namespace osu.Game.Overlays.Login
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
@@ -46,46 +49,18 @@ namespace osu.Game.Overlays.Login
|
||||
Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
contentFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "An email has been sent to you with a verification code. Enter the code.",
|
||||
},
|
||||
codeTextBox = new OsuTextBox
|
||||
{
|
||||
InputProperties = new TextInputProperties(TextInputType.Code),
|
||||
PlaceholderText = "Enter code",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
errorText = new ErrorTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
new LinkFlowContainer
|
||||
errorText = new ErrorTextFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -95,6 +70,56 @@ namespace osu.Game.Overlays.Login
|
||||
}
|
||||
};
|
||||
|
||||
if (api.LastLoginError?.Message is string error)
|
||||
{
|
||||
errorText.Alpha = 1;
|
||||
errorText.AddErrors(new[] { error });
|
||||
}
|
||||
|
||||
showContent(api.SessionVerificationMethod!.Value);
|
||||
}
|
||||
|
||||
private void showContent(SessionVerificationMethod sessionVerificationMethod)
|
||||
{
|
||||
switch (sessionVerificationMethod)
|
||||
{
|
||||
case SessionVerificationMethod.EmailMessage:
|
||||
showEmailVerification();
|
||||
break;
|
||||
|
||||
case SessionVerificationMethod.TimedOneTimePassword:
|
||||
showTotpVerification();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void showEmailVerification()
|
||||
{
|
||||
LinkFlowContainer explainText;
|
||||
|
||||
contentFlow.Clear();
|
||||
contentFlow.AddRange(new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "An email has been sent to you with a verification code. Enter the code.",
|
||||
},
|
||||
codeTextBox = new OsuTextBox
|
||||
{
|
||||
InputProperties = new TextInputProperties(TextInputType.Code),
|
||||
PlaceholderText = "Enter code",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
});
|
||||
|
||||
explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
|
||||
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
|
||||
explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
|
||||
@@ -131,12 +156,58 @@ namespace osu.Game.Overlays.Login
|
||||
codeTextBox.Current.Disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (api.LastLoginError?.Message is string error)
|
||||
private void showTotpVerification()
|
||||
{
|
||||
LinkFlowContainer explainText;
|
||||
|
||||
contentFlow.Clear();
|
||||
contentFlow.AddRange(new Drawable[]
|
||||
{
|
||||
errorText.Alpha = 1;
|
||||
errorText.AddErrors(new[] { error });
|
||||
}
|
||||
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "Please enter the code from your authenticator app.",
|
||||
},
|
||||
codeTextBox = new OsuNumberBox
|
||||
{
|
||||
InputProperties = new TextInputProperties(TextInputType.NumericalPassword),
|
||||
PlaceholderText = "Enter code",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
});
|
||||
|
||||
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
|
||||
explainText.AddParagraph("If you can't access your app, ");
|
||||
explainText.AddLink("you can verify using email instead", () =>
|
||||
{
|
||||
var fallbackRequest = new VerificationMailFallbackRequest();
|
||||
fallbackRequest.Success += showEmailVerification;
|
||||
fallbackRequest.Failure += ex => errorText.Text = ex.Message;
|
||||
Task.Run(() => api.Perform(fallbackRequest));
|
||||
});
|
||||
explainText.AddText(". You can also ");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); });
|
||||
explainText.AddText(".");
|
||||
|
||||
codeTextBox.Current.BindValueChanged(code =>
|
||||
{
|
||||
string trimmedCode = code.NewValue.Trim();
|
||||
|
||||
if (trimmedCode.Length == 6)
|
||||
{
|
||||
api.AuthenticateSecondFactor(trimmedCode);
|
||||
codeTextBox.Current.Disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
@@ -31,6 +31,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
Child = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fit,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = textures.Get(badge.ImageUrl),
|
||||
|
||||
@@ -262,14 +262,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
.Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText;
|
||||
};
|
||||
}
|
||||
|
||||
togglesCollection.AddRange(CreateTernaryButtons().ToArray());
|
||||
|
||||
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates);
|
||||
|
||||
@@ -16,7 +16,10 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
Tool = tool;
|
||||
|
||||
TooltipText = tool.TooltipText;
|
||||
Selected.BindDisabledChanged(isDisabled =>
|
||||
{
|
||||
TooltipText = isDisabled ? "Add at least one timing point first!" : Tool.TooltipText;
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
get
|
||||
{
|
||||
if (!SpeedChange.IsDefault)
|
||||
yield return ("Speed change", $"{SpeedChange.Value:N2}x");
|
||||
yield return ("Speed change", FormattableString.Invariant($@"{SpeedChange.Value:N2}x"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
@@ -10,40 +9,31 @@ namespace osu.Game.Scoring
|
||||
public enum ScoreRank
|
||||
{
|
||||
// TODO: Localisable?
|
||||
[Description(@"F")]
|
||||
F = -1,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankD))]
|
||||
[Description(@"D")]
|
||||
D,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankC))]
|
||||
[Description(@"C")]
|
||||
C,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankB))]
|
||||
[Description(@"B")]
|
||||
B,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankA))]
|
||||
[Description(@"A")]
|
||||
A,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankS))]
|
||||
[Description(@"S")]
|
||||
S,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankSH))]
|
||||
[Description(@"S+")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
SH,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankX))]
|
||||
[Description(@"SS")]
|
||||
X,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankXH))]
|
||||
[Description(@"SS+")]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
XH,
|
||||
}
|
||||
|
||||
@@ -263,6 +263,7 @@ namespace osu.Game.Screens.Edit.Submission
|
||||
iconContainer.Colour = colours.Red1;
|
||||
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
|
||||
errorSample?.Play();
|
||||
progressSampleChannel?.Stop();
|
||||
break;
|
||||
|
||||
case StageStatusType.Canceled:
|
||||
@@ -274,6 +275,7 @@ namespace osu.Game.Screens.Edit.Submission
|
||||
iconContainer.Colour = colours.Gray8;
|
||||
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
|
||||
cancelSample?.Play();
|
||||
progressSampleChannel?.Stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
@@ -46,6 +47,7 @@ namespace osu.Game.Screens.Menu
|
||||
public Action? OnSolo;
|
||||
public Action? OnSettings;
|
||||
public Action? OnMultiplayer;
|
||||
public Action? OnMatchmaking;
|
||||
public Action? OnPlaylists;
|
||||
public Action<Room>? OnDailyChallenge;
|
||||
|
||||
@@ -84,6 +86,7 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
|
||||
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
|
||||
private readonly List<MainMenuButton> buttonsMulti = new List<MainMenuButton>();
|
||||
private readonly List<MainMenuButton> buttonsEdit = new List<MainMenuButton>();
|
||||
|
||||
private Sample? sampleBackToLogo;
|
||||
@@ -109,7 +112,19 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
Padding = new MarginPadding { Right = WEDGE_WIDTH },
|
||||
},
|
||||
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel)
|
||||
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) =>
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case ButtonSystemState.Multi:
|
||||
State = ButtonSystemState.Play;
|
||||
break;
|
||||
|
||||
default:
|
||||
State = ButtonSystemState.TopLevel;
|
||||
break;
|
||||
}
|
||||
})
|
||||
{
|
||||
Padding = new MarginPadding { Right = WEDGE_WIDTH },
|
||||
VisibleStateMin = ButtonSystemState.Play,
|
||||
@@ -137,28 +152,39 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
||||
});
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M));
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), (_, _) => State = ButtonSystemState.Multi, Key.M));
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L));
|
||||
buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
|
||||
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
|
||||
|
||||
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E)
|
||||
buttonsMulti.Add(new MainMenuButton("lounge", @"button-default-select", FontAwesome.Solid.Couch, new Color4(94, 63, 186, 255), onMultiplayer, Key.L, Key.M)
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH }
|
||||
});
|
||||
buttonsMulti.Add(new MainMenuButton("quick play", @"button-default-select", FontAwesome.Solid.Bolt, new Color4(94, 63, 186, 255), onMatchmaking, Key.Q));
|
||||
buttonsMulti.ForEach(b => b.VisibleState = ButtonSystemState.Multi);
|
||||
|
||||
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B,
|
||||
Key.E)
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
||||
});
|
||||
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S));
|
||||
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
|
||||
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M,
|
||||
Key.L)
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
||||
});
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B,
|
||||
Key.D));
|
||||
|
||||
if (host.CanExit)
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q));
|
||||
|
||||
buttonArea.AddRange(buttonsMulti);
|
||||
buttonArea.AddRange(buttonsPlay);
|
||||
buttonArea.AddRange(buttonsEdit);
|
||||
buttonArea.AddRange(buttonsTopLevel);
|
||||
@@ -191,6 +217,17 @@ namespace osu.Game.Screens.Menu
|
||||
OnMultiplayer?.Invoke();
|
||||
}
|
||||
|
||||
private void onMatchmaking(MainMenuButton mainMenuButton, UIEvent uiEvent)
|
||||
{
|
||||
if (api.State.Value != APIState.Online)
|
||||
{
|
||||
loginOverlay?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
OnMatchmaking?.Invoke();
|
||||
}
|
||||
|
||||
private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent)
|
||||
{
|
||||
if (api.State.Value != APIState.Online)
|
||||
@@ -315,6 +352,7 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
case ButtonSystemState.Edit:
|
||||
case ButtonSystemState.Play:
|
||||
case ButtonSystemState.Multi:
|
||||
StopSamplePlayback();
|
||||
backButton.TriggerClick();
|
||||
return true;
|
||||
@@ -327,6 +365,7 @@ namespace osu.Game.Screens.Menu
|
||||
public void StopSamplePlayback()
|
||||
{
|
||||
buttonsPlay.ForEach(button => button.StopSamplePlayback());
|
||||
buttonsMulti.ForEach(button => button.StopSamplePlayback());
|
||||
buttonsTopLevel.ForEach(button => button.StopSamplePlayback());
|
||||
logo?.StopSamplePlayback();
|
||||
}
|
||||
@@ -350,6 +389,10 @@ namespace osu.Game.Screens.Menu
|
||||
buttonsPlay.First().TriggerClick();
|
||||
return false;
|
||||
|
||||
case ButtonSystemState.Multi:
|
||||
buttonsPlay.First().TriggerClick();
|
||||
return false;
|
||||
|
||||
case ButtonSystemState.Edit:
|
||||
buttonsEdit.First().TriggerClick();
|
||||
return false;
|
||||
@@ -471,6 +514,7 @@ namespace osu.Game.Screens.Menu
|
||||
Initial,
|
||||
TopLevel,
|
||||
Play,
|
||||
Multi,
|
||||
Edit,
|
||||
EnteringMode,
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ namespace osu.Game.Screens.Menu
|
||||
},
|
||||
OnSolo = loadSongSelect,
|
||||
OnMultiplayer = () => this.Push(new Multiplayer()),
|
||||
OnMatchmaking = joinOrLeaveMatchmakingQueue,
|
||||
OnPlaylists = () => this.Push(new Playlists()),
|
||||
OnDailyChallenge = room =>
|
||||
{
|
||||
@@ -481,6 +482,8 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
private void loadSongSelect() => this.Push(new SoloSongSelect());
|
||||
|
||||
private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.IntroScreen());
|
||||
|
||||
private partial class MobileDisclaimerDialog : PopupDialog
|
||||
{
|
||||
public MobileDisclaimerDialog(Action confirmed)
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Queue;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro
|
||||
{
|
||||
/// <summary>
|
||||
/// A brief intro animation that introduces matchmaking to the user.
|
||||
/// </summary>
|
||||
public partial class IntroScreen : OsuScreen
|
||||
{
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => false;
|
||||
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
|
||||
public override bool ShowFooter => true;
|
||||
|
||||
private Container introContent = null!;
|
||||
|
||||
private Container titleContainer = null!;
|
||||
|
||||
private bool animationBegan;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
|
||||
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; } = null!;
|
||||
|
||||
private Sample? dateWindupSample;
|
||||
private Sample? dateImpactSample;
|
||||
private Sample? beatmapWindupSample;
|
||||
private Sample? beatmapImpactSample;
|
||||
|
||||
private SampleChannel? dateWindupChannel;
|
||||
private SampleChannel? dateImpactChannel;
|
||||
private SampleChannel? beatmapWindupChannel;
|
||||
private SampleChannel? beatmapImpactChannel;
|
||||
|
||||
private IDisposable? duckOperation;
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider);
|
||||
|
||||
public IntroScreen()
|
||||
{
|
||||
ValidForResume = false;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
introContent = new Container
|
||||
{
|
||||
Alpha = 0f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
X = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Quick Play",
|
||||
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dateWindupSample = audio.Samples.Get(@"DailyChallenge/date-windup");
|
||||
dateImpactSample = audio.Samples.Get(@"DailyChallenge/date-impact");
|
||||
beatmapWindupSample = audio.Samples.Get(@"DailyChallenge/beatmap-windup");
|
||||
beatmapImpactSample = audio.Samples.Get(@"DailyChallenge/beatmap-impact");
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
this.FadeInFromZero(400, Easing.OutQuint);
|
||||
|
||||
updateAnimationState();
|
||||
playDateWindupSample();
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
duckOperation?.Dispose();
|
||||
|
||||
this.FadeOut(800, Easing.OutQuint);
|
||||
base.OnSuspending(e);
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (base.OnExiting(e))
|
||||
return true;
|
||||
|
||||
duckOperation?.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateAnimationState()
|
||||
{
|
||||
if (animationBegan)
|
||||
return;
|
||||
|
||||
beginAnimation();
|
||||
animationBegan = true;
|
||||
}
|
||||
|
||||
private void beginAnimation()
|
||||
{
|
||||
using (BeginDelayedSequence(200))
|
||||
{
|
||||
introContent.Show();
|
||||
|
||||
titleContainer
|
||||
.ScaleTo(2)
|
||||
.Then()
|
||||
.ScaleTo(1, 400, Easing.In);
|
||||
|
||||
using (BeginDelayedSequence(150))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
playDateImpactSample();
|
||||
playBeatmapWindupSample();
|
||||
|
||||
duckOperation?.Dispose();
|
||||
duckOperation = musicController.Duck(new DuckParameters
|
||||
{
|
||||
RestoreDuration = 1500f,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(1000))
|
||||
{
|
||||
using (BeginDelayedSequence(100))
|
||||
{
|
||||
titleContainer
|
||||
.ScaleTo(0.4f, 400, Easing.In)
|
||||
.FadeOut(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(240))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (this.IsCurrentScreen())
|
||||
this.Push(new ScreenQueue());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void playDateWindupSample()
|
||||
{
|
||||
dateWindupChannel = dateWindupSample?.GetChannel();
|
||||
dateWindupChannel?.Play();
|
||||
}
|
||||
|
||||
private void playDateImpactSample()
|
||||
{
|
||||
dateImpactChannel = dateImpactSample?.GetChannel();
|
||||
dateImpactChannel?.Play();
|
||||
}
|
||||
|
||||
private void playBeatmapWindupSample()
|
||||
{
|
||||
beatmapWindupChannel = beatmapWindupSample?.GetChannel();
|
||||
beatmapWindupChannel?.Play();
|
||||
}
|
||||
|
||||
private void playBeatmapImpactSample()
|
||||
{
|
||||
beatmapImpactChannel = beatmapImpactSample?.GetChannel();
|
||||
beatmapImpactChannel?.Play();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
resetAudio();
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private void resetAudio()
|
||||
{
|
||||
dateWindupChannel?.Stop();
|
||||
dateImpactChannel?.Stop();
|
||||
beatmapWindupChannel?.Stop();
|
||||
beatmapImpactChannel?.Stop();
|
||||
duckOperation?.Dispose();
|
||||
}
|
||||
|
||||
private partial class MatchmakingIntroBackgroundScreen : RoomBackgroundScreen
|
||||
{
|
||||
private readonly OverlayColourProvider colourProvider;
|
||||
|
||||
public MatchmakingIntroBackgroundScreen(OverlayColourProvider colourProvider)
|
||||
: base(null)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(new Box
|
||||
{
|
||||
Depth = float.MinValue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5.Opacity(0.6f),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapSet;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class BeatmapCardMatchmaking : BeatmapCard
|
||||
{
|
||||
private readonly APIBeatmap beatmap;
|
||||
|
||||
protected override Drawable IdleContent => idleBottomContent;
|
||||
protected override Drawable DownloadInProgressContent => downloadProgressBar;
|
||||
|
||||
public const float HEIGHT = 80;
|
||||
|
||||
[Cached]
|
||||
private readonly BeatmapCardContent content;
|
||||
|
||||
private BeatmapCardThumbnail thumbnail = null!;
|
||||
private CollapsibleButtonContainer buttonContainer = null!;
|
||||
|
||||
private FillFlowContainer<BeatmapCardStatistic> statisticsContainer = null!;
|
||||
|
||||
private FillFlowContainer idleBottomContent = null!;
|
||||
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public BeatmapCardMatchmaking(APIBeatmap beatmap)
|
||||
: base(beatmap.BeatmapSet!, false)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
content = new BeatmapCardContent(HEIGHT);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Width = WIDTH;
|
||||
Height = HEIGHT;
|
||||
|
||||
FillFlowContainer leftIconArea = null!;
|
||||
FillFlowContainer titleBadgeArea = null!;
|
||||
GridContainer artistContainer = null!;
|
||||
|
||||
Child = content.With(c =>
|
||||
{
|
||||
c.MainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true)
|
||||
{
|
||||
Name = @"Left (icon) area",
|
||||
Size = new Vector2(HEIGHT),
|
||||
Padding = new MarginPadding { Right = CORNER_RADIUS },
|
||||
Child = leftIconArea = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding(4),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(1)
|
||||
}
|
||||
},
|
||||
buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
|
||||
{
|
||||
X = HEIGHT - CORNER_RADIUS,
|
||||
Width = WIDTH - HEIGHT + CORNER_RADIUS,
|
||||
FavouriteState = { BindTarget = FavouriteState },
|
||||
ButtonsCollapsedWidth = 0,
|
||||
ButtonsExpandedWidth = 24,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
|
||||
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
titleBadgeArea = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
artistContainer = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = createArtistText(),
|
||||
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
Empty()
|
||||
},
|
||||
}
|
||||
},
|
||||
new LinkFlowContainer(s =>
|
||||
{
|
||||
s.Shadow = false;
|
||||
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
|
||||
}).With(d =>
|
||||
{
|
||||
d.AutoSizeAxes = Axes.Both;
|
||||
d.Margin = new MarginPadding { Top = 1 };
|
||||
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
|
||||
d.AddUserLink(BeatmapSet.Author);
|
||||
}),
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = @"Bottom content",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
idleBottomContent = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 2),
|
||||
AlwaysPresent = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(8, 0),
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
ChildrenEnumerable = createStatistics()
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(2),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(4, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = beatmap.DifficultyName,
|
||||
Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
downloadProgressBar = new BeatmapCardDownloadProgressBar
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 5,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
State = { BindTarget = DownloadTracker.State },
|
||||
Progress = { BindTarget = DownloadTracker.Progress }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
c.Expanded.BindTarget = Expanded;
|
||||
});
|
||||
|
||||
if (BeatmapSet.HasVideo)
|
||||
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
|
||||
|
||||
if (BeatmapSet.HasStoryboard)
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
|
||||
|
||||
if (BeatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
titleBadgeArea.Add(new SpotlightBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
});
|
||||
}
|
||||
|
||||
if (BeatmapSet.HasExplicitContent)
|
||||
{
|
||||
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
});
|
||||
}
|
||||
|
||||
if (BeatmapSet.TrackId != null)
|
||||
{
|
||||
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private LocalisableString createArtistText()
|
||||
{
|
||||
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
|
||||
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
|
||||
}
|
||||
|
||||
private IEnumerable<BeatmapCardStatistic> createStatistics()
|
||||
{
|
||||
var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
|
||||
if (hypesStatistic != null)
|
||||
yield return hypesStatistic;
|
||||
|
||||
var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
|
||||
if (nominationsStatistic != null)
|
||||
yield return nominationsStatistic;
|
||||
|
||||
yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
|
||||
yield return new PlayCountStatistic(BeatmapSet);
|
||||
|
||||
var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
|
||||
if (dateStatistic != null)
|
||||
yield return dateStatistic;
|
||||
}
|
||||
|
||||
protected override void UpdateState()
|
||||
{
|
||||
base.UpdateState();
|
||||
|
||||
bool showDetails = IsHovered;
|
||||
|
||||
buttonContainer.ShowDetails.Value = showDetails;
|
||||
thumbnail.Dimmed.Value = showDetails;
|
||||
|
||||
statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = base.ContextMenuItems.ToList();
|
||||
|
||||
foreach (var button in buttonContainer.Buttons)
|
||||
{
|
||||
if (button.Enabled.Value)
|
||||
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.HighPerformance;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class BeatmapSelectGrid : CompositeDrawable
|
||||
{
|
||||
public const double ARRANGE_DELAY = 200;
|
||||
|
||||
private const double hide_duration = 800;
|
||||
private const double arrange_duration = 1000;
|
||||
private const double roll_duration = 4000;
|
||||
private const double present_beatmap_delay = 1200;
|
||||
private const float panel_spacing = 4;
|
||||
|
||||
public event Action<MultiplayerPlaylistItem>? ItemSelected;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private readonly Dictionary<long, BeatmapSelectPanel> panelLookup = new Dictionary<long, BeatmapSelectPanel>();
|
||||
|
||||
private readonly PanelGridContainer panelGridContainer;
|
||||
private readonly Container<BeatmapSelectPanel> rollContainer;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
private bool allowSelection = true;
|
||||
|
||||
private readonly Sample?[] spinSamples = new Sample?[5];
|
||||
private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4];
|
||||
private Sample? resultSample;
|
||||
private Sample? swooshSample;
|
||||
private double? lastSamplePlayback;
|
||||
|
||||
public BeatmapSelectGrid()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
scroll = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ScrollbarVisible = false,
|
||||
Child = panelGridContainer = new PanelGridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(panel_spacing)
|
||||
},
|
||||
},
|
||||
rollContainer = new Container<BeatmapSelectPanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
for (int i = 0; i < spinSamples.Length; i++)
|
||||
spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}");
|
||||
|
||||
resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result");
|
||||
swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
const double enter_duration = 500;
|
||||
|
||||
// the scroll container has a 1 frame delay until it receives the correct height for the scrollable area which leads to the scrollbar resizing awkwardly
|
||||
// if we wait until the panels have entered we get to avoid having to see that and the scrollbar it will appear synchronized with the rest of the content as a bonus
|
||||
Scheduler.AddDelayed(() => scroll.ScrollbarVisible = true, enter_duration);
|
||||
|
||||
SchedulerAfterChildren.Add(() =>
|
||||
{
|
||||
foreach (var panel in panelGridContainer)
|
||||
{
|
||||
double delay = panel.Y / 3;
|
||||
|
||||
panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void AddItem(MultiplayerPlaylistItem item)
|
||||
{
|
||||
var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item)
|
||||
{
|
||||
AllowSelection = allowSelection,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = ItemSelected,
|
||||
};
|
||||
|
||||
panelGridContainer.Add(panel);
|
||||
panelGridContainer.SetLayoutPosition(panel, (float)item.StarRating);
|
||||
}
|
||||
|
||||
public void RemoveItem(long id)
|
||||
{
|
||||
if (!panelLookup.Remove(id, out var panel))
|
||||
return;
|
||||
|
||||
panel.Expire();
|
||||
}
|
||||
|
||||
public void SetUserSelection(APIUser user, long itemId, bool selected)
|
||||
{
|
||||
if (!panelLookup.TryGetValue(itemId, out var panel))
|
||||
return;
|
||||
|
||||
if (selected)
|
||||
panel.AddUser(user, user.Equals(api.LocalUser.Value));
|
||||
else
|
||||
panel.RemoveUser(user);
|
||||
}
|
||||
|
||||
public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId)
|
||||
{
|
||||
Debug.Assert(candidateItemIds.Length >= 1);
|
||||
Debug.Assert(candidateItemIds.Contains(finalItemId));
|
||||
Debug.Assert(panelLookup.ContainsKey(finalItemId));
|
||||
Debug.Assert(candidateItemIds.All(id => panelLookup.ContainsKey(id)));
|
||||
|
||||
allowSelection = false;
|
||||
|
||||
TransferCandidatePanelsToRollContainer(candidateItemIds);
|
||||
|
||||
if (candidateItemIds.Length == 1)
|
||||
{
|
||||
this.Delay(ARRANGE_DELAY)
|
||||
.Schedule(() => ArrangeItemsForRollAnimation())
|
||||
.Delay(arrange_duration + present_beatmap_delay)
|
||||
.Schedule(() => PresentUnanimouslyChosenBeatmap(finalItemId));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Delay(ARRANGE_DELAY)
|
||||
.Schedule(() => ArrangeItemsForRollAnimation())
|
||||
.Delay(arrange_duration)
|
||||
.Schedule(() => PlayRollAnimation(finalItemId, roll_duration))
|
||||
.Delay(roll_duration + present_beatmap_delay)
|
||||
.Schedule(() => PresentRolledBeatmap(finalItemId));
|
||||
}
|
||||
}
|
||||
|
||||
internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration)
|
||||
{
|
||||
scroll.ScrollbarVisible = false;
|
||||
panelGridContainer.LayoutDisabled = true;
|
||||
|
||||
var rng = new Random();
|
||||
|
||||
var remainingPanels = new List<BeatmapSelectPanel>();
|
||||
|
||||
foreach (var panel in panelGridContainer.Children.ToArray())
|
||||
{
|
||||
panel.AllowSelection = false;
|
||||
|
||||
if (!candidateItemIds.Contains(panel.Item.ID))
|
||||
{
|
||||
panel.PopOutAndExpire(duration: duration / 2, delay: rng.NextDouble() * duration / 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
remainingPanels.Add(panel);
|
||||
}
|
||||
|
||||
rng.Shuffle(remainingPanels.AsSpan());
|
||||
|
||||
foreach (var panel in remainingPanels)
|
||||
{
|
||||
var position = panel.ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
panelGridContainer.Remove(panel, false);
|
||||
|
||||
panel.Anchor = panel.Origin = Anchor.Centre;
|
||||
panel.Position = rollContainer.ToLocalSpace(position) - rollContainer.ChildSize / 2;
|
||||
|
||||
rollContainer.Add(panel);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ArrangeItemsForRollAnimation(double duration = arrange_duration, double stagger = 30)
|
||||
{
|
||||
var positions = calculateLayoutPositionsForRollAnimation(rollContainer.Children.Count);
|
||||
|
||||
Debug.Assert(positions.Length == rollContainer.Children.Count);
|
||||
|
||||
for (int i = 0; i < positions.Length; i++)
|
||||
{
|
||||
var panel = rollContainer.Children[i];
|
||||
|
||||
var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing));
|
||||
|
||||
panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f));
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
var chan = swooshSample?.GetChannel();
|
||||
if (chan == null) return;
|
||||
|
||||
chan.Frequency.Value = 1.25f - RNG.NextDouble(0.5f);
|
||||
chan.Play();
|
||||
}, stagger * i);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2[] calculateLayoutPositionsForRollAnimation(int panelCount)
|
||||
{
|
||||
if (panelCount == 1)
|
||||
return new[] { Vector2.Zero };
|
||||
|
||||
// goal is to get the positions arranged in clockwise order, with the top-left position being the first one
|
||||
// to keep things simple the positions are first inserted in the order: right row, optional bottom center panel, left row backwards
|
||||
// then the positions get shifted by 1 to move the top-left position into the first spot
|
||||
|
||||
bool hasCenterPanel = panelCount % 2 == 1;
|
||||
int rowCount = (panelCount + 1) / 2;
|
||||
int outerRowCount = hasCenterPanel ? rowCount - 1 : rowCount;
|
||||
|
||||
float yOffset = -(rowCount - 1f) / 2;
|
||||
|
||||
var positions = new Vector2[panelCount];
|
||||
|
||||
for (int row = 0; row < outerRowCount; row++)
|
||||
{
|
||||
positions[row] = new Vector2(0.5f, row + yOffset);
|
||||
}
|
||||
|
||||
if (hasCenterPanel)
|
||||
{
|
||||
int centerIndex = panelCount / 2;
|
||||
|
||||
positions[centerIndex] = new Vector2(0, outerRowCount + yOffset);
|
||||
}
|
||||
|
||||
for (int row = 0; row < outerRowCount; row++)
|
||||
{
|
||||
int index = positions.Length - 1 - row;
|
||||
|
||||
positions[index] = new Vector2(-0.5f, row + yOffset);
|
||||
}
|
||||
|
||||
return positions.TakeLast(1).Concat(positions.SkipLast(1)).ToArray();
|
||||
}
|
||||
|
||||
internal void PlayRollAnimation(long finalItem, double duration = roll_duration)
|
||||
{
|
||||
const int minimum_steps = 20;
|
||||
|
||||
int finalItemIndex = rollContainer.Children
|
||||
.Select(it => it.Item.ID)
|
||||
.ToImmutableList()
|
||||
.IndexOf(finalItem);
|
||||
|
||||
Debug.Assert(finalItemIndex >= 0);
|
||||
|
||||
int numSteps = minimum_steps;
|
||||
while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex)
|
||||
numSteps++;
|
||||
|
||||
BeatmapSelectPanel? lastPanel = null;
|
||||
|
||||
for (int i = 0; i < numSteps; i++)
|
||||
{
|
||||
float progress = ((float)i) / (numSteps - 1);
|
||||
|
||||
double delay = Math.Pow(progress, 2.5) * duration;
|
||||
var panel = rollContainer.Children[i % rollContainer.Children.Count];
|
||||
|
||||
int ii = i;
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
lastPanel?.HideBorder();
|
||||
panel.ShowBorder();
|
||||
|
||||
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
|
||||
{
|
||||
int sequenceIdx = ii % spin_sample_sequence.Length;
|
||||
spinSamples[spin_sample_sequence[sequenceIdx]]?.Play();
|
||||
lastSamplePlayback = Time.Current;
|
||||
}
|
||||
|
||||
lastPanel = panel;
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
internal void PresentRolledBeatmap(long finalItem)
|
||||
{
|
||||
Debug.Assert(rollContainer.Children.Any(it => it.Item.ID == finalItem));
|
||||
|
||||
foreach (var panel in rollContainer.Children)
|
||||
{
|
||||
if (panel.Item.ID != finalItem)
|
||||
{
|
||||
panel.FadeOut(200);
|
||||
panel.PopOutAndExpire(easing: Easing.InQuad);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we changed child depth without scheduling we'd change the order of the panels while iterating
|
||||
Schedule(() =>
|
||||
{
|
||||
rollContainer.ChangeChildDepth(panel, float.MinValue);
|
||||
|
||||
panel.ShowChosenBorder();
|
||||
panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo)
|
||||
.ScaleTo(1.5f, 1000, Easing.OutExpo);
|
||||
|
||||
resultSample?.Play();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal void PresentUnanimouslyChosenBeatmap(long finalItem)
|
||||
{
|
||||
// TODO: display special animation in this case
|
||||
|
||||
PresentRolledBeatmap(finalItem);
|
||||
}
|
||||
|
||||
private partial class PanelGridContainer : FillFlowContainer<BeatmapSelectPanel>
|
||||
{
|
||||
public bool LayoutDisabled;
|
||||
|
||||
protected override IEnumerable<Vector2> ComputeLayoutPositions()
|
||||
{
|
||||
if (LayoutDisabled)
|
||||
return FlowingChildren.Select(c => c.Position);
|
||||
|
||||
return base.ComputeLayoutPositions();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct SplitEasingFunction(DefaultEasingFunction easeIn, DefaultEasingFunction easeOut, float ratio) : IEasingFunction
|
||||
{
|
||||
public SplitEasingFunction(Easing easeIn, Easing easeOut, float ratio = 0.5f)
|
||||
: this(new DefaultEasingFunction(easeIn), new DefaultEasingFunction(easeOut), ratio)
|
||||
{
|
||||
}
|
||||
|
||||
public double ApplyEasing(double time)
|
||||
{
|
||||
if (time < ratio)
|
||||
return easeIn.ApplyEasing(time / ratio) * ratio;
|
||||
|
||||
return double.Lerp(ratio, 1, easeOut.ApplyEasing((time - ratio) / (1 - ratio)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class BeatmapSelectPanel : Container
|
||||
{
|
||||
public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT);
|
||||
|
||||
public bool AllowSelection { get; set; }
|
||||
|
||||
public readonly MultiplayerPlaylistItem Item;
|
||||
|
||||
public Action<MultiplayerPlaylistItem>? Action { private get; init; }
|
||||
|
||||
private const float border_width = 3;
|
||||
|
||||
private Container scaleContainer = null!;
|
||||
private AvatarOverlay selectionOverlay = null!;
|
||||
private Drawable lighting = null!;
|
||||
|
||||
private Container border = null!;
|
||||
private Container mainContent = null!;
|
||||
|
||||
public override bool PropagatePositionalInputSubTree => AllowSelection;
|
||||
|
||||
public BeatmapSelectPanel(MultiplayerPlaylistItem item)
|
||||
{
|
||||
Item = item;
|
||||
Size = SIZE;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider)
|
||||
{
|
||||
InternalChild = scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
CornerExponent = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
lighting = new Box
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
},
|
||||
selectionOverlay = new AvatarOverlay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
}
|
||||
}
|
||||
},
|
||||
border = new Container
|
||||
{
|
||||
Alpha = 0,
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
CornerExponent = 10,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BorderThickness = border_width,
|
||||
BorderColour = colourProvider.Light1,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 40,
|
||||
Roundness = 300,
|
||||
Colour = colourProvider.Light3.Opacity(0.1f),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
|
||||
{
|
||||
var beatmap = b.GetResultSafely()!;
|
||||
beatmap.StarRating = Item.StarRating;
|
||||
|
||||
mainContent.Add(new BeatmapCardMatchmaking(beatmap)
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Action = () => Action?.Invoke(Item),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser);
|
||||
public bool RemoveUser(APIUser user) => selectionOverlay.RemoveUser(user.Id);
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
lighting.FadeTo(0.2f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 300);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
|
||||
lighting.FadeOut(200);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
lighting.FadeTo(0.5f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 400);
|
||||
|
||||
// pass through to let the beatmap card handle actual click.
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ShowChosenBorder()
|
||||
{
|
||||
border.FadeTo(1, 1000, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void ShowBorder()
|
||||
{
|
||||
border.FadeTo(1, 80, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeTo(0.7f, 800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void HideBorder()
|
||||
{
|
||||
border.FadeOut(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
|
||||
{
|
||||
scaleContainer
|
||||
.FadeOut()
|
||||
.MoveToY(distance)
|
||||
.Delay(delay)
|
||||
.FadeIn(duration / 2)
|
||||
.MoveToY(0, duration, Easing.OutExpo);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
|
||||
{
|
||||
AllowSelection = false;
|
||||
|
||||
scaleContainer.Delay(delay)
|
||||
.ScaleTo(0, duration, easing)
|
||||
.FadeOut(duration);
|
||||
|
||||
this.Delay(delay + duration).FadeOut().Expire();
|
||||
}
|
||||
|
||||
private partial class AvatarOverlay : CompositeDrawable
|
||||
{
|
||||
private readonly Container<SelectionAvatar> avatars;
|
||||
|
||||
private Sample? userAddedSample;
|
||||
private double? lastSamplePlayback;
|
||||
|
||||
public AvatarOverlay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = avatars = new Container<SelectionAvatar>
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = SelectionAvatar.AVATAR_SIZE,
|
||||
};
|
||||
|
||||
Padding = new MarginPadding(5);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
|
||||
}
|
||||
|
||||
public bool AddUser(APIUser user, bool isOwnUser)
|
||||
{
|
||||
if (avatars.Any(a => a.User.Id == user.Id))
|
||||
return false;
|
||||
|
||||
var avatar = new SelectionAvatar(user, isOwnUser);
|
||||
|
||||
avatars.Add(avatar);
|
||||
|
||||
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
|
||||
{
|
||||
userAddedSample?.Play();
|
||||
lastSamplePlayback = Time.Current;
|
||||
}
|
||||
|
||||
updateAvatarLayout();
|
||||
|
||||
avatar.FinishTransforms();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveUser(int id)
|
||||
{
|
||||
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
|
||||
return false;
|
||||
|
||||
avatar.PopOutAndExpire();
|
||||
avatars.ChangeChildDepth(avatar, float.MaxValue);
|
||||
|
||||
updateAvatarLayout();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateAvatarLayout()
|
||||
{
|
||||
const double stagger = 30;
|
||||
const float spacing = 4;
|
||||
|
||||
double delay = 0;
|
||||
float x = 0;
|
||||
|
||||
for (int i = avatars.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var avatar = avatars[i];
|
||||
|
||||
if (avatar.Expired)
|
||||
continue;
|
||||
|
||||
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
|
||||
|
||||
x -= avatar.LayoutSize.X + spacing;
|
||||
|
||||
delay += stagger;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SelectionAvatar : CompositeDrawable
|
||||
{
|
||||
public const float AVATAR_SIZE = 30;
|
||||
|
||||
public APIUser User { get; }
|
||||
|
||||
public bool Expired { get; private set; }
|
||||
|
||||
private readonly MatchmakingAvatar avatar;
|
||||
|
||||
public SelectionAvatar(APIUser user, bool isOwnUser)
|
||||
{
|
||||
User = user;
|
||||
Size = new Vector2(AVATAR_SIZE);
|
||||
|
||||
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
avatar.ScaleTo(0)
|
||||
.ScaleTo(1, 500, Easing.OutElasticHalf)
|
||||
.FadeIn(200);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire()
|
||||
{
|
||||
avatar.ScaleTo(0, 400, Easing.OutExpo);
|
||||
|
||||
this.FadeOut(100).Expire();
|
||||
Expired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class SubScreenBeatmapSelect : MatchmakingSubScreen
|
||||
{
|
||||
public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Split;
|
||||
public override Drawable PlayersDisplayArea { get; }
|
||||
|
||||
private readonly BeatmapSelectGrid beatmapSelectGrid;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
public SubScreenBeatmapSelect()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = 200 },
|
||||
Child = beatmapSelectGrid = new BeatmapSelectGrid
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = PlayersDisplayArea = new Container().With(d =>
|
||||
{
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.ItemAdded += onItemAdded;
|
||||
|
||||
foreach (var item in client.Room!.Playlist)
|
||||
onItemAdded(item);
|
||||
|
||||
beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID);
|
||||
|
||||
client.MatchmakingItemSelected += onItemSelected;
|
||||
client.MatchmakingItemDeselected += onItemDeselected;
|
||||
}
|
||||
|
||||
private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() =>
|
||||
{
|
||||
if (item.Expired)
|
||||
return;
|
||||
|
||||
beatmapSelectGrid.AddItem(item);
|
||||
});
|
||||
|
||||
private void onItemSelected(int userId, long itemId)
|
||||
{
|
||||
var user = client.Room!.Users.First(it => it.UserID == userId).User!;
|
||||
beatmapSelectGrid.SetUserSelection(user, itemId, true);
|
||||
}
|
||||
|
||||
private void onItemDeselected(int userId, long itemId)
|
||||
{
|
||||
var user = client.Room!.Users.First(it => it.UserID == userId).User!;
|
||||
beatmapSelectGrid.SetUserSelection(user, itemId, false);
|
||||
}
|
||||
|
||||
public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.ItemAdded -= onItemAdded;
|
||||
client.MatchmakingItemSelected -= onItemSelected;
|
||||
client.MatchmakingItemDeselected -= onItemDeselected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay
|
||||
{
|
||||
public partial class ScreenGameplay : MultiplayerPlayer
|
||||
{
|
||||
public ScreenGameplay(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users)
|
||||
: base(room, playlistItem, users)
|
||||
{
|
||||
}
|
||||
|
||||
protected override async Task PrepareScoreForResultsAsync(Score score)
|
||||
{
|
||||
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
|
||||
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
/// <summary>
|
||||
/// A circular player avatar used in matchmaking displays.
|
||||
/// Is part of a <see cref="PlayerPanel"/> but can also be used in isolation for a more ambient/decorative user display.
|
||||
/// </summary>
|
||||
public partial class MatchmakingAvatar : CompositeDrawable
|
||||
{
|
||||
public static readonly Vector2 SIZE = new Vector2(30);
|
||||
|
||||
private readonly APIUser user;
|
||||
private readonly bool isOwnUser;
|
||||
|
||||
public MatchmakingAvatar(APIUser user, bool isOwnUser = false)
|
||||
{
|
||||
this.user = user;
|
||||
this.isOwnUser = isOwnUser;
|
||||
|
||||
Size = SIZE;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colour)
|
||||
{
|
||||
if (isOwnUser)
|
||||
{
|
||||
AddInternal(new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
Padding = new MarginPadding(-2),
|
||||
Child = new FastCircle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colour.Yellow,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AddInternal(new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.LightSlateGray,
|
||||
},
|
||||
new ClickableAvatar(user, true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
public partial class MatchmakingChatDisplay : MatchChatDisplay, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected new ChatTextBox TextBox => base.TextBox!;
|
||||
|
||||
public MatchmakingChatDisplay(Room room, bool leaveChannelOnDispose = true)
|
||||
: base(room, leaveChannelOnDispose)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RealmKeyBindingStore keyBindingStore)
|
||||
{
|
||||
resetPlaceholderText();
|
||||
|
||||
TextBox.HoldFocus = false;
|
||||
TextBox.ReleaseFocusOnCommit = true;
|
||||
TextBox.Focus = () => TextBox.PlaceholderText = ChatStrings.InputPlaceholder;
|
||||
TextBox.FocusLost = resetPlaceholderText;
|
||||
|
||||
void resetPlaceholderText() => TextBox.PlaceholderText = Localisation.ChatStrings.InGameInputPlaceholder(keyBindingStore.GetBindingsStringFor(GlobalAction.ToggleChatFocus));
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Back:
|
||||
if (TextBox.HasFocus)
|
||||
{
|
||||
Schedule(() => TextBox.KillFocus());
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case GlobalAction.ToggleChatFocus:
|
||||
if (TextBox.HasFocus)
|
||||
{
|
||||
Schedule(() => TextBox.KillFocus());
|
||||
}
|
||||
else
|
||||
{
|
||||
Schedule(() => TextBox.TakeFocus());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
public abstract partial class MatchmakingSubScreen : Screen
|
||||
{
|
||||
public abstract PanelDisplayStyle PlayersDisplayStyle { get; }
|
||||
public abstract Drawable? PlayersDisplayArea { get; }
|
||||
|
||||
protected MatchmakingSubScreen()
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
this.FadeInFromZero(200);
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnSuspending(e);
|
||||
this.FadeOutFromOne(200);
|
||||
}
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
this.FadeInFromZero(200);
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (base.OnExiting(e))
|
||||
return true;
|
||||
|
||||
this.FadeOutFromOne(200);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// 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.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
/// <summary>
|
||||
/// A panel used throughout matchmaking to represent a user, including local information like their
|
||||
/// rank and high level statistics in the matchmaking system.
|
||||
/// </summary>
|
||||
public partial class PlayerPanel : UserPanel
|
||||
{
|
||||
public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100);
|
||||
public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200);
|
||||
private static readonly Vector2 avatar_size = new Vector2(80);
|
||||
|
||||
public readonly MultiplayerRoomUser RoomUser;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText rankText = null!;
|
||||
private OsuSpriteText scoreText = null!;
|
||||
|
||||
private Drawable avatarPositionTarget = null!;
|
||||
private Drawable avatarJumpTarget = null!;
|
||||
private MatchmakingAvatar avatar = null!;
|
||||
private OsuSpriteText username = null!;
|
||||
|
||||
private Container scaleContainer = null!;
|
||||
private Container mainContent = null!;
|
||||
|
||||
public bool Horizontal
|
||||
{
|
||||
get => horizontal;
|
||||
set
|
||||
{
|
||||
horizontal = value;
|
||||
if (IsLoaded)
|
||||
updateLayout(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool horizontal;
|
||||
|
||||
public PlayerPanel(MultiplayerRoomUser user)
|
||||
: base(user.User!)
|
||||
{
|
||||
RoomUser = user;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 10;
|
||||
CornerExponent = 10;
|
||||
|
||||
Add(scaleContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = mainContent = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
avatarPositionTarget = new Container
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Size = avatar_size,
|
||||
Child = avatarJumpTarget = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One
|
||||
}
|
||||
}
|
||||
},
|
||||
rankText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Margin = new MarginPadding(4),
|
||||
Font = OsuFont.Style.Title.With(size: 70),
|
||||
},
|
||||
username = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Text = User.Username,
|
||||
Font = OsuFont.Style.Heading1,
|
||||
},
|
||||
scoreText = new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding(10),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Text = "0 pts"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateLayout() => Empty();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateLayout(true);
|
||||
|
||||
client.MatchRoomStateChanged += onRoomStateChanged;
|
||||
client.MatchEvent += onMatchEvent;
|
||||
|
||||
onRoomStateChanged(client.Room!.MatchState);
|
||||
|
||||
avatar.ScaleTo(0)
|
||||
.ScaleTo(1, 500, Easing.OutElasticHalf)
|
||||
.FadeIn(200);
|
||||
|
||||
rankText.Hide();
|
||||
scoreText.Hide();
|
||||
username.Hide();
|
||||
|
||||
using (BeginDelayedSequence(100))
|
||||
{
|
||||
username.FadeInFromZero(600);
|
||||
|
||||
using (BeginDelayedSequence(100))
|
||||
{
|
||||
scoreText.FadeInFromZero(600);
|
||||
|
||||
using (BeginDelayedSequence(100))
|
||||
{
|
||||
rankText.FadeTo(0.6f, 600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 avatarPosition => horizontal ? new Vector2(50) : new Vector2(75, 50);
|
||||
|
||||
private void updateLayout(bool instant)
|
||||
{
|
||||
double duration = instant ? 0 : 1000;
|
||||
|
||||
avatarPositionTarget.MoveTo(avatarPosition, duration, Easing.OutPow10);
|
||||
this.ResizeTo(horizontal ? SIZE_HORIZONTAL : SIZE_VERTICAL, duration, Easing.OutPow10);
|
||||
|
||||
rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10);
|
||||
username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10);
|
||||
scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
scaleContainer.ScaleTo(1.03f, 750, Easing.OutPow10);
|
||||
mainContent.ScaleTo(1.03f, 750, Easing.OutPow10);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
scaleContainer.ScaleTo(1f, 750, Easing.OutPow10);
|
||||
mainContent.ScaleTo(1, 750, Easing.OutPow10);
|
||||
|
||||
mainContent.MoveTo(Vector2.Zero, 1250, Easing.OutPow10);
|
||||
avatarPositionTarget.MoveTo(avatarPosition, 1250, Easing.OutPow10);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
var offset = (avatarPositionTarget.ToLocalSpace(e.ScreenSpaceMousePosition) - avatarPositionTarget.DrawSize / 2) * 0.02f;
|
||||
|
||||
mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutPow10);
|
||||
avatarPositionTarget.MoveTo(avatarPosition + offset, 400, Easing.OutPow10);
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
|
||||
{
|
||||
if (state is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore))
|
||||
return;
|
||||
|
||||
rankText.Text = $"#{userScore.Placement}";
|
||||
scoreText.Text = $"{userScore.Points} pts";
|
||||
});
|
||||
|
||||
private void onMatchEvent(MatchServerEvent e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case MatchmakingAvatarActionEvent action:
|
||||
if (action.UserId != RoomUser.UserID)
|
||||
break;
|
||||
|
||||
switch (action.Action)
|
||||
{
|
||||
case MatchmakingAvatarAction.Jump:
|
||||
avatarJumpTarget.MoveToY(-10, 200, Easing.Out)
|
||||
.Then().MoveToY(0, 200, Easing.In);
|
||||
avatarJumpTarget.ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out)
|
||||
.Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In)
|
||||
.Then().ScaleTo(Vector2.One, 800, Easing.OutElastic);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.MatchRoomStateChanged -= onRoomStateChanged;
|
||||
client.MatchEvent -= onMatchEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which maintains the layout of the players in a matchmaking room.
|
||||
/// Can be controlled to display the panels in a certain location and in multiple styles.
|
||||
/// </summary>
|
||||
public partial class PlayerPanelOverlay : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private Container<PlayerPanel> panels = null!;
|
||||
private PlayerPanelCellContainer gridLayout = null!;
|
||||
private PlayerPanelCellContainer splitLayoutLeft = null!;
|
||||
private PlayerPanelCellContainer splitLayoutRight = null!;
|
||||
|
||||
private PanelDisplayStyle displayStyle;
|
||||
private Drawable? displayArea;
|
||||
private bool isAnimatingToDisplayArea;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
gridLayout = new PlayerPanelCellContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(20),
|
||||
},
|
||||
splitLayoutLeft = new PlayerPanelCellContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
},
|
||||
splitLayoutRight = new PlayerPanelCellContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
},
|
||||
panels = new Container<PlayerPanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// Set position/size so we don't initially animate.
|
||||
Position = getFinalPosition();
|
||||
Size = getFinalSize();
|
||||
|
||||
client.MatchRoomStateChanged += onRoomStateChanged;
|
||||
client.UserJoined += onUserJoined;
|
||||
client.UserLeft += onUserLeft;
|
||||
|
||||
if (client.Room != null)
|
||||
{
|
||||
onRoomStateChanged(client.Room.MatchState);
|
||||
foreach (var user in client.Room.Users)
|
||||
onUserJoined(user);
|
||||
}
|
||||
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
public PanelDisplayStyle DisplayStyle
|
||||
{
|
||||
set
|
||||
{
|
||||
displayStyle = value;
|
||||
if (IsLoaded)
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable? DisplayArea
|
||||
{
|
||||
set
|
||||
{
|
||||
displayArea = value;
|
||||
isAnimatingToDisplayArea = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() =>
|
||||
{
|
||||
panels.Add(new PlayerPanel(user)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f)
|
||||
});
|
||||
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() =>
|
||||
{
|
||||
panels.Single(p => p.RoomUser.Equals(user)).Expire();
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(updateDisplay);
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
gridLayout.ReleasePanels();
|
||||
splitLayoutLeft.ReleasePanels();
|
||||
splitLayoutRight.ReleasePanels();
|
||||
|
||||
switch (displayStyle)
|
||||
{
|
||||
case PanelDisplayStyle.Grid:
|
||||
foreach (var panel in panels)
|
||||
{
|
||||
panel.FadeTo(1, 200);
|
||||
panel.Horizontal = false;
|
||||
}
|
||||
|
||||
gridLayout.AcquirePanels(panels.ToArray());
|
||||
break;
|
||||
|
||||
case PanelDisplayStyle.Split:
|
||||
foreach (var panel in panels)
|
||||
{
|
||||
panel.FadeTo(1, 200);
|
||||
panel.Horizontal = true;
|
||||
}
|
||||
|
||||
int leftCount = (int)Math.Ceiling(panels.Count / 2f);
|
||||
|
||||
splitLayoutLeft.AcquirePanels(panels.Take(leftCount).ToArray());
|
||||
splitLayoutRight.AcquirePanels(panels.Skip(leftCount).ToArray());
|
||||
break;
|
||||
|
||||
case PanelDisplayStyle.Hidden:
|
||||
foreach (var panel in panels)
|
||||
panel.FadeTo(0, 200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var targetPos = getFinalPosition();
|
||||
var targetSize = getFinalSize();
|
||||
|
||||
double duration = isAnimatingToDisplayArea ? 60 : 0;
|
||||
|
||||
if (Time.Elapsed > 0)
|
||||
{
|
||||
Position = new Vector2(
|
||||
(float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed),
|
||||
(float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed)
|
||||
);
|
||||
|
||||
Size = new Vector2(
|
||||
(float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed),
|
||||
(float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed)
|
||||
);
|
||||
}
|
||||
|
||||
// If we don't track the animating state, the animation will also occur when resizing the window.
|
||||
isAnimatingToDisplayArea &= !Precision.AlmostEquals(Size, targetSize, 0.5f);
|
||||
}
|
||||
|
||||
private Vector2 getFinalPosition()
|
||||
=> displayArea == null ? Vector2.Zero : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft);
|
||||
|
||||
private Vector2 getFinalSize()
|
||||
=> displayArea == null ? Parent!.DrawSize : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.BottomRight) - Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.MatchRoomStateChanged -= onRoomStateChanged;
|
||||
client.UserJoined -= onUserJoined;
|
||||
client.UserLeft -= onUserLeft;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class PlayerPanelCellContainer : FillFlowContainer<PlayerPanelCell>
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
public void AcquirePanels(PlayerPanel[] panels)
|
||||
{
|
||||
while (Count < panels.Length)
|
||||
{
|
||||
Add(new PlayerPanelCell
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
}
|
||||
|
||||
while (Count > panels.Length)
|
||||
Remove(Children[^1], true);
|
||||
|
||||
for (int i = 0; i < panels.Length; i++)
|
||||
{
|
||||
// We'll invalidate the layout position to represent the new placements and the re-flow will happen in UpdateAfterChildren().
|
||||
// But the cells expect their positions to be valid as they're updated, which won't be the case until the re-flow happens.
|
||||
int i2 = i;
|
||||
ScheduleAfterChildren(() => Children[i2].AcquirePanel(panels[i2]));
|
||||
|
||||
if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState)
|
||||
continue;
|
||||
|
||||
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user))
|
||||
SetLayoutPosition(Children[i], user.Placement);
|
||||
else
|
||||
SetLayoutPosition(Children[i], float.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReleasePanels()
|
||||
{
|
||||
// Matches the schedule in AcquirePanels.
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
foreach (var panel in Children)
|
||||
panel.ReleasePanel();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private partial class PlayerPanelCell : Drawable
|
||||
{
|
||||
private PlayerPanel? panel;
|
||||
private bool isAnimating;
|
||||
|
||||
public void AcquirePanel(PlayerPanel panel)
|
||||
{
|
||||
this.panel = panel;
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
public void ReleasePanel()
|
||||
{
|
||||
panel = null;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (panel?.Parent == null)
|
||||
return;
|
||||
|
||||
Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL;
|
||||
Size *= panel.Scale;
|
||||
|
||||
var targetPos = getFinalPosition();
|
||||
|
||||
double duration = isAnimating ? 60 : 0;
|
||||
|
||||
if (Time.Elapsed > 0)
|
||||
{
|
||||
panel.Position = new Vector2(
|
||||
(float)Interpolation.DampContinuously(panel.Position.X, targetPos.X, duration, Time.Elapsed),
|
||||
(float)Interpolation.DampContinuously(panel.Position.Y, targetPos.Y, duration, Time.Elapsed)
|
||||
);
|
||||
}
|
||||
|
||||
// If we don't track the animating state, the animation will also occur when resizing the window.
|
||||
isAnimating &= !Precision.AlmostEquals(panel.Position, targetPos, 0.5f);
|
||||
|
||||
Vector2 getFinalPosition()
|
||||
=> panel.Parent.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PanelDisplayStyle
|
||||
{
|
||||
Grid,
|
||||
Split,
|
||||
Hidden
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
{
|
||||
public partial class PanelRoomAward : CompositeDrawable
|
||||
{
|
||||
private readonly Color4 backgroundColour = Color4.SaddleBrown;
|
||||
|
||||
private readonly string text;
|
||||
private readonly int userId;
|
||||
|
||||
public PanelRoomAward(string text, int userId)
|
||||
{
|
||||
this.text = text;
|
||||
this.userId = userId;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(UserLookupCache userLookupCache)
|
||||
{
|
||||
// Should be cached by this point.
|
||||
APIUser? user = userLookupCache.GetUserAsync(userId).GetResultSafely();
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = backgroundColour
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding(10),
|
||||
Text = $"{text}: {user?.Username}"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
{
|
||||
public partial class PanelUserStatistic : CompositeDrawable
|
||||
{
|
||||
private readonly Color4 backgroundColour = Color4.SaddleBrown;
|
||||
|
||||
private readonly string text;
|
||||
|
||||
public PanelUserStatistic(string text)
|
||||
{
|
||||
this.text = text;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = backgroundColour
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding(10),
|
||||
Text = text
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Final room results, during <see cref="MatchmakingStage.Ended"/>
|
||||
/// </summary>
|
||||
public partial class SubScreenResults : MatchmakingSubScreen
|
||||
{
|
||||
private const float grid_spacing = 5;
|
||||
|
||||
public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Grid;
|
||||
public override Drawable PlayersDisplayArea { get; }
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private readonly OsuSpriteText placementText;
|
||||
private readonly FillFlowContainer<PanelUserStatistic> userStatistics;
|
||||
private readonly FillFlowContainer<PanelRoomAward> roomStatistics;
|
||||
|
||||
public SubScreenResults()
|
||||
{
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, grid_spacing),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, grid_spacing),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 75)
|
||||
],
|
||||
Content = new Drawable[]?[]
|
||||
{
|
||||
[
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(grid_spacing),
|
||||
Children = new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = "Placement",
|
||||
Font = OsuFont.Default.With(size: 12)
|
||||
},
|
||||
placementText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.Default.With(size: 72),
|
||||
UseFullGlyphHeight = false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
[
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, grid_spacing),
|
||||
new Dimension()
|
||||
],
|
||||
Content = new Drawable?[][]
|
||||
{
|
||||
[
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(grid_spacing),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = "Breakdown",
|
||||
Font = OsuFont.Default.With(size: 12)
|
||||
},
|
||||
userStatistics = new FillFlowContainer<PanelUserStatistic>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(grid_spacing)
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
PlayersDisplayArea = Empty().With(d =>
|
||||
{
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
[
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(grid_spacing),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = "Statistics",
|
||||
Font = OsuFont.Default.With(size: 12)
|
||||
},
|
||||
roomStatistics = new FillFlowContainer<PanelRoomAward>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(grid_spacing)
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.MatchRoomStateChanged += onRoomStateChanged;
|
||||
|
||||
onRoomStateChanged(client.Room?.MatchState);
|
||||
}
|
||||
|
||||
private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
|
||||
{
|
||||
if (state is not MatchmakingRoomState matchmakingState || matchmakingState.Stage != MatchmakingStage.Ended)
|
||||
return;
|
||||
|
||||
populateUserStatistics(matchmakingState);
|
||||
populateRoomStatistics(matchmakingState);
|
||||
});
|
||||
|
||||
private void populateUserStatistics(MatchmakingRoomState state)
|
||||
{
|
||||
userStatistics.Clear();
|
||||
|
||||
if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0)
|
||||
{
|
||||
placementText.Text = "-";
|
||||
addStatistic("No rounds played");
|
||||
return;
|
||||
}
|
||||
|
||||
int overallPlacement = state.Users[client.LocalUser!.UserID].Placement;
|
||||
int overallPoints = state.Users[client.LocalUser!.UserID].Points;
|
||||
int bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.Min(r => r.Placement);
|
||||
var accuracyPlacement = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average()))
|
||||
.OrderByDescending(t => t.avgAcc)
|
||||
.Select((t, i) => (info: t, index: i))
|
||||
.Single(t => t.info.user.UserId == client.LocalUser!.UserID);
|
||||
|
||||
placementText.Text = $"#{state.Users[client.LocalUser!.UserID].Placement}";
|
||||
addStatistic($"#{overallPlacement} overall ({overallPoints}pts)");
|
||||
addStatistic($"#{bestPlacement} best placement");
|
||||
addStatistic($"#{accuracyPlacement.index + 1} accuracy ({accuracyPlacement.info.avgAcc.FormatAccuracy()})");
|
||||
|
||||
void addStatistic(string text)
|
||||
{
|
||||
userStatistics.Add(new PanelUserStatistic(text)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void populateRoomStatistics(MatchmakingRoomState state)
|
||||
{
|
||||
roomStatistics.Clear();
|
||||
|
||||
long maxScore = long.MinValue;
|
||||
int maxScoreUserId = 0;
|
||||
|
||||
double maxAccuracy = double.MinValue;
|
||||
int maxAccuracyUserId = 0;
|
||||
|
||||
int maxCombo = int.MinValue;
|
||||
int maxComboUserId = 0;
|
||||
|
||||
long maxBonusScore = 0;
|
||||
int maxBonusScoreUserId = 0;
|
||||
|
||||
long largestScoreDifference = long.MinValue;
|
||||
int largestScoreDifferenceUserId = 0;
|
||||
|
||||
long smallestScoreDifference = long.MaxValue;
|
||||
int smallestScoreDifferenceUserId = 0;
|
||||
|
||||
for (int round = 1; round <= state.CurrentRound; round++)
|
||||
{
|
||||
long roundHighestScore = long.MinValue;
|
||||
int roundHighestScoreUserId = 0;
|
||||
|
||||
long roundLowestScore = long.MaxValue;
|
||||
|
||||
foreach (MatchmakingUser user in state.Users)
|
||||
{
|
||||
if (!user.Rounds.RoundsDictionary.TryGetValue(round, out MatchmakingRound? mmRound))
|
||||
continue;
|
||||
|
||||
if (mmRound.TotalScore > maxScore)
|
||||
{
|
||||
maxScore = mmRound.TotalScore;
|
||||
maxScoreUserId = user.UserId;
|
||||
}
|
||||
|
||||
if (mmRound.Accuracy > maxAccuracy)
|
||||
{
|
||||
maxAccuracy = mmRound.Accuracy;
|
||||
maxAccuracyUserId = user.UserId;
|
||||
}
|
||||
|
||||
if (mmRound.MaxCombo > maxCombo)
|
||||
{
|
||||
maxCombo = mmRound.MaxCombo;
|
||||
maxComboUserId = user.UserId;
|
||||
}
|
||||
|
||||
if (mmRound.TotalScore > roundHighestScore)
|
||||
{
|
||||
roundHighestScore = mmRound.TotalScore;
|
||||
roundHighestScoreUserId = user.UserId;
|
||||
}
|
||||
|
||||
if (mmRound.TotalScore < roundLowestScore)
|
||||
roundLowestScore = mmRound.TotalScore;
|
||||
}
|
||||
|
||||
long roundScoreDifference = roundHighestScore - roundLowestScore;
|
||||
|
||||
if (roundScoreDifference > 0 && roundScoreDifference > largestScoreDifference)
|
||||
{
|
||||
largestScoreDifference = roundScoreDifference;
|
||||
largestScoreDifferenceUserId = roundHighestScoreUserId;
|
||||
}
|
||||
|
||||
if (roundScoreDifference > 0 && roundScoreDifference < smallestScoreDifference)
|
||||
{
|
||||
smallestScoreDifference = roundScoreDifference;
|
||||
smallestScoreDifferenceUserId = roundHighestScoreUserId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (MatchmakingUser user in state.Users)
|
||||
{
|
||||
int userBonusScore = 0;
|
||||
|
||||
foreach (MatchmakingRound round in user.Rounds)
|
||||
{
|
||||
userBonusScore += round.Statistics.TryGetValue(HitResult.LargeBonus, out int bonus) ? bonus * 5 : 0;
|
||||
userBonusScore += round.Statistics.TryGetValue(HitResult.SmallBonus, out bonus) ? bonus : 0;
|
||||
}
|
||||
|
||||
if (userBonusScore > maxBonusScore)
|
||||
{
|
||||
maxBonusScore = userBonusScore;
|
||||
maxBonusScoreUserId = user.UserId;
|
||||
}
|
||||
}
|
||||
|
||||
// Highest score - highest score across all rounds.
|
||||
addStatistic(maxScoreUserId, "Highest score");
|
||||
|
||||
// Most accurate - highest accuracy across all rounds.
|
||||
addStatistic(maxAccuracyUserId, "Most accurate");
|
||||
|
||||
// Most combo - highest combo across all rounds.
|
||||
addStatistic(maxComboUserId, "Most combo");
|
||||
|
||||
// Most bonus - most bonus score across all rounds.
|
||||
if (maxBonusScoreUserId > 0)
|
||||
addStatistic(maxBonusScoreUserId, "Most bonus");
|
||||
|
||||
// Most clutch - smallest victory in any round.
|
||||
if (smallestScoreDifferenceUserId > 0)
|
||||
addStatistic(smallestScoreDifferenceUserId, "Most clutch");
|
||||
|
||||
// Best finish - largest victory in any round.
|
||||
if (largestScoreDifferenceUserId > 0)
|
||||
addStatistic(largestScoreDifferenceUserId, "Best finish");
|
||||
|
||||
void addStatistic(int userId, string text)
|
||||
{
|
||||
roomStatistics.Add(new PanelRoomAward(text, userId)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.MatchRoomStateChanged -= onRoomStateChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user