diff --git a/osu.Game/Online/Multiplayer/MatchRoomState.cs b/osu.Game/Online/Multiplayer/MatchRoomState.cs index 25de8c7fab..5313959806 100644 --- a/osu.Game/Online/Multiplayer/MatchRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchRoomState.cs @@ -4,6 +4,7 @@ using System; using MessagePack; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online.Multiplayer @@ -16,6 +17,7 @@ namespace osu.Game.Online.Multiplayer [MessagePackObject] [Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(1, typeof(MatchmakingRoomState))] + [Union(2, typeof(RankedPlayRoomState))] public abstract class MatchRoomState { } diff --git a/osu.Game/Online/Multiplayer/MatchServerEvent.cs b/osu.Game/Online/Multiplayer/MatchServerEvent.cs index 529a299438..402de392a8 100644 --- a/osu.Game/Online/Multiplayer/MatchServerEvent.cs +++ b/osu.Game/Online/Multiplayer/MatchServerEvent.cs @@ -5,6 +5,7 @@ using System; using MessagePack; using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Online.RankedPlay; namespace osu.Game.Online.Multiplayer { @@ -17,6 +18,7 @@ namespace osu.Game.Online.Multiplayer [Union(0, typeof(CountdownStartedEvent))] [Union(1, typeof(CountdownStoppedEvent))] [Union(2, typeof(MatchmakingAvatarActionEvent))] + [Union(3, typeof(RankedPlayCardHandReplayEvent))] public abstract class MatchServerEvent { } diff --git a/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayCardItem.cs b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayCardItem.cs new file mode 100644 index 0000000000..65cb1987fe --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayCardItem.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using MessagePack; + +namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay +{ + [Serializable] + [MessagePackObject] + public class RankedPlayCardItem : IEquatable + { + /// + /// A unique identifier for this card. + /// + [Key(0)] + public Guid ID { get; set; } = Guid.NewGuid(); + + public bool Equals(RankedPlayCardItem? other) + => other != null && ID.Equals(other.ID); + + public override bool Equals(object? obj) + => obj is RankedPlayCardItem other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + return ID.GetHashCode(); + } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayRoomState.cs b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayRoomState.cs new file mode 100644 index 0000000000..95fab16a7b --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayRoomState.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using MessagePack; + +namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay +{ + [Serializable] + [MessagePackObject] + public class RankedPlayRoomState : MatchRoomState + { + /// + /// The current room stage. + /// + [Key(0)] + public RankedPlayStage Stage { get; set; } + + /// + /// The current round number (1-based). + /// + [Key(1)] + public int CurrentRound { get; set; } + + /// + /// A multiplier applied to life point damage. + /// + [Key(2)] + public double DamageMultiplier { get; set; } + + /// + /// A dictionary containing all users in the room. + /// + [Key(3)] + public Dictionary Users { get; set; } = []; + + /// + /// The ID of the user currently playing a card. + /// + [Key(4)] + public int ActiveUserId { get; set; } + + /// + /// The average star rating of all cards. + /// + [Key(5)] + public double StarRating { get; set; } + + /// + /// The winner of the match. + /// + [Key(6)] + public int? WinningUserId { get; set; } + + /// + /// The user currently playing a card. + /// + [IgnoreMember] + public RankedPlayUserInfo ActiveUser => Users[ActiveUserId]; + + [IgnoreMember] + public RankedPlayUserInfo? WinningUser => WinningUserId == null ? null : Users[WinningUserId.Value]; + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayStage.cs b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayStage.cs new file mode 100644 index 0000000000..1a23b5224f --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayStage.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay +{ + public enum RankedPlayStage + { + /// + /// Waiting for clients to join. + /// + WaitForJoin, + + /// + /// Period of time before the round starts. + /// + RoundWarmup, + + /// + /// Users are discarding cards and drawing new ones. + /// + CardDiscard, + + /// + /// Users have finished discarding their cards. + /// + FinishCardDiscard, + + /// + /// The active user is selecting a card to play. + /// + CardPlay, + + /// + /// The active user has made a selection, both players should now start downloading it. + /// + FinishCardPlay, + + /// + /// Period of time before gameplay starts. + /// + GameplayWarmup, + + /// + /// Gameplay is in progress. + /// + Gameplay, + + /// + /// Users are viewing the gameplay results + /// + Results, + + /// + /// The match has concluded. + /// + Ended + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayUserInfo.cs b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayUserInfo.cs new file mode 100644 index 0000000000..a5064676c6 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchTypes/RankedPlay/RankedPlayUserInfo.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using MessagePack; + +namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay +{ + [Serializable] + [MessagePackObject] + public class RankedPlayUserInfo + { + /// + /// This user's matchmaking rating. + /// + [Key(0)] + public required int Rating { get; set; } + + /// + /// The current life points. + /// + [Key(1)] + public int Life { get; set; } = 1_000_000; + + /// + /// The cards in this user's hand. + /// + [Key(2)] + public List Hand { get; set; } = []; + + /// + /// Rating after conclusion of the match. + /// + [Key(3)] + public int RatingAfter { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 02704ea161..fdcbf75682 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -6,6 +6,7 @@ using MessagePack; using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.RankedPlay; namespace osu.Game.Online.Multiplayer { @@ -19,6 +20,7 @@ namespace osu.Game.Online.Multiplayer [Union(1, typeof(StartMatchCountdownRequest))] [Union(2, typeof(StopCountdownRequest))] [Union(3, typeof(MatchmakingAvatarActionRequest))] + [Union(4, typeof(RankedPlayCardHandReplayRequest))] public abstract class MatchUserRequest { } diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs index bc2536848b..9f4d7f1039 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -5,6 +5,7 @@ using System; using MessagePack; using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Online.RankedPlay; namespace osu.Game.Online.Multiplayer { @@ -16,6 +17,7 @@ namespace osu.Game.Online.Multiplayer [Union(1, typeof(ForceGameplayStartCountdown))] [Union(2, typeof(ServerShuttingDownCountdown))] [Union(3, typeof(MatchmakingStageCountdown))] + [Union(4, typeof(RankedPlayStageCountdown))] public abstract class MultiplayerCountdown { /// diff --git a/osu.Game/Online/RankedPlay/IRankedPlayClient.cs b/osu.Game/Online/RankedPlay/IRankedPlayClient.cs new file mode 100644 index 0000000000..ea4f9d5581 --- /dev/null +++ b/osu.Game/Online/RankedPlay/IRankedPlayClient.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; +using osu.Game.Online.Rooms; + +namespace osu.Game.Online.RankedPlay +{ + public interface IRankedPlayClient + { + /// + /// Indicates that a card has been added to a user's hand. + /// + /// The user whose hand has changed. + /// The card added to the user's hand. + Task RankedPlayCardAdded(int userId, RankedPlayCardItem card); + + /// + /// Indicates that a card has been removed from a user's hand. + /// + /// The user whose hand has changed. + /// The card removed from the user's hand. + Task RankedPlayCardRemoved(int userId, RankedPlayCardItem card); + + /// + /// Indicates that a card has been revealed to the local user. + /// + /// The card that was revealed. + /// The playlist item the card corresponds to. + Task RankedPlayCardRevealed(RankedPlayCardItem card, MultiplayerPlaylistItem item); + + /// + /// Indicates a card was played. + /// + /// The card played. + Task RankedPlayCardPlayed(RankedPlayCardItem card); + } +} diff --git a/osu.Game/Online/RankedPlay/IRankedPlayServer.cs b/osu.Game/Online/RankedPlay/IRankedPlayServer.cs new file mode 100644 index 0000000000..1e3f56d081 --- /dev/null +++ b/osu.Game/Online/RankedPlay/IRankedPlayServer.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; + +namespace osu.Game.Online.RankedPlay +{ + public interface IRankedPlayServer + { + Task DiscardCards(RankedPlayCardItem[] cards); + + Task PlayCard(RankedPlayCardItem card); + } +} diff --git a/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayEvent.cs b/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayEvent.cs new file mode 100644 index 0000000000..9c4d0afe31 --- /dev/null +++ b/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayEvent.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.RankedPlay +{ + [Serializable] + [MessagePackObject] + public class RankedPlayCardHandReplayEvent : MatchServerEvent + { + /// + /// The user performing the action. + /// + [Key(0)] + public int UserId { get; set; } + + [Key(1)] + public required RankedPlayCardHandReplayFrame[] Frames { get; init; } + } +} diff --git a/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayFrame.cs b/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayFrame.cs new file mode 100644 index 0000000000..de6b312b91 --- /dev/null +++ b/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayFrame.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using MessagePack; + +namespace osu.Game.Online.RankedPlay +{ + [Serializable] + [MessagePackObject] + public readonly record struct RankedPlayCardHandReplayFrame + { + /// + /// Duration in milliseconds since the previous frame. + /// + [Key(0)] + public required double Delay { get; init; } + + /// + /// Dictionary containing the state of each card. + /// + [Key(1)] + public required Dictionary Cards { get; init; } + + /// + /// Creates a replay frame that only contains state entries that differ from the previous frame + /// + public RankedPlayCardHandReplayFrame RelativeTo(RankedPlayCardHandReplayFrame other) => this with + { + Cards = Cards.Where(entry => !other.Cards.Contains(entry)).ToDictionary(), + }; + } +} diff --git a/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayRequest.cs b/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayRequest.cs new file mode 100644 index 0000000000..1bfbfe1cd4 --- /dev/null +++ b/osu.Game/Online/RankedPlay/RankedPlayCardHandReplayRequest.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.RankedPlay +{ + [Serializable] + [MessagePackObject] + public class RankedPlayCardHandReplayRequest : MatchUserRequest + { + [Key(0)] + public required RankedPlayCardHandReplayFrame[] Frames { get; init; } + } +} diff --git a/osu.Game/Online/RankedPlay/RankedPlayCardState.cs b/osu.Game/Online/RankedPlay/RankedPlayCardState.cs new file mode 100644 index 0000000000..bd938fb9db --- /dev/null +++ b/osu.Game/Online/RankedPlay/RankedPlayCardState.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; + +namespace osu.Game.Online.RankedPlay +{ + [Serializable] + [MessagePackObject] + public readonly record struct RankedPlayCardState + { + [Key(0)] + public required bool Hovered { get; init; } + + [Key(1)] + public required bool Pressed { get; init; } + + [Key(2)] + public required bool Selected { get; init; } + } +} diff --git a/osu.Game/Online/RankedPlay/RankedPlayStageCountdown.cs b/osu.Game/Online/RankedPlay/RankedPlayStageCountdown.cs new file mode 100644 index 0000000000..541fbb31c3 --- /dev/null +++ b/osu.Game/Online/RankedPlay/RankedPlayStageCountdown.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MessagePack; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; + +namespace osu.Game.Online.RankedPlay +{ + [MessagePackObject] + public class RankedPlayStageCountdown : MultiplayerCountdown + { + [Key(2)] + public RankedPlayStage Stage { get; set; } + } +} diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index e509891486..70c6fcf2b7 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -8,7 +8,9 @@ using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.RankedPlay; using osu.Game.Users; namespace osu.Game.Online @@ -57,6 +59,12 @@ namespace osu.Game.Online (typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)), (typeof(MatchmakingAvatarActionRequest), typeof(MatchUserRequest)), (typeof(MatchmakingAvatarActionEvent), typeof(MatchServerEvent)), + + // ranked play + (typeof(RankedPlayRoomState), typeof(MatchRoomState)), + (typeof(RankedPlayStageCountdown), typeof(MultiplayerCountdown)), + (typeof(RankedPlayCardHandReplayRequest), typeof(MatchUserRequest)), + (typeof(RankedPlayCardHandReplayEvent), typeof(MatchServerEvent)), }; } }