// MIT License // // Copyright (c) 2019 Stormancer // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #pragma once #include "gamefinder/GameFinder.hpp" #include "Users/ClientAPI.hpp" #include "Users/Users.hpp" #include "stormancer/IClient.h" #include "stormancer/Event.h" #include "stormancer/msgpack_define.h" #include "stormancer/Scene.h" #include "stormancer/StormancerTypes.h" #include "stormancer/Tasks.h" #include "stormancer/Utilities/Macros.h" #include "stormancer/Utilities/PointerUtilities.h" #include "stormancer/Utilities/StringUtilities.h" #include "stormancer/Utilities/TaskUtilities.h" #include "stormancer/cpprestsdk/cpprest/json.h" #include #include #include #include namespace Stormancer { namespace Party { struct PartyUserDto; struct PartySettings; struct PartyInvitation; struct PartyCreationOptions; struct PartyGameFinderFailure; struct MembersUpdate; struct JoinPartyFromSystemArgs; enum class PartyUserStatus { NotReady = 0, Ready = 1 }; enum class PartyUserConnectionStatus { Disconnected = 0, Reservation = 1, Connected = 2 }; enum class PartyGameFinderStatus { SearchStopped = 0, SearchInProgress = 1 }; enum class MemberDisconnectionReason { Left = 0, Kicked = 1 }; /// /// Errors of the party system. /// /// /// An instance of this class represents a specific error. /// This class also contains static helpers to parse error strings. /// struct PartyError { /// /// Represents well-known causes of error. /// enum Value { UnspecifiedError, InvalidInvitation, /* You tried to perform an operation on an invitation that is no longer valid. */ AlreadyInParty, /* You tried to join a party while already being in a party. Call leaveParty() before joining the other party. */ NotInParty, /* An operation that requires you to be in a party could not be performed because you are not in a party. */ PartyNotReady, /* The party cannot enter the GameFinder yet because no GameFinder has been set in the party settings. */ Unauthorized, /* A party operation failed because you do not have the required privileges. */ StormancerClientDestroyed, /* An operation could not complete because the Stormancer client has been destroyed. */ UnsupportedPlatform /* An operation could not be performed because of missing platform-specific support. */ }; /// /// Represents the different methods of PartyApi that can emit a PartyError object. /// enum class Api { JoinParty }; struct Str { static constexpr const char* InvalidInvitation = "party.invalidInvitation"; static constexpr const char* AlreadyInParty = "party.alreadyInParty"; static constexpr const char* AlreadyInSameParty = "party.alreadyInSameParty"; static constexpr const char* NotInParty = "party.notInParty"; static constexpr const char* PartyNotReady = "party.partyNotReady"; static constexpr const char* Unauthorized = "unauthorized"; static constexpr const char* StormancerClientDestroyed = "party.clientDestroyed"; static constexpr const char* UnsupportedPlatform = "party.unsupportedPlatform"; Str() = delete; }; static Value fromString(const char* error) { if (std::strcmp(error, Str::AlreadyInParty) == 0) { return AlreadyInParty; } if (std::strcmp(error, Str::InvalidInvitation) == 0) { return InvalidInvitation; } if (std::strcmp(error, Str::NotInParty) == 0) { return NotInParty; } if (std::strcmp(error, Str::PartyNotReady) == 0) { return PartyNotReady; } if (std::strcmp(error, Str::Unauthorized) == 0) { return Unauthorized; } if (std::strcmp(error, Str::StormancerClientDestroyed) == 0) { return StormancerClientDestroyed; } if (std::strcmp(error, Str::UnsupportedPlatform) == 0) { return UnsupportedPlatform; } return UnspecifiedError; } /// /// The API call that failed /// Api apiCalled; /// /// The reason for the failure /// std::string error; /// /// Get the error code for this particular error. /// /// If the error has no particular code associated to it, this method will return UnspecifiedError. /// The error code (member of the PartyError::Value enum) corresponding to the error member. Value getErrorCode() const { return fromString(error.c_str()); } /// /// Construct a PartyError, specifying the API (PartyApi method) that failed, and the error string. /// /// The PartyApi method that failed /// The error string. This is a const char* because this value often comes from exception.what(). We avoid creating a temporary string. PartyError(Api api, const char* error) : apiCalled(api) , error(error) { } }; struct LocalPlayerInfos { std::string stormancerUserId; std::string platform; std::string pseudo; std::string platformId; std::string customData; int localPlayerIndex; MSGPACK_DEFINE(platform, stormancerUserId, pseudo, platformId, customData, localPlayerIndex) }; struct PartyUserDto { std::string userId; PartyUserStatus partyUserStatus; std::vector userData; Stormancer::SessionId sessionId; std::vector localPlayers; PartyUserConnectionStatus connectionStatus; bool isLeader = false; // Computed locally PartyUserDto(std::string userId) : userId(userId) {} PartyUserDto() = default; MSGPACK_DEFINE(userId, partyUserStatus, userData, sessionId, localPlayers, connectionStatus); }; /// /// Abstraction for a party identifier. /// /// /// Could be a stormancer scene Id, a platform-specific session Id, and more. /// struct PartyId { /// /// Platform-specific type of the PartyId. /// std::string type; /// /// Identifier for a party. /// std::string id; /// /// Platform of this PartyId. Can be empty if type is scene Id or connection token. /// std::string platform; MSGPACK_DEFINE(type, id, platform); static constexpr const char* TYPE_SCENE_ID = "stormancer.sceneId"; static constexpr const char* TYPE_PARTY_ID = "stormancer.partyId"; static constexpr const char* TYPE_CONNECTION_TOKEN = "stormancer.connectionToken"; static constexpr const char* STRING_PLATFORM_FIELD = "platform"; static constexpr const char* STRING_TYPE_FIELD = "type"; static constexpr const char* STRING_ID_FIELD = "id"; static constexpr const char* STRING_SEP_1 = ", "; static constexpr const char* STRING_SEP_2 = ": "; std::string toJson() const { auto jsonObject = web::json::value::object(); jsonObject[utility::conversions::to_string_t(STRING_ID_FIELD)] = web::json::value(utility::conversions::to_string_t(id)); jsonObject[utility::conversions::to_string_t(STRING_TYPE_FIELD)] = web::json::value(utility::conversions::to_string_t(type)); jsonObject[utility::conversions::to_string_t(STRING_PLATFORM_FIELD)] = web::json::value(utility::conversions::to_string_t(platform)); return utility::conversions::to_utf8string(jsonObject.serialize()); } static PartyId fromJson(const std::string& jsonString) { PartyId partyId; auto jsonValue = web::json::value::parse(utility::conversions::to_string_t(jsonString)); if (jsonValue.is_object()) { auto jsonObject = jsonValue.as_object(); auto idIt = jsonObject.find(utility::conversions::to_string_t(STRING_ID_FIELD)); if (idIt != jsonObject.end() && idIt->second.is_string()) { partyId.id = utility::conversions::to_utf8string(idIt->second.as_string()); } auto typeIt = jsonObject.find(utility::conversions::to_string_t(STRING_TYPE_FIELD)); if (typeIt != jsonObject.end() && typeIt->second.is_string()) { partyId.type = utility::conversions::to_utf8string(typeIt->second.as_string()); } auto platformIt = jsonObject.find(utility::conversions::to_string_t(STRING_PLATFORM_FIELD)); if (platformIt != jsonObject.end() && platformIt->second.is_string()) { partyId.type = utility::conversions::to_utf8string(platformIt->second.as_string()); } } return partyId; } std::string toString() const { std::stringstream ss; ss << STRING_PLATFORM_FIELD << STRING_SEP_2 << platform << STRING_SEP_1 << STRING_TYPE_FIELD << STRING_SEP_2 << type << STRING_SEP_1 << STRING_ID_FIELD << STRING_SEP_2 << id; return ss.str(); } static PartyId fromString(const std::string& partyIdStr) { PartyId partyId; auto parts = stringSplit(partyIdStr, STRING_SEP_1); if (parts.size() == 3) { auto platform = stringSplit(parts[0], STRING_SEP_2); if (platform[0] == STRING_PLATFORM_FIELD) { partyId.platform = platform[1]; } auto type = stringSplit(parts[1], STRING_SEP_2); if (type[1] == STRING_TYPE_FIELD) { partyId.type = type[1]; } auto id = stringSplit(parts[2], STRING_SEP_2); if (id[2] == STRING_ID_FIELD) { partyId.id = id[1]; } } return partyId; } bool operator==(const PartyId& right) { return !((*this) != right); } bool operator!=(const PartyId& right) { return (id != right.id || type != right.type || (!platform.empty() && !right.platform.empty() && platform != right.platform)); } }; /// /// Contains information about a party that the current user can join. /// struct AdvertisedParty { /// /// A friend of the current user. /// struct Friend { /// /// Stormancer user Id of the friend. May be empty. /// std::string stormancerId; /// /// Platform-specific user Id of the friend. May be empty. /// std::string platformId; /// /// Username of the friend. May be empty. /// std::string username; /// /// Additional data for this friend. /// std::unordered_map data; MSGPACK_DEFINE(stormancerId, platformId, username, data); }; /// /// Abstract party Id, possibly platform-specific. /// PartyId partyId; /// /// Stormancer user Id of the party leader. May be empty. /// std::string leaderUserId; /// /// List of friends who are in the party. /// std::vector friends; /// /// Additional metadata for the party. /// std::unordered_map metadata; MSGPACK_DEFINE(partyId, leaderUserId, friends, metadata); }; struct PartyDocument { std::string id; std::string content; MSGPACK_DEFINE(id, content) }; struct SearchResult { Stormancer::uint32 total; std::vector hits; MSGPACK_DEFINE(total, hits) }; class PartyApi { public: virtual ~PartyApi() = default; /// /// Create and join a new party. /// /// /// If the local player is currently in a party, the operation fails. /// The local player will be the leader of the newly created party. /// /// Party creation parameters /// A task that completes when the party has been created and joined. virtual pplx::task createParty(const PartyCreationOptions& partyRequest, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Creates a party if the user is not connected to one. /// /// Party creation parameters. /// Cancellation token that cancels party creation. /// virtual pplx::task createPartyIfNotJoined(const PartyCreationOptions& partyRequest, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Join an existing party using a connection token provided by the server /// /// Token required to connect to the party. /// A task that completes once the party has been joined. virtual pplx::task joinParty(const std::string& connectionToken, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Join a party using an abstract PartyId. /// /// Abstract PartyId. /// A task that completes once the party has been joined. virtual pplx::task joinParty(const PartyId& partyId, const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Join an existing party using its unique scene Id. /// /// Id of the party scene. /// A task that completes once the party has been joined. virtual pplx::task joinPartyBySceneId(const std::string& sceneId, const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Join an existing party using an invitationCode. /// /// /// custom data associated with the party member on join. /// /// virtual pplx::task joinPartyByInvitationCode(const std::string& invitationCode, const std::vector& userData = {}, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Gets a boolean indicating if the party is currently in a game session. /// /// virtual bool isInGameSession() = 0; /// /// If the party is in a game session, gets a token to connect to it. /// /// /// virtual pplx::task getCurrentGameSessionConnectionToken(pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Leave the party /// /// A task that completes with the operation. virtual pplx::task leaveParty(pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Check if you are currently in a party. /// /// /// true if you are in a party, false otherwise. /// Note that if you are in the process of joining or creating a party, but are not finished yet, this method will also return false. /// virtual bool isInParty() const noexcept = 0; /// /// Get the party scene. /// /// The party scene. virtual std::shared_ptr getPartyScene() const = 0; /// /// Get the member list of the currently joined party. /// /// /// It is invalid to call this method while not in a party. /// Call isInParty() to check. /// /// A vector of structs that describe every user who is currently in the party. /// If you are not in a party. virtual std::vector getPartyMembers() const = 0; /// /// Get the local member's party data. /// /// /// This method is a shortcut for calling getPartyMembers() and iterating over the list to find the local member. /// /// /// It is invalid to call this method while not in a party. /// Call isInParty() to check. /// /// The struct containing the local player's party data. /// If you are not in a party. virtual PartyUserDto getLocalMember() const = 0; virtual bool tryGetLocalMember(PartyUserDto* localMember) const = 0; /// /// Set the local player's status (ready/not ready). /// /// /// By default, a GameFinder request (matchmaking group queuing) is automatically started when all players in the party are ready. /// This behavior can be controlled server-side. See the Party documentation for details. /// /// Ready or not ready /// A task that completes when the update has been sent. /// If you are not in a party. virtual pplx::task updatePlayerStatus(PartyUserStatus playerStatus) = 0; /// /// Get the settings of the current party. /// /// The settings of the current party, if the current user is currently in a party. /// If you are not in a party. virtual PartySettings getPartySettings() const = 0; /// /// Get the partyId of the current party. /// /// The partyId of the current party, if the current user is currently in a party. /// If you are not in a party. virtual PartyId getPartyId() const = 0; /// /// Get the User Id of the party leader. /// /// The Stormancer User Id of the party leader. /// If you are not in a party. virtual std::string getPartyLeaderId() const = 0; /// /// Update the party settings /// /// /// Party settings can only be set by the party leader. /// Party settings are automatically replicated to other players. The current value is available /// in the current party object. Subscribe to the onUpdatedPartySettings event to listen to update events. /// /// New settings /// A task that completes when the settings have been updated and replicated to other players. /// If you are not in a party. virtual pplx::task updatePartySettings(PartySettings partySettings) = 0; /// /// Update the data associated with the local player /// /// /// player data are automatically replicated to other players. The current value is available /// in the current party members list. Subscribe to the OnUpdatedPartyMembers event to listen to update events. /// /// New player data /// The local players /// A task that completes when the data has been updated and replicated to other players. /// If you are not in a party. virtual pplx::task updatePlayerData(std::vector data, std::vector localPlayers) = 0; /// /// Update the data associated with the local player /// /// /// player data are automatically replicated to other players. The current value is available /// in the current party members list. Subscribe to the OnUpdatedPartyMembers event to listen to update events. /// /// New player data /// A task that completes when the data has been updated and replicated to other players. /// If you are not in a party. pplx::task updatePlayerData(std::vector data) { return updatePlayerData(data, this->getLocalMember().localPlayers); } /// /// Check if the local user is the leader of the party. /// /// true if the local user is the leader, false otherwise. /// If you are not in a party. virtual bool isLeader() const = 0; /// /// Promote the specified user as leader /// /// /// The caller must be the leader of the party /// The new leader must be in the party /// /// The id of the player to promote /// A task that completes when the underlying RPC (remote procedure call) has returned. /// If you are not in a party. virtual pplx::task promoteLeader(std::string userId) = 0; /// /// Kick the specified user from the party /// /// /// The caller must be the leader of the party /// If the user has already left the party, the operation succeeds. /// /// The id of the player to kick /// A task that completes when the underlying RPC (remote procedure call) has returned. /// If you are not in a party. virtual pplx::task kickPlayer(std::string userId) = 0; /// /// Creates an invitation code that can be used by users to join the party. /// virtual pplx::task createInvitationCode(pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; virtual pplx::task cancelInvitationCode(pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Get pending party invitations for the player. /// /// /// Call subscribeOnInvitationReceived() in order to be notified when an invitation is received. /// /// A vector of invitations that have been received and have not yet been accepted. virtual std::vector getPendingInvitations() = 0; /// /// Get the list of invitations the player has sent for the current party. /// /// /// This list will only contain invitations that support cancellation. /// Invitations that are backed by a system which doesn't support cancellation, like most platform-specific invitation systems, will not appear in the list. /// If your game needs cancelable invitations as a feature, you should always set forceStormancerInvite to true when calling sendInvitation(). /// /// A vector of user ids to whom invitations have been sent but not yet accepted or declined. virtual std::vector getSentPendingInvitations() = 0; /// /// Check whether the local player can send invitations with sendInvitation(). /// /// true if the local player is in a party, and is authorized to send invitations, false otherwise. /// /// By default, invitations can only be sent by the leader of the party. /// This restriction can be lifted by setting PartyCreationOptions::onlyLeaderCanInvite to false when creating a party, /// or later on by changing the party settings with updatePartySettings(). /// virtual bool canSendInvitations() const = 0; /// /// Send an invitation to another player. /// /// Stormancer Id of the player to be invited. /// If true, always send a Stormancer invitation, even if a platform-specific invitation system is available. /// /// The stormancer server determines the kind of invitation that should be sent according to the sender and the recipient's platform. /// Unless is set to true, stormancer will prioritize platform-specific invitation systems where possible. /// If your game needs cancelable invitations as a feature, you should always set to true. /// /// A task that completes when the invitation has been sent. virtual pplx::task sendInvitation(const std::string& recipient, bool forceStormancerInvite = false) = 0; /// /// Show the system UI to send invitations to the current party, if the current platform supports it. /// /// true if we were able to show the UI, false otherwise. virtual bool showSystemInvitationUI() = 0; /// /// Cancel an invitation that was previously sent. /// /// Stormancer Id of the player who was previously invited through sendInvitation(). /// /// This call will only have an effect if the invitation is backed by a system which supports canceling an invitation, such as Stormancer invitations, /// and the invitation has not yet been accepted or declined by the recipient. /// In all other circumstances, it will have no effect. /// virtual void cancelInvitation(const std::string& recipient) = 0; /// /// Get advertised parties. /// /// A list of advertised parties. virtual pplx::task> getAdvertisedParties(pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; /// /// Get the PartyApi's DependencyScope. /// /// The DependencyScope of the PartyApi instance. virtual const DependencyScope& dependencyScope() const = 0; /// /// Register a callback to be notified when the list of sent invitations changes. /// /// Callable object taking a vector if strings as parameter. /// A Subscription object to track the lifetime of the subscription. /// /// The vector of strings passed to is the list of stormancer Ids to which you have sent an invitation to the current party. /// Only invitations that are cancelable, and have not yet been accepted or declined by their recipient, will appear in the list. /// virtual Subscription subscribeOnSentInvitationsListUpdated(std::function)> callback) = 0; /// /// Register a callback to be notified when an invitation that you previously sent has been declined by its recipient. /// /// Callable object taking the stormancer Id of the recipient of the ivitation as parameter. /// A Subscription object to track the lifetime of the subscription. /// /// An invtitation system may have the notion of a user declining an invitation that they received. The Stormancer invitation system does. /// When an invitation that was sent through such a system is declined, and said system supports notifying the sender about the declination, this event will be triggered on the sender's side. /// virtual Subscription subscribeOnSentInvitationDeclined(std::function callback) = 0; /// /// Register a callback to be run when the party leader changes the party settings. /// /// Callable object taking a PartySettings struct as parameter. /// A Subscription object to track the lifetime of the subscription. virtual Event::Subscription subscribeOnUpdatedPartySettings(std::function callback) = 0; /// /// Register a callback to be run when the party member list changes. /// /// /// This event is triggered for any kind of change to the list: /// - Member addition and removal /// - Member data change /// - Member status change /// - Leader change /// The list of PartyUserDto passed to the callback contains only the entries that have changed. /// To retrieve the updated full list of members, call getPartyMembers() (it is safe to call from inside the callback too). /// /// Callable object taking a vector of PartyUserDto structs as parameter. /// A Subscription object to track the lifetime of the subscription. STORM_DEPRECATED("Use subscribeOnPartyMembersUpdated() instead") virtual Event>::Subscription subscribeOnUpdatedPartyMembers(std::function)> callback) = 0; /// /// Register a callback to be run when there is a change to any party member. /// /// /// This event is triggered for any kind of change to the party members: /// - Member joining, leaving or being kicked /// - Member data change /// - Member status change /// - Leader change /// A single event can contain multiple kinds of changes for multiple party members. /// The MembersUpdate object passed to the callback contains the details of every change. /// To retrieve the updated full list of members, call getPartyMembers() (it is safe to call from inside the callback too). /// /// Callable object taking a MembersUpdate struct as parameter. /// A Subscription object to track the lifetime of the subscription. virtual Subscription subscribeOnPartyMembersUpdated(std::function callback) = 0; /// /// Register a callback to be run when the local player has joined a party. /// /// Callable object. /// A Subscription object to track the lifetime of the subscription. virtual Event<>::Subscription subscribeOnJoinedParty(std::function callback) = 0; /// /// Register a callback to be run when the local player has left the party. /// /// /// The callback parameter MemberDisconnectionReason will be set to Kicked if you were kicked by the party leader. /// In any other case, it will be set to Left. /// /// Callable object taking a MemberDisconnectionReason parameter. /// A Subscription object to track the lifetime of the subscription. virtual Event::Subscription subscribeOnLeftParty(std::function callback) = 0; /// /// Register a callback to be run when the local player receives an invitation to a party from a remote player. /// /// /// To accept the invitation, call joinParty(PartyInvitation). /// To retrieve the list of all pending invitations received by the local player, call getPendingInvitations(). /// /// Callable object taking a PartyInvitation parameter. /// A Subscription object to track the lifetime of the subscription. virtual Event::Subscription subscribeOnInvitationReceived(std::function callback) = 0; /// /// Register a callback to be run when an invitation sent to the local player was canceled by the sender. /// /// Callable object taking the Id of the user who canceled the invitation. /// A Subscription object to track the lifetime of the subscription. virtual Event::Subscription subscribeOnInvitationCanceled(std::function callback) = 0; /// /// Register a callback to be run when the status of the GameFinder for this party is updated. /// /// /// Monitoring the status of the GameFinder can be useful to provide visual feedback to the player. /// /// Callable object taking a GameFinderStatus. /// A Subscription object to track the lifetime of the subscription. virtual Subscription subscribeOnGameFinderStatusUpdate(std::function callback) = 0; /// /// Register a callback to be run when a game session has been found for this party. /// /// /// This event happens as a result of a successful GameFinder request. Call subscribeOnGameFinderStatusUpdate() to monitor the state of the request. /// /// Callable object taking a GameFinder::GameFinderResponse containing the information needed to join the game session. /// A Subscription object to track the lifetime of the subscription. virtual Subscription subscribeOnGameFound(std::function callback) = 0; /// /// Register a callback to be run when an error occurs while looking for a game session. /// /// /// This event is triggered when an ongoing GameFinder request for this party fails for any reason. /// GameFinder failure conditions are fully customizable on the server side ; please see the GameFinder documentation for details. /// /// Callable object taking a PartyGameFinderFailure containing details about the failure. /// A Subscription object to track the lifetime of the subscription. virtual Subscription subscribeOnGameFinderFailure(std::function callback) = 0; /// /// Register a callback to be run when an error occurs in the party system. /// /// Callable object taking a const PartyError& that holds data about the error. /// A Subscription object to control the lifetime of the subscription. virtual Subscription subscribeOnPartyError(std::function callback) = 0; virtual pplx::task searchParties(const std::string& jsonQuery, Stormancer::uint32 skip, Stormancer::uint32 size, pplx::cancellation_token cancellationToken) = 0; }; /// /// Arguments passed to the callback set by setJoinPartyFromSystemHandler() when a join party from system event occurs. /// struct JoinPartyFromSystemArgs { std::shared_ptr client; std::shared_ptr party; std::shared_ptr user; PartyId partyId; pplx::cancellation_token cancellationToken = pplx::cancellation_token::none(); std::vector userData; }; /// /// Party creation settings. /// /// /// Some of these settings can be changed by the party leader after the party has been created, by calling PartyApi::updatePartySettings(). /// struct PartyCreationOptions { /// /// Optional: Set this if you want to force the party's scene Id to a specific value. /// /// /// This should be left empty, unless you have very specific needs. /// For instance, it could be used if you wanted to bypass stormancer's built-in platform-specific session and invitation integration. /// This cannot be changed after the party has been created. /// std::string platformSessionId; /// /// Required: Name of the GameFinder that the party will use. /// /// /// This GameFinder must exist and be accessible from the party on the server application. /// This setting can be changed after the party has been created. /// std::string GameFinderName; /// /// Optional: Game-specific, party-wide custom data. /// /// /// This is the custom data for the whole party. After the party has been created, it can be changed by the party leader using PartyApi::updatePartySettings(). /// This must not be confused with per-player custom data, which can be set using PartyApi::updatePlayerData(). /// std::string CustomData; /// /// Optional: Settings for server-side extensions of the Party system. /// /// /// If you are using any Party extensions that require settings at party creation time, these settings should be put in this map. /// These settings cannot be changed after the party has been created. /// std::unordered_map serverSettings; /// /// Optional: If true, only the party leader can send invitations to other players. If false, all party members can send invitations. /// /// /// By default, the party leader is the player who created the party. It can be changed later by calling PartyApi::promoteLeader(). /// This setting can be changed after the party has been created. /// bool onlyLeaderCanInvite = true; /// /// Optional: Whether the party can be joined by other players, including players who have been invited. /// /// /// When this is false, nobody can join the party. /// This setting can be changed after the party has been created. /// bool isJoinable = true; /// /// Whether the party is public or private. /// /// /// A public party is always visible to other players. /// A private party is visible only to players who have received an invitation. /// On some platforms, only public parties can be advertised. /// bool isPublic = false; /// /// Gets or sets binary member data to associate the party leader with on party join. /// std::vector userData; MSGPACK_DEFINE(platformSessionId, GameFinderName, CustomData, serverSettings, onlyLeaderCanInvite, isJoinable, isPublic, userData); }; namespace details { class IPartyInvitationInternal { public: virtual ~IPartyInvitationInternal() = default; virtual std::string getSenderId() = 0; virtual std::string getSenderPlatformId() = 0; virtual pplx::task acceptAndJoinParty(const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) = 0; virtual void decline() = 0; virtual bool isValid() = 0; }; } struct PartyInvitation { PartyInvitation(std::shared_ptr invite) : _internal(invite) { } #ifdef __clang__ // Avoid clang warnings with implicit default constructors. Note: the same solution cannot be applied with MSVC (and isn't needed). STORM_WARNINGS_PUSH; STORM_CLANG_DIAGNOSTIC("clang diagnostic ignored \"-Wdeprecated-declarations\"") PartyInvitation(const PartyInvitation& other) = default; PartyInvitation(PartyInvitation&& other) = default; PartyInvitation& operator=(const PartyInvitation& other) = default; STORM_WARNINGS_POP; #endif /// /// Get the stormancer Id of the user who sent the invitation. /// /// The stormancer Id of the player who sent the invitation. std::string getSenderId() const { return _internal->getSenderId(); } std::string getSenderPlatformId() const { return _internal->getSenderPlatformId(); } /// /// Accept the invitation and join the corresponding party. /// /// A task that completes once the party has been joined. pplx::task acceptAndJoinParty(const std::vector& userData = {}, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) { return _internal->acceptAndJoinParty(userData, userMetadata, ct); } /// /// Decline the invitation. /// /// /// This will remove the invitation from the list obtained via PartyApi::getPendingInvitations(), /// and, if the underlying invitation system supports it, send a declination message. /// void decline() { _internal->decline(); } /// /// Check whether this invitation is still valid. /// /// /// An invitation becomes invalid once it has been accepted or denied. /// /// true if the invitation is valid, false otherwise. bool isValid() const { return _internal->isValid(); } private: std::shared_ptr _internal; }; struct PartySettings { std::string gameFinderName; std::string customData; bool onlyLeaderCanInvite = true; bool isJoinable = true; std::unordered_map publicServerData; // Not in MSGPACK_DEFINE because cannot be set by the client /// /// Json document used to search the party. /// /// /// Must be a valid json object. /// The party is not searchable if set to empty or an invalid json object. /// The content of the document are indexed using the field paths as keys, with '.' as separator. /// /// For example, the following document: /// { /// "maxPlayers":3, /// "gamemode":{ /// "map":"level3-a", /// "extraFooEnabled":true /// } /// } /// /// will be indexed with the following keys: /// - "numplayers": 3 (numeric) /// - "gamemode.map":"level3-a" (string) /// - "gamemode.extraFooEnabled":true (bool) /// /// To enable search without filtering, set indexedDocument to an empty json object '{}'. /// std::string indexedDocument; std::string partyId; MSGPACK_DEFINE(gameFinderName, customData, onlyLeaderCanInvite, isJoinable, indexedDocument, partyId); }; struct PartyGameFinderFailure { std::string reason; MSGPACK_DEFINE(reason); }; /// /// This event is triggered when the state of one or more party members changes. /// struct MembersUpdate { /// /// The possible kinds of changes that can affect a party member. /// enum Kind { Joined, /* This member just joined the party */ Left, /* This member just left the party */ Kicked, /* This member was kicked from the party. The 'Left' bit will be set too in this case. */ StatusUpdated, /* member.partyUserStatus has changed. */ DataUpdated, /* member.userData has changed. */ PromotedToLeader, /* member is the new party leader */ DemotedFromLeader, /* member is no longer the party leader */ NUM_KINDS }; struct MemberUpdate { /// /// The kind of changes that affect member. /// /// /// Multiple kinds of changes can happen at the same time for the same member. /// When a certain kind of change is present, the corresponding Kind bit will be set. /// /// /// Checking if this member's data has changed: /// /// if (changes[MembersUpdate::DataUpdated]) /// // member.userData has changed /// /// std::bitset changes; /// /// The member whose state has changed. /// PartyUserDto member; MemberUpdate() = default; MemberUpdate(PartyUserDto member, Kind updateKind) : member(std::move(member)) { changes.set(updateKind); } }; /// /// Convenience pointer to the PartyApi. /// /// /// Calling getPartyMembers() from inside this event will yield the updated member list. /// std::shared_ptr partyApi; /// /// This list of member updates which have occurred. /// std::vector updatedMembers; }; namespace Platform { struct PlatformInvitationRequestContext { /// If the error string is empty, the party api will try to join the filled partyId. /// /// Maybe you will need to set error with `Party::PartyError::Str::InvalidInvitation`. /// std::string error; /// Party Id to join. PartyId partyId; /// Invited user. std::shared_ptr invitedUser; /// Cancellation token. pplx::cancellation_token cancellationToken = pplx::cancellation_token::none(); }; /// /// Interface for a platform-specific invitation to a party. /// class IPlatformInvitation { public: virtual ~IPlatformInvitation() = default; /// /// This method is called when the user accepts the invitation. /// /// /// Inside this method, you must do the operations required by your platform to accept the invitation, if any. /// You must also provide a PartyId for the party to be joined. /// /// PartyApi instance /// /// A task which result is a PartyId to the party. /// virtual pplx::task accept(std::shared_ptr party) = 0; virtual PartyId getPartyId() = 0; /// /// This method is called when the user declines the invitation. /// /// /// If your platform has support for declining an invitation, you must do the necessary operations there. /// Otherwise, you should return pplx::task_from_result(). /// /// PartyApi instance virtual pplx::task decline(std::shared_ptr party) = 0; /// /// Get the stormancer user Id of the sender. /// /// /// You must provide a way to retrieve the stormancer user Id of the user who sent the invitation. /// /// The stormancer user Id of the player who sent the invitation. virtual std::string getSenderId() = 0; /// /// Get the platform-specific user Id of the sender. /// /// The platform-specific user Id of the sender. virtual std::string getSenderPlatformId() = 0; // Called by PartyApi Subscription subscribeOnInvitationCanceled(std::function callback) { return _invitationCanceledEvent.subscribe(callback); } protected: /// /// Notify the party system that this invitation was canceled by its sender. /// /// This is relevant for invitation systems that support invitation canceling. void notifyInvitationCanceled() { _invitationCanceledEvent(); } private: Event<> _invitationCanceledEvent; }; /// /// This class transmits platform-specific invitation events from the platform support providers to the PartyApi. /// /// /// It allows decoupling PartyApi and IPlatformSupportProvider to avoid cyclic dependency issues. /// class InvitationMessenger { public: InvitationMessenger(std::shared_ptr logger) : _logger(logger) { } void notifyInvitationReceived(std::shared_ptr invitation) { if (_invitationReceivedEvent.hasSubscribers()) { _logger->log(LogLevel::Trace, "InvitationMessenger", "Fire event invitation received"); _invitationReceivedEvent(invitation); } else { _logger->log(LogLevel::Trace, "InvitationMessenger", "Store invitation as pending invitation"); _pendingInvitation = invitation; } } Subscription subscribeOnInvitationReceived(std::function)> callback) { auto subscription = _invitationReceivedEvent.subscribe(callback); if (_pendingInvitation) { _logger->log(LogLevel::Trace, "InvitationMessenger", "Fire event invitation received (pending invitation)"); _invitationReceivedEvent(_pendingInvitation); _pendingInvitation.reset(); } return subscription; } private: Event> _invitationReceivedEvent; std::shared_ptr _pendingInvitation; std::shared_ptr _logger; }; /// /// Platform-specific extensibility points for the party system. /// class IPlatformSupportProvider { public: virtual ~IPlatformSupportProvider() = default; IPlatformSupportProvider(std::shared_ptr messenger) : _invitationMessenger(messenger) { } /// /// The name of the platform. There cannot be more than one IPlatformSupportProvider implementation per platform. /// /// Name of the platform virtual std::string getPlatformName() = 0; // Platform-specific session /// /// Retrieve the stormancer PartyId for a platform-specific PartyId. /// /// Platform-specific Id for the party for this platform. /// The stormancer scene Id for the party. If the function can't find the sceneId, it returns an empty string. virtual pplx::task getPartyId(const PartyId&, pplx::cancellation_token = pplx::cancellation_token::none()) { STORM_RETURN_TASK_FROM_EXCEPTION(std::runtime_error("Unsupported"), PartyId); } /// /// Create or join a platform-specific session for the party. /// /// /// This method is called when creating or joining a party. /// /// Scene Id of the party being joined /// A task that should complete when the platform-specific session has been joined. virtual pplx::task createOrJoinSessionForParty(const std::string& /*partySceneId*/) { return pplx::task_from_result(); } /// /// Leave a platform-specific session that backs a party. /// /// Scene Id of the party that we are leaving /// A task that should complete when the platform-specific session has been left. virtual pplx::task leaveSessionForParty(const std::string& /*partySceneId*/) { return pplx::task_from_result(); } /// /// Register additional routes on the party scene. /// /// Scene of the party virtual void onPartySceneInitialization(std::shared_ptr /*partyScene*/) {} /// /// Kick a player from the platform-specific session that backs the party. /// /// Stormancer Id of the player to kick. /// A task that should complete when the player has been kicked from the platform-specific session. virtual pplx::task kickPlayer(const std::string& /*playerId*/) { return pplx::task_from_result(); } /// /// Update the platform-specific session settings according to the party settings. /// /// /// Implement this method if you need to keep settings in sync between the party and your platform-specific session. /// /// Party settings /// A task that should complete when the platform-specific session settings have been updated, if needed. virtual pplx::task updateSessionSettings(const PartySettings& /*settings*/) { return pplx::task_from_result(); } /// /// Update the platform-specific session members according to their counterparts in the party. /// /// /// Implement this method if you need to keep member data in sync between the party and your platform-specific session. /// /// Object describing the changes that have occurred for party members. /// A task that should complete when the platform-specific session members have been updated, if needed. virtual pplx::task updateSessionMembers(const MembersUpdate&) { return pplx::task_from_result(); } // Advertised parties /// /// Get a list of parties advertised by this platform. /// /// /// For instance, these can be parties joined by friends of the current user. /// /// List of parties advertised on this platform virtual pplx::task> getAdvertisedParties(pplx::cancellation_token = pplx::cancellation_token::none()) { return pplx::task_from_result(std::vector{}); } /// /// Show a platform-specific UI to send invitations to the current party /// /// /// true if the invitation UI could be shown, false otherwise. virtual bool tryShowSystemInvitationUI(std::shared_ptr /*partyApi*/) { return false; } protected: /// /// Call this method when the user receives an invitation on this platform. /// /// /// This method notifies the party system that an invitation has been received. /// void notifyInvitationReceived(std::shared_ptr invitation) { _invitationMessenger->notifyInvitationReceived(invitation); } private: std::shared_ptr _invitationMessenger; }; } /// /// This context is used by . /// It contains data used to connect to the party scene. /// struct JoiningPartyContext { std::vector memberData; std::string partySceneId; PartyId partyId; void* customContext; std::shared_ptr partyApi; std::unordered_map metadata; }; struct JoinedPartyContext { std::string partySceneId; PartyId partyId; std::shared_ptr partyApi; }; struct LeavingPartyContext { std::string partySceneId; PartyId partyId; std::shared_ptr partyApi; }; struct LeftPartyContext { std::string partySceneId; PartyId partyId; std::shared_ptr partyApi; MemberDisconnectionReason reason; }; class IPartyEventHandler { public: virtual ~IPartyEventHandler() = default; /// /// This event is fired during the initialization of a party scene that is being joined. /// /// /// This event enables you to add handlers for custom routes and server-to-client RPCs. /// /// Scene of the party you are currently joining. virtual void onPartySceneInitialization(std::shared_ptr /*partyScene*/) { } /// /// This event is fired before a connection token is requested to join a party. tasks. /// /// /// This gives you a chance to add additional operations as part of the JoinParty process. /// For instance, you could join a platform-specific online session, as an alternative to implementing this functionality in the server application. /// /// The general Party API /// Id of the party's scene /// /// A task that should complete when your custom operation is done. /// If this task is faulted or canceled, the user will be disconnected from the party immediately. /// virtual pplx::task onJoiningParty(std::shared_ptr /*ctx*/) { return pplx::task_from_result(); } /// /// This event is fired upon leaving the Party you were previously in. /// /// /// This gives you a chance to perform additional operations when you are leaving a party. /// For instance, if you joined a platform-specific online session in onJoiningParty(), /// you probably want to leave this session in onLeavingParty(). /// /// The general Party API /// Id of the party's scene /// /// A task that should complete when your custom operation is done. /// virtual pplx::task onLeavingParty(std::shared_ptr /*ctx*/) { return pplx::task_from_result(); } /// /// This event is fired when a party member has been kicked by the local member. /// /// /// This will only be called if the local player has the permission to kick. Currently, only the leader can kick other players. /// Using this handler, you can perform additional operations, such as kicking the player from a platform-specific session. /// /// /// Stormancer Id of the player who is being kicked. virtual void onPlayerKickedByLocalMember(std::shared_ptr, std::string /*playerId*/) {} /// /// This event is fired when a change happens to one or more party members, and when members join or leave the party. /// /// Structure containing details about the updated members. virtual void onPartyMembersUpdated(const MembersUpdate& /*update*/) {} /// /// The event is fired when the party settings change. /// /// /// The updated party settings. virtual void onPartySettingsUpdated(std::shared_ptr, const PartySettings&) {} /// /// This event is fired when the local player joins a party. /// /// virtual void onJoinedParty(std::shared_ptr /*ctx*/) {} /// /// This event is fired when the local player leaves the party. /// /// /// The cause of the player leaving. virtual void onLeftParty(std::shared_ptr /*ctx*/) {} }; namespace details { struct PartySettingsInternal { std::string gameFinderName; std::string customData; int settingsVersionNumber = 0; bool onlyLeaderCanInvite = true; bool isJoinable = true; std::unordered_map publicServerData; std::string indexedDocument; std::string partyId; operator PartySettings() const { PartySettings settings; settings.gameFinderName = gameFinderName; settings.customData = customData; settings.onlyLeaderCanInvite = onlyLeaderCanInvite; settings.isJoinable = isJoinable; settings.publicServerData = publicServerData; settings.indexedDocument = indexedDocument; settings.partyId = partyId; return settings; } static PartySettingsInternal fromPartySettings(const PartySettings& settings) { PartySettingsInternal settingsInternal; settingsInternal.gameFinderName = settings.gameFinderName; settingsInternal.customData = settings.customData; settingsInternal.onlyLeaderCanInvite = settings.onlyLeaderCanInvite; settingsInternal.isJoinable = settings.isJoinable; settingsInternal.publicServerData = settings.publicServerData; settingsInternal.indexedDocument = settings.indexedDocument; settingsInternal.partyId = settings.partyId; return settingsInternal; } std::string toString() const { std::string result; result += gameFinderName + ", "; result += customData + ", "; result += "isJoinable=" + std::to_string(isJoinable); result += "versionSettings=" + std::to_string(settingsVersionNumber); result += "publicServerData={"; for (auto& it : publicServerData) { result += it.first + "=" + it.second + ","; } result += "}"; return result; } MSGPACK_DEFINE(gameFinderName, customData, settingsVersionNumber, onlyLeaderCanInvite, isJoinable, publicServerData, indexedDocument, partyId); }; struct InvitationRequest { enum class Operation { None, Send, Cancel }; // The operation that is currently pending for this invitation. This member helps handling the case where send/cancel/send are called repeatedly. Operation pendingOperation = Operation::None; // The invitation's task. The result is true when the user accepts, false when they refuse. For platform-specific invitations, it is always true. pplx::task task; // Used to cancel the invitation when calling cancelInvitation() pplx::cancellation_token_source cts; }; struct PartyState { PartySettingsInternal settings; std::string leaderId; std::vector members; int version = 0; MSGPACK_DEFINE(settings, leaderId, members, version); }; struct MemberStatusUpdateRequest { PartyUserStatus desiredStatus; int localSettingsVersion; MSGPACK_DEFINE(desiredStatus, localSettingsVersion); }; struct MemberStatusUpdate { std::string userId; PartyUserStatus status; MSGPACK_DEFINE(userId, status); }; struct BatchStatusUpdate { std::vector memberStatus; MSGPACK_DEFINE(memberStatus); }; struct PartyUserData { std::string userId; std::vector userData; std::vector localPlayers; MSGPACK_DEFINE(userId, userData, localPlayers); }; struct MemberDisconnection { std::string userId; MemberDisconnectionReason reason; MSGPACK_DEFINE(userId, reason); }; inline bool tryParseVersion(const char* version, int& outVersionNumber) { int year = 0, month = 0, day = 0, revision = 0; #if defined(_WINDOWS_) || defined(__ORBIS__) int numMatches = sscanf_s(version, "%4d-%2d-%2d.%d", &year, &month, &day, &revision); #else int numMatches = sscanf(version, "%4d-%2d-%2d.%d", &year, &month, &day, &revision); #endif if (numMatches != 4 || year < 2019 || month < 1 || month > 12 || day < 1 || day > 31 || revision < 1) { return false; } // Make a decimal number out of the version string outVersionNumber = revision + (day * 10) + (month * 1000) + (year * 100000); return true; } inline int parseVersion(const char* version) { int versionInt = 0; if (!tryParseVersion(version, versionInt)) { throw std::runtime_error("Could not parse version"); } return versionInt; } class PartyService : public std::enable_shared_from_this { public: // stormancer.party => // stormancer.party.revision => // Revision is server-side only. It is independent from protocol version. Revision changes when a modification is made to server code (e.g bugfix). // Protocol version changes when a change to the communication protocol is made. // Protocol versions between client and server are not obligated to match. static constexpr const char* METADATA_KEY = "stormancer.party"; static constexpr const char* REVISION_METADATA_KEY = "stormancer.party.revision"; static constexpr const char* PROTOCOL_VERSION = "2022-06-09.1"; static constexpr const char* IS_JOINABLE_VERSION = "2019-12-13.1"; static constexpr const char* NEW_INVITATIONS_VERSION = "2019-11-22.1"; static int getProtocolVersionInt() { static int protocolVersionInt = parseVersion(PROTOCOL_VERSION); return protocolVersionInt; } PartyService(std::weak_ptr scene) : _scene(scene) , _logger(scene.lock()->dependencyResolver().resolve()) , _rpcService(_scene.lock()->dependencyResolver().resolve()) , _gameFinder(scene.lock()->dependencyResolver().resolve()) , _dispatcher(scene.lock()->dependencyResolver().resolve()) , _users(scene.lock()->dependencyResolver().resolve()) , _myUserId(_users->userId()) { auto serverProtocolVersion = _scene.lock()->getHostMetadata(METADATA_KEY); auto serverRevision = _scene.lock()->getHostMetadata(REVISION_METADATA_KEY); _logger->log(LogLevel::Info, "PartyService", "Protocol version: client=" + std::string(PROTOCOL_VERSION) + ", server=" + serverProtocolVersion); _logger->log(LogLevel::Info, "PartyService", "Server revision=" + serverRevision); if (!tryParseVersion(serverProtocolVersion.c_str(), _serverProtocolVersion)) { // Older versions are not in the correct format _serverProtocolVersion = 201910231; } } ~PartyService() { std::lock_guard lg(_stateMutex); _gameFinderConnectionTask.then([](pplx::task task) { try { task.get(); } catch (...) {} }); } // This is for compatibility with server plugins older than NEW_INVITATIONS_VERSION struct PartySettingsCompatibility { std::string gameFinderName; std::string customData; MSGPACK_DEFINE(gameFinderName, customData); }; /// /// Sent to server the new party status /// pplx::task updatePartySettings(const PartySettings& newPartySettings) { std::lock_guard lg(_stateMutex); static const int isJoinableProtocolVersion = parseVersion(IS_JOINABLE_VERSION); if (newPartySettings.isJoinable == false && _serverProtocolVersion < isJoinableProtocolVersion) { _logger->log(LogLevel::Warn, "PartyService::updatePartySettings", "The server does not support joinability restriction ; 'isJoinable' will have no effect. Please update your server-side Party plugin."); } // Apply settings locally immediately. If the update RPC fails, we will re-sync the party state. /*PartySettingsInternal update = PartySettingsInternal::fromPartySettings(newPartySettings); update.settingsVersionNumber = _state.settings.settingsVersionNumber + 1; applySettingsUpdate(update);*/ std::weak_ptr wThat = this->shared_from_this(); static const int newInvitationsProtocolVersion = parseVersion(NEW_INVITATIONS_VERSION); if (newPartySettings.onlyLeaderCanInvite == true && _serverProtocolVersion < newInvitationsProtocolVersion) { _logger->log(LogLevel::Warn, "PartyService::updatePartySettings", "The server does not support invitation restriction ; 'onlyLeaderCanInvite' will have no effect, and every party member will be able to send invitations. Please update your server-side Party plugin."); // Also, the server DTO from these older versions is not compatible with the new client DTO. Need to send a compatible DTO. PartySettingsCompatibility compatible; compatible.gameFinderName = newPartySettings.gameFinderName; compatible.customData = newPartySettings.customData; return syncStateOnError(_rpcService->rpc("party.updatepartysettings", compatible)); } else { return syncStateOnError(_rpcService->rpc("party.updatepartysettings", newPartySettings)); } } pplx::task getCurrentGameSessionConnectionToken(pplx::cancellation_token ct = pplx::cancellation_token::none()) { return _rpcService->rpc("JoinGameParty.RequestReservationInCurrentGamesession", ct); } /// /// Set our party status (ready/not ready). /// Also make sure that we are connected to the party's GameFinder before telling the server that we're ready. /// pplx::task updatePlayerStatus(const PartyUserStatus newStatus) { std::lock_guard lg(_stateMutex); bool statusHasChanged = std::any_of(_state.members.begin(), _state.members.end(), [newStatus, this](const auto& member) { return member.userId == _myUserId && member.partyUserStatus != newStatus; }); if (!statusHasChanged) { return pplx::task_from_result(pplx::task_options(_dispatcher)); } if (_state.settings.gameFinderName.empty()) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::PartyNotReady), _dispatcher, void); } BatchStatusUpdate update; update.memberStatus.emplace_back(MemberStatusUpdate{ _myUserId, newStatus }); applyMemberStatusUpdate(update); return syncStateOnError(updatePlayerStatusWithRetries(newStatus)); } /// /// Update party user data all data are replecated between all connected party scene /// pplx::task updatePlayerData(std::vector data, std::vector localPlayers) { PartyUserData update; update.userData = data; update.localPlayers = localPlayers; update.userId = _myUserId; applyUserDataUpdate(update); return syncStateOnError(_rpcService->rpc("Party.UpdatePartyUserData2", data, localPlayers)); } /// /// Promote player to leader of the party /// \param playerId party userid will be promote pplx::task promoteLeader(const std::string playerId) { std::lock_guard lg(_stateMutex); if (_state.leaderId == _myUserId) { applyLeaderChange(playerId); return syncStateOnError(_rpcService->rpc("party.promoteleader", playerId)); } STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::Unauthorized), _dispatcher, void); } /// /// Remove player from party this method can be call only by party leader. /// \param playerToKick is the user player id to be kicked pplx::task kickPlayer(const std::string playerId) { std::lock_guard lg(_stateMutex); if (_state.leaderId == _myUserId) { MemberDisconnection disconnection; disconnection.userId = playerId; disconnection.reason = MemberDisconnectionReason::Kicked; applyMemberDisconnection(disconnection); return syncStateOnError(_rpcService->rpc("party.kickplayer", playerId)); } STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::Unauthorized), _dispatcher, void); } pplx::task sendInvitation(const std::string& recipientId, bool forceStormancerInvite) { if (!forceStormancerInvite) { return sendInvitationInternal(recipientId, false, pplx::cancellation_token::none()); } std::lock_guard lg(_invitationsMutex); auto& request = _pendingStormancerInvitations[recipientId]; auto currentOperation = request.pendingOperation; request.pendingOperation = InvitationRequest::Operation::Send; if (currentOperation == InvitationRequest::Operation::None) { auto token = request.cts.get_token(); std::weak_ptr wThat(this->shared_from_this()); request.task = sendInvitationInternal(recipientId, true, token) .then([wThat, recipientId](pplx::task task) { if (auto that = wThat.lock()) { return that->onInvitationComplete(task, recipientId); } return task; }, _dispatcher); } return request.task; } pplx::task cancelInvitation(const std::string& recipientId) { std::lock_guard lg(_invitationsMutex); auto invitationIt = _pendingStormancerInvitations.find(recipientId); if (invitationIt != _pendingStormancerInvitations.end()) { auto& invitation = invitationIt->second; invitation.pendingOperation = InvitationRequest::Operation::Cancel; invitation.cts.cancel(); std::weak_ptr wThat = this->shared_from_this(); auto cancellationTask = invitation.task.then([wThat, recipientId](pplx::task task) { // Consume the boolean, let the caller handle errors task.wait(); }); return cancellationTask; } return pplx::task_from_result(); } std::vector getPendingStormancerInvitations() const { std::lock_guard lg(_invitationsMutex); std::vector invitations; invitations.reserve(_pendingStormancerInvitations.size()); for (const auto& invite : _pendingStormancerInvitations) { invitations.push_back(invite.first); } return invitations; } pplx::task createInvitationCode(pplx::cancellation_token ct) { return _rpcService->rpc("Party.CreateInvitationCode", ct); } pplx::task cancelInvitationCode(pplx::cancellation_token ct = pplx::cancellation_token::none()) { return _rpcService->rpc("Party.CancelInvitationCode", ct); } /// /// Callback member /// Event LeftParty; Event<> JoinedParty; Event PartyMembersUpdated; Event UpdatedPartySettings; Event> UpdatedInviteList; Event OnGameFinderFailed; std::vector members() const { std::lock_guard lg(_stateMutex); return _state.members; } PartySettings settings() const { std::lock_guard lg(_stateMutex); return _state.settings; } std::string leaderId() const { std::lock_guard lg(_stateMutex); return _state.leaderId; } void initialize() { std::weak_ptr wThat = this->shared_from_this(); auto scene = _scene.lock(); auto rpcService = scene->dependencyResolver().resolve(); rpcService->addProcedure("party.getPartyStateResponse", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handlePartyStateResponse(ctx); } return pplx::task_from_result(); }); rpcService->addProcedure("party.settingsUpdated", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handleSettingsUpdateMessage(ctx); } return pplx::task_from_result(); }); rpcService->addProcedure("party.memberDataUpdated", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handleUserDataUpdateMessage(ctx); } return pplx::task_from_result(); }); rpcService->addProcedure("party.memberStatusUpdated", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handleMemberStatusUpdateMessage(ctx); } return pplx::task_from_result(); }); rpcService->addProcedure("party.memberConnected", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handleMemberConnected(ctx); } return pplx::task_from_result(); }); rpcService->addProcedure("party.memberDisconnected", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handleMemberDisconnectedMessage(ctx); } return pplx::task_from_result(); }); rpcService->addProcedure("party.leaderChanged", [wThat](RpcRequestContext_ptr ctx) { if (auto that = wThat.lock()) { return that->handleLeaderChangedMessage(ctx); } return pplx::task_from_result(); }); scene->addRoute("party.gameFinderFailed", [wThat](PartyGameFinderFailure dto) { if (auto that = wThat.lock()) { return that->handleGameFinderFailureMessage(dto); } }); scene->getConnectionStateChangedObservable().subscribe([wThat](ConnectionState state) { if (auto that = wThat.lock()) { try { if (state == ConnectionState::Connected) { that->JoinedParty(); } else if (state == ConnectionState::Disconnected) { that->failInit(state.reason); that->_gameFinder->disconnectFromGameFinder(that->_state.settings.gameFinderName) .then([](pplx::task t) { try { t.get(); } catch (...) {} }); MemberDisconnectionReason reason = MemberDisconnectionReason::Left; if (state.reason == "party.kicked") { reason = MemberDisconnectionReason::Kicked; } that->LeftParty(reason); } } catch (const std::exception& ex) { that->_logger->log(LogLevel::Error, "PartyService::ConnectionStateChanged", "An exception was thrown by a connection event handler", ex); } } }); } pplx::task waitForPartyReady(pplx::cancellation_token ct = pplx::cancellation_token::none()) { return waitForTaskCompletionEvent(_partyStateReceived, ct); } void failInit(std::string error) { _partyStateReceived.set_exception(std::runtime_error(error)); } private: pplx::task syncStateOnError(pplx::task task) { std::weak_ptr wThat = this->shared_from_this(); return task.then([wThat](pplx::task task) { try { task.get(); } catch (...) { if (auto that = wThat.lock()) { that->syncPartyState(); } throw; } }, _dispatcher); } void updateGameFinder() { std::lock_guard lg(_stateMutex); if (_currentGameFinder == _state.settings.gameFinderName) { return; } // This CTS prevents multiple game finder connection requests from queuing up. _gameFinderConnectionCts.cancel(); _gameFinderConnectionCts = pplx::cancellation_token_source(); // No need to wait for the old GF disconnection before connecting to the new GF _gameFinder->disconnectFromGameFinder(_currentGameFinder).then([](pplx::task task) { try { task.wait(); } catch (...) {} }); _currentGameFinder = _state.settings.gameFinderName; if (_currentGameFinder.empty()) { return; } _logger->log(LogLevel::Trace, "PartyService", "Connecting to the party's GameFinder", _state.settings.gameFinderName); std::string newGameFinderName = _currentGameFinder; auto token = _gameFinderConnectionCts.get_token(); std::weak_ptr wThat = this->shared_from_this(); _gameFinderConnectionTask = _gameFinderConnectionTask.then([wThat, newGameFinderName, token](pplx::task task) { // I want to recover from cancellation, but not from error, since error means we're leaving the party task.wait(); auto that = wThat.lock(); if (!that || token.is_canceled()) { pplx::cancel_current_task(); } return that->_gameFinder->connectToGameFinder(newGameFinderName); }, token) .then([wThat, newGameFinderName](pplx::task task) { auto that = wThat.lock(); try { auto status = task.wait(); if (that && status == pplx::completed) { that->_logger->log(LogLevel::Trace, "PartyService", "Connected to the GameFinder", newGameFinderName); } } catch (const std::exception& ex) { if (that) { that->_logger->log(LogLevel::Error, "PartyService", "Error connecting to the GameFinder '" + newGameFinderName + "'", ex); if (auto scene = that->_scene.lock()) { std::lock_guard lg(that->_stateMutex); scene->disconnect().then([](pplx::task t) { try { t.get(); } catch (...) {}}); that->_scene.reset(); } } throw; } }, token); } bool checkVersionNumber(RpcRequestContext_ptr ctx) { auto versionNumber = ctx->readObject(); if (_state.version > 0 && versionNumber == _state.version + 1) { _state.version = versionNumber; return true; } else { _logger->log(LogLevel::Trace, "PartyService::checkVersionNumber", "Version number mismatch ; current=" + std::to_string(_state.version) + ", received=" + std::to_string(versionNumber)); syncPartyState(); return false; } } // This returns void because we must not block on it (or else we would cause a timeout in party update RPC) void syncPartyState() { syncPartyStateTask().then([](pplx::task task) { try { task.get(); } catch (...) {} }); } pplx::task getPartyStateImpl() { static const int originalGetPartyStateVersion = parseVersion("2019-08-30.1"); if (_serverProtocolVersion == originalGetPartyStateVersion) { return _rpcService->rpc("party.getpartystate"); } else { std::weak_ptr wThat = this->shared_from_this(); return _rpcService->rpc("party.getpartystate2").then([wThat](PartyState state) { if (auto that = wThat.lock()) { that->applyPartyStateResponse(state); } }); } } pplx::task syncPartyStateTaskWithRetries() { std::weak_ptr wThat = this->shared_from_this(); return getPartyStateImpl().then([wThat](pplx::task task) { try { task.get(); } catch (const std::exception& ex) { if (auto that = wThat.lock()) { that->_logger->log(LogLevel::Error, "PartyService::syncPartyStateTaskWithRetries", "An error occurred during syncPartyState, retrying", ex); return taskDelay(std::chrono::milliseconds(200)) .then([wThat] { if (auto that = wThat.lock()) { return that->syncPartyStateTaskWithRetries(); } return pplx::task_from_result(); }); } } return pplx::task_from_result(); }); } pplx::task syncPartyStateTask() { std::lock_guard lg(_stateMutex); if (_stateSyncRequest.is_done()) { _stateSyncRequest = syncPartyStateTaskWithRetries(); } return _stateSyncRequest; } pplx::task updatePlayerStatusWithRetries(const PartyUserStatus newStatus) { std::lock_guard lg(_stateMutex); MemberStatusUpdateRequest request; request.desiredStatus = newStatus; request.localSettingsVersion = _state.settings.settingsVersionNumber; // If the player wants to be Ready, we must make sure they are connected to the game finder beforehand pplx::task preliminaryTask = pplx::task_from_result(); if (newStatus == PartyUserStatus::Ready) { preliminaryTask = _gameFinderConnectionTask; } std::weak_ptr wThat = this->shared_from_this(); return preliminaryTask.then([wThat, request] { if (auto that = wThat.lock()) { return that->_rpcService->rpc("party.updategamefinderplayerstatus", request); } return pplx::task_from_result(); }).then([wThat, newStatus](pplx::task task) { try { task.get(); } catch (const std::exception& ex) { if (auto that = wThat.lock()) { if (strcmp(ex.what(), "party.settingsOutdated") == 0) { that->_logger->log(LogLevel::Debug, "PartyService::updatePlayerStatusWithRetries", "Local settings outdated ; retrying"); return that->syncPartyStateTask() .then([wThat, newStatus] { if (auto that = wThat.lock()) { return that->updatePlayerStatusWithRetries(newStatus); } return pplx::task_from_result(); }); } else { throw; } } } return pplx::task_from_result(); }); } pplx::task handlePartyStateResponse(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); applyPartyStateResponse(ctx->readObject()); return pplx::task_from_result(); } static const PartyUserDto* findMember(const std::vector& users, const std::string& userId) { auto it = std::find_if(users.begin(), users.end(), [&userId](const PartyUserDto& dto) { return dto.userId == userId; }); if (it != users.end()) { return &(*it); } else { return nullptr; } } static std::unordered_map makeMemberMap(const std::vector& users) { std::unordered_map map; map.reserve(users.size()); std::transform(users.begin(), users.end(), std::inserter(map, map.end()), [](PartyUserDto user) { return std::make_pair(user.userId, std::move(user)); }); return map; } void applyPartyStateResponse(PartyState state) { std::lock_guard lg(_stateMutex); _logger->log(LogLevel::Trace, "PartyService::applyPartyStateResponse", "Received party state, version = " + std::to_string(state.version)); // Compare the up-to-date member list with the one we currently have, and generate MemberUpdate events where appropriate MembersUpdate updates; auto previousMembers = makeMemberMap(_state.members); for (auto& newMember : state.members) { if (state.leaderId == newMember.userId) { newMember.isLeader = true; } if (previousMembers.count(newMember.userId) == 0) { updates.updatedMembers.emplace_back(newMember, MembersUpdate::Joined); continue; } const auto& oldMember = previousMembers[newMember.userId]; MembersUpdate::MemberUpdate update; if (oldMember.isLeader != newMember.isLeader) { update.changes.set(newMember.isLeader ? MembersUpdate::PromotedToLeader : MembersUpdate::DemotedFromLeader); } if (oldMember.partyUserStatus != newMember.partyUserStatus) { update.changes.set(MembersUpdate::StatusUpdated); } if (oldMember.userData != newMember.userData) { update.changes.set(MembersUpdate::DataUpdated); } previousMembers.erase(newMember.userId); if (update.changes.any()) { update.member = newMember; updates.updatedMembers.push_back(std::move(update)); } } for (const auto& memberWhoLeft : previousMembers) { updates.updatedMembers.emplace_back(memberWhoLeft.second, MembersUpdate::Left); } _state = std::move(state); updateGameFinder(); _partyStateReceived.set(); this->UpdatedPartySettings(_state.settings); PartyMembersUpdated(updates); } void applySettingsUpdate(const PartySettingsInternal& update) { std::lock_guard lg(_stateMutex); if (_state.settings.settingsVersionNumber != update.settingsVersionNumber) { _state.settings = update; updateGameFinder(); this->UpdatedPartySettings(_state.settings); } } pplx::task handleSettingsUpdateMessage(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); if (checkVersionNumber(ctx)) { _logger->log(LogLevel::Trace, "PartyService::handleSettingsUpdate", "Received settings update, version = " + std::to_string(_state.version)); applySettingsUpdate(ctx->readObject()); } return pplx::task_from_result(); } void applyUserDataUpdate(const PartyUserData& update) { std::lock_guard lg(_stateMutex); auto member = std::find_if(_state.members.begin(), _state.members.end(), [&update](const PartyUserDto& user) { return update.userId == user.userId; }); if (member != _state.members.end()) { member->userData = update.userData; member->localPlayers = update.localPlayers; MembersUpdate updates; updates.updatedMembers.emplace_back(*member, MembersUpdate::DataUpdated); PartyMembersUpdated(updates); } } pplx::task handleUserDataUpdateMessage(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); if (checkVersionNumber(ctx)) { _logger->log(LogLevel::Trace, "PartyService::handleUserDataUpdate", "Received user data update, version = " + std::to_string(_state.version)); applyUserDataUpdate(ctx->readObject()); } return pplx::task_from_result(); } void applyMemberStatusUpdate(const BatchStatusUpdate& updates) { std::lock_guard lg(_stateMutex); MembersUpdate membersUpdate; bool updated = false; for (const auto& update : updates.memberStatus) { auto member = std::find_if(_state.members.begin(), _state.members.end(), [&update](const PartyUserDto& user) { return update.userId == user.userId; }); if (member != _state.members.end()) { updated = updated || (member->partyUserStatus != update.status); member->partyUserStatus = update.status; membersUpdate.updatedMembers.emplace_back(*member, MembersUpdate::StatusUpdated); } } if (updated) { PartyMembersUpdated(membersUpdate); } } pplx::task handleMemberStatusUpdateMessage(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); if (checkVersionNumber(ctx)) { _logger->log(LogLevel::Trace, "PartyService::handleMemberStatusUpdate", "Received member status update, version = " + std::to_string(_state.version)); applyMemberStatusUpdate(ctx->readObject()); } return pplx::task_from_result(); } pplx::task handleMemberConnected(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); if (checkVersionNumber(ctx)) { auto member = ctx->readObject(); _logger->log(LogLevel::Trace, "PartyService::handleMemberConnected", "New party member: Id=" + member.userId + ", version = " + std::to_string(_state.version)); _state.members.push_back(member); MembersUpdate update; update.updatedMembers.emplace_back(member, MembersUpdate::Joined); PartyMembersUpdated(update); } return pplx::task_from_result(); } void applyMemberDisconnection(const MemberDisconnection& message) { std::lock_guard lg(_stateMutex); auto member = std::find_if(_state.members.begin(), _state.members.end(), [&message](const PartyUserDto& user) { return message.userId == user.userId; }); if (member != _state.members.end()) { MembersUpdate update; MembersUpdate::MemberUpdate memberUpdate(*member, MembersUpdate::Left); if (message.reason == MemberDisconnectionReason::Kicked) { memberUpdate.changes.set(MembersUpdate::Kicked); } update.updatedMembers.push_back(memberUpdate); _state.members.erase(member); PartyMembersUpdated(update); } } pplx::task handleMemberDisconnectedMessage(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); if (checkVersionNumber(ctx)) { auto message = ctx->readObject(); _logger->log(LogLevel::Trace, "PartyService::handleMemberDisconnected", "Member disconnected: Id=" + message.userId + ", Reason=" + std::to_string(static_cast(message.reason)) + ", version = " + std::to_string(_state.version)); applyMemberDisconnection(message); } return pplx::task_from_result(); } void applyLeaderChange(const std::string& newLeaderId) { std::lock_guard lg(_stateMutex); if (_state.leaderId != newLeaderId) { _state.leaderId = newLeaderId; MembersUpdate update; updateLeader(update); PartyMembersUpdated(update); } } pplx::task handleLeaderChangedMessage(RpcRequestContext_ptr ctx) { std::lock_guard lg(_stateMutex); if (checkVersionNumber(ctx)) { auto leaderId = ctx->readObject(); _logger->log(LogLevel::Trace, "PartyService::handleLeaderChanged", "New leader: Id=" + leaderId + ", version = " + std::to_string(_state.version)); applyLeaderChange(leaderId); } return pplx::task_from_result(); } void updateLeader(MembersUpdate& update) { const std::string& newLeaderId = _state.leaderId; auto currentLeader = std::find_if(_state.members.begin(), _state.members.end(), [](const PartyUserDto& user) { return user.isLeader; }); if (currentLeader != _state.members.end()) { currentLeader->isLeader = false; update.updatedMembers.emplace_back(*currentLeader, MembersUpdate::DemotedFromLeader); } auto newLeader = std::find_if(_state.members.begin(), _state.members.end(), [&newLeaderId](const PartyUserDto& user) { return newLeaderId == user.userId; }); if (newLeader != _state.members.end()) { newLeader->isLeader = true; update.updatedMembers.emplace_back(*newLeader, MembersUpdate::PromotedToLeader); } } void handleGameFinderFailureMessage(const PartyGameFinderFailure& dto) { OnGameFinderFailed(dto); } pplx::task sendInvitationInternal(const std::string& recipientId, bool forceStormancerInvite, pplx::cancellation_token ct) { static const int sendInvitationVersion = parseVersion("2019-11-22.1"); if (_serverProtocolVersion >= sendInvitationVersion) { return _rpcService->rpc("party.sendinvitation", ct, recipientId, forceStormancerInvite); } else { return _users->sendRequestToUser(recipientId, "party.invite", ct, _scene.lock()->id()) .then([] { return true; }); } } pplx::task onInvitationComplete(pplx::task task, const std::string& recipientId) { pplx::task_status status; try { status = task.wait(); } catch (...) { // Errors are handled by the caller status = pplx::not_complete; } { std::lock_guard lg(_invitationsMutex); auto& invite = _pendingStormancerInvitations[recipientId]; if (status != pplx::canceled || invite.pendingOperation == InvitationRequest::Operation::Cancel) { _pendingStormancerInvitations.erase(recipientId); UpdatedInviteList(getPendingStormancerInvitations()); return task; } else { // Another sendInvitation() to the same recipient has been issued after a cancelInvitation() invite.cts = pplx::cancellation_token_source(); std::weak_ptr wThat = this->shared_from_this(); invite.task = sendInvitationInternal(recipientId, true, invite.cts.get_token()) .then([wThat, recipientId](pplx::task task) { if (auto that = wThat.lock()) { return that->onInvitationComplete(task, recipientId); } return task; }, _dispatcher); return invite.task; } } } PartyState _state; std::string _currentGameFinder; std::weak_ptr _scene; std::shared_ptr _logger; std::shared_ptr _rpcService; std::shared_ptr _gameFinder; std::shared_ptr _dispatcher; std::shared_ptr _users; std::string _myUserId; // Synchronize async state update, as well as getters. // This is "coarse grain" synchronization, but the simplicity gains vs. multiple mutexes win against the possible performance loss imo. mutable std::recursive_mutex _stateMutex; // Prevent having multiple game finder connection tasks at the same time (could happen if multiple settings updates are received in a short amount of time) pplx::task _gameFinderConnectionTask = pplx::task_from_result(); pplx::cancellation_token_source _gameFinderConnectionCts; // Used to signal to client code when the party is ready pplx::task_completion_event _partyStateReceived; pplx::task _stateSyncRequest = pplx::task_from_result(); int _serverProtocolVersion = 0; std::unordered_map _pendingStormancerInvitations; mutable std::recursive_mutex _invitationsMutex; }; class PartyContainer { friend class Party_Impl; public: PartyContainer( std::shared_ptr scene, Event::Subscription LeftPartySubscription, Event>::Subscription UpdatedPartyMembersSubscription, Event::Subscription UpdatedPartySettingsSubscription, Subscription UpdatedInvitationListSubscription, Subscription GameFinderFailedSubscription ) : _partyScene(scene) , _partyService(scene->dependencyResolver().resolve()) , LeftPartySubscription(LeftPartySubscription) , UpdatedPartyMembersSubscription(UpdatedPartyMembersSubscription) , UpdatedPartySettingsSubscription(UpdatedPartySettingsSubscription) , UpdatedInvitationListSubscription(UpdatedInvitationListSubscription) , GameFinderFailedSubscription(GameFinderFailedSubscription) { } PartySettings settings() const { return _partyService->settings(); } std::vector members() const { return _partyService->members(); } bool isLeader() const { return (_partyService->leaderId() == _partyScene->dependencyResolver().resolve()->userId()); } std::string leaderId() const { return _partyService->leaderId(); } std::shared_ptr getScene() const { return _partyScene; } std::string getSceneId() const { return _partyScene->id(); } std::shared_ptr partyService() const { return _partyService; } PartyId getPartyId() const { PartyId partyId; if (!settings().partyId.empty()) { partyId.id = settings().partyId; partyId.type = PartyId::TYPE_PARTY_ID; } else { partyId.id = getSceneId(); partyId.type = PartyId::TYPE_SCENE_ID; } return partyId; } private: std::shared_ptr _partyScene; std::shared_ptr _partyService; Event::Subscription LeftPartySubscription; Event>::Subscription UpdatedPartyMembersSubscription; Event::Subscription UpdatedPartySettingsSubscription; Subscription UpdatedInvitationListSubscription; Subscription GameFinderFailedSubscription; }; class PartyManagementService : public std::enable_shared_from_this { public: static constexpr const char* METADATA_KEY = "stormancer.partymanagement"; static constexpr const char* PROTOCOL_VERSION = "2020-05-20.1"; static constexpr const char* IS_JOINABLE_VERSION = "2019-12-13.1"; PartyManagementService(std::shared_ptr scene) : _scene(scene) , _logger(scene->dependencyResolver().resolve()) { auto serverVersion = scene->getHostMetadata(METADATA_KEY); _logger->log(LogLevel::Info, "PartyManagementService", "Protocol version: client=" + std::string(PROTOCOL_VERSION) + ", server=" + serverVersion); if (!tryParseVersion(serverVersion.c_str(), _serverProtocolVersion)) { _logger->log(LogLevel::Warn, "PartyManagementService", "Could not parse server protocol version"); _serverProtocolVersion = 0; } } pplx::task createParty(const PartyCreationOptions& partyRequestDto, pplx::cancellation_token ct = pplx::cancellation_token::none()) { static const int isJoinableProtocolVersion = parseVersion(IS_JOINABLE_VERSION); if (partyRequestDto.isJoinable == false && _serverProtocolVersion < isJoinableProtocolVersion) { _logger->log(LogLevel::Warn, "PartyManagementService::createParty", "The server does not support joinability restriction ; 'isJoinable' will have no effect. Please update your server-side Party plugin."); } auto rpc = _scene.lock()->dependencyResolver().resolve(); return rpc->rpc("partymanagement.createsession", ct, partyRequestDto); } pplx::task getConnectionTokenFromInvitationCode(const std::string& invitationCode, const std::vector& userData, pplx::cancellation_token ct) { auto rpc = _scene.lock()->dependencyResolver().resolve(); return rpc->rpc("PartyManagement.CreateConnectionTokenFromInvitationCode", ct, invitationCode, userData); } pplx::task getConnectionTokenFromPartyId(const std::string& partyId, const std::vector& userData, pplx::cancellation_token ct) { auto rpc = _scene.lock()->dependencyResolver().resolve(); return rpc->rpc("PartyManagement.CreateConnectionTokenFromPartyId", ct, partyId, userData); } pplx::task searchParties(const std::string& jsonQuery, Stormancer::uint32 skip, Stormancer::uint32 size, pplx::cancellation_token cancellationToken) { auto rpc = _scene.lock()->dependencyResolver().resolve(); return rpc->rpc("PartyManagement.SearchParties", cancellationToken, jsonQuery, skip, size); } private: std::weak_ptr _scene; ILogger_ptr _logger; int _serverProtocolVersion = 0; }; // Disable deprecation warnings on implementations of deprecated methods STORM_WARNINGS_PUSH; STORM_MSVC_WARNING(disable: 4996); STORM_CLANG_DIAGNOSTIC("clang diagnostic ignored \"-Wdeprecated-declarations\"") class Party_Impl : public ClientAPI, public PartyApi { public: Party_Impl( std::weak_ptr users, std::weak_ptr logger, std::shared_ptr dispatcher, std::shared_ptr gameFinder, std::shared_ptr client ) : ClientAPI(users, "stormancer.plugins.partyManagement") , _logger(logger) , _dispatcher(dispatcher) , _gameFinder(gameFinder) , _scope(client->dependencyResolver().beginLifetimeScope("party")) , _wClient(client) // _wClient is a weak_ptr so no cycle here { } const DependencyScope& dependencyScope() const override { return _scope; } pplx::task createParty(const PartyCreationOptions& partySettings, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { if (_party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::AlreadyInParty), _dispatcher, void); } auto wThat = STORM_WEAK_FROM_THIS(); auto partyTask = getPartyManagementService(ct) .then([partySettings, ct](std::shared_ptr partyManagement) { return partyManagement->createParty(partySettings, ct); }) .then([wThat, partySettings, userMetadata, ct](std::string sceneToken) { auto that = wThat.lock(); if (!that) { throw ObjectDeletedException("PartyApi"); } PartyId partyId; partyId.type = PartyId::TYPE_CONNECTION_TOKEN; partyId.id = sceneToken; //user data already setup in the sceneToken. return that->joinPartyInternal(partyId, {}, userMetadata, ct); }); setPartySafe(std::make_shared>>(partyTask)); return partyTask .then([wThat](pplx::task> task) { triggerPartyJoinedEvents(wThat, task); }); } pplx::task createPartyIfNotJoined(const PartyCreationOptions& partyRequest, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { auto wThat = STORM_WEAK_FROM_THIS(); return pplx::task_from_result(this->isInParty()) .then([wThat, partyRequest, userMetadata, ct](bool isInParty) { auto that = wThat.lock(); if (!that) { throw ObjectDeletedException("PartyApi"); } if (isInParty) { return pplx::task_from_result(); } else { return that->createParty(partyRequest, userMetadata, ct); } }); } pplx::task joinParty(const std::string& token, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { PartyId partyId; partyId.type = PartyId::TYPE_CONNECTION_TOKEN; partyId.id = token; //userdata included in token. return joinParty(partyId, {}, userMetadata, ct); } pplx::task joinPartyByInvitationCode(const std::string& invitationCode, const std::vector& userData = {}, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { auto wThat = STORM_WEAK_FROM_THIS(); return getConnectionTokenFromInvitationCode(invitationCode, userData, ct) .then([userMetadata, ct, wThat](std::string connectionToken) { if (ct.is_canceled()) { pplx::cancel_current_task(); } auto that = wThat.lock(); if (that == nullptr) { throw ObjectDeletedException("PartyApi"); } return that->joinParty(connectionToken, userMetadata, ct); }); } pplx::task joinParty(const PartyId& partyId, const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { std::lock_guard lg(_partyMutex); if (_party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::AlreadyInParty), _dispatcher, void); } auto wThat = STORM_WEAK_FROM_THIS(); auto partyTask = joinPartyInternal(partyId, userData, userMetadata, ct); setPartySafe(std::make_shared>>(partyTask)); return partyTask .then([wThat](pplx::task> task) { triggerPartyJoinedEvents(wThat, task); }); } pplx::task> joinPartyInternal(const PartyId& partyId, const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) { auto wThat = STORM_WEAK_FROM_THIS(); return _leavePartyTask .then([wThat, partyId, userData, userMetadata, ct, logger = _logger]() { return withRetries>([wThat, partyId, userData, userMetadata](pplx::cancellation_token ct) { if (auto that = wThat.lock()) { return that->obtainConnectionToken(partyId, userData, ct) .then([wThat, partyId, userMetadata, ct](std::string connectionToken) { if (auto that = wThat.lock()) { return that->getPartySceneByToken(connectionToken, partyId, userMetadata, ct); } throw std::runtime_error(PartyError::Str::StormancerClientDestroyed); }); } throw std::runtime_error(PartyError::Str::StormancerClientDestroyed); }, 1000ms, 2, [logger](const std::exception& ex) { logger->log(LogLevel::Error, "Party", "Join party failed", ex); if (std::string(ex.what()).find("party.joinDenied") == 0) { return false; } return true; }, pplx::get_ambient_scheduler(), ct); }) .then([wThat](pplx::task> task) { try { return pplx::task_from_result(task.get()); } catch (std::exception& ex) { if (auto that = wThat.lock()) { if (that->isInParty()) { return that->leaveParty().then([ex]() { return pplx::task_from_exception>(ex); }); } } throw; } }, _dispatcher); } pplx::task obtainConnectionToken(const PartyId& partyId, const std::vector& userData, pplx::cancellation_token ct = pplx::cancellation_token::none()) { if (partyId.type == PartyId::TYPE_CONNECTION_TOKEN) { return pplx::task_from_result(partyId.id); } auto wThat = STORM_WEAK_FROM_THIS(); return normalizePartyId(partyId, ct) .then([wThat, ct](PartyId partyId) { if (auto that = wThat.lock()) { if (that->isInParty() && that->getPartyId() == partyId) { throw std::runtime_error(PartyError::Str::AlreadyInSameParty); } return that->getPartyManagementService(ct) .then([partyId](std::shared_ptr service) { return std::make_tuple(service, partyId.id); }); } else { throw ObjectDeletedException("PartyApi"); } }) .then([userData, ct](std::tuple< std::shared_ptr, std::string> tuple) { return std::get<0>(tuple)->getConnectionTokenFromPartyId(std::get<1>(tuple), userData, ct); }); } pplx::task normalizePartyId(const PartyId& partyId, pplx::cancellation_token ct = pplx::cancellation_token::none()) { if (partyId.type == PartyId::TYPE_PARTY_ID) { return pplx::task_from_result(partyId); } else if (partyId.type == PartyId::TYPE_SCENE_ID) // TODO : deprecated, we should get a connexion token from a partyId only { return pplx::task_from_result(partyId); } else { auto provider = getProviderForPlatform(partyId.platform); if (provider == nullptr) { STORM_RETURN_TASK_FROM_EXCEPTION(std::runtime_error(PartyError::Str::UnsupportedPlatform), PartyId); } return provider->getPartyId(partyId, ct); } } std::shared_ptr getProviderForPlatform(const std::string& platformName) { auto providers = platformProviders(); auto it = std::find_if(providers.begin(), providers.end(), [&platformName](std::shared_ptr provider) { return provider->getPlatformName() == platformName; }); if (it != providers.end()) { return *it; } return nullptr; } static void triggerPartyJoinedEvents(std::weak_ptr partyWeak, pplx::task> joinPartyTask) { auto party = partyWeak.lock(); if (!party) { return; } try { joinPartyTask.get(); if (!party->isInParty()) { return; } party->raiseJoinedParty(); auto members = party->getPartyMembers(); MembersUpdate initialUpdate; initialUpdate.partyApi = party; initialUpdate.updatedMembers.reserve(members.size()); for (auto& member : members) { initialUpdate.updatedMembers.emplace_back(std::move(member), MembersUpdate::Joined); } party->raisePartyMembersUpdated(initialUpdate); party->raisePartySettingsUpdated(party->getPartySettings()); } catch (const std::exception& ex) { party->setPartySafe(nullptr); party->_onPartyError(PartyError(PartyError::Api::JoinParty, ex.what())); throw; } } pplx::task leaveParty(pplx::cancellation_token ct = pplx::cancellation_token::none()) override { std::lock_guard lg(_partyMutex); if (!_party) { return pplx::task_from_result(pplx::task_options(_dispatcher)); } auto party = *_party; _party = nullptr; auto logger = _logger; party.then([ct, logger](std::shared_ptr partyContainer) { return partyContainer->getScene()->disconnect(ct) .then([logger, partyContainer](pplx::task task) { // Need to keep partyContainer alive so that onLeaving/onLeft are triggered try { task.wait(); } catch (const std::exception& ex) { logger->log(LogLevel::Debug, "PartyApi::leaveParty", "An error occurred while leaving the party", ex); } catch (...) {} }); }); setGameFinderStatus(PartyGameFinderStatus::SearchStopped); _leavePartyTask = pplx::create_task(_leavePartyTce, _dispatcher); return _leavePartyTask; } /// /// Gets a boolean indicating if the party is currently in a gamesession. /// /// bool isInGameSession() override { auto party = tryGetParty(); if (party != nullptr) { const auto& serverData = party->settings().publicServerData; auto it = serverData.find("stormancer.partyStatus"); return it != serverData.end() && it->second == "gamesession"; } else { return false; } } /// /// If the party is in a gamesession, gets a token to connect to it. /// /// /// pplx::task getCurrentGameSessionConnectionToken(pplx::cancellation_token ct = pplx::cancellation_token::none()) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, std::string); } return party->partyService()->getCurrentGameSessionConnectionToken(ct); } bool isInParty() const noexcept override { PartyUserDto* _ = nullptr; return tryGetLocalMember(_); } std::shared_ptr getPartyScene() const override { auto partyContainer = tryGetParty(); if (!partyContainer) { return nullptr; } return partyContainer->getScene(); } std::vector getPartyMembers() const override { auto party = tryGetParty(); if (!party) { throw std::runtime_error(PartyError::Str::NotInParty); } return party->members(); } PartyUserDto getLocalMember() const override { PartyUserDto result; if (tryGetLocalMember(&result)) { return result; } else { throw std::runtime_error(PartyError::Str::NotInParty); } } bool tryGetLocalMember(PartyUserDto* localMember) const { auto party = tryGetParty(); if (!party) { return false; } auto users = _wUsers.lock(); if (!users) { return false; } auto myId = users->userId(); auto members = party->members(); auto it = std::find_if(members.begin(), members.end(), [&myId](const PartyUserDto& user) { return user.userId == myId; }); if (it != members.end()) { if (localMember) { *localMember = *it; } return true; } else { return false; } } PartySettings getPartySettings() const override { auto party = tryGetParty(); if (!party) { throw std::runtime_error(PartyError::Str::NotInParty); } return party->settings(); } PartyId getPartyId() const override { auto party = tryGetParty(); if (!party) { throw std::runtime_error(PartyError::Str::NotInParty); } return party->getPartyId(); } std::string getPartyLeaderId() const override { auto party = tryGetParty(); if (!party) { throw std::runtime_error(PartyError::Str::NotInParty); } return party->leaderId(); } bool isLeader() const override { auto party = tryGetParty(); if (!party) { throw std::runtime_error(PartyError::Str::NotInParty); } return party->isLeader(); } std::vector getSentPendingInvitations() override { auto party = tryGetParty(); if (!party) { return std::vector(); } return party->partyService()->getPendingStormancerInvitations(); } pplx::task createInvitationCode(pplx::cancellation_token ct = pplx::cancellation_token::none()) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, std::string); } return party->partyService()->createInvitationCode(ct); } pplx::task getConnectionTokenFromInvitationCode(const std::string& invitationCode, const std::vector& userData, pplx::cancellation_token ct) { return getPartyManagementService(ct) .then([invitationCode, userData, ct](std::shared_ptr service) { return service->getConnectionTokenFromInvitationCode(invitationCode, userData, ct); }); } pplx::task getConnectionTokenFromPartyId(const std::string& partyId, const std::vector& userData, pplx::cancellation_token ct) { return getPartyManagementService(ct) .then([partyId, userData, ct](std::shared_ptr service) { return service->getConnectionTokenFromPartyId(partyId, userData, ct); }); } pplx::task searchParties(const std::string& jsonQuery, Stormancer::uint32 skip, Stormancer::uint32 size, pplx::cancellation_token cancellationToken) override { return getPartyManagementService(cancellationToken) .then([jsonQuery, skip, size, cancellationToken](std::shared_ptr service) { return service->searchParties(jsonQuery, skip, size, cancellationToken); }); } pplx::task cancelInvitationCode(pplx::cancellation_token ct = pplx::cancellation_token::none()) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } if (party->isLeader()) { return party->partyService()->cancelInvitationCode(ct); } else { return pplx::task_from_exception(std::runtime_error("unauthorized")); } } // Not const because of mutex lock std::vector getPendingInvitations() override { std::vector pendingInvitations; { std::lock_guard lg(_invitationsMutex); pendingInvitations.reserve(_invitationsNew.size()); for (auto invitation : _invitationsNew) { pendingInvitations.emplace_back(invitation); } } return pendingInvitations; } pplx::task updatePlayerStatus(PartyUserStatus playerStatus) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } return party->partyService()->updatePlayerStatus(playerStatus); } pplx::task updatePartySettings(PartySettings partySettingsDto) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } if (partySettingsDto.customData == "") { partySettingsDto.customData = "{}"; } return party->partyService()->updatePartySettings(partySettingsDto); } pplx::task updatePlayerData(std::vector data, std::vector localPlayers) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } return party->partyService()->updatePlayerData(data, localPlayers); } pplx::task promoteLeader(std::string userId) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } return party->partyService()->promoteLeader(userId); } pplx::task kickPlayer(std::string userId) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } std::weak_ptr wThat = this->shared_from_this(); return party->partyService()->kickPlayer(userId) .then([userId, wThat] { auto handlersTask = pplx::task_from_result(); if (auto that = wThat.lock()) { auto logger = that->_logger; for (auto provider : that->platformProviders()) { handlersTask = handlersTask.then([that, provider, userId, logger] { return provider->kickPlayer(userId) .then([logger, provider, userId](pplx::task task) { try { task.get(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "PartyApi::kickPlayer", "An error occurred while kicking player " + userId + " from session on platform " + provider->getPlatformName(), ex); } }); }, that->_dispatcher); } } return handlersTask; }) .then([wThat, userId] { if (auto that = wThat.lock()) { auto eventHandlers = that->getEventHandlers(); for (auto handler : eventHandlers) { try { handler->onPlayerKickedByLocalMember(that, userId); } catch (const std::exception& ex) { that->_logger->log(LogLevel::Error, "Party_Impl::kickPlayer", "An exception was thrown by an onPlayerKickedByLocalMember event handler", ex); } } } }, _dispatcher); } bool canSendInvitations() const override { auto party = tryGetParty(); if (!party) { return false; } return party->isLeader() || !party->settings().onlyLeaderCanInvite; } pplx::task sendInvitation(const std::string& recipient, bool forceStormancerInvitation) override { auto party = tryGetParty(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION_OPT(std::runtime_error(PartyError::Str::NotInParty), _dispatcher, void); } std::weak_ptr wThat = this->shared_from_this(); auto logger = _logger; std::weak_ptr wParty = party; party->partyService()->sendInvitation(recipient, forceStormancerInvitation) .then([wParty, wThat, logger, recipient](pplx::task task) { auto that = wThat.lock(); auto party = wParty.lock(); try { auto status = task.wait(); if (that && status == pplx::completed) { bool accepted = task.get(); if (!accepted) { that->_onSentInvitationDeclined(recipient); } } } catch (const std::exception& ex) { logger->log(LogLevel::Error, "PartyApi::sendInvitation", "Could not send an invitation to " + recipient, ex); } }, _dispatcher); // TODO Use an observable RPC to tell when the invite has been sent as well as when it has been accepted or declined. return pplx::task_from_result(); } void cancelInvitation(const std::string& recipient) override { auto party = tryGetParty(); if (!party) { return; } auto logger = _logger; party->partyService()->cancelInvitation(recipient).then([logger, recipient](pplx::task task) { try { task.wait(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "PartyApi::cancelInvitation", "Error while canceling invitation to " + recipient, ex); } }); } bool showSystemInvitationUI() override { std::lock_guard lock(_partyMutex); if (!isInParty()) { return false; } for (auto& provider : platformProviders()) { if (provider->tryShowSystemInvitationUI(this->shared_from_this())) { return true; } } return false; } pplx::task> getAdvertisedParties(pplx::cancellation_token ct = pplx::cancellation_token::none()) override { std::vector>> tasks; auto cts = ct.is_cancelable() ? pplx::cancellation_token_source::create_linked_source(ct) : pplx::cancellation_token_source(); for (auto& partyAdvertiser : platformProviders()) { auto task = partyAdvertiser->getAdvertisedParties(cts.get_token()); tasks.push_back(task); task.then([cts, logger = _logger](pplx::task> task) { try { task.get(); } catch (const std::exception& ex) { cts.cancel(); logger->log(LogLevel::Error, "Party", "An IPartyAdvertiser failed", ex); } }); } return pplx::when_all(tasks.begin(), tasks.end(), _dispatcher); } Subscription subscribeOnSentInvitationsListUpdated(std::function)> callback) override { return _onSentInvitationsUpdated.subscribe(callback); } Subscription subscribeOnSentInvitationDeclined(std::function callback) override { return _onSentInvitationDeclined.subscribe(callback); } Event::Subscription subscribeOnUpdatedPartySettings(std::function callback) override { return _onUpdatedPartySettings.subscribe(callback); } Event>::Subscription subscribeOnUpdatedPartyMembers(std::function)> callback) override { return _onUpdatedPartyMembers.subscribe(callback); } Subscription subscribeOnPartyMembersUpdated(std::function callback) override { return _onPartyMembersUpdated.subscribe(callback); } Event<>::Subscription subscribeOnJoinedParty(std::function callback) override { return _onJoinedParty.subscribe(callback); } Event::Subscription subscribeOnLeftParty(std::function callback) override { return _onLeftParty.subscribe(callback); } Event::Subscription subscribeOnInvitationReceived(std::function callback) override { // Initialize platform providers so that they can listen to platform invitations platformProviders(); auto subscription = _invitationReceivedEvent.subscribe(callback); if (_pendingInvitation) { _invitationReceivedEvent(*_pendingInvitation); _pendingInvitation.reset(); } return subscription; } Event::Subscription subscribeOnInvitationCanceled(std::function callback) override { return _onInvitationCanceled.subscribe(callback); } Subscription subscribeOnGameFinderStatusUpdate(std::function callback) override { return _onGameFinderStatusUpdate.subscribe(callback); } Subscription subscribeOnGameFound(std::function callback) override { return _onGameFound.subscribe(callback); } Subscription subscribeOnGameFinderFailure(std::function callback) override { return _onGameFinderFailure.subscribe(callback); } Subscription subscribeOnPartyError(std::function callback) override { return _onPartyError.subscribe(std::move(callback)); } void setGameFinderStatus(PartyGameFinderStatus status) { std::lock_guard lg(_partyMutex); if (status != _gameFinderStatus) { _gameFinderStatus = status; _onGameFinderStatusUpdate(status); } } void initialize() { auto wThat = STORM_WEAK_FROM_THIS(); _subscriptions.push_back(_gameFinder->subscribeGameFinderStateChanged([wThat](GameFinder::GameFinderStatusChangedEvent evt) { if (auto that = wThat.lock()) { auto party = that->tryGetParty(); if (party && party->settings().gameFinderName == evt.gameFinder) { switch (evt.status) { case GameFinder::GameFinderStatus::Searching: that->setGameFinderStatus(PartyGameFinderStatus::SearchInProgress); break; default: that->setGameFinderStatus(PartyGameFinderStatus::SearchStopped); break; } } } })); _subscriptions.push_back(_gameFinder->subscribeGameFound([wThat](GameFinder::GameFoundEvent evt) { if (auto that = wThat.lock()) { auto party = that->tryGetParty(); if (party && party->settings().gameFinderName == evt.gameFinder) { that->_onGameFound(evt); } } })); auto messenger = _scope.resolve(); _subscriptions.push_back(messenger->subscribeOnInvitationReceived([wThat](std::shared_ptr invite) { if (auto that = wThat.lock()) { that->onInvitationReceived(invite); } })); } private: void onJoinPartyRequestedByPlatform(const Platform::PlatformInvitationRequestContext& ctx) { if (!ctx.error.empty()) { auto that = this->shared_from_this(); auto str = ctx.error; _dispatcher->post([that, str]() { that->_onPartyError(PartyError(PartyError::Api::JoinParty, str.c_str())); }); return; } _logger->log(LogLevel::Trace, "PartyApi::onJoinpartyRequestedByPlatform", "Received a platform join party request", ctx.partyId.toString()); std::lock_guard lock(_partyMutex); auto that = this->shared_from_this(); JoinPartyFromSystemArgs args; args.party = that; args.client = _wClient.lock(); args.user = ctx.invitedUser; args.partyId = ctx.partyId; args.cancellationToken = ctx.cancellationToken; auto ct = ctx.cancellationToken; std::vector userData = args.userData; _joinPartyFromSystemHandler(args) .then([partyId = ctx.partyId, that, ct, userData, ctx](bool accept) { if (accept) { pplx::task task = pplx::task_from_result(); if (that->isInParty()) { auto partyContainer = that->tryGetParty(); if (partyContainer != nullptr && partyContainer->getPartyId() != partyId) { task = that->leaveParty(); } } return task.then([partyId, that, ct, userData, ctx]() { return that->joinParty(partyId, userData, std::unordered_map{ { "invitedUser", ctx.invitedUser->userId } }, ct); }); } else { return pplx::task_from_result(); } }) .then([that](pplx::task task) { try { task.get(); } catch (const std::exception& ex) { that->_logger->log(LogLevel::Error, "PartyApi::onJoinpartyRequestedByPlatform", "Could not join party", ex); } }); } std::vector> getEventHandlers() { try { return _scope.resolveAll(); } catch (...) { // The scope can be invalid when the client is being destroyed. return std::vector>{}; } } void raisePartyMembersUpdated(const MembersUpdate& update) { _onUpdatedPartyMembers(getPartyMembers()); _onPartyMembersUpdated(update); auto eventHandlers = getEventHandlers(); for (auto handler : eventHandlers) { try { handler->onPartyMembersUpdated(update); } catch (const std::exception& ex) { _logger->log(LogLevel::Error, "Party_Impl::raisePartyMembersUpdated", "An exception was thrown by an onPartyMembersUpdated handler", ex); } } auto logger = _logger; for (auto provider : platformProviders()) { // Keep this task as member to prevent rapid settings updates from overlapping _platformPartyMembersUpdateTask = _platformPartyMembersUpdateTask.then([provider, update] { return provider->updateSessionMembers(update); }, _dispatcher) .then([logger, provider](pplx::task task) { try { task.get(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "Party_Impl::raisePartyMembersUpdated", "An error occurred while updating platform-specific session members for platform " + provider->getPlatformName(), ex); } }); } } void raisePartySettingsUpdated(const PartySettings& settings) { _onUpdatedPartySettings(settings); auto eventHandlers = getEventHandlers(); for (auto handler : eventHandlers) { try { handler->onPartySettingsUpdated(this->shared_from_this(), settings); } catch (const std::exception& ex) { _logger->log(LogLevel::Error, "Party_Impl::raisePartySettingsUpdated", "An exception was thrown by an onPartySettingsUpdated handler", ex); } } auto logger = _logger; for (auto provider : platformProviders()) { // Keep this task as member to prevent rapid settings updates from overlapping _platformPartySettingsUpdateTask = _platformPartySettingsUpdateTask.then([provider, settings] { return provider->updateSessionSettings(settings); }, _dispatcher) .then([logger, provider](pplx::task task) { try { task.get(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "Party_Impl::raisePartySettingsUpdated", "An error occurred while updating platform-specific session settings for platform " + provider->getPlatformName(), ex); } }); } } void raiseJoinedParty() { _onJoinedParty(); auto eventHandlers = getEventHandlers(); for (auto handler : eventHandlers) { try { auto scene = getPartyScene(); std::string partySceneId = (scene ? scene->id() : ""); auto ctx = std::make_shared(); ctx->partyId = getPartyId(); ctx->partySceneId = partySceneId; ctx->partyApi = this->shared_from_this(); handler->onJoinedParty(ctx); } catch (const std::exception& ex) { _logger->log(LogLevel::Error, "Party_Impl::raiseJoinedParty", "An exception was thrown by an onJoinedParty handler", ex); } } } void raiseLeftParty(MemberDisconnectionReason reason) { _onLeftParty(reason); auto eventHandlers = getEventHandlers(); for (auto handler : eventHandlers) { try { auto scene = getPartyScene(); std::string partySceneId = scene ? scene->id() : ""; auto ctx = std::make_shared(); ctx->partyId = getPartyId(); ctx->partySceneId = partySceneId; ctx->partyApi = this->shared_from_this(); ctx->reason = reason; handler->onLeftParty(ctx); } catch (const std::exception& ex) { _logger->log(LogLevel::Error, "Party_Impl::raiseLeftParty", "An exception was thrown by an onLeftParty handler", ex); } } } class InvitationInternal : public IPartyInvitationInternal, public std::enable_shared_from_this { public: InvitationInternal(std::shared_ptr impl, std::shared_ptr party) : _impl(impl) , _party(party) , _senderId(impl->getSenderId()) { } InvitationInternal(const InvitationInternal&) = delete; InvitationInternal& operator=(const InvitationInternal&) = delete; void initialize() { std::weak_ptr wThat(this->shared_from_this()); _cancellationSubscription = _impl->subscribeOnInvitationCanceled([wThat] { if (auto that = wThat.lock()) { if (auto party = that->_party.lock()) { // While we are in this cancellation event, the user could be calling one of the other methods, hence the mutex lock on top of each method. // I want that when the IsValid() call has returned true in these methods, the rest of the method can execute // with the certitude that the invitation will not be removed from the list while it is executing. // We could have one mutex per invitation instead, but mutexes are a limited resource, esp. on consoles, so we probably shouldn't std::lock_guard lg(party->_invitationsMutex); that->_isValid = false; party->removeInvitation((const Stormancer::Party::details::Party_Impl::InvitationInternal&)*that); party->_logger->log(LogLevel::Trace, "InvitationInternal", "Invitation from " + that->_senderId + " was canceled"); party->_onInvitationCanceled(that->_senderId); } } }); } std::string getSenderId() override { if (!_impl) { throw std::runtime_error(PartyError::Str::InvalidInvitation); } return _impl->getSenderId(); } std::string getSenderPlatformId() override { if (!_impl) { throw std::runtime_error(PartyError::Str::InvalidInvitation); } return _impl->getSenderPlatformId(); } pplx::task acceptAndJoinParty(const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { auto party = _party.lock(); if (!party) { STORM_RETURN_TASK_FROM_EXCEPTION(std::runtime_error(PartyError::Str::InvalidInvitation), void); } std::lock_guard invitesLock(party->_invitationsMutex); std::lock_guard partyLock(party->_partyMutex); party->removeInvitation(*this); if (!isValid()) { STORM_RETURN_TASK_FROM_EXCEPTION(std::runtime_error(PartyError::Str::InvalidInvitation), void); } auto impl = this->_impl; auto wParty = _party; return party->normalizePartyId(_impl->getPartyId(), ct) .then([wParty, impl, party, userMetadata, userData, ct, invitation = shared_from_this()](PartyId partyId) { if (party->isInParty()) { if (partyId == party->getPartyId()) { throw std::runtime_error(PartyError::Str::AlreadyInSameParty); } else { party->leaveParty(); } } auto partyTask = impl->accept(party) .then([party, userMetadata, userData, ct](PartyId partyId) { return party->joinPartyInternal(partyId, userData, userMetadata, ct); }); party->setPartySafe(std::make_shared>>(partyTask)); return partyTask .then([wParty](pplx::task> task) { triggerPartyJoinedEvents(wParty, task); }) .then([invitation]() // On success { invitation->_isValid = false; }); }); } void decline() override { auto party = _party.lock(); if (!party) { return; } std::lock_guard lg(party->_invitationsMutex); if (!isValid()) { return; } party->removeInvitation(*this); _isValid = false; auto logger = party->_logger; _impl->decline(party).then([logger](pplx::task task) { try { task.wait(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "InvitationInternal::decline", "An error occurred while declining an invitation", ex); } }); } bool isValid() override { return _impl && _isValid && !_party.expired(); } private: std::shared_ptr _impl; std::weak_ptr _party; std::string _senderId; Subscription _cancellationSubscription; bool _isValid = true; }; // Events Event _onUpdatedPartySettings; Event> _onUpdatedPartyMembers; Event _onPartyMembersUpdated; Event<> _onJoinedParty; Event _onLeftParty; Event _invitationReceivedEvent; std::shared_ptr _pendingInvitation; Event _onInvitationCanceled; Event> _onSentInvitationsUpdated; Event _onSentInvitationDeclined; Event _onGameFinderStatusUpdate; Event _onGameFound; Event _onGameFinderFailure; Event _onPartyError; std::shared_ptr tryGetParty() const noexcept { std::lock_guard lg(_partyMutex); if (_party && _party->is_done()) { // The task could be faulted. In that case, we consider that we are not in the party. try { return _party->get(); } catch (...) {} } return nullptr; } void setPartySafe(std::shared_ptr>> party) { std::lock_guard lg(_partyMutex); _party = party; } void runSceneInitEventHandlers(std::shared_ptr scene) { for (const auto& provider : platformProviders()) { provider->onPartySceneInitialization(scene); } auto eventHandlers = getEventHandlers(); for (const auto& handler : eventHandlers) { handler->onPartySceneInitialization(scene); } } pplx::task> getPartySceneByToken(const std::string& token, const PartyId& partyId, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) { auto users = _wUsers.lock(); if (!users) { STORM_RETURN_TASK_FROM_EXCEPTION(ObjectDeletedException("UsersApi"), std::shared_ptr); } auto joiningPartyContext = std::make_shared(); joiningPartyContext->metadata = userMetadata; joiningPartyContext->partyId = partyId; joiningPartyContext->partySceneId = (partyId.type == PartyId::TYPE_SCENE_ID || partyId.type == PartyId::TYPE_PARTY_ID ? partyId.id : ""); auto wThat = STORM_WEAK_FROM_THIS(); return runEventHandlers(getEventHandlers(), [joiningPartyContext](std::shared_ptr eventHandler) { return eventHandler->onJoiningParty(joiningPartyContext); }, [logger = _logger](const std::exception& ex) { logger->log(LogLevel::Error, "Party_Impl.getPartySceneByToken", "Party onJoiningParty event handler failed", ex.what()); throw; }) .then([users, token, wThat, ct]() { return users->connectToPrivateSceneByToken(token, [wThat](std::shared_ptr scene) { if (auto that = wThat.lock()) { that->runSceneInitEventHandlers(scene); } }, ct); }) .then([ct, wThat](std::shared_ptr scene) { auto that = wThat.lock(); if (!that) { throw ObjectDeletedException("PartyApi"); } return that->initPartyFromScene(scene, ct); }) .then([wThat, userMetadata](std::shared_ptr container) { auto that = wThat.lock(); if (!that) { throw ObjectDeletedException("PartyApi"); } pplx::task handlersTask = pplx::task_from_result(); for (auto provider : that->platformProviders()) { handlersTask = handlersTask.then([wThat, provider, container] { if (auto that = wThat.lock()) { return provider->createOrJoinSessionForParty(container->getSceneId()); } throw ObjectDeletedException("PartyApi"); }, that->_dispatcher); } auto eventHandlers = that->getEventHandlers(); return handlersTask.then([container](pplx::task task) { try { task.get(); return container; } catch (...) { // Keep container alive so that OnLeftParty gets triggered for event handlers container->getScene()->disconnect() .then([container](pplx::task t) { try { t.wait(); } catch (...) { } }); throw; } }); }); } pplx::task> getPartyManagementService(pplx::cancellation_token ct = pplx::cancellation_token::none()) { return this->getService([](auto, auto, auto) {}, [](auto, auto) {}, ct); } pplx::task runLeavingPartyHandlers(std::string partySceneId) { pplx::task handlersTask = pplx::task_from_result(); auto logger = _logger; auto partyApi = this->shared_from_this(); for (auto provider : platformProviders()) { handlersTask = handlersTask.then([partySceneId, provider] { return provider->leaveSessionForParty(partySceneId); }, _dispatcher) .then([logger, provider](pplx::task task) { // As these handlers could do important cleanup (e.g leaving a session), it is important that we run all of them even if some fail // This is why I handle errors for each of them try { task.wait(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "Party_Impl::runLeavingPartyEventHandlers", "An exception was thrown by leaveSessionForParty() for platform " + provider->getPlatformName(), ex); } }); } auto eventHandlers = getEventHandlers(); for (auto handler : eventHandlers) { // Capture a shared_ptr because the handlers could do important cleanup and need access to PartyApi handlersTask = handlersTask.then([partyApi, partySceneId, handler] { auto ctx = std::make_shared(); ctx->partyId = partyApi->getPartyId(); ctx->partySceneId = partySceneId; ctx->partyApi = partyApi; return handler->onLeavingParty(ctx); }, _dispatcher) .then([logger](pplx::task task) { try { task.wait(); } catch (const std::exception& ex) { logger->log(LogLevel::Error, "Party_Impl::runLeavingPartyEventHandlers", "An exception was thrown by an onLeavingParty() handler", ex); } }); } return handlersTask; } pplx::task> initPartyFromScene(std::shared_ptr scene, pplx::cancellation_token ct = pplx::cancellation_token::none()) { auto wPartyManagement = STORM_WEAK_FROM_THIS(); std::shared_ptr partyService; try { partyService = scene->dependencyResolver().resolve(); } catch (const DependencyResolutionException&) { STORM_RETURN_TASK_FROM_EXCEPTION(std::runtime_error(("The scene " + scene->id() + " does not contain a PartyService").c_str()), std::shared_ptr); } auto sceneId = scene->id(); auto party = std::make_shared( scene, partyService->LeftParty.subscribe([wPartyManagement, sceneId](MemberDisconnectionReason reason) { if (auto partyManagement = wPartyManagement.lock()) { auto handlersTask = partyManagement->runLeavingPartyHandlers(sceneId); // Wait for the handlers to be done before effectively completing the _leavePartyTce. // This is important for handlers which manage party-related state such as platform-specific game sessions (e.g steam plugin). handlersTask.then([wPartyManagement, reason] // Exceptions have already been handled for this task { if (auto partyManagement = wPartyManagement.lock()) { if (partyManagement->isInParty()) { partyManagement->setPartySafe(nullptr); } partyManagement->raiseLeftParty(reason); partyManagement->_leavePartyTce.set(); partyManagement->_leavePartyTce = pplx::task_completion_event(); } }, partyManagement->_dispatcher); } }), partyService->PartyMembersUpdated.subscribe([wPartyManagement](MembersUpdate update) { if (auto partyManagement = wPartyManagement.lock()) { if (partyManagement->isInParty()) { update.partyApi = partyManagement; partyManagement->raisePartyMembersUpdated(update); } } }), partyService->UpdatedPartySettings.subscribe([wPartyManagement](PartySettings settings) { if (auto partyManagement = wPartyManagement.lock()) { if (partyManagement->isInParty()) { partyManagement->raisePartySettingsUpdated(settings); } } }), partyService->UpdatedInviteList.subscribe([wPartyManagement](std::vector invitations) { if (auto partyManagement = wPartyManagement.lock()) { if (partyManagement->isInParty()) { partyManagement->_onSentInvitationsUpdated(invitations); } } }), partyService->OnGameFinderFailed.subscribe([wPartyManagement](PartyGameFinderFailure dto) { if (auto partyManagement = wPartyManagement.lock()) { if (partyManagement->isInParty()) { partyManagement->_onGameFinderFailure(dto); } } }) ); return partyService->waitForPartyReady(ct) .then([party] { return party; }); } void onInvitationReceived(std::shared_ptr invite) { auto inviteInternal = std::make_shared(invite, this->shared_from_this()); inviteInternal->initialize(); { std::lock_guard lg(_invitationsMutex); _invitationsNew.push_back(inviteInternal); } auto that = this->shared_from_this(); if (_invitationReceivedEvent.hasSubscribers()) { _invitationReceivedEvent(PartyInvitation(inviteInternal)); } else { _pendingInvitation = std::make_shared(inviteInternal); } } void removeInvitation(const InvitationInternal& invite) { std::lock_guard lg(_invitationsMutex); auto it = std::find_if(_invitationsNew.begin(), _invitationsNew.end(), [&invite](const std::shared_ptr& other) { return &invite == other.get(); }); if (it != _invitationsNew.end()) { _invitationsNew.erase(it); } } pplx::task joinPartyBySceneId(const std::string& sceneId, const std::vector& userData, const std::unordered_map& userMetadata = {}, pplx::cancellation_token ct = pplx::cancellation_token::none()) override { PartyId partyId; partyId.type = PartyId::TYPE_SCENE_ID; partyId.id = sceneId; return joinParty(partyId, userData, userMetadata, ct); } std::vector> platformProviders() const { // Retrieve handlers from the client's scope to avoid instantiating them in the partyApi's scope, // which could cause cyclic references if providers hold a shared_ptr to PartyApi. auto client = _wClient.lock(); if (client) { return client->dependencyResolver().resolveAll(); } else { throw std::runtime_error(PartyError::Str::StormancerClientDestroyed); } } std::shared_ptr _logger; // This mutex mainly protects the _party member mutable std::recursive_mutex _partyMutex; std::shared_ptr>> _party; // This mutex protects the invitations vector, and each individual invitation's API. // Recursive mutex needed because the user can call getPendingInvitations() while in a callback where the mutex is already held std::recursive_mutex _invitationsMutex; std::shared_ptr _dispatcher; std::shared_ptr _gameFinder; DependencyScope _scope; // Things Party_Impl is subscribed to, that outlive the party scene (e.g GameFinder events) std::vector _subscriptions; pplx::task _leavePartyTask = pplx::task_from_result(); std::vector> _invitationsNew; PartyGameFinderStatus _gameFinderStatus = PartyGameFinderStatus::SearchStopped; // When doing a manual leaveParty(), this will ensure the resulting task completes only when every OnLeavingParty event handler has run. pplx::task_completion_event _leavePartyTce; // Prevent platform-specific settings updates from overlapping pplx::task _platformPartySettingsUpdateTask = pplx::task_from_result(); pplx::task _platformPartyMembersUpdateTask = pplx::task_from_result(); std::function(JoinPartyFromSystemArgs)> _joinPartyFromSystemHandler; std::weak_ptr _wClient; // These subscriptions are separated from the main one because when want to be able to unsub when the user does. std::vector _joinPartyFromSystemSubs; }; STORM_WARNINGS_POP; class StormancerInvitationProvider : public Platform::IPlatformSupportProvider, public std::enable_shared_from_this { public: StormancerInvitationProvider(std::shared_ptr messenger, std::shared_ptr users, ILogger_ptr logger) : Platform::IPlatformSupportProvider(messenger) , _users(users) , _logger(logger) { } void initialize() { std::weak_ptr wThat = this->shared_from_this(); _users->setOperationHandler("party.invite", [wThat](Users::OperationCtx& ctx) { if (auto that = wThat.lock()) { return that->invitationHandler(ctx); } ctx.request->sendValueTemplated(false); return pplx::task_from_result(); }); } std::string getPlatformName() override { return "stormancer"; } pplx::task getPartyId(const PartyId&, pplx::cancellation_token = pplx::cancellation_token::none()) override { assert(false); throw std::runtime_error("stormancer platform support has no PartyId"); } pplx::task createOrJoinSessionForParty(const std::string&) override { return pplx::task_from_result(); } pplx::task leaveSessionForParty(const std::string&) override { return pplx::task_from_result(); } pplx::task kickPlayer(const std::string&) override { return pplx::task_from_result(); } pplx::task updateSessionSettings(const PartySettings&) override { return pplx::task_from_result(); } pplx::task updateSessionMembers(const MembersUpdate&) override { return pplx::task_from_result(); } pplx::task> getAdvertisedParties(pplx::cancellation_token = pplx::cancellation_token::none()) override { return pplx::task_from_result(std::vector()); }; bool tryShowSystemInvitationUI(std::shared_ptr) override { return false; } private: class StormancerInvitation : public Platform::IPlatformInvitation { public: StormancerInvitation(std::string senderId, std::string sceneId, pplx::task_completion_event tce, pplx::cancellation_token ct) : senderId(std::move(senderId)) , sceneId(std::move(sceneId)) , requestTce(tce) , requestCt(ct) { // This cannot be in the initialization list because of a compiler bug which denies access to the protected notifyInvitationCanceled() ctRegistration = ct.register_callback([this] { notifyInvitationCanceled(); }); } ~StormancerInvitation() { requestCt.deregister_callback(ctRegistration); } PartyId getPartyId() override { PartyId partyId; partyId.type = PartyId::TYPE_SCENE_ID; partyId.id = sceneId; return partyId; } private: pplx::task accept(std::shared_ptr) override { requestTce.set(true); PartyId partyId; partyId.type = PartyId::TYPE_SCENE_ID; partyId.id = sceneId; return pplx::task_from_result(partyId); } pplx::task decline(std::shared_ptr) override { requestTce.set(false); return pplx::task_from_result(); } std::string getSenderId() override { return senderId; } std::string getSenderPlatformId() override { return senderId; } std::string senderId; std::string sceneId; pplx::task_completion_event requestTce; pplx::cancellation_token requestCt; pplx::cancellation_token_registration ctRegistration; }; pplx::task invitationHandler(Users::OperationCtx& ctx) { Serializer serializer; auto senderId = ctx.originId; auto sceneId = serializer.deserializeOne(ctx.request->inputStream()); _logger->log(LogLevel::Trace, "StormancerInvitationProvider::invitationHandler", "Received an invitation: sender=" + senderId + " ; sceneId=" + sceneId); pplx::task_completion_event inviteResponseTce; auto invitation = std::make_shared(senderId, sceneId, inviteResponseTce, ctx.request->cancellationToken()); notifyInvitationReceived(invitation); auto logger = _logger; return pplx::create_task(inviteResponseTce) .then([ctx, logger](bool response) { logger->log(LogLevel::Trace, "StormancerInvitationProvider::invitationHandler", "Sending invitation response to user " + ctx.originId, std::to_string(response)); ctx.request->sendValueTemplated(response); }); } std::shared_ptr _users; ILogger_ptr _logger; }; } class PartyPlugin : public IPlugin { public: /// /// Plugin-wide revision, to increment every time there is a meaningful change (e.g bugfix...) /// /// /// Unlike protocol versions, its only purpose is to help debugging. /// static constexpr const char* PLUGIN_NAME = "Party"; static constexpr const char* PLUGIN_REVISION = "2020-08-21.1"; static constexpr const char* PLUGIN_METADATA_KEY = "stormancer.party.plugin"; PluginDescription getDescription() override { return PluginDescription(PLUGIN_NAME, PLUGIN_REVISION); } private: void registerSceneDependencies(ContainerBuilder& builder, std::shared_ptr scene) override { auto version = scene->getHostMetadata(details::PartyService::METADATA_KEY); if (!version.empty()) { builder.registerDependency().singleInstance(); } version = scene->getHostMetadata(details::PartyManagementService::METADATA_KEY); if (!version.empty()) { builder.registerDependency().singleInstance(); } } void sceneCreated(std::shared_ptr scene) override { if (!scene->getHostMetadata(details::PartyService::METADATA_KEY).empty()) { scene->dependencyResolver().resolve()->initialize(); } } void registerClientDependencies(ContainerBuilder& builder) override { builder.registerDependency([](const DependencyScope& dr) { auto partyImpl = std::make_shared( dr.resolve(), dr.resolve(), dr.resolve(), dr.resolve(), dr.resolve() ); // initialize() needs weak_from_this(), so it can't be called from Party_Impl's constructor partyImpl->initialize(); return partyImpl; }).singleInstance(); builder.registerDependency().singleInstance(); builder.registerDependency([](const DependencyScope& dr) { auto provider = std::make_shared(dr.resolve(), dr.resolve(), dr.resolve()); provider->initialize(); return provider; }).as().singleInstance(); } void clientCreated(std::shared_ptr client) override { client->setMetadata(details::PartyService::METADATA_KEY, details::PartyService::PROTOCOL_VERSION); client->setMetadata(details::PartyManagementService::METADATA_KEY, details::PartyManagementService::PROTOCOL_VERSION); client->setMetadata(PLUGIN_METADATA_KEY, PLUGIN_REVISION); auto logger = client->dependencyResolver().resolve(); logger->log(LogLevel::Info, "PartyPlugin", "Registered Party plugin, revision", PLUGIN_REVISION); } }; } } MSGPACK_ADD_ENUM(Stormancer::Party::PartyUserStatus); MSGPACK_ADD_ENUM(Stormancer::Party::MemberDisconnectionReason); MSGPACK_ADD_ENUM(Stormancer::Party::PartyUserConnectionStatus);