mirror of
https://github.com/ppy/osu.git
synced 2026-06-03 15:04:26 +08:00
Add matchmaking
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
// 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.Screens.Pick;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapPanel : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("add beatmap panel", () =>
|
||||
{
|
||||
Child = new BeatmapPanel(CreateAPIBeatmap())
|
||||
{
|
||||
Size = new Vector2(300, 70),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectionGrid : OnlinePlayTestScene
|
||||
{
|
||||
private MultiplayerPlaylistItem[] items = null!;
|
||||
|
||||
private BeatmapSelectionGrid 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 BeatmapSelectionGrid
|
||||
{
|
||||
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 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(BeatmapSelectionGrid.ARRANGE_DELAY)
|
||||
.Schedule(() => grid.ArrangeItemsForRollAnimation());
|
||||
});
|
||||
|
||||
AddWaitStep("wait for movement", 5);
|
||||
|
||||
AddStep("display roll order", () =>
|
||||
{
|
||||
var panels = grid.ChildrenOfType<BeatmapSelectionPanel>().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,68 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectionOverlay : OsuTestScene
|
||||
{
|
||||
private BeatmapSelectionOverlay selectionOverlay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
{
|
||||
AddStep("add drawable", () => Child = new Container
|
||||
{
|
||||
Width = 100,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.1f,
|
||||
},
|
||||
selectionOverlay = new BeatmapSelectionOverlay
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectionOverlay()
|
||||
{
|
||||
AddStep("add maarvin", () => selectionOverlay.AddUser(new APIUser
|
||||
{
|
||||
Id = 6411631,
|
||||
Username = "Maarvin",
|
||||
}, isOwnUser: true));
|
||||
AddStep("add peppy", () => selectionOverlay.AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
}, false));
|
||||
AddStep("add smogipoo", () => selectionOverlay.AddUser(new APIUser
|
||||
{
|
||||
Id = 1040328,
|
||||
Username = "smoogipoo",
|
||||
}, false));
|
||||
AddStep("remove smogipoo", () => selectionOverlay.RemoveUser(1040328));
|
||||
AddStep("remove peppy", () => selectionOverlay.RemoveUser(2));
|
||||
AddStep("remove maarvin", () => selectionOverlay.RemoveUser(6411631));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Screens.Pick;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectionPanel : MultiplayerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapPanel()
|
||||
{
|
||||
BeatmapSelectionPanel? panel = null;
|
||||
|
||||
AddStep("add panel", () => Child = panel = new BeatmapSelectionPanel(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,89 @@
|
||||
// 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.Screens.OnlinePlay.Matchmaking.Screens.Idle;
|
||||
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()));
|
||||
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 IdleScreen())
|
||||
{
|
||||
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;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingCloud : OsuTestScene
|
||||
{
|
||||
private MatchmakingCloud cloud = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Child = cloud = new MatchmakingCloud
|
||||
{
|
||||
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,55 @@
|
||||
// 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;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingQueueScreen : ScreenTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly MatchmakingController controller = new MatchmakingController();
|
||||
|
||||
private MatchmakingQueueScreen? queueScreen => Stack.CurrentScreen as MatchmakingQueueScreen;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
AddStep("load screen", () => LoadScreen(new MatchmakingIntroScreen()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddUntilStep("wait for queue screen", () => queueScreen != null);
|
||||
|
||||
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(MatchmakingQueueScreen.MatchmakingScreenState.Idle));
|
||||
|
||||
AddStep("change state to queueing", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Queueing));
|
||||
|
||||
AddStep("change state to found match", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept));
|
||||
|
||||
AddStep("change state to waiting for room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.AcceptedWaitingForRoom));
|
||||
|
||||
AddStep("change state to in room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.InRoom));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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.Graphics.Primitives;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
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;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
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 MatchmakingScreen screen = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom();
|
||||
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
})).ToArray();
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
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 MatchmakingScreen(new MultiplayerRoom(0)
|
||||
{
|
||||
Users = users,
|
||||
Playlist = beatmaps
|
||||
}));
|
||||
});
|
||||
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayFlow()
|
||||
{
|
||||
// Initial "ready" status of the room".
|
||||
AddWaitStep("wait", 5);
|
||||
|
||||
AddStep("round start", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.RoundWarmupTime
|
||||
}).WaitSafely());
|
||||
|
||||
// Next round starts with picks.
|
||||
AddWaitStep("wait", 5);
|
||||
|
||||
AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.UserBeatmapSelect
|
||||
}).WaitSafely());
|
||||
|
||||
// Make some selections
|
||||
AddWaitStep("wait", 5);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int j = i * 2;
|
||||
AddStep("click a beatmap", () =>
|
||||
{
|
||||
Quad panelQuad = this.ChildrenOfType<BeatmapPanel>().ElementAt(j).ScreenSpaceDrawQuad;
|
||||
|
||||
InputManager.MoveMouseTo(new Vector2(panelQuad.Centre.X, panelQuad.TopLeft.Y + 5));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddWaitStep("wait", 2);
|
||||
}
|
||||
|
||||
// Lock in the gameplay beatmap
|
||||
|
||||
AddStep("selection", () =>
|
||||
{
|
||||
MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.ServerBeatmapFinalised,
|
||||
CandidateItems = beatmaps.Select(b => b.ID).ToArray(),
|
||||
CandidateItem = beatmaps[0].ID
|
||||
}).WaitSafely();
|
||||
});
|
||||
|
||||
// Prepare gameplay.
|
||||
AddWaitStep("wait", 25);
|
||||
|
||||
AddStep("prepare gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.GameplayWarmupTime
|
||||
}).WaitSafely());
|
||||
|
||||
// Start gameplay.
|
||||
AddWaitStep("wait", 5);
|
||||
|
||||
AddStep("gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.Gameplay
|
||||
}).WaitSafely());
|
||||
|
||||
AddStep("start gameplay", () => MultiplayerClient.StartMatch().WaitSafely());
|
||||
// AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true);
|
||||
|
||||
// Finish gameplay.
|
||||
AddWaitStep("wait", 5);
|
||||
|
||||
AddStep("round end", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.ResultsDisplaying
|
||||
}).WaitSafely());
|
||||
|
||||
AddWaitStep("wait", 10);
|
||||
|
||||
AddStep("room end", () =>
|
||||
{
|
||||
MatchmakingRoomState state = new MatchmakingRoomState
|
||||
{
|
||||
CurrentRound = 1,
|
||||
Stage = MatchmakingStage.Ended
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
|
||||
});
|
||||
}
|
||||
|
||||
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,119 @@
|
||||
// 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.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.Screens;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneMatchmakingScreenStack : MultiplayerTestScene
|
||||
{
|
||||
private const int user_count = 8;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom();
|
||||
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("add carousel", () =>
|
||||
{
|
||||
Child = new MatchmakingScreenStack
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
var users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"Player {i}"
|
||||
}
|
||||
}).ToArray();
|
||||
|
||||
foreach (var user in users)
|
||||
MultiplayerClient.AddUser(user);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStatus()
|
||||
{
|
||||
AddWaitStep("wait for scroll", 5);
|
||||
AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.UserBeatmapSelect
|
||||
}).WaitSafely());
|
||||
|
||||
AddWaitStep("wait for scroll", 5);
|
||||
AddStep("selection", () =>
|
||||
{
|
||||
MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
|
||||
beatmaps = Random.Shared.GetItems(beatmaps, 8);
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = MatchmakingStage.ServerBeatmapFinalised,
|
||||
CandidateItems = beatmaps.Select(b => b.ID).ToArray(),
|
||||
CandidateItem = beatmaps[0].ID
|
||||
}).WaitSafely();
|
||||
});
|
||||
|
||||
AddWaitStep("wait for scroll", 35);
|
||||
AddStep("room end", () =>
|
||||
{
|
||||
var state = new MatchmakingRoomState
|
||||
{
|
||||
CurrentRound = 1,
|
||||
Stage = MatchmakingStage.Ended
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
state.Users[1].Placement = 2;
|
||||
state.Users[1].Rounds[1].Placement = 2;
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick;
|
||||
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();
|
||||
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>();
|
||||
|
||||
PickScreen screen = null!;
|
||||
|
||||
AddStep("add screen", () => LoadScreen(screen = new PickScreen()));
|
||||
|
||||
AddStep("select maps", () =>
|
||||
{
|
||||
selectedItems.Clear();
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
var item = items[Random.Shared.Next(items.Length)];
|
||||
selectedItems.Add(item.ID);
|
||||
|
||||
MultiplayerClient.MatchmakingToggleUserSelection(user.Id, item.ID).FireAndForget();
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("show final map", () =>
|
||||
{
|
||||
long[] candidateItems = selectedItems.ToArray();
|
||||
long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)];
|
||||
|
||||
screen.RollFinalBeatmap(candidateItems, finalItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle;
|
||||
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()));
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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.Rulesets.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.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()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add results screen", () =>
|
||||
{
|
||||
Child = new ScreenStack(new ResultsScreen())
|
||||
{
|
||||
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,32 @@
|
||||
// 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.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneRoomStatisticPanel : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("add statistic", () => Child = new RoomStatisticPanel("Statistic description", new MultiplayerRoomUser(1)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 1,
|
||||
Username = "peppy"
|
||||
}
|
||||
})
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Screens.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()));
|
||||
WaitForJoined();
|
||||
|
||||
setupRequestHandler();
|
||||
|
||||
AddStep("load screen", () =>
|
||||
{
|
||||
Child = new ScreenStack(new RoundResultsScreen())
|
||||
{
|
||||
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,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.Screens.OnlinePlay.Matchmaking;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneStageBubble : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add bubble", () => Child = new StageBubble(MatchmakingStage.RoundWarmupTime, "Next Round")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 100
|
||||
});
|
||||
}
|
||||
|
||||
[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,56 @@
|
||||
// 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.MatchTypes.Matchmaking;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneStageDisplay : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add bubble", () => Child = new StageDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStartCountdown()
|
||||
{
|
||||
foreach (var status in Enum.GetValues<MatchmakingStage>())
|
||||
{
|
||||
AddStep($"{status}", () =>
|
||||
{
|
||||
MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
|
||||
{
|
||||
Stage = status
|
||||
}).WaitSafely();
|
||||
|
||||
MultiplayerClient.StartCountdown(new MatchmakingStageCountdown
|
||||
{
|
||||
Stage = status,
|
||||
TimeRemaining = TimeSpan.FromSeconds(5)
|
||||
}).WaitSafely();
|
||||
});
|
||||
|
||||
AddWaitStep("wait a bit", 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneStageText : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create display", () => Child = new StageText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(MatchmakingStage.WaitingForClientsJoin)]
|
||||
[TestCase(MatchmakingStage.RoundWarmupTime)]
|
||||
[TestCase(MatchmakingStage.UserBeatmapSelect)]
|
||||
[TestCase(MatchmakingStage.ServerBeatmapFinalised)]
|
||||
[TestCase(MatchmakingStage.WaitingForClientsBeatmapDownload)]
|
||||
[TestCase(MatchmakingStage.GameplayWarmupTime)]
|
||||
[TestCase(MatchmakingStage.Gameplay)]
|
||||
[TestCase(MatchmakingStage.ResultsDisplaying)]
|
||||
[TestCase(MatchmakingStage.Ended)]
|
||||
public void TestStatus(MatchmakingStage status)
|
||||
{
|
||||
AddStep("set status", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState { Stage = status }).WaitSafely());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -177,5 +177,28 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}));
|
||||
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMatchmaking()
|
||||
{
|
||||
AddStep("add content", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DependencyProvidingContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new MatchmakingButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ButtonSystemState = ButtonSystemState.TopLevel,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
@@ -13,7 +14,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// An interface defining a multiplayer client instance.
|
||||
/// </summary>
|
||||
public interface IMultiplayerClient : IStatefulUserHubClient
|
||||
public interface IMultiplayerClient : IStatefulUserHubClient, IMatchmakingClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Signals that the room has changed state.
|
||||
|
||||
@@ -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
|
||||
{
|
||||
public Action<Notification>? PostNotification { protected get; set; }
|
||||
|
||||
@@ -112,6 +113,22 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
public event Action<MultiplayerCountdown>? CountdownStarted;
|
||||
|
||||
public event Action<MultiplayerCountdown>? CountdownStopped;
|
||||
|
||||
public event Action<MultiplayerRoomUser, MultiplayerUserState>? UserStateChanged;
|
||||
|
||||
public event Action? MatchmakingQueueJoined;
|
||||
public event Action? MatchmakingQueueLeft;
|
||||
public event Action? MatchmakingRoomInvited;
|
||||
public event Action<long>? 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 +196,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 +275,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 +664,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
user.State = state;
|
||||
updateUserPlayingState(userId, state);
|
||||
|
||||
UserStateChanged?.Invoke(user, state);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
@@ -672,6 +697,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Debug.Assert(Room != null);
|
||||
|
||||
Room.MatchState = state;
|
||||
MatchRoomStateChanged?.Invoke(state);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
@@ -688,6 +714,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
case CountdownStartedEvent countdownStartedEvent:
|
||||
Room.ActiveCountdowns.Add(countdownStartedEvent.Countdown);
|
||||
CountdownStarted?.Invoke(countdownStartedEvent.Countdown);
|
||||
|
||||
switch (countdownStartedEvent.Countdown)
|
||||
{
|
||||
@@ -700,8 +727,13 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1001,6 +1033,80 @@ 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)
|
||||
{
|
||||
Scheduler.Add(() => MatchmakingRoomReady?.Invoke(roomId));
|
||||
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 MatchmakingJoinLobby();
|
||||
|
||||
public abstract Task MatchmakingLeaveLobby();
|
||||
|
||||
public abstract Task MatchmakingJoinQueue(MatchmakingSettings settings);
|
||||
|
||||
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(IMultiplayerClient.MatchmakingQueueJoined), ((IMultiplayerClient)this).MatchmakingQueueJoined);
|
||||
connection.On(nameof(IMultiplayerClient.MatchmakingQueueLeft), ((IMultiplayerClient)this).MatchmakingQueueLeft);
|
||||
connection.On(nameof(IMultiplayerClient.MatchmakingRoomInvited), ((IMultiplayerClient)this).MatchmakingRoomInvited);
|
||||
connection.On<long>(nameof(IMultiplayerClient.MatchmakingRoomReady), ((IMultiplayerClient)this).MatchmakingRoomReady);
|
||||
connection.On<MatchmakingLobbyStatus>(nameof(IMultiplayerClient.MatchmakingLobbyStatusChanged), ((IMultiplayerClient)this).MatchmakingLobbyStatusChanged);
|
||||
connection.On<MatchmakingQueueStatus>(nameof(IMultiplayerClient.MatchmakingQueueStatusChanged), ((IMultiplayerClient)this).MatchmakingQueueStatusChanged);
|
||||
connection.On<int, long>(nameof(IMultiplayerClient.MatchmakingItemSelected), ((IMultiplayerClient)this).MatchmakingItemSelected);
|
||||
connection.On<int, long>(nameof(IMultiplayerClient.MatchmakingItemDeselected), ((IMultiplayerClient)this).MatchmakingItemDeselected);
|
||||
};
|
||||
|
||||
IsConnected.BindTo(connector.IsConnected);
|
||||
@@ -310,6 +320,78 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connector.Disconnect();
|
||||
}
|
||||
|
||||
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(MatchmakingSettings settings)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMatchmakingServer.MatchmakingJoinQueue), settings);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
@@ -1270,6 +1271,7 @@ namespace osu.Game
|
||||
|
||||
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
|
||||
loadComponentSingleFile<BeatmapStore>(detachedBeatmapStore = new RealmDetachedBeatmapStore(), Add, true);
|
||||
loadComponentSingleFile(new MatchmakingController(), Add, true);
|
||||
|
||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||
Add(new MusicKeyBindingHandler());
|
||||
|
||||
@@ -46,6 +46,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;
|
||||
|
||||
@@ -138,23 +139,27 @@ 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 MatchmakingButton(@"button-default-select", new Color4(94, 63, 186, 255), onMatchmaking, Key.N));
|
||||
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)
|
||||
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));
|
||||
@@ -191,6 +196,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)
|
||||
|
||||
@@ -37,6 +37,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
@@ -159,6 +160,7 @@ namespace osu.Game.Screens.Menu
|
||||
},
|
||||
OnSolo = loadSongSelect,
|
||||
OnMultiplayer = () => this.Push(new Multiplayer()),
|
||||
OnMatchmaking = joinOrLeaveMatchmakingQueue,
|
||||
OnPlaylists = () => this.Push(new Playlists()),
|
||||
OnDailyChallenge = room =>
|
||||
{
|
||||
@@ -481,6 +483,8 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
private void loadSongSelect() => this.Push(new SoloSongSelect());
|
||||
|
||||
private void joinOrLeaveMatchmakingQueue() => this.Push(new MatchmakingIntroScreen());
|
||||
|
||||
private partial class MobileDisclaimerDialog : PopupDialog
|
||||
{
|
||||
public MobileDisclaimerDialog(Action confirmed)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Menu
|
||||
{
|
||||
public partial class MatchmakingButton : MainMenuButton
|
||||
{
|
||||
public MatchmakingButton(string sampleName, Color4 colour, Action<MainMenuButton, UIEvent>? clickAction = null, params Key[] triggerKeys)
|
||||
: base("matchmaking", sampleName, FontAwesome.Solid.Newspaper, colour, clickAction, triggerKeys)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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
|
||||
{
|
||||
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,117 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking
|
||||
{
|
||||
public partial class MatchmakingCloud : CompositeDrawable
|
||||
{
|
||||
private APIUser[] users = [];
|
||||
private Container usersContainer = null!;
|
||||
|
||||
public APIUser[] Users
|
||||
{
|
||||
get => users;
|
||||
set
|
||||
{
|
||||
users = value;
|
||||
|
||||
foreach (var u in usersContainer)
|
||||
u.Delay(RNG.Next(0, 1000)).FadeOut(500).Expire();
|
||||
|
||||
LoadComponentsAsync(users.Select(u => new MovingAvatar(u)), avatars =>
|
||||
{
|
||||
if (usersContainer.Count == 0)
|
||||
{
|
||||
usersContainer.ScaleTo(0)
|
||||
.ScaleTo(1, 5000, Easing.OutPow10);
|
||||
}
|
||||
|
||||
usersContainer.AddRange(avatars);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
usersContainer = new AspectContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public partial class MovingAvatar : MatchmakingAvatar
|
||||
{
|
||||
private float angle;
|
||||
private float angularSpeed;
|
||||
|
||||
private float targetSpeed;
|
||||
private float targetScale;
|
||||
private float targetAlpha;
|
||||
|
||||
public MovingAvatar(APIUser apiUser)
|
||||
: base(apiUser)
|
||||
{
|
||||
RelativePositionAxes = Axes.Both;
|
||||
Scale = new Vector2(2);
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateParams();
|
||||
|
||||
angle = RNG.NextSingle(0f, MathF.Tau);
|
||||
|
||||
angularSpeed = targetSpeed;
|
||||
Scale = new Vector2(targetScale);
|
||||
|
||||
Hide();
|
||||
this.Delay(RNG.Next(0, 1000)).FadeTo(targetAlpha, 2000, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateParams()
|
||||
{
|
||||
targetSpeed = RNG.NextSingle(0.05f, 0.5f);
|
||||
targetScale = RNG.NextSingle(0.2f, 3f);
|
||||
targetAlpha = RNG.NextSingle(0.5f, 1f);
|
||||
|
||||
Scheduler.AddDelayed(updateParams, RNG.Next(500, 5000));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
float elapsed = (float)Math.Min(20, Time.Elapsed) / 1000;
|
||||
|
||||
Scale = new Vector2((float)Interpolation.Lerp(Scale.X, targetScale, elapsed / 100));
|
||||
Alpha = (float)Interpolation.Lerp(Alpha, targetAlpha, elapsed / 100);
|
||||
angularSpeed = (float)Interpolation.Lerp(angularSpeed, targetSpeed, elapsed / 100);
|
||||
|
||||
angle += angularSpeed * elapsed * 0.5f;
|
||||
|
||||
Position = new Vector2(0.5f) +
|
||||
new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * angularSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking
|
||||
{
|
||||
public partial class MatchmakingController : Component
|
||||
{
|
||||
public readonly Bindable<MatchmakingQueueScreen.MatchmakingScreenState> CurrentState = new Bindable<MatchmakingQueueScreen.MatchmakingScreenState>();
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay? notifications { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IPerformFromScreenRunner? performer { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
private ProgressNotification? backgroundNotification;
|
||||
private Notification? readyNotification;
|
||||
private bool isBackgrounded;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.MatchmakingQueueJoined += onMatchmakingQueueJoined;
|
||||
client.MatchmakingQueueLeft += onMatchmakingQueueLeft;
|
||||
client.MatchmakingRoomInvited += onMatchmakingRoomInvited;
|
||||
client.MatchmakingRoomReady += onMatchmakingRoomReady;
|
||||
|
||||
ruleset.BindValueChanged(_ => client.MatchmakingLeaveQueue().FireAndForget());
|
||||
}
|
||||
|
||||
public void SearchInBackground()
|
||||
{
|
||||
if (isBackgrounded)
|
||||
return;
|
||||
|
||||
isBackgrounded = true;
|
||||
postNotification();
|
||||
}
|
||||
|
||||
public void SearchInForeground()
|
||||
{
|
||||
if (!isBackgrounded)
|
||||
return;
|
||||
|
||||
isBackgrounded = false;
|
||||
closeNotifications();
|
||||
}
|
||||
|
||||
private void onRoomUpdated() => Scheduler.Add(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle;
|
||||
});
|
||||
|
||||
private void onMatchmakingQueueJoined() => Scheduler.Add(() =>
|
||||
{
|
||||
CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Queueing;
|
||||
|
||||
if (isBackgrounded)
|
||||
{
|
||||
closeNotifications();
|
||||
postNotification();
|
||||
}
|
||||
});
|
||||
|
||||
private void onMatchmakingQueueLeft() => Scheduler.Add(() =>
|
||||
{
|
||||
if (CurrentState.Value != MatchmakingQueueScreen.MatchmakingScreenState.InRoom)
|
||||
CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle;
|
||||
|
||||
closeNotifications();
|
||||
});
|
||||
|
||||
private void onMatchmakingRoomInvited() => Scheduler.Add(() =>
|
||||
{
|
||||
CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept;
|
||||
|
||||
if (backgroundNotification != null)
|
||||
{
|
||||
backgroundNotification.State = ProgressNotificationState.Completed;
|
||||
backgroundNotification = null;
|
||||
}
|
||||
});
|
||||
|
||||
private void onMatchmakingRoomReady(long roomId) => Scheduler.Add(() =>
|
||||
{
|
||||
client.JoinRoom(new Room { RoomID = roomId })
|
||||
.FireAndForget(() => Scheduler.Add(() =>
|
||||
{
|
||||
CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.InRoom;
|
||||
}));
|
||||
});
|
||||
|
||||
private void postNotification()
|
||||
{
|
||||
if (backgroundNotification != null)
|
||||
return;
|
||||
|
||||
notifications?.Post(backgroundNotification = new ProgressNotification
|
||||
{
|
||||
Text = "Searching for opponents...",
|
||||
CompletionTarget = n => notifications.Post(readyNotification = n),
|
||||
CompletionText = "Your match is ready! Click to join.",
|
||||
CompletionClickAction = () =>
|
||||
{
|
||||
client.MatchmakingAcceptInvitation().FireAndForget();
|
||||
performer?.PerformFromScreen(s => s.Push(new MatchmakingIntroScreen()));
|
||||
|
||||
closeNotifications();
|
||||
return true;
|
||||
},
|
||||
CancelRequested = () =>
|
||||
{
|
||||
client.MatchmakingLeaveQueue().FireAndForget();
|
||||
|
||||
closeNotifications();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void closeNotifications()
|
||||
{
|
||||
if (backgroundNotification != null)
|
||||
{
|
||||
backgroundNotification.State = ProgressNotificationState.Cancelled;
|
||||
backgroundNotification.Close(false);
|
||||
}
|
||||
|
||||
readyNotification?.Close(false);
|
||||
|
||||
backgroundNotification = null;
|
||||
readyNotification = null;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.MatchmakingQueueJoined -= onMatchmakingQueueJoined;
|
||||
client.MatchmakingQueueLeft -= onMatchmakingQueueLeft;
|
||||
client.MatchmakingRoomInvited -= onMatchmakingRoomInvited;
|
||||
client.MatchmakingRoomReady -= onMatchmakingRoomReady;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
public partial class MatchmakingPlayer : MultiplayerPlayer
|
||||
{
|
||||
public MatchmakingPlayer(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,342 @@
|
||||
// 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 System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking
|
||||
{
|
||||
public partial class MatchmakingScreen : OsuScreen
|
||||
{
|
||||
/// <summary>
|
||||
/// Padding between rows of the content.
|
||||
/// </summary>
|
||||
private const float row_padding = 10;
|
||||
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public override bool ShowFooter => true;
|
||||
|
||||
[Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))]
|
||||
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker();
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; } = null!;
|
||||
|
||||
private readonly MultiplayerRoom room;
|
||||
|
||||
private CancellationTokenSource? downloadCheckCancellation;
|
||||
private int? lastDownloadCheckedBeatmapId;
|
||||
|
||||
public MatchmakingScreen(MultiplayerRoom room)
|
||||
{
|
||||
this.room = room;
|
||||
|
||||
Activity.Value = new UserActivity.InLobby(room);
|
||||
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
beatmapAvailabilityTracker,
|
||||
new MultiplayerRoomSounds(),
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
|
||||
Bottom = ScreenFooter.HEIGHT + 20
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, row_padding),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new Drawable[]?[]
|
||||
{
|
||||
[
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
|
||||
},
|
||||
new MatchmakingScreenStack(),
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
[
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 100,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = 200,
|
||||
},
|
||||
Child = new MatchChatDisplay(new Room(room))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
new RoundedButton
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Text = "Don't click me",
|
||||
Size = new Vector2(100, 30),
|
||||
Action = () => client.MatchmakingSkipToNextStage()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
client.UserStateChanged += onUserStateChanged;
|
||||
client.SettingsChanged += onSettingsChanged;
|
||||
client.LoadRequested += onLoadRequested;
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
|
||||
}
|
||||
|
||||
private void onRoomUpdated()
|
||||
{
|
||||
if (this.IsCurrentScreen() && client.Room == null)
|
||||
{
|
||||
Logger.Log($"{this} exiting due to loss of room or connection");
|
||||
this.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state)
|
||||
{
|
||||
if (user.Equals(client.LocalUser) && state == MultiplayerUserState.Idle)
|
||||
this.MakeCurrent();
|
||||
}
|
||||
|
||||
private void onSettingsChanged(MultiplayerRoomSettings _) => Scheduler.Add(() =>
|
||||
{
|
||||
checkForAutomaticDownload();
|
||||
updateGameplayState();
|
||||
});
|
||||
|
||||
private void onBeatmapAvailabilityChanged(ValueChangedEvent<BeatmapAvailability> e) => Scheduler.Add(() =>
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return;
|
||||
|
||||
client.ChangeBeatmapAvailability(e.NewValue).FireAndForget();
|
||||
|
||||
switch (e.NewValue.State)
|
||||
{
|
||||
case DownloadState.NotDownloaded:
|
||||
case DownloadState.LocallyAvailable:
|
||||
updateGameplayState();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
private void updateGameplayState()
|
||||
{
|
||||
if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
if (matchmakingState.Stage != MatchmakingStage.WaitingForClientsBeatmapDownload)
|
||||
return;
|
||||
|
||||
MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem;
|
||||
RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!;
|
||||
Ruleset rulesetInstance = ruleset.CreateInstance();
|
||||
|
||||
// Update global gameplay state to correspond to the new selection.
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
Ruleset.Value = ruleset;
|
||||
Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
if (Beatmap.Value is DummyWorkingBeatmap)
|
||||
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
|
||||
else
|
||||
client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
|
||||
}
|
||||
|
||||
private void onLoadRequested() => Scheduler.Add(() =>
|
||||
{
|
||||
updateGameplayState();
|
||||
this.Push(new MultiplayerPlayerLoader(() => new MatchmakingPlayer(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray())));
|
||||
});
|
||||
|
||||
private void checkForAutomaticDownload()
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
|
||||
|
||||
// This method is called every time anything changes in the room.
|
||||
// This could result in download requests firing far too often, when we only expect them to fire once per beatmap.
|
||||
//
|
||||
// Without this check, we would see especially egregious behaviour when a user has hit the download rate limit.
|
||||
if (lastDownloadCheckedBeatmapId == item.BeatmapID)
|
||||
return;
|
||||
|
||||
lastDownloadCheckedBeatmapId = item.BeatmapID;
|
||||
|
||||
downloadCheckCancellation?.Cancel();
|
||||
|
||||
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
|
||||
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
|
||||
beatmapLookupCache
|
||||
.GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
|
||||
.ContinueWith(resolved => Schedule(() =>
|
||||
{
|
||||
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
|
||||
|
||||
if (beatmapSet == null)
|
||||
return;
|
||||
|
||||
if (beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID }))
|
||||
return;
|
||||
|
||||
beatmapDownloader.Download(beatmapSet);
|
||||
}));
|
||||
}
|
||||
|
||||
private bool exitConfirmed;
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (base.OnExiting(e))
|
||||
return true;
|
||||
|
||||
if (exitConfirmed)
|
||||
{
|
||||
client.LeaveRoom().FireAndForget();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
|
||||
confirmDialog.PerformOkAction();
|
||||
else
|
||||
{
|
||||
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
|
||||
{
|
||||
exitConfirmed = true;
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
}));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
|
||||
if (e.Last is not MultiplayerPlayerLoader playerLoader)
|
||||
return;
|
||||
|
||||
if (!playerLoader.GameplayPassed)
|
||||
{
|
||||
client.AbortGameplay().FireAndForget();
|
||||
return;
|
||||
}
|
||||
|
||||
client.ChangeState(MultiplayerUserState.Idle);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.UserStateChanged -= onUserStateChanged;
|
||||
client.SettingsChanged -= onSettingsChanged;
|
||||
client.LoadRequested -= onLoadRequested;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Screens;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle
|
||||
{
|
||||
public partial class IdleScreen : MatchmakingSubScreen
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new PlayerPanelList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
this.MoveToX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// 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.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle
|
||||
{
|
||||
public partial class PlayerPanel : UserPanel
|
||||
{
|
||||
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 MatchmakingAvatar avatar = null!;
|
||||
private OsuSpriteText username = 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;
|
||||
|
||||
Add(mainContent = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(80),
|
||||
},
|
||||
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;
|
||||
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;
|
||||
|
||||
avatar.MoveTo(avatarPosition, duration, Easing.OutPow10);
|
||||
this.ResizeTo(horizontal ? new Vector2(250, 100) : new Vector2(150, 200), 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)
|
||||
{
|
||||
this.ScaleTo(1.02f, 1000, Easing.OutQuint);
|
||||
mainContent.ScaleTo(1.03f, 1000, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
this.ScaleTo(1f, 500, Easing.OutQuint);
|
||||
mainContent.ScaleTo(1, 500, Easing.OutQuint);
|
||||
|
||||
mainContent.MoveTo(Vector2.Zero, 500, Easing.OutElasticHalf);
|
||||
avatar.MoveTo(avatarPosition, 1500, Easing.OutElastic);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
var offset = (avatar.ToLocalSpace(e.ScreenSpaceMousePosition) - avatar.DrawSize / 2) * 0.02f;
|
||||
|
||||
mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutQuint);
|
||||
avatar.MoveTo(avatarPosition + offset, 400, Easing.OutQuint);
|
||||
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";
|
||||
});
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.MatchRoomStateChanged -= onRoomStateChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle
|
||||
{
|
||||
public partial class PlayerPanelList : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
public bool Horizontal { get; init; }
|
||||
|
||||
private FillFlowContainer<PlayerPanel> panels = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = panels = new FillFlowContainer<PlayerPanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(20, 5),
|
||||
LayoutEasing = Easing.InOutQuint,
|
||||
LayoutDuration = 500
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() =>
|
||||
{
|
||||
panels.Add(new PlayerPanel(user)
|
||||
{
|
||||
Horizontal = Horizontal,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
});
|
||||
|
||||
private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() =>
|
||||
{
|
||||
panels.Single(p => p.RoomUser.Equals(user)).Expire();
|
||||
});
|
||||
|
||||
private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
|
||||
{
|
||||
if (state is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
foreach (var panel in panels)
|
||||
{
|
||||
if (matchmakingState.Users.UserDictionary.TryGetValue(panel.User.Id, out MatchmakingUser? user))
|
||||
panels.SetLayoutPosition(panel, user.Placement);
|
||||
else
|
||||
panels.SetLayoutPosition(panel, float.MaxValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens
|
||||
{
|
||||
public partial class MatchmakingIntroScreen : 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!;
|
||||
|
||||
[Resolved]
|
||||
private MatchmakingController controller { get; set; } = null!;
|
||||
|
||||
public override bool AllowUserExit => !ValidForResume;
|
||||
|
||||
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);
|
||||
|
||||
[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 = "Matchmaking",
|
||||
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();
|
||||
|
||||
controller.SearchInForeground();
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
ValidForResume = false;
|
||||
|
||||
this.FadeOut(800, Easing.OutQuint);
|
||||
base.OnSuspending(e);
|
||||
}
|
||||
|
||||
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(2750))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
duckOperation?.Dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 MatchmakingQueueScreen());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens
|
||||
{
|
||||
public partial class MatchmakingQueueScreen : OsuScreen
|
||||
{
|
||||
public override bool ShowFooter => true;
|
||||
|
||||
private Container mainContent = null!;
|
||||
|
||||
private MatchmakingScreenState state;
|
||||
private MatchmakingCloud cloud = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MatchmakingController controller { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<MatchmakingScreenState> currentState = new Bindable<MatchmakingScreenState>();
|
||||
private CancellationTokenSource userLookupCancellation = new CancellationTokenSource();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
cloud = new MatchmakingCloud
|
||||
{
|
||||
Y = -100,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.6f)
|
||||
},
|
||||
new MatchmakingAvatar(api.LocalUser.Value, true)
|
||||
{
|
||||
Y = -100,
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Y = 0.25f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
mainContent = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
AutoSizeDuration = 300,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
Padding = new MarginPadding(20),
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
currentState.BindTo(controller.CurrentState);
|
||||
currentState.BindValueChanged(s => SetState(s.NewValue));
|
||||
|
||||
client.MatchmakingLobbyStatusChanged += onMatchmakingLobbyStatusChanged;
|
||||
}
|
||||
|
||||
private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() =>
|
||||
{
|
||||
userLookupCancellation.Cancel();
|
||||
var cancellation = userLookupCancellation = new CancellationTokenSource();
|
||||
|
||||
userLookupCache.GetUsersAsync(status.UsersInQueue, cancellation.Token)
|
||||
.ContinueWith(result => Schedule(() =>
|
||||
{
|
||||
APIUser?[] users = result.GetResultSafely();
|
||||
if (!cancellation.IsCancellationRequested)
|
||||
Users = users.OfType<APIUser>().ToArray();
|
||||
}), cancellation.Token);
|
||||
});
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
client.MatchmakingJoinLobby().FireAndForget();
|
||||
|
||||
using (BeginDelayedSequence(800))
|
||||
Schedule(() => SetState(currentState.Value));
|
||||
}
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
|
||||
client.MatchmakingJoinLobby().FireAndForget();
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnSuspending(e);
|
||||
|
||||
client.MatchmakingLeaveLobby().FireAndForget();
|
||||
}
|
||||
|
||||
private bool exitConfirmed;
|
||||
private bool isBackgrounded;
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (base.OnExiting(e))
|
||||
return true;
|
||||
|
||||
client.MatchmakingLeaveLobby().FireAndForget();
|
||||
|
||||
if (isBackgrounded)
|
||||
return false;
|
||||
|
||||
if (exitConfirmed)
|
||||
{
|
||||
client.MatchmakingLeaveQueue().FireAndForget();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentState.Value == MatchmakingScreenState.Idle)
|
||||
return false;
|
||||
|
||||
if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
|
||||
confirmDialog.PerformOkAction();
|
||||
else
|
||||
{
|
||||
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave the matchmaking queue?", () =>
|
||||
{
|
||||
exitConfirmed = true;
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
}));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public APIUser[] Users
|
||||
{
|
||||
set => cloud.Users = value;
|
||||
}
|
||||
|
||||
public void SetState(MatchmakingScreenState newState)
|
||||
{
|
||||
state = newState;
|
||||
|
||||
mainContent.FadeInFromZero(500, Easing.OutQuint);
|
||||
mainContent.Clear();
|
||||
|
||||
switch (newState)
|
||||
{
|
||||
case MatchmakingScreenState.Idle:
|
||||
mainContent.Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearedButton(200)
|
||||
{
|
||||
DarkerColour = colours.Blue2,
|
||||
LighterColour = colours.Blue1,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Action = () => client.MatchmakingJoinQueue(new MatchmakingSettings { RulesetId = ruleset.Value.OnlineID }).FireAndForget(),
|
||||
Text = "Begin queueing",
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
|
||||
case MatchmakingScreenState.Queueing:
|
||||
ShearedButton sendToBackgroundButton;
|
||||
|
||||
mainContent.Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Waiting for a game...",
|
||||
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
new LoadingSpinner
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
sendToBackgroundButton = new ShearedButton(200)
|
||||
{
|
||||
DarkerColour = colours.Orange3,
|
||||
LighterColour = colours.Orange4,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Queue in background",
|
||||
Action = () =>
|
||||
{
|
||||
controller.SearchInBackground();
|
||||
isBackgrounded = true;
|
||||
this.Exit();
|
||||
},
|
||||
Enabled = { Value = false },
|
||||
TooltipText = "Wait 5 seconds for this option to become available."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (state != newState)
|
||||
return;
|
||||
|
||||
sendToBackgroundButton.Enabled.Value = true;
|
||||
sendToBackgroundButton.TooltipText = "You will receive a notification when your game is ready. Make sure to watch out for it!";
|
||||
}, 5000);
|
||||
break;
|
||||
|
||||
case MatchmakingScreenState.PendingAccept:
|
||||
mainContent.Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Found a match!",
|
||||
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Regular, typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
new ShearedButton(200)
|
||||
{
|
||||
DarkerColour = colours.YellowDark,
|
||||
LighterColour = colours.YellowLight,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Action = () =>
|
||||
{
|
||||
client.MatchmakingAcceptInvitation().FireAndForget();
|
||||
SetState(MatchmakingScreenState.AcceptedWaitingForRoom);
|
||||
},
|
||||
Text = "Join match!",
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
|
||||
case MatchmakingScreenState.AcceptedWaitingForRoom:
|
||||
mainContent.Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Waiting for all players...",
|
||||
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
new LoadingSpinner
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
}
|
||||
};
|
||||
break;
|
||||
|
||||
case MatchmakingScreenState.InRoom:
|
||||
// room received, show users and transition to next screen.
|
||||
mainContent.Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Good luck!",
|
||||
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
using (BeginDelayedSequence(2000))
|
||||
Schedule(() => this.Push(new MatchmakingScreen(client.Room!)));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(newState), newState, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.MatchmakingLobbyStatusChanged -= onMatchmakingLobbyStatusChanged;
|
||||
}
|
||||
|
||||
public enum MatchmakingScreenState
|
||||
{
|
||||
Idle,
|
||||
Queueing,
|
||||
PendingAccept,
|
||||
AcceptedWaitingForRoom,
|
||||
InRoom
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens
|
||||
{
|
||||
public partial class MatchmakingScreenStack : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private ScreenStack screenStack = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Padding = new MarginPadding(10);
|
||||
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) },
|
||||
Content = new Drawable[][]
|
||||
{
|
||||
[
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.AutoSize) },
|
||||
Padding = new MarginPadding { Bottom = 20 },
|
||||
Content = new Drawable?[][]
|
||||
{
|
||||
[
|
||||
screenStack = new ScreenStack(),
|
||||
null,
|
||||
new PlayerPanelList
|
||||
{
|
||||
Horizontal = true,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 250,
|
||||
Scale = new Vector2(0.8f),
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
new StageDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
screenStack.Push(new IdleScreen());
|
||||
|
||||
client.MatchRoomStateChanged += onMatchRoomStateChanged;
|
||||
onMatchRoomStateChanged(client.Room!.MatchState);
|
||||
}
|
||||
|
||||
private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
|
||||
{
|
||||
if (state is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
switch (matchmakingState.Stage)
|
||||
{
|
||||
case MatchmakingStage.WaitingForClientsJoin:
|
||||
case MatchmakingStage.RoundWarmupTime:
|
||||
while (screenStack.CurrentScreen is not IdleScreen)
|
||||
screenStack.Exit();
|
||||
break;
|
||||
|
||||
case MatchmakingStage.UserBeatmapSelect:
|
||||
screenStack.Push(new PickScreen());
|
||||
break;
|
||||
|
||||
case MatchmakingStage.ServerBeatmapFinalised:
|
||||
Debug.Assert(screenStack.CurrentScreen is PickScreen);
|
||||
((PickScreen)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem);
|
||||
break;
|
||||
|
||||
case MatchmakingStage.ResultsDisplaying:
|
||||
screenStack.Push(new RoundResultsScreen());
|
||||
break;
|
||||
|
||||
case MatchmakingStage.Ended:
|
||||
screenStack.Push(new ResultsScreen());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.MatchRoomStateChanged -= onMatchRoomStateChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.Screens
|
||||
{
|
||||
public partial class MatchmakingSubScreen : Screen
|
||||
{
|
||||
public MatchmakingSubScreen()
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
this.MoveToX(1).MoveToX(0, 200);
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnSuspending(e);
|
||||
this.MoveToX(-1, 200);
|
||||
}
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
this.MoveToX(0, 200);
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (base.OnExiting(e))
|
||||
return true;
|
||||
|
||||
this.MoveToX(1, 200);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// 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.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick
|
||||
{
|
||||
public partial class BeatmapPanel : CompositeDrawable
|
||||
{
|
||||
public static readonly Vector2 SIZE = new Vector2(300, 70);
|
||||
|
||||
public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public APIBeatmap? Beatmap
|
||||
{
|
||||
get => beatmap;
|
||||
set
|
||||
{
|
||||
if (beatmap?.OnlineID == value?.OnlineID)
|
||||
return;
|
||||
|
||||
beatmap = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateContent();
|
||||
}
|
||||
}
|
||||
|
||||
private APIBeatmap? beatmap;
|
||||
|
||||
private Container content = null!;
|
||||
private UpdateableOnlineBeatmapSetCover cover = null!;
|
||||
|
||||
public BeatmapPanel(APIBeatmap? beatmap = null)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 6;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3
|
||||
},
|
||||
cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.1f), Color4.White.Opacity(0.3f))
|
||||
},
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 6,
|
||||
BorderThickness = 2,
|
||||
BorderColour = ColourInfo.GradientVertical(colourProvider.Background1, colourProvider.Background1.Opacity(0)),
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
},
|
||||
OverlayLayer,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateContent();
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateContent()
|
||||
{
|
||||
foreach (var child in content.Children)
|
||||
child.FadeOut(300).Expire();
|
||||
|
||||
cover.OnlineInfo = beatmap?.BeatmapSet;
|
||||
|
||||
if (beatmap != null)
|
||||
{
|
||||
var panelContent = new BeatmapPanelContent(beatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
content.Add(panelContent);
|
||||
|
||||
panelContent.FadeInFromZero(300);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class BeatmapPanelContent : CompositeDrawable
|
||||
{
|
||||
private readonly APIBeatmap beatmap;
|
||||
|
||||
public BeatmapPanelContent(APIBeatmap beatmap)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding { Horizontal = 12 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode),
|
||||
Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold),
|
||||
Shadow = false,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new TextFlowContainer(s =>
|
||||
{
|
||||
s.Shadow = false;
|
||||
s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold);
|
||||
}).With(d =>
|
||||
{
|
||||
d.RelativeSizeAxes = Axes.X;
|
||||
d.AutoSizeAxes = Axes.Y;
|
||||
d.AddText("by ");
|
||||
d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist));
|
||||
}),
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Top = 6 },
|
||||
Spacing = new Vector2(4),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = beatmap.DifficultyName,
|
||||
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
|
||||
Shadow = false,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
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.Screens.Pick
|
||||
{
|
||||
public partial class BeatmapSelectionGrid : 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 = 20;
|
||||
|
||||
public event Action<MultiplayerPlaylistItem>? ItemSelected;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private readonly Dictionary<long, BeatmapSelectionPanel> panelLookup = new Dictionary<long, BeatmapSelectionPanel>();
|
||||
|
||||
private readonly PanelGridContainer panelGridContainer;
|
||||
private readonly Container<BeatmapSelectionPanel> rollContainer;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
private bool allowSelection = true;
|
||||
|
||||
public BeatmapSelectionGrid()
|
||||
{
|
||||
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<BeatmapSelectionPanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 BeatmapSelectionPanel(item)
|
||||
{
|
||||
Size = new Vector2(300, 70),
|
||||
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<BeatmapSelectionPanel>();
|
||||
|
||||
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] * (BeatmapPanel.SIZE + new Vector2(panel_spacing));
|
||||
|
||||
panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f));
|
||||
}
|
||||
}
|
||||
|
||||
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++;
|
||||
|
||||
BeatmapSelectionPanel? 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];
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
lastPanel?.HideBorder();
|
||||
panel.ShowBorder();
|
||||
|
||||
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.ShowBorder();
|
||||
panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo)
|
||||
.ScaleTo(1.5f, 1000, Easing.OutExpo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal void PresentUnanimouslyChosenBeatmap(long finalItem)
|
||||
{
|
||||
// TODO: display special animation in this case
|
||||
|
||||
PresentRolledBeatmap(finalItem);
|
||||
}
|
||||
|
||||
private partial class PanelGridContainer : FillFlowContainer<BeatmapSelectionPanel>
|
||||
{
|
||||
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,139 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick
|
||||
{
|
||||
public partial class BeatmapSelectionOverlay : CompositeDrawable
|
||||
{
|
||||
private readonly Dictionary<int, SelectionAvatar> avatars = new Dictionary<int, SelectionAvatar>();
|
||||
|
||||
private readonly Container<SelectionAvatar> avatarContainer;
|
||||
|
||||
public new Axes AutoSizeAxes
|
||||
{
|
||||
get => base.AutoSizeAxes;
|
||||
set => base.AutoSizeAxes = value;
|
||||
}
|
||||
|
||||
public new MarginPadding Padding
|
||||
{
|
||||
get => base.Padding;
|
||||
set => base.Padding = value;
|
||||
}
|
||||
|
||||
public BeatmapSelectionOverlay()
|
||||
{
|
||||
InternalChild = avatarContainer = new Container<SelectionAvatar>();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
avatarContainer.AutoSizeAxes = AutoSizeAxes;
|
||||
avatarContainer.RelativeSizeAxes = RelativeSizeAxes;
|
||||
}
|
||||
|
||||
public bool AddUser(APIUser user, bool isOwnUser)
|
||||
{
|
||||
if (avatars.ContainsKey(user.Id))
|
||||
return false;
|
||||
|
||||
var avatar = new SelectionAvatar(user, isOwnUser)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
};
|
||||
|
||||
avatarContainer.Add(avatars[user.Id] = avatar);
|
||||
|
||||
updateLayout();
|
||||
|
||||
avatar.FinishTransforms();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveUser(int id)
|
||||
{
|
||||
if (!avatars.Remove(id, out var avatar))
|
||||
return false;
|
||||
|
||||
avatar.PopOutAndExpire();
|
||||
avatarContainer.ChangeChildDepth(avatar, float.MaxValue);
|
||||
|
||||
updateLayout();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateLayout()
|
||||
{
|
||||
const double stagger = 30;
|
||||
const float spacing = 4;
|
||||
|
||||
double delay = 0;
|
||||
float x = 0;
|
||||
|
||||
for (int i = avatarContainer.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var avatar = avatarContainer[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 bool Expired { get; private set; }
|
||||
|
||||
private readonly Container content;
|
||||
|
||||
public SelectionAvatar(APIUser user, bool isOwnUser)
|
||||
{
|
||||
Size = new Vector2(30);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new MatchmakingAvatar(user, isOwnUser)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
content.ScaleTo(0)
|
||||
.ScaleTo(1, 500, Easing.OutElasticHalf)
|
||||
.FadeIn(200);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire()
|
||||
{
|
||||
content.ScaleTo(0, 400, Easing.OutExpo);
|
||||
|
||||
this.FadeOut(100).Expire();
|
||||
Expired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick
|
||||
{
|
||||
public partial class BeatmapSelectionPanel : Container
|
||||
{
|
||||
private const float corner_radius = 6;
|
||||
private const float border_width = 3;
|
||||
|
||||
public readonly MultiplayerPlaylistItem Item;
|
||||
|
||||
private readonly Container scaleContainer;
|
||||
private readonly BeatmapPanel beatmapPanel;
|
||||
private readonly BeatmapSelectionOverlay selectionOverlay;
|
||||
private readonly Container border;
|
||||
private readonly Box flash;
|
||||
private readonly Container shadow;
|
||||
|
||||
public bool AllowSelection;
|
||||
|
||||
public Action<MultiplayerPlaylistItem>? Action;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
public override bool PropagatePositionalInputSubTree => AllowSelection;
|
||||
|
||||
public BeatmapSelectionPanel(MultiplayerPlaylistItem item)
|
||||
{
|
||||
Item = item;
|
||||
Size = BeatmapPanel.SIZE;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
shadow = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(4),
|
||||
Y = 8,
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 7,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Alpha = 0.15f,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(-border_width),
|
||||
Child = border = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = corner_radius + border_width,
|
||||
Alpha = 0,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both },
|
||||
}
|
||||
},
|
||||
beatmapPanel = new BeatmapPanel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OverlayLayer =
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
flash = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
selectionOverlay = new BeatmapSelectionOverlay
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
new HoverClickSounds(),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
|
||||
{
|
||||
var beatmap = b.GetResultSafely()!;
|
||||
|
||||
beatmap.StarRating = Item.StarRating;
|
||||
|
||||
beatmapPanel.Beatmap = beatmap;
|
||||
}));
|
||||
}
|
||||
|
||||
public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser);
|
||||
|
||||
public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId);
|
||||
|
||||
public bool RemoveUser(APIUser user) => RemoveUser(user.Id);
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
flash.FadeTo(0.2f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 300);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
|
||||
flash.FadeOut(200);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
|
||||
shadow.MoveToY(4, 400, Easing.OutExpo)
|
||||
.TransformTo(nameof(Padding), new MarginPadding(2), 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);
|
||||
shadow.MoveToY(8, 500, Easing.OutElasticHalf)
|
||||
.TransformTo(nameof(Padding), new MarginPadding(4), 400, Easing.OutExpo);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Action?.Invoke(Item);
|
||||
|
||||
flash.FadeTo(0.5f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 400);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ShowBorder() => border.Show();
|
||||
|
||||
public void HideBorder() => border.Hide();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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.Screens.Pick
|
||||
{
|
||||
public partial class PickScreen : OsuScreen
|
||||
{
|
||||
private BeatmapSelectionGrid selectionGrid = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = selectionGrid = new BeatmapSelectionGrid
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.ItemAdded += onItemAdded;
|
||||
|
||||
foreach (var item in client.Room!.Playlist)
|
||||
onItemAdded(item);
|
||||
|
||||
selectionGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID);
|
||||
|
||||
client.MatchmakingItemSelected += onItemSelected;
|
||||
client.MatchmakingItemDeselected += onItemDeselected;
|
||||
}
|
||||
|
||||
private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() =>
|
||||
{
|
||||
if (item.Expired)
|
||||
return;
|
||||
|
||||
selectionGrid.AddItem(item);
|
||||
});
|
||||
|
||||
private void onItemSelected(int userId, long itemId)
|
||||
{
|
||||
var user = client.Room!.Users.First(it => it.UserID == userId).User!;
|
||||
selectionGrid.SetUserSelection(user, itemId, true);
|
||||
}
|
||||
|
||||
private void onItemDeselected(int userId, long itemId)
|
||||
{
|
||||
var user = client.Room!.Users.First(it => it.UserID == userId).User!;
|
||||
selectionGrid.SetUserSelection(user, itemId, false);
|
||||
}
|
||||
|
||||
public void RollFinalBeatmap(long[] candidateItems, long finalItem) => selectionGrid.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,345 @@
|
||||
// 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.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.Screens.OnlinePlay.Matchmaking.Screens.Idle;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results
|
||||
{
|
||||
public partial class ResultsScreen : MatchmakingSubScreen
|
||||
{
|
||||
private const float grid_spacing = 5;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText placementText = null!;
|
||||
private FillFlowContainer<UserStatisticPanel> userStatistics = null!;
|
||||
private FillFlowContainer<RoomStatisticPanel> roomStatistics = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
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<UserStatisticPanel>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(grid_spacing)
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
new PlayerPanelList
|
||||
{
|
||||
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<RoomStatisticPanel>
|
||||
{
|
||||
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 UserStatisticPanel(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)
|
||||
{
|
||||
MultiplayerRoomUser? user = client.Room?.Users.FirstOrDefault(u => u.UserID == userId);
|
||||
|
||||
if (user == null)
|
||||
throw new InvalidOperationException($"User not found in room: {userId}");
|
||||
|
||||
roomStatistics.Add(new RoomStatisticPanel(text, user)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.MatchRoomStateChanged -= onRoomStateChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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 osu.Game.Online.Multiplayer;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results
|
||||
{
|
||||
public partial class RoomStatisticPanel : CompositeDrawable
|
||||
{
|
||||
private readonly Color4 backgroundColour = Color4.SaddleBrown;
|
||||
|
||||
private readonly string text;
|
||||
private readonly MultiplayerRoomUser user;
|
||||
|
||||
public RoomStatisticPanel(string text, MultiplayerRoomUser user)
|
||||
{
|
||||
this.text = text;
|
||||
this.user = user;
|
||||
|
||||
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}: {user.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.Screens.Results
|
||||
{
|
||||
public partial class UserStatisticPanel : CompositeDrawable
|
||||
{
|
||||
private readonly Color4 backgroundColour = Color4.SaddleBrown;
|
||||
|
||||
private readonly string text;
|
||||
|
||||
public UserStatisticPanel(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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// 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.Graphics.Containers;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults
|
||||
{
|
||||
internal partial class RoundResultsScorePanel : CompositeDrawable
|
||||
{
|
||||
public RoundResultsScorePanel(ScoreInfo score)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
InternalChild = new InstantSizingScorePanel(score);
|
||||
}
|
||||
|
||||
public override bool PropagateNonPositionalInputSubTree => false;
|
||||
public override bool PropagatePositionalInputSubTree => false;
|
||||
|
||||
private partial class InstantSizingScorePanel : ScorePanel
|
||||
{
|
||||
public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false)
|
||||
: base(score, isNewLocalScore)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
FinishTransforms(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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 System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults
|
||||
{
|
||||
public partial class RoundResultsScreen : MatchmakingSubScreen
|
||||
{
|
||||
private const int panel_spacing = 5;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
private AutoScrollContainer scrollContainer = null!;
|
||||
private LoadingSpinner loadingSpinner = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
scrollContainer = new AutoScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
loadingSpinner = new LoadingSpinner
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
loadingSpinner.Show();
|
||||
|
||||
queryScores().FireAndForget();
|
||||
}
|
||||
|
||||
private async Task queryScores()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
Task<APIBeatmap?> beatmapTask = beatmapLookupCache.GetBeatmapAsync(client.Room.CurrentPlaylistItem.BeatmapID);
|
||||
TaskCompletionSource<List<MultiplayerScore>> scoreTask = new TaskCompletionSource<List<MultiplayerScore>>();
|
||||
|
||||
var request = new IndexPlaylistScoresRequest(client.Room.RoomID, client.Room.Settings.PlaylistItemId);
|
||||
request.Success += req => scoreTask.SetResult(req.Scores);
|
||||
request.Failure += e => scoreTask.SetException(e);
|
||||
api.Queue(request);
|
||||
|
||||
await Task.WhenAll(beatmapTask, scoreTask.Task).ConfigureAwait(false);
|
||||
|
||||
APIBeatmap? apiBeatmap = beatmapTask.GetResultSafely();
|
||||
List<MultiplayerScore> apiScores = scoreTask.Task.GetResultSafely();
|
||||
|
||||
if (apiBeatmap == null)
|
||||
return;
|
||||
|
||||
// Reference: PlaylistItemResultsScreen
|
||||
setScores(apiScores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, new BeatmapInfo
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty(apiBeatmap.Difficulty),
|
||||
Metadata =
|
||||
{
|
||||
Artist = apiBeatmap.Metadata.Artist,
|
||||
Title = apiBeatmap.Metadata.Title,
|
||||
Author = new RealmUser
|
||||
{
|
||||
Username = apiBeatmap.Metadata.Author.Username,
|
||||
OnlineID = apiBeatmap.Metadata.Author.OnlineID,
|
||||
}
|
||||
},
|
||||
DifficultyName = apiBeatmap.DifficultyName,
|
||||
StarRating = apiBeatmap.StarRating,
|
||||
Length = apiBeatmap.Length,
|
||||
BPM = apiBeatmap.BPM
|
||||
})).ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to load scores for playlist item.");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Scheduler.Add(() => loadingSpinner.Hide());
|
||||
}
|
||||
}
|
||||
|
||||
private void setScores(ScoreInfo[] scores) => Scheduler.Add(() =>
|
||||
{
|
||||
Container panels;
|
||||
|
||||
scrollContainer.Child = panels = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = scores.Length * (ScorePanel.CONTRACTED_WIDTH + panel_spacing),
|
||||
ChildrenEnumerable = scores.Select(s => new RoundResultsScorePanel(s)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
})
|
||||
};
|
||||
|
||||
for (int i = 0; i < panels.Count; i++)
|
||||
{
|
||||
panels[i].MoveToX(panels.DrawWidth * 2)
|
||||
.Delay(i * 100)
|
||||
.MoveToX((ScorePanel.CONTRACTED_WIDTH + panel_spacing) * i, 500, Easing.OutQuint);
|
||||
}
|
||||
});
|
||||
|
||||
private partial class AutoScrollContainer : UserTrackingScrollContainer
|
||||
{
|
||||
private const float initial_offset = -0.5f;
|
||||
private const double scroll_duration = 20000;
|
||||
|
||||
private double? scrollStartTime;
|
||||
|
||||
public AutoScrollContainer()
|
||||
: base(Direction.Horizontal)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!UserScrolling && Children.Count > 0)
|
||||
{
|
||||
scrollStartTime ??= Time.Current;
|
||||
|
||||
double scrollOffset = (Time.Current - scrollStartTime.Value) / scroll_duration;
|
||||
|
||||
if (scrollOffset < 1)
|
||||
ScrollTo(DrawWidth * (initial_offset + scrollOffset), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// 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.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking
|
||||
{
|
||||
internal partial class StageBubble : CompositeDrawable
|
||||
{
|
||||
private readonly Color4 backgroundColour = Color4.Salmon;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private readonly MatchmakingStage stage;
|
||||
private readonly LocalisableString displayText;
|
||||
private Drawable progressBar = null!;
|
||||
|
||||
private DateTimeOffset countdownStartTime;
|
||||
private DateTimeOffset countdownEndTime;
|
||||
|
||||
public StageBubble(MatchmakingStage stage, LocalisableString displayText)
|
||||
{
|
||||
this.stage = stage;
|
||||
this.displayText = displayText;
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = backgroundColour.Darken(0.2f)
|
||||
},
|
||||
progressBar = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = backgroundColour
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = displayText,
|
||||
Padding = new MarginPadding(10)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.MatchRoomStateChanged += onMatchRoomStateChanged;
|
||||
client.CountdownStarted += onCountdownStarted;
|
||||
client.CountdownStopped += onCountdownStopped;
|
||||
|
||||
if (client.Room != null)
|
||||
{
|
||||
onMatchRoomStateChanged(client.Room.MatchState);
|
||||
foreach (var countdown in client.Room.ActiveCountdowns)
|
||||
onCountdownStarted(countdown);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
TimeSpan duration = countdownEndTime - countdownStartTime;
|
||||
|
||||
if (duration.TotalMilliseconds == 0)
|
||||
progressBar.Width = 0;
|
||||
else
|
||||
{
|
||||
TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime;
|
||||
progressBar.Width = (float)(elapsed.TotalMilliseconds / duration.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
|
||||
{
|
||||
if (state is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
if (matchmakingState.Stage == MatchmakingStage.RoundWarmupTime)
|
||||
{
|
||||
countdownStartTime = countdownEndTime = DateTimeOffset.Now;
|
||||
activate();
|
||||
}
|
||||
});
|
||||
|
||||
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
|
||||
{
|
||||
if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage)
|
||||
return;
|
||||
|
||||
countdownStartTime = DateTimeOffset.Now;
|
||||
countdownEndTime = countdownStartTime + countdown.TimeRemaining;
|
||||
activate();
|
||||
});
|
||||
|
||||
private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() =>
|
||||
{
|
||||
if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage)
|
||||
return;
|
||||
|
||||
countdownEndTime = DateTimeOffset.Now;
|
||||
deactivate();
|
||||
});
|
||||
|
||||
private void activate()
|
||||
{
|
||||
this.FadeTo(1, 200);
|
||||
}
|
||||
|
||||
private void deactivate()
|
||||
{
|
||||
this.FadeTo(0.5f, 200);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.MatchRoomStateChanged -= onMatchRoomStateChanged;
|
||||
client.CountdownStarted -= onCountdownStarted;
|
||||
client.CountdownStopped -= onCountdownStopped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking
|
||||
{
|
||||
public partial class StageDisplay : CompositeDrawable
|
||||
{
|
||||
public static readonly (MatchmakingStage status, LocalisableString text)[] DISPLAYED_STAGES =
|
||||
[
|
||||
(MatchmakingStage.RoundWarmupTime, "Next Round"),
|
||||
(MatchmakingStage.UserBeatmapSelect, "Beatmap Selection"),
|
||||
(MatchmakingStage.GameplayWarmupTime, "Get Ready"),
|
||||
(MatchmakingStage.ResultsDisplaying, "Results"),
|
||||
(MatchmakingStage.Ended, "Match End")
|
||||
];
|
||||
|
||||
public StageDisplay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
List<Dimension> columnDimensions = new List<Dimension>();
|
||||
List<Drawable> columnContent = new List<Drawable>();
|
||||
|
||||
for (int i = 0; i < DISPLAYED_STAGES.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
columnDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||
columnContent.Add(new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(16),
|
||||
Icon = FontAwesome.Solid.ArrowRight,
|
||||
Margin = new MarginPadding { Horizontal = 10 }
|
||||
});
|
||||
}
|
||||
|
||||
columnDimensions.Add(new Dimension());
|
||||
columnContent.Add(new StageBubble(DISPLAYED_STAGES[i].status, DISPLAYED_STAGES[i].text)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
});
|
||||
}
|
||||
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
],
|
||||
Content = new Drawable[][]
|
||||
{
|
||||
[
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = columnDimensions.ToArray(),
|
||||
RowDimensions = [new Dimension(GridSizeMode.AutoSize)],
|
||||
Content = new[] { columnContent.ToArray() }
|
||||
}
|
||||
],
|
||||
[
|
||||
new StageText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking
|
||||
{
|
||||
public partial class StageText : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
public StageText()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = text = new OsuSpriteText
|
||||
{
|
||||
Height = 16,
|
||||
Font = OsuFont.Default,
|
||||
AlwaysPresent = true,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.MatchRoomStateChanged += onMatchRoomStateChanged;
|
||||
onMatchRoomStateChanged(client.Room!.MatchState);
|
||||
}
|
||||
|
||||
private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() =>
|
||||
{
|
||||
if (state is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
text.Text = getTextForStatus(matchmakingState.Stage);
|
||||
});
|
||||
|
||||
private LocalisableString getTextForStatus(MatchmakingStage status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case MatchmakingStage.WaitingForClientsJoin:
|
||||
return "Players are joining the match...";
|
||||
|
||||
case MatchmakingStage.WaitingForClientsBeatmapDownload:
|
||||
return "Players are downloading the beatmap...";
|
||||
|
||||
case MatchmakingStage.Gameplay:
|
||||
return "Game is in progress...";
|
||||
|
||||
case MatchmakingStage.Ended:
|
||||
return "Thanks for playing! The match will close shortly.";
|
||||
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.MatchRoomStateChanged -= onMatchRoomStateChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
@@ -73,6 +74,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private long lastPlaylistItemId;
|
||||
private int lastCountdownId;
|
||||
|
||||
private readonly Dictionary<int, long> matchmakingUserPicks = new Dictionary<int, long>();
|
||||
|
||||
private readonly TestRoomRequestsHandler apiRequestHandler;
|
||||
|
||||
public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null)
|
||||
@@ -409,22 +412,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
break;
|
||||
|
||||
case StartMatchCountdownRequest startCountdown:
|
||||
ServerRoom.ActiveCountdowns.Add(new MatchStartCountdown
|
||||
{
|
||||
ID = ++lastCountdownId,
|
||||
TimeRemaining = startCountdown.Duration
|
||||
});
|
||||
|
||||
await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStartedEvent(ServerRoom.ActiveCountdowns[^1]))).ConfigureAwait(false);
|
||||
await StartCountdown(new MatchStartCountdown { TimeRemaining = startCountdown.Duration }).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case StopCountdownRequest stopCountdown:
|
||||
ServerRoom.ActiveCountdowns.Remove(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID));
|
||||
await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStoppedEvent(stopCountdown.ID))).ConfigureAwait(false);
|
||||
await StopCountdown(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartCountdown(MultiplayerCountdown countdown)
|
||||
{
|
||||
countdown.ID = ++lastCountdownId;
|
||||
countdown = clone(countdown);
|
||||
|
||||
Debug.Assert(ServerRoom != null);
|
||||
Debug.Assert(LocalUser != null);
|
||||
|
||||
if (countdown.IsExclusive)
|
||||
{
|
||||
MultiplayerCountdown? existingCountdown = ServerRoom.ActiveCountdowns.FirstOrDefault(c => c.GetType() == countdown.GetType());
|
||||
if (existingCountdown != null)
|
||||
await StopCountdown(existingCountdown).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ServerRoom.ActiveCountdowns.Add(countdown);
|
||||
await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStartedEvent(ServerRoom.ActiveCountdowns[^1]))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StopCountdown(MultiplayerCountdown countdown)
|
||||
{
|
||||
Debug.Assert(ServerRoom != null);
|
||||
Debug.Assert(LocalUser != null);
|
||||
|
||||
ServerRoom.ActiveCountdowns.Remove(ServerRoom.ActiveCountdowns.First(c => c.ID == countdown.ID));
|
||||
await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStoppedEvent(countdown.ID))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override Task StartMatch()
|
||||
{
|
||||
Debug.Assert(ServerRoom != null);
|
||||
@@ -718,6 +742,66 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task ChangeMatchRoomState(MatchRoomState state)
|
||||
{
|
||||
Debug.Assert(ServerRoom != null);
|
||||
|
||||
ServerRoom.MatchState = state;
|
||||
await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override Task MatchmakingJoinLobby()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task MatchmakingLeaveLobby()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task MatchmakingJoinQueue(MatchmakingSettings settings)
|
||||
{
|
||||
await ((IMultiplayerClient)this).MatchmakingQueueJoined().ConfigureAwait(false);
|
||||
await ((IMultiplayerClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task MatchmakingLeaveQueue()
|
||||
{
|
||||
await ((IMultiplayerClient)this).MatchmakingQueueLeft().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override Task MatchmakingAcceptInvitation()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task MatchmakingDeclineInvitation()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task MatchmakingToggleSelection(long playlistItemId)
|
||||
=> MatchmakingToggleUserSelection(api.LocalUser.Value.OnlineID, playlistItemId);
|
||||
|
||||
public override Task MatchmakingSkipToNextStage()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task MatchmakingToggleUserSelection(int userId, long playlistItemId)
|
||||
{
|
||||
if (matchmakingUserPicks.TryGetValue(userId, out long existingId))
|
||||
{
|
||||
if (existingId == playlistItemId)
|
||||
return;
|
||||
|
||||
await ((IMultiplayerClient)this).MatchmakingItemDeselected(clone(userId), clone(existingId)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
matchmakingUserPicks[userId] = playlistItemId;
|
||||
|
||||
await ((IMultiplayerClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#region API Room Handling
|
||||
|
||||
public IReadOnlyList<Room> ServerSideRooms
|
||||
|
||||
Reference in New Issue
Block a user