2021-08-19 11:24:06 +08:00
// 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 ;
2022-06-03 20:12:09 +08:00
using System.Diagnostics ;
2021-08-19 11:24:06 +08:00
using System.Linq ;
2022-06-03 18:17:31 +08:00
using Newtonsoft.Json ;
2022-02-15 21:02:33 +08:00
using osu.Game.Beatmaps ;
2024-08-01 17:52:41 +08:00
using osu.Game.Database ;
2021-08-19 11:24:06 +08:00
using osu.Game.Online.API ;
2022-02-15 21:02:33 +08:00
using osu.Game.Online.API.Requests ;
2021-11-04 17:02:44 +08:00
using osu.Game.Online.API.Requests.Responses ;
2021-08-19 11:24:06 +08:00
using osu.Game.Online.Rooms ;
2022-02-15 21:02:33 +08:00
using osu.Game.Rulesets ;
2021-08-19 11:24:06 +08:00
using osu.Game.Rulesets.Scoring ;
using osu.Game.Scoring ;
using osu.Game.Screens.OnlinePlay.Components ;
2022-02-15 21:02:33 +08:00
using osu.Game.Tests.Beatmaps ;
2024-08-24 05:40:51 +08:00
using osu.Game.Utils ;
2021-08-19 11:24:06 +08:00
namespace osu.Game.Tests.Visual.OnlinePlay
{
/// <summary>
/// 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.
/// </summary>
public class TestRoomRequestsHandler
{
public IReadOnlyList < Room > ServerSideRooms = > serverSideRooms ;
private readonly List < Room > serverSideRooms = new List < Room > ( ) ;
2021-12-01 19:00:31 +08:00
private int currentRoomId = 1 ;
private int currentPlaylistItemId = 1 ;
private int currentScoreId = 1 ;
2021-08-19 11:24:06 +08:00
/// <summary>
/// Handles an API request, while also updating the local state to match
/// how the server would eventually respond and update an <see cref="RoomManager"/>.
/// </summary>
/// <param name="request">The API request to handle.</param>
/// <param name="localUser">The local user to store in responses where required.</param>
2022-02-15 21:02:33 +08:00
/// <param name="beatmapManager">The beatmap manager to attempt to retrieve beatmaps from, prior to returning dummy beatmaps.</param>
2021-08-19 11:24:06 +08:00
/// <returns>Whether the request was successfully handled.</returns>
2022-02-15 21:02:33 +08:00
public bool HandleRequest ( APIRequest request , APIUser localUser , BeatmapManager beatmapManager )
2021-08-19 11:24:06 +08:00
{
switch ( request )
{
case CreateRoomRequest createRoomRequest :
2022-06-03 18:17:31 +08:00
var apiRoom = cloneRoom ( createRoomRequest . Room ) ;
2021-08-19 11:24:06 +08:00
// Passwords are explicitly not copied between rooms.
2024-11-13 18:11:05 +08:00
apiRoom . Password = createRoomRequest . Room . Password ;
2021-08-19 11:24:06 +08:00
2021-11-26 16:23:50 +08:00
AddServerSideRoom ( apiRoom , localUser ) ;
2021-08-19 11:24:06 +08:00
var responseRoom = new APICreatedRoom ( ) ;
responseRoom . CopyFrom ( createResponseRoom ( apiRoom , false ) ) ;
createRoomRequest . TriggerSuccess ( responseRoom ) ;
return true ;
case JoinRoomRequest joinRoomRequest :
{
2024-11-13 15:28:39 +08:00
var room = ServerSideRooms . Single ( r = > r . RoomID = = joinRoomRequest . Room . RoomID ) ;
2021-08-19 11:24:06 +08:00
2024-11-13 18:11:05 +08:00
if ( joinRoomRequest . Password ! = room . Password )
2021-08-19 11:24:06 +08:00
{
joinRoomRequest . TriggerFailure ( new InvalidOperationException ( "Invalid password." ) ) ;
return true ;
}
joinRoomRequest . TriggerSuccess ( ) ;
return true ;
}
2022-01-18 12:58:12 +08:00
case GetRoomLeaderboardRequest roomLeaderboardRequest :
roomLeaderboardRequest . TriggerSuccess ( new APILeaderboard
{
Leaderboard = new List < APIUserScoreAggregate >
{
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 ;
2024-06-06 17:06:22 +08:00
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" } ,
2024-07-29 16:45:03 +08:00
Mods = [ new APIMod { Acronym = @"DT" } ] ,
2024-06-06 17:06:22 +08:00
Statistics = new Dictionary < HitResult , int > ( )
} ,
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 < HitResult , int > ( )
} ,
} ,
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 < HitResult , int > ( )
} ,
} ) ;
return true ;
2021-08-19 11:24:06 +08:00
case PartRoomRequest partRoomRequest :
partRoomRequest . TriggerSuccess ( ) ;
return true ;
case GetRoomsRequest getRoomsRequest :
var roomsWithoutParticipants = new List < Room > ( ) ;
foreach ( var r in ServerSideRooms )
roomsWithoutParticipants . Add ( createResponseRoom ( r , false ) ) ;
getRoomsRequest . TriggerSuccess ( roomsWithoutParticipants ) ;
return true ;
case GetRoomRequest getRoomRequest :
2024-11-13 15:28:39 +08:00
getRoomRequest . TriggerSuccess ( createResponseRoom ( ServerSideRooms . Single ( r = > r . RoomID = = getRoomRequest . RoomId ) , true ) ) ;
2021-08-19 11:24:06 +08:00
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 < HitResult , int > ( )
} ) ;
return true ;
2022-02-15 21:02:33 +08:00
2022-09-27 19:30:41 +08:00
case GetBeatmapRequest getBeatmapRequest :
2022-07-14 07:31:37 +08:00
{
2024-10-30 15:00:57 +08:00
getBeatmapRequest . TriggerSuccess ( createResponseBeatmaps ( getBeatmapRequest . OnlineID ) . Single ( ) ) ;
2022-09-27 19:30:41 +08:00
return true ;
}
2022-02-15 21:02:33 +08:00
2022-09-27 19:30:41 +08:00
case GetBeatmapsRequest getBeatmapsRequest :
{
getBeatmapsRequest . TriggerSuccess ( new GetBeatmapsResponse { Beatmaps = createResponseBeatmaps ( getBeatmapsRequest . BeatmapIds . ToArray ( ) ) } ) ;
2022-02-15 21:02:33 +08:00
return true ;
2022-07-14 07:31:37 +08:00
}
case GetBeatmapSetRequest getBeatmapSetRequest :
{
2024-08-01 17:52:41 +08:00
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 ( ) ) ;
2022-07-14 07:31:37 +08:00
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 ;
}
2024-10-08 20:08:59 +08:00
case GetUsersRequest getUsersRequest :
{
getUsersRequest . TriggerSuccess ( new GetUsersResponse
{
Users = getUsersRequest . UserIds . Select ( id = > id = = TestUserLookupCache . UNRESOLVED_USER_ID
? null
: new APIUser
{
Id = id ,
Username = $"User {id}"
} )
. Where ( u = > u ! = null ) . ToList ( ) ,
} ) ;
return true ;
}
2021-08-19 11:24:06 +08:00
}
2022-09-27 19:30:41 +08:00
List < APIBeatmap > createResponseBeatmaps ( params int [ ] beatmapIds )
{
var result = new List < APIBeatmap > ( ) ;
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 ;
}
2021-08-19 11:24:06 +08:00
return false ;
}
/// <summary>
/// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired.
/// </summary>
/// <param name="room">The room.</param>
2021-11-26 16:23:50 +08:00
/// <param name="host">The room host.</param>
public void AddServerSideRoom ( Room room , APIUser host )
2021-08-19 11:24:06 +08:00
{
2024-11-13 15:28:39 +08:00
room . RoomID ? ? = currentRoomId + + ;
2024-11-13 16:32:32 +08:00
room . Host = host ;
2021-11-25 21:17:18 +08:00
2021-08-19 11:24:06 +08:00
for ( int i = 0 ; i < room . Playlist . Count ; i + + )
2021-11-25 21:17:18 +08:00
{
2021-08-19 11:24:06 +08:00
room . Playlist [ i ] . ID = currentPlaylistItemId + + ;
2024-11-13 16:32:32 +08:00
room . Playlist [ i ] . OwnerID = room . Host . OnlineID ;
2021-11-25 21:17:18 +08:00
}
2021-08-19 11:24:06 +08:00
serverSideRooms . Add ( room ) ;
}
private Room createResponseRoom ( Room room , bool withParticipants )
{
2022-06-03 18:17:31 +08:00
var responseRoom = cloneRoom ( room ) ;
2022-06-03 20:17:47 +08:00
// Password is hidden from the response, and is only propagated via HasPassword.
2024-11-13 18:11:05 +08:00
responseRoom . Password = responseRoom . HasPassword ? Guid . NewGuid ( ) . ToString ( ) : null ;
2022-06-03 20:17:47 +08:00
2021-08-19 11:24:06 +08:00
if ( ! withParticipants )
2024-11-14 14:07:16 +08:00
responseRoom . RecentParticipants = [ ] ;
2022-06-03 20:17:47 +08:00
2021-08-19 11:24:06 +08:00
return responseRoom ;
}
2022-06-03 18:17:31 +08:00
2022-06-03 20:12:09 +08:00
private Room cloneRoom ( Room source )
{
var result = JsonConvert . DeserializeObject < Room > ( JsonConvert . SerializeObject ( source ) ) ;
Debug . Assert ( result ! = null ) ;
2024-11-14 16:57:32 +08:00
// When serialising, only beatmap IDs are sent to the server.
// When deserialising, full beatmaps and IDs are expected to arrive.
2024-08-24 05:40:51 +08:00
2024-11-14 16:57:32 +08:00
PlaylistItem ? finalCurrentItem = result . CurrentPlaylistItem ? . With ( id : source . CurrentPlaylistItem ! . ID , beatmap : new Optional < IBeatmapInfo > ( source . CurrentPlaylistItem . Beatmap ) ) ;
PlaylistItem [ ] finalPlaylist = result . Playlist . Select ( ( pi , i ) = > pi . With ( id : source . Playlist [ i ] . ID , beatmap : new Optional < IBeatmapInfo > ( source . Playlist [ i ] . Beatmap ) ) ) . ToArray ( ) ;
// When setting the properties, we do a clear-then-add, otherwise equality comparers (that only compare by ID) pass early and members don't get replaced.
result . CurrentPlaylistItem = null ;
result . CurrentPlaylistItem = finalCurrentItem ;
result . Playlist = [ ] ;
result . Playlist = finalPlaylist ;
2022-06-03 20:12:09 +08:00
return result ;
}
2021-08-19 11:24:06 +08:00
}
}