MatchmakingSystemComponent.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  3. *
  4. * SPDX-License-Identifier: Apache-2.0 OR MIT
  5. *
  6. */
  7. #include "MatchmakingSystemComponent.h"
  8. #include <AzCore/Serialization/SerializeContext.h>
  9. #include <AWSCoreBus.h>
  10. #include <ResourceMapping/AWSResourceMappingBus.h>
  11. #include <Framework/Error.h>
  12. #include <Framework/ServiceRequestJob.h>
  13. #include <Multiplayer/Session/ISessionHandlingRequests.h>
  14. #include <Request/AWSGameLiftRequestBus.h>
  15. namespace MPSGameLift
  16. {
  17. namespace ServiceAPI
  18. {
  19. //! A collection of key:value pairs containing player information for use in matchmaking
  20. //! Capturing values returned by GameLift's MatchmakingTicket::Players::PlayerAttributes response
  21. //! The MultiplayerSample game doesn't match players based on any game-specific attributes,
  22. //! but the FlexMatch JSON response returns a "PlayerAttributes" table so this is here to avoid asserting.
  23. //! See https://github.com/o3de/o3de/issues/16468
  24. //! https://docs.aws.amazon.com/gamelift/latest/apireference/API_Player.html
  25. struct PlayerAttributes
  26. {
  27. bool OnJsonKey([[maybe_unused]]const char* key, AWSCore::JsonReader& reader)
  28. {
  29. return reader.Ignore();
  30. }
  31. };
  32. //! Struct for storing a player's regional latency map
  33. //! Capturing values returned by GameLift's MatchmakingTicket::Players::LatencyInMs response
  34. //! https://docs.aws.amazon.com/gamelift/latest/apireference/API_Player.html
  35. struct Latencies
  36. {
  37. bool OnJsonKey(const char* key, AWSCore::JsonReader& reader)
  38. {
  39. return reader.Accept(latencies[key]);
  40. }
  41. AZStd::unordered_map<AZStd::string, int> latencies;
  42. };
  43. //! Struct for storing a player in matchmaking
  44. //! Capturing values returned by GameLift's MatchmakingTicket::Players response
  45. //! https://docs.aws.amazon.com/gamelift/latest/apireference/API_Player.html
  46. struct Player
  47. {
  48. bool OnJsonKey(const char* key, AWSCore::JsonReader& reader)
  49. {
  50. if (strcmp(key, "LatencyInMs") == 0)
  51. {
  52. return reader.Accept(latencies);
  53. }
  54. if (strcmp(key, "PlayerId") == 0)
  55. {
  56. return reader.Accept(playerId);
  57. }
  58. if (strcmp(key, "Team") == 0)
  59. {
  60. return reader.Accept(team);
  61. }
  62. if (strcmp(key, "PlayerAttributes") == 0)
  63. {
  64. return reader.Accept(playerAttributes);
  65. }
  66. return reader.Ignore();
  67. }
  68. AZStd::string playerId;
  69. AZStd::string team;
  70. Latencies latencies;
  71. PlayerAttributes playerAttributes;
  72. };
  73. struct GameSessionConnectionInfo
  74. {
  75. bool OnJsonKey(const char* key, AWSCore::JsonReader& reader)
  76. {
  77. if (strcmp(key, "DnsName") == 0)
  78. {
  79. return reader.Accept(dnsName);
  80. }
  81. if (strcmp(key, "IpAddress") == 0)
  82. {
  83. return reader.Accept(ipAddress);
  84. }
  85. if (strcmp(key, "GameSessionArn") == 0)
  86. {
  87. return reader.Accept(gameSessionArn);
  88. }
  89. return reader.Ignore();
  90. }
  91. AZStd::string dnsName;
  92. AZStd::string ipAddress;
  93. AZStd::string gameSessionArn;
  94. };
  95. //! Struct for storing the success response.
  96. //! Capturing ticket-id and players data provided by GameLift's Matchmaking response
  97. //! https://docs.aws.amazon.com/gamelift/latest/apireference/API_MatchmakingTicket.html
  98. struct RequestMatchmakingResponse
  99. {
  100. bool OnJsonKey(const char* key, AWSCore::JsonReader& reader)
  101. {
  102. if (strcmp(key, "TicketId") == 0)
  103. {
  104. return reader.Accept(ticketId);
  105. }
  106. if (strcmp(key, "Players") == 0)
  107. {
  108. return reader.Accept(players);
  109. }
  110. if (strcmp(key, "GameSessionConnectionInfo") == 0)
  111. {
  112. return reader.Accept(gameSessionConnectionInfo);
  113. }
  114. if (strcmp(key, "Status") == 0)
  115. {
  116. return reader.Accept(status);
  117. }
  118. return reader.Ignore();
  119. }
  120. AZStd::string ticketId;
  121. AZStd::vector<Player> players;
  122. GameSessionConnectionInfo gameSessionConnectionInfo;
  123. AZStd::string status;
  124. };
  125. // Service RequestJobs
  126. AWS_FEATURE_GEM_SERVICE(MPSGameLift);
  127. //! GET request to place a matchmaking request "/requestmatchmaking".
  128. class RequestMatchmaking
  129. : public AWSCore::ServiceRequest
  130. {
  131. public:
  132. SERVICE_REQUEST(MPSGameLift, HttpMethod::HTTP_GET, "");
  133. struct Parameters
  134. {
  135. bool BuildRequest(AWSCore::RequestBuilder& request)
  136. {
  137. return request.WriteJsonBodyParameter(*this);
  138. }
  139. bool WriteJson([[maybe_unused]]AWSCore::JsonWriter& writer) const
  140. {
  141. return true;
  142. }
  143. };
  144. RequestMatchmakingResponse result;
  145. AWSCore::Error error;
  146. Parameters parameters; //! Request parameter.
  147. };
  148. using RequestMatchmakingJob = AWSCore::ServiceRequestJob<RequestMatchmaking>;
  149. struct RequestMatchStatusResponse
  150. {
  151. bool OnJsonKey(const char* key, AWSCore::JsonReader& reader)
  152. {
  153. if (strcmp(key, "PlayerSessionId") == 0)
  154. {
  155. return reader.Accept(playerSessionId);
  156. }
  157. if (strcmp(key, "IpAddress") == 0)
  158. {
  159. return reader.Accept(ipAddress);
  160. }
  161. if (strcmp(key, "DnsName") == 0)
  162. {
  163. return reader.Accept(dnsName);
  164. }
  165. if (strcmp(key, "Port") == 0)
  166. {
  167. return reader.Accept(port);
  168. }
  169. return reader.Ignore();
  170. }
  171. AZStd::string playerSessionId;
  172. AZStd::string ipAddress;
  173. AZStd::string dnsName;
  174. int port;
  175. };
  176. //! GET request to find matchmaking status "/requestmatchstatus".
  177. class RequestMatchStatus
  178. : public AWSCore::ServiceRequest
  179. {
  180. public:
  181. SERVICE_REQUEST(MPSGameLift, HttpMethod::HTTP_GET, "");
  182. struct Parameters
  183. {
  184. bool BuildRequest(AWSCore::RequestBuilder& request)
  185. {
  186. return request.WriteJsonBodyParameter(*this);
  187. }
  188. bool WriteJson([[maybe_unused]] AWSCore::JsonWriter& writer) const
  189. {
  190. return true;
  191. }
  192. };
  193. RequestMatchStatusResponse result;
  194. AWSCore::Error error;
  195. Parameters parameters; //! Request parameter.
  196. };
  197. using RequestMatchStatusJob = AWSCore::ServiceRequestJob<RequestMatchStatus>;
  198. } // ServiceAPI
  199. void MatchmakingSystemComponent::Activate()
  200. {
  201. AZ::Interface<IMatchmaking>::Register(this);
  202. AZ::Interface<Multiplayer::IMultiplayer>::Get()->AddEndpointDisconnectedHandler(m_onHostDisconnect);
  203. }
  204. void MatchmakingSystemComponent::Deactivate()
  205. {
  206. AZ::Interface<IMatchmaking>::Unregister(this);
  207. }
  208. void MatchmakingSystemComponent::Reflect(AZ::ReflectContext* context)
  209. {
  210. if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
  211. {
  212. serializeContext->Class<MatchmakingSystemComponent, AZ::Component>()
  213. ->Version(0)
  214. ;
  215. }
  216. }
  217. void MatchmakingSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
  218. {
  219. provided.push_back(AZ_CRC_CE("MPSGameLiftMatchmaking"));
  220. }
  221. bool MatchmakingSystemComponent::RequestMatch(const RegionalLatencies& regionalLatencies)
  222. {
  223. if (!m_ticketId.empty())
  224. {
  225. AZ_Warning("MatchmakingSystemComponent", false, "Ticket already exists %s", m_ticketId.c_str())
  226. return true;
  227. }
  228. // Digest latencies for the HTTP GET parameter
  229. AZ_Assert(!regionalLatencies.empty(), "IMatchmaking::RequestMatch failed! Client needs to provide regional latencies in order to determine the best server to join!")
  230. AZStd::string httpLatenciesParam;
  231. for (auto const& [region, latencyMs] : regionalLatencies)
  232. {
  233. httpLatenciesParam += AZStd::string::format("%s_%" PRIi64 "_", region.c_str(), aznumeric_cast<uint32_t>(latencyMs.count()));
  234. }
  235. httpLatenciesParam.pop_back(); // pop the trailing underscore
  236. // Set API endpoint and region
  237. ServiceAPI::RequestMatchmakingJob::Config* config = ServiceAPI::RequestMatchmakingJob::GetDefaultConfig();
  238. AZStd::string defaultRegion;
  239. AWSCore::AWSResourceMappingRequestBus::BroadcastResult(defaultRegion, &AWSCore::AWSResourceMappingRequests::GetDefaultRegion);
  240. if (defaultRegion.empty())
  241. {
  242. AZLOG_ERROR("MatchmakingSystemComponent::RequestMatch failed. Client doesn't have a default region defined and so can't find an endpoint to request an available match."
  243. "Please fill out default_aws_resource_mappings.json");
  244. return false;
  245. }
  246. AZStd::string restApi;
  247. AWSCore::AWSResourceMappingRequestBus::BroadcastResult(restApi, &AWSCore::AWSResourceMappingRequests::GetResourceNameId, "MPSMatchmaking");
  248. config->region = defaultRegion.c_str();
  249. config->endpointOverride = AZStd::string::format("https://%s.execute-api.%s.amazonaws.com/%s?latencies=%s",
  250. restApi.c_str(), defaultRegion.c_str(), "Prod/requestmatchmaking", httpLatenciesParam.c_str()).c_str();
  251. // Request serverless backend to make match
  252. ServiceAPI::RequestMatchmakingJob* requestJob = ServiceAPI::RequestMatchmakingJob::Create(
  253. [this](ServiceAPI::RequestMatchmakingJob* successJob)
  254. {
  255. m_ticketId = successJob->result.ticketId;
  256. m_matchmakingTicketReceivedEvent.Signal(m_ticketId);
  257. // Make a request to check match status every second, until we timeout, or receive a valid match
  258. m_requestMatchStatusEvent.Enqueue(AZ::SecondsToTimeMs(1.0));
  259. // Begin counting a timeout
  260. m_matchRequestTimeout = false;
  261. m_requestMatchTimeoutEvent.Enqueue(AZ::SecondsToTimeMs(MatchRequestTimeoutSeconds));
  262. },
  263. [this]([[maybe_unused]] ServiceAPI::RequestMatchmakingJob* failJob)
  264. {
  265. AZ_Error("MatchmakingSystemComponent", false, "Unable to request match error: %s", failJob->error.message.c_str());
  266. m_matchmakingFailedEvent.Signal(MatchmakingFailReason::FailedToReceiveTicket);
  267. },
  268. config);
  269. requestJob->Start();
  270. return true;
  271. }
  272. void MatchmakingSystemComponent::RequestMatchStatus()
  273. {
  274. ServiceAPI::RequestMatchStatusJob::Config* config = ServiceAPI::RequestMatchStatusJob::GetDefaultConfig();
  275. AZStd::string defaultRegion;
  276. AWSCore::AWSResourceMappingRequestBus::BroadcastResult(defaultRegion, &AWSCore::AWSResourceMappingRequests::GetDefaultRegion);
  277. if (defaultRegion.empty())
  278. {
  279. AZLOG_ERROR("MatchmakingSystemComponent::RequestMatchStatus failed. Client doesn't have a default region defined, so cannot find an endpoint to ask about the match status."
  280. "Please fill out default_aws_resource_mappings.json");
  281. return;
  282. }
  283. AZStd::string restApi;
  284. AWSCore::AWSResourceMappingRequestBus::BroadcastResult(restApi, &AWSCore::AWSResourceMappingRequests::GetResourceNameId, "MPSMatchmaking");
  285. config->region = defaultRegion.c_str();
  286. config->endpointOverride = AZStd::string::format("https://%s.execute-api.%s.amazonaws.com/%s?ticketId=%s",
  287. restApi.c_str(), defaultRegion.c_str(), "Prod/requestmatchstatus", m_ticketId.c_str()).c_str();
  288. // Ask backend for match status
  289. ServiceAPI::RequestMatchStatusJob* requestJob = ServiceAPI::RequestMatchStatusJob::Create(
  290. [this](ServiceAPI::RequestMatchStatusJob* successJob)
  291. {
  292. if (successJob->result.playerSessionId.empty() || successJob->result.playerSessionId == "NotPlacedYet")
  293. {
  294. // Make a request to check match status every second, until we timeout, or receive a valid match
  295. if (!m_matchRequestTimeout)
  296. {
  297. m_requestMatchStatusEvent.Enqueue(AZ::SecondsToTimeMs(1.0));
  298. }
  299. return;
  300. }
  301. // Enable GameLift game client system and connect to the host server
  302. m_requestMatchTimeoutEvent.RemoveFromQueue();
  303. m_matchmakingSuccessEvent.Signal();
  304. AWSGameLift::AWSGameLiftRequestBus::Broadcast(&AWSGameLift::AWSGameLiftRequestBus::Events::ConfigureGameLiftClient, "");
  305. Multiplayer::SessionConnectionConfig sessionConnectionConfig {
  306. successJob->result.playerSessionId,
  307. successJob->result.dnsName,
  308. successJob->result.ipAddress,
  309. aznumeric_cast<uint16_t>(successJob->result.port)
  310. };
  311. if (auto clientRequestHandler = AZ::Interface<Multiplayer::ISessionHandlingClientRequests>::Get())
  312. {
  313. clientRequestHandler->RequestPlayerJoinSession(sessionConnectionConfig);
  314. }
  315. },
  316. [this]([[maybe_unused]] ServiceAPI::RequestMatchStatusJob* failJob)
  317. {
  318. AZ_Error("MatchmakingSystemComponent", false, "Unable to request match status error: %s", failJob->error.message.c_str());
  319. m_matchmakingFailedEvent.Signal(MatchmakingFailReason::FailedToReceiveStatusUpdate);
  320. },
  321. config);
  322. requestJob->Start();
  323. }
  324. void MatchmakingSystemComponent::AddMatchmakingTicketReceivedEventHandler(MatchmakingTicketReceivedEvent::Handler& handler)
  325. {
  326. handler.Connect(m_matchmakingTicketReceivedEvent);
  327. }
  328. void MatchmakingSystemComponent::AddMatchmakingSuccessEventHandler(MatchmakingSuccessEvent::Handler& handler)
  329. {
  330. handler.Connect(m_matchmakingSuccessEvent);
  331. }
  332. void MatchmakingSystemComponent::AddMatchmakingFailedEventHandler(MatchmakingFailedEvent::Handler& handler)
  333. {
  334. handler.Connect(m_matchmakingFailedEvent);
  335. }
  336. }