// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Database; 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; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.OnlinePlay { /// /// Represents a handler which pretends to be a server, handling room retrieval and manipulation requests /// and returning a roughly expected state, without the need for a server to be running. /// public class TestRoomRequestsHandler { public IReadOnlyList ServerSideRooms => serverSideRooms; private readonly List serverSideRooms = new List(); private int currentRoomId = 1; private int currentPlaylistItemId = 1; private int currentScoreId = 1; /// /// Handles an API request, while also updating the local state to match /// how the server would eventually respond and update an . /// /// The API request to handle. /// The local user to store in responses where required. /// The beatmap manager to attempt to retrieve beatmaps from, prior to returning dummy beatmaps. /// Whether the request was successfully handled. public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) { switch (request) { case CreateRoomRequest createRoomRequest: var apiRoom = cloneRoom(createRoomRequest.Room); // Passwords are explicitly not copied between rooms. apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value); apiRoom.Password.Value = createRoomRequest.Room.Password.Value; AddServerSideRoom(apiRoom, localUser); var responseRoom = new APICreatedRoom(); responseRoom.CopyFrom(createResponseRoom(apiRoom, false)); createRoomRequest.TriggerSuccess(responseRoom); return true; case JoinRoomRequest joinRoomRequest: { var room = ServerSideRooms.Single(r => r.RoomID.Value == joinRoomRequest.Room.RoomID.Value); if (joinRoomRequest.Password != room.Password.Value) { joinRoomRequest.TriggerFailure(new InvalidOperationException("Invalid password.")); return true; } joinRoomRequest.TriggerSuccess(); return true; } case GetRoomLeaderboardRequest roomLeaderboardRequest: roomLeaderboardRequest.TriggerSuccess(new APILeaderboard { Leaderboard = new List { new APIUserScoreAggregate { TotalScore = 1000000, TotalAttempts = 5, CompletedBeatmaps = 2, User = new APIUser { Username = "best user" } }, new APIUserScoreAggregate { TotalScore = 50, TotalAttempts = 1, CompletedBeatmaps = 1, User = new APIUser { Username = "worst user" } } } }); return true; case IndexPlaylistScoresRequest roomLeaderboardRequest: roomLeaderboardRequest.TriggerSuccess(new IndexedMultiplayerScores { Scores = { new MultiplayerScore { ID = currentScoreId++, Accuracy = 1, Position = 1, EndedAt = DateTimeOffset.Now, Passed = true, Rank = ScoreRank.S, MaxCombo = 1000, TotalScore = 1000000, User = new APIUser { Username = "best user" }, Statistics = new Dictionary() }, new MultiplayerScore { ID = currentScoreId++, Accuracy = 0.7, Position = 2, EndedAt = DateTimeOffset.Now, Passed = true, Rank = ScoreRank.B, MaxCombo = 100, TotalScore = 200000, User = new APIUser { Username = "worst user" }, Statistics = new Dictionary() }, }, UserScore = new MultiplayerScore { ID = currentScoreId++, Accuracy = 0.91, Position = 4, EndedAt = DateTimeOffset.Now, Passed = true, Rank = ScoreRank.A, MaxCombo = 100, TotalScore = 800000, User = localUser, Statistics = new Dictionary() }, }); return true; case PartRoomRequest partRoomRequest: partRoomRequest.TriggerSuccess(); return true; case GetRoomsRequest getRoomsRequest: var roomsWithoutParticipants = new List(); foreach (var r in ServerSideRooms) roomsWithoutParticipants.Add(createResponseRoom(r, false)); getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); return true; case GetRoomRequest getRoomRequest: getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId), true)); return true; case CreateRoomScoreRequest createRoomScoreRequest: createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); return true; case SubmitRoomScoreRequest submitRoomScoreRequest: submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore { ID = currentScoreId++, Accuracy = 1, EndedAt = DateTimeOffset.Now, Passed = true, Rank = ScoreRank.S, MaxCombo = 1000, TotalScore = 1000000, User = localUser, Statistics = new Dictionary() }); return true; case GetBeatmapRequest getBeatmapRequest: { getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.BeatmapInfo.OnlineID).Single()); return true; } case GetBeatmapsRequest getBeatmapsRequest: { getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = createResponseBeatmaps(getBeatmapsRequest.BeatmapIds.ToArray()) }); return true; } case GetBeatmapSetRequest getBeatmapSetRequest: { var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId ? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID) : beatmapManager.QueryBeatmapSet(s => s.OnlineID == getBeatmapSetRequest.ID)?.PerformRead(s => s.Beatmaps.First().Detach()); if (baseBeatmap == null) { baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; baseBeatmap.OnlineID = getBeatmapSetRequest.ID; baseBeatmap.BeatmapSet!.OnlineID = getBeatmapSetRequest.ID; } getBeatmapSetRequest.TriggerSuccess(OsuTestScene.CreateAPIBeatmapSet(baseBeatmap)); return true; } } List createResponseBeatmaps(params int[] beatmapIds) { var result = new List(); foreach (int id in beatmapIds) { var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); if (baseBeatmap == null) { baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; baseBeatmap.OnlineID = id; baseBeatmap.BeatmapSet!.OnlineID = id; } result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap)); } return result; } return false; } /// /// Adds a room to a local "server-side" list that's returned when a is fired. /// /// The room. /// The room host. public void AddServerSideRoom(Room room, APIUser host) { room.RoomID.Value ??= currentRoomId++; room.Host.Value = host; for (int i = 0; i < room.Playlist.Count; i++) { room.Playlist[i].ID = currentPlaylistItemId++; room.Playlist[i].OwnerID = room.Host.Value.OnlineID; } serverSideRooms.Add(room); } private Room createResponseRoom(Room room, bool withParticipants) { var responseRoom = cloneRoom(room); // Password is hidden from the response, and is only propagated via HasPassword. bool hadPassword = responseRoom.HasPassword.Value; responseRoom.Password.Value = null; responseRoom.HasPassword.Value = hadPassword; if (!withParticipants) responseRoom.RecentParticipants.Clear(); return responseRoom; } private Room cloneRoom(Room source) { var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); Debug.Assert(result != null); // Playlist item IDs aren't serialised. if (source.CurrentPlaylistItem.Value != null) result.CurrentPlaylistItem.Value.ID = source.CurrentPlaylistItem.Value.ID; for (int i = 0; i < source.Playlist.Count; i++) result.Playlist[i].ID = source.Playlist[i].ID; return result; } } }