.. _Protocol: ******** Protocol ******** Workflows ========= General ------- .. _workflow-authentication: Authentication .............. To authenticate, a client sends an :ref:`reference-message-com.daysofwonder.async.AsyncAuthRequest` to the server. The server can answer with: * :ref:`reference-message-com.daysofwonder.async.AsyncConnectedRequest` if the authentication is successful * :ref:`reference-message-com.daysofwonder.async.AsyncConnectionErrorRequest` if the authentication is not successful .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 400; // default is 192 "Client"; "Server"; // normal edge and doted edge Client -> Server [label = "AsyncAuthRequest"]; === error === Server --> Client [label = "AsyncConnectionErrorRequest"]; === connected === Server --> Client [label = "AsyncConnectedRequest"]; Server [color = yellow] } When successful, the client will receive the full :ref:`reference-message-com.daysofwonder.Player` and a :ref:`concept-session`. The client authenticates the end-user to the server with an |dow| API OAuth2 access token. .. _workflow-authentication-with-an-oauth2-access-token: Authentication with an OAuth2 access token ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The client should authenticate beforehand through the |dow| API, and request an `OAuth2 access token `_. Then the client will fill the ``name`` field with the user login name, and the :ref:`reference-field-com.daysofwonder.async.PartnerToken` ``sessionToken`` field with the OAuth2 access token. .. _workflow-log-out: Logging out ........... For the user to log out of the server, the client should send an :ref:`reference-message-com.daysofwonder.async.AsyncDisconnectRequest`. After this, all requests coming from the client will be rejected until the next :ref:`reference-message-com.daysofwonder.async.AsyncAuthRequest`. The server keeps track of successful connections, by binding the device to the DoW account. When the player logs out, the DoW account is still linked to the device, so the server can send relevant push notifications. If the client logs in with a different DoW account, this account will be bound to the same device, along with the previous account. This means that a given device might receive notifications for a different user than the one currently logged in. Thus, the client should unlink the device when connecting with a different DoW account, by sending an :ref:`reference-message-com.daysofwonder.async.AsyncUnlinkDeviceRequest`. .. note:: You should keep track on the device of the links between the DoW account and the :ref:`concept-session` (if known), so it is easy to check if the account changed or if the player just wants to reconnect with the same account. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 600; "Client"; "Server"; // normal edge and doted edge Client -> Server [label = "AsyncAuthRequest(playerA, sessionA, currentDevice)"]; Server --> Client [label = "AsyncConnectedRequest(sessionA, playerA)"]; === player A logs out === Client -> Server [label = "AsyncUnlinkDeviceRequest(currentDevice)"]; Server --> Client [label = "AsyncDeviceUnlinkedRequest(currentDevice)"] Client -> Server [label = "AsyncDisconnectRequest"]; ... notifications for A will not be received on this device anymore ... === player B logs in === Client -> Server [label = "AsyncAuthRequest(playerB, sessionB, currentDevice)"]; Server --> Client [label = "AsyncConnectedRequest(sessionB, playerB)"]; ... notifications for B will be received on this device ... Server [color = yellow] } .. _workflow-ping: Heartbeat ......... The client must send regularly :ref:`req-PingRequest` to signal the server it is still connected. The server will respond with the exact same :ref:`req-PingRequest`. This allows also the client to notice that the connection with the |ss| has been dropped (a TCP timeout can take a long time). The :ref:`req-PingRequest` contains a ``timestamp`` field where you can write the local timestamp. Since the server sends back the same request, in return you'll get the same timestamp you sent, this allows to compute the time distance between the client and the server. .. _workflow-whatsnew: Current Games Information ......................... It is possible at any time to get access to the list of games the |player| is participating in. It is also possible to get information about only one game at a time. To do so the client needs to send a :ref:`reference-message-com.daysofwonder.async.WhatsNewPussycatRequest`, to which the server will respond with a :ref:`reference-message-com.daysofwonder.async.GameStatusReportRequest`. The returned status is a list (possibly of zero or one element) of :ref:`reference-message-com.daysofwonder.async.StatusReport`. These StatusReport contains all information about a given game, including (but not limited to): * Player list * Status * Game data, and/or summary game data * Active player * Player clocks * ... .. _workflow-invitation: Invitation .......... The simplest way to create a game for a player is to create an :ref:`concept-invitation`. For a |player| to create an invitation, the client sends an :ref:`req-EngageGameWithFriendsRequest` with the list of friends id (this list can't be empty). It is possible to specify robots by adding the special id ``0`` to the list (a list containing only robots will result in an error). Upon reception of that request, the server will send a :ref:`req-GameCreatedRequest` to the sender and all the invitees (if they're not present a Push Notification will be sent). Note that the invitees have to accept or decline the invitation by sending back an :ref:`req-AnswerInvitationRequest` which is confirmed by the server with an :ref:`req-InvitationAnsweredRequest`. If any of the invitees declines the invitation the game is aborted (1), if all accept the invitation the game starts (2). .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Creator"; "Server"; "Invitees" === game is created, asking for answers from invitees === Creator -> Server [label = "EngageGameWithFriends"]; Server -> Creator [label = "GameCreatedRequest(invited_by=1)"]; Server --> Invitees [label = "GameCreatedRequest(invited_by=1)"]; === (1) an invitee declines === Invitees -> Server [label = "AnswerInvitationRequest(accept=false)"]; Server --> Invitees [label = "InvitationAnsweredRequest"]; ... game becomes ABORTED ... === (2) all invitees accept === Invitees -> Server [label = "AnswerInvitationRequest(accept=true)"]; Server --> Invitees [label = "InvitationAnsweredRequest"]; ... game starts ... Server [color = yellow] } .. note:: It is considered best practice to send a :ref:`req-SwitchedToGameRequest` before positive acceptation in order to signal that the player is ready in the waiting room. To quit an invitation there are two possibilities: 1) if the inviter wants to quit, she has to send a :ref:`req-GameForfeitRequest` and this will abort the whole invitation 2) invitees will have to decline the invitation by sending an :ref:`req-InvitationAnsweredRequest` with parameter ``accept=false`` .. _workflow-presence: Presence ........ To monitor presence of other players, the client needs to send their |dow| player id with a :ref:`req-RegisterPresenceRequest` and subscribe to the presence stream with a :ref:`req-SubscribePresenceServiceRequest`. Once subscribed, anytime the presence of one of the monitored player changes, an :ref:`req-AsyncBuddyPresencePartialUpdateRequest` is sent with the state change. Note that presence events are batched, so an :ref:`req-AsyncBuddyPresencePartialUpdateRequest` can contain more than one player presence information. To be noted that while in a game, the opponents presence is automatically monitored, and their presence change is sent with :ref:`req-PlayerPresenceUpdateRequest` (there is no need to subscribe to the presence stream, but the client needs to :ref:`switch to the correct game `). .. _workflow-game-switching: Switching to a Game ................... For in-game presence to work correctly, and also for synchronous games, it is necessary that the client sends a :ref:`req-SwitchedToGameRequest` whenever the client UI enters a specific game (for newly joined or created games, send it after receiving the :ref:`req-GameCreatedRequest`) or leaves completely a game. There is no response from the server, except that when switching into a game, the server will propagate this event in a :ref:`req-PlayerPresenceUpdateRequest` to the other players. .. _workflow-buddylist: Buddy List .......... It is possible for a player to get the content of her :ref:`concept-buddylist`, by sending an :ref:`req-AsyncBuddyListRequest`. In return the server will send back an :ref:`req-AsyncBuddyListContentRequest` containing the list of :ref:`buddies `. To add a buddy, send an :ref:`req-AsyncBuddyManagementRequest` with the :ref:`operation ` ``ADD``, and the |dow| player id to add. On return the server will send back an :ref:`req-AsyncBuddyAddedRequest`. To remove a buddy, follow the same workflow with the :ref:`operation ` ``REMOVE``. The server will send back an :ref:`req-AsyncBuddyRemovedRequest` to confirm the operation. .. _workflow-ignorelist: Ignore List ........... As with the :ref:`workflow-buddylist`, the client can manage the |player| ignore list. To get access to the list, send an :ref:`req-AsyncIgnoreListRequest`, the server will return an :ref:`req-AsyncIgnoreListContentRequest`. To manage the ignore list, send an :ref:`req-AsyncIgnoreManagementRequest` with the proper ``ADD`` or ``REMOVE`` :ref:`operation `, and the server will respond with either an :ref:`req-AsyncIgnoreAddedRequest` or an :ref:`req-AsyncIgnoreRemovedRequest`. .. _workflow-statistics: Server Statistics ................. It is possible to get some statistics the server keeps with :ref:`req-AskServerStatisticsRequest`, for instance to give an overview of the server state when entering the lobby: the number of games, the number of live players (connected), and the overall number of live players (connected and not connected). The distinction between connected and not connected is subtle: we keep hosting live data for an unconnected player, waiting for her to connect back. The server will respond a :ref:`req-ServerStatisticsRequest` with aforementioned data. .. note:: The :ref:`req-AskServerStatisticsRequest` contains a ``subscribe`` parameter: when `false` (or not provided) the server will respond once, if `true` then the client will regularly receive statistics updates (until you re-send the request with ``subscribe=false``). .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 600; "Client"; "Server"; Client -> Server [label = "AskServerStatisticsRequest(subscribe=true)"]; Server --> Client [label = "ServerStatisticsRequest(hosted_games=21, players=42, connected_players=27)"]; ... time passes ... Server ->> Client [label = "ServerStatisticsRequest(hosted_games=22, players=44, connected_players=36)"]; ... time passes ... Server ->> Client [label = "ServerStatisticsRequest(hosted_games=20, players=44, connected_players=31)"]; Server [color = yellow] } .. _workflow-chat: Chat .... Players can send chat messages from the lobby or from a game with a :ref:`req-MulticastChatRequest`. To send a chat visible to all the players in the lobby, use :ref:`req-MulticastChatRequest` without specifying a ``game_id`` (or with ``game_id = 0``). .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Client"; "Server"; "O" Client -> Server [label = "MulticastChatRequest\n(text='test')"]; Server --> Client [label = "ClientChatRequest\n(text='test',sender=A)"]; Server -> O [label = "ClientChatRequest\n(text='test',sender=A)"]; O [width=200, label="Other Clients in lobby"] Server [color = yellow] } To send a chat to all the players in a given game, use :ref:`req-MulticastChatRequest` and specify its ``game_id``. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Player A"; Server; "Player B" "Player A" -> Server [label = "MulticastChatRequest\n(game_id=12,text='test')"]; Server --> "Player A" [label = "ClientChatRequest\n(game_id=12,text='test',sender=A)"]; Server -> "Player B" [label = "ClientChatRequest\n(game_id=12,text='test',sender=A)"]; Server [color = yellow] } If the chat message text triggers the anti-profanity filter several times in a row, the sender will be muted for a certain time, and the workflow becomes: .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Player A"; Server; "Player B" "Player A" -> Server [label = "MulticastChatRequest\n(game_id=12,text='profanity')"]; Server --> "Player A" [label = "ClientChatBlockedRequest\n(game_id=12,text='',sender=A,muted=true)"]; Server -> "Player B" [label = "ClientChatBlockedRequest\n(game_id=12,text='',sender=A,muted=true)"]; "Player A" -> Server [label = "MulticastChatRequest\n(game_id=12,text='message')\n\n", color=red, failed]; Server [color = yellow] } and no chat message is sent to the other players. When sending chat requests you can also specify some players in the field ``recipient_ids``. This is useful if you want to filter who receives the message (for instance, to implement private messaging so players can share a private game's password). In the lobby, ``recipient_ids`` must contain `global ids`. In a game, ``recipient_ids`` must contain `local ids`. * If ``recipient_ids`` is empty, the message will be sent to `all players` (of the lobby, or of the game, according to the context). * If ``recipient_ids`` is filled, only these player ids will receive the message (and the sender will always receive it back). .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; Server; "Player B"; "Player C" "Player A" -> Server [label = "MulticastChatRequest\n(game_id=12,\ntext='test',\nrecipients_ids=[idB])"]; Server --> "Player A" [label = "ClientChatRequest\n(game_id=12,\ntext='test',\nrecipients_ids=[idB],\nsender=A)"]; Server -> "Player B" [label = "ClientChatRequest\n(game_id=12,\ntext='test',\nrecipients_ids=[idB],\nsender=A)"]; Server -> "Player C" [label = "ClientChatRequest\n(game_id=12,\ntext='test',\nrecipients_ids=[idB],\nsender=A)", color=red, failed]; Server [color = yellow] } +---------------------+--------------+--------------------------+ | Chat context | game_id | recipients_ids | +=====================+==============+==========================+ | Lobby | none or 0 | global ids | +---------------------+--------------+--------------------------+ | In game | the game id | local ids (in the game) | +---------------------+--------------+--------------------------+ .. note:: It is possible to get the history of the previous chat messages by sending a :ref:`req-GetChatHistoryRequest` (by specifying a ``game_id``, or with id 0 / no id for the lobby). The server will send back a :ref:`req-ClientChatHistoryRequest` with the latest previously exchanged messages. Note that the chat history will never display private messages. .. _workflow-chatcode: Chat codes .......... The server supports message ``codes``, useful for implementing custom messages like pre-made quickchat messages, commands, emojis, special animation in the lobby... Just fill the ``code`` integer parameter and it will be relayed. Please be aware that chat codes between 0 and 255 are reserved by the server and are not free to use. Provide a higher value. The code 0 is the "dummy code" and will behave the same way as if no code was provided. You also have to provide a non-empty ``text`` (matching the message in default game locale) for older clients to display the message correctly, and for debugging purpose. The text will still pass through the profanity filter as usual. Currently implemented chat codes in the |ss|: +------------+-----------------------------------------------------------+ | Chat code | Description | +============+===========================================================+ | 1 | Welcome message (in game) | +------------+-----------------------------------------------------------+ | 2 | Inappropriate message from a player | +------------+-----------------------------------------------------------+ | 3 | Advertising /report command feature | +------------+-----------------------------------------------------------+ | 4 | "Thanks for your report" | +------------+-----------------------------------------------------------+ | 5 | Report cooldown (players cannot spam report) | +------------+-----------------------------------------------------------+ | 6 | Help (list of commands) | +------------+-----------------------------------------------------------+ | 7 | Lobby statistics (command /statistics) | +------------+-----------------------------------------------------------+ | 8 | In-game statistics (command /statistics) | +------------+-----------------------------------------------------------+ | 9 | Welcome message (in lobby) | +------------+-----------------------------------------------------------+ | 10 ~ 255 | Reserved for future use | +------------+-----------------------------------------------------------+ Chat commands ............. The |ss| offers a few commands, to be issued from the chat like a regular message. Currently, there are: * ``/help``: displays the list of all supported commands * ``/report toto``: to report the user `toto` (please note that no check is done server-side regarding the username) * ``/statistics``: displays statistics on the lobby (number of players, number of open games) or the current game (number of active players, of forfeiters...) Lobby ----- .. _workflow-lobby-entering: Entering ........ To be part of the lobby one needs to enter it by sending an :ref:`req-EnterLobbyRequest`, the server will return :ref:`req-LobbyEnteredRequest`. If the player was already in the lobby, the server will return an :ref:`req-ErrorRequest` (``PLAYER_ALREADY_IN_LOBBY``). .. _workflow-lobby-exiting: Exiting ....... To exit the lobby, send an :ref:`req-ExitLobbyRequest`, the server will return a :ref:`req-LobbyExitedRequest`. Note that upon joining a game (or creating a game), when that game starts, the player is automatically exited from the lobby. .. _workflow-lobby-player-list: Player List ........... When entering the lobby, the client will automatically and regularly receive :ref:`req-LobbyPlayerListRequest`. This request contains the list of the players connected in the lobby. .. note:: The embedded :ref:`reference-field-com.daysofwonder.async.LobbyPlayerListRequest.PlayerList` is in fact stored as a binary string, result of the `Zlib Deflate `_ compression. The protobuf C++ library contains utilities to uncompress this format, other languages usually have this feature in their standard library. The player list contains instances of :ref:`reference-field-com.daysofwonder.async.SmallPlayer` which are not regular :ref:`reference-message-com.daysofwonder.Player`. Those contain less information than regular Player instances. If more information is needed, please see the :ref:`workflow-lobby-ask` workflow. .. _workflow-lobby-game-list: Open Game List .............. Like with the :ref:`workflow-lobby-player-list` workflow, the client once in the lobby will regularly receive a :ref:`req-LobbyGameListRequest`, containing the list of the open games and their details. .. note:: The embedded :ref:`reference-message-com.daysofwonder.async.GameList` is in fact stored as a binary string, result of the `Zlib Deflate `_ compression. The protobuf C++ library contains utilities to uncompress this format, other languages usually have this feature in their standard library. .. _workflow-lobby-ask: Player Information .................. To get access to another player information, just send an :ref:`req-AskPlayerInfoRequest` with the |dow| player id. In return the server will send a :ref:`req-LobbyPlayerInfoRequest` containing a :ref:`reference-field-com.daysofwonder.Player` instance with the information needed. .. _workflow-lobby-join: Joining an Open Game .................... To join an open game, the client must send a :ref:`req-LobbyJoinGameRequest`: .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Client"; "Server"; "Other Players" Client -> Server [label = "LobbyJoinGameRequest"]; Server --> Client [label = "LobbyNewPlayerRequest"]; Server -> "Other Players" [label = "LobbyNewPlayerRequest"]; Server [color = yellow] } If the join is denied (see :ref:`reference-field-com.daysofwonder.async.LobbyJoinDeniedRequest.JoinError` for the list of possible cause), a :ref:`req-LobbyJoinDeniedRequest` is sent back to the sender, the other players of the game are not informed. To join a :ref:`concept-private-games`, its ``password`` needs to be provided. .. warning:: It is the client responsibility to ensure the client compliance with the rules engine before joining by checking the ``rules_engine_version`` field of :ref:`reference-field-com.daysofwonder.async.GameConfiguration` (for more information, see `Manage online versioning `_). .. _workflow-lobby-create: Creating an Open Game ..................... To create an open game, the client sends a :ref:`req-LobbyCreateGameRequest`, with the correct :ref:`reference-field-com.daysofwonder.async.GameConfiguration` and possibly initial state and summary state. The server can answer with an :ref:`req-ErrorRequest` (``TOO_MANY_OFFERS``) if the creator has too many games in progress, or with a :ref:`req-LobbyGameCreatedRequest` if it succeeded. The Game UI should then show a waiting screen until either the creator :ref:`aborts by leaving ` or enough players :ref:`join ` this open game. .. _workflow-lobby-leaving: Leaving an Open Game .................... Until the game is :ref:`started ` it is possible for a player to leave an open game, by sending a :ref:`req-LobbyLeaveGameRequest`: .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Client"; "Server"; "Other Players" Client -> Server [label = "LobbyLeaveGameRequest"]; Server --> Client [label = "LobbyPlayerLeftGameRequest"]; Server -> "Other Players" [label = "LobbyPlayerLeftGameRequest"]; Server [color = yellow] } .. warning:: Note that if the game creator leaves the game while it is still open, there would be no way to start the game as it is her responsibility. Thus, the open game will abort. .. _workflow-lobby-starting: Starting the Game .................... A game starts when the correct number of player have joined it. In this case all players will receive a :ref:`req-GameCreatedRequest`. The game creator can decide to start the game before the game is complete (*i.e.* before ``max_players`` players have joined). For this to work, the client must set the appropriate ``min_players`` and ``max_players`` fields of :ref:`req-GameConfiguration` during game creation. When the game creator (and only the game creator) thinks there are enough players she can send a :ref:`req-LobbyStartGameRequest` which will either: 1) start the game, the rest of the protocol is the same as when a game is complete. 2) return an error :ref:`req-LobbyStartGameDeniedRequest` with a dedicated ``cause`` (*e.g.*: if the game configuration minimum player is not reached). The complete error list is :ref:`reference-field-com.daysofwonder.async.LobbyStartGameDeniedRequest.StartError`. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Client"; "Server"; "Other Players" Client -> Server [label = "LobbyStartGameRequest"]; === Game configuration reached (1) === Server --> Client [label = "GameCreatedRequest"]; Server -> "Other Players" [label = "GameCreatedRequest"]; === Start denied (2) === Server --> Client [label = "LobbyStartGameDeniedRequest"]; Server [color = yellow] } .. note:: When the open game is complete (all player slots are full) or received an "early start" from the creator, the game will really start automatically and all players will be exited from the lobby. .. _workflow-lobby-observable-list: Observable Game List .................... While in the lobby, it is optionally possible to subscribe to the list of observable games. This list contains games that have been started and for which you can observe what the players are doing. Subscribing to the list update is done by sending a :ref:`req-SubscribeToObservableGameListRequest` with ``subscribe`` to ``true`` to the |ss|. The server will reply with a :ref:`req-SubscribedToObservableGameListRequest` where ``subscribed`` is ``true``. To unsubscribe, one needs to send a :ref:`req-SubscribeToObservableGameListRequest` with ``subscribe`` to ``false``. The server will respond with a :ref:`req-SubscribedToObservableGameListRequest` where ``subscribed`` is ``false``. When subscribed the client will receive a stream of :ref:`req-ObservableGameListRequest` containing the list of games that can be observed. To be noted that this list doesn't contain live data of the games. It is required to start observing a game to get the real game state. .. _workflow-lobby-observing: Observing a Game ................ To observe a game, just send a :ref:`req-StartObserveGameRequest` with the ``game_id`` of the game to observe. To stop observing send a :ref:`req-StopObserveGameRequest` with the ``game_id`` of the game to stop observing. The server answers to these two requests with a :ref:`req-GameObservedRequest` which might either contain an error status or ``OK`` and the full game :ref:`req-StatusReport`. Note that it isn't possible to observe: * a game you're playing in * a private game * a non-observable game * an observable game restricting observers to buddies if you're not a buddy of the creator * if you're not in the lobby * if the game is ``OVER`` or ``NOT_STARTED`` * if the game doesn't exist All those cases generates a :ref:`req-GameObservedRequest` with the appropriate error ``status``. In order for a player to watch a game outcome, just after forfeiting or timing out, the player can start observing this game by sending :ref:`req-StartObserveGameRequest` with the game id (this is irrespective of the observability properties of the game). The client GUI should list games in which the player has timed out or forfeited (see :ref:`req-WhatsNewPussycatRequest`) as observable games along with the list of games the player is active in. In game ------- The |ss| doesn’t implement any specific game logic, it can only accommodate turn-by-turn games. It's up to the client to implement the correct game workflow, the |ss| will just provide the communication layer and persistence for the game state. This turn workflow has been designed so that the Game State is unique and is controlled only by one Client at a time, preventing state corruption. The server decides which client can modify the Game State, based on what the previous player told it. The Game State modifications are fully atomic, and the client must send the whole Game State. .. _workflow-turn: Standard Game Turn .................. A standard game turn is a sequence of the following events: 1. Server sends an :ref:`req-ActionRequiredRequest` to the current player's client with the current ``state``, and some other information useful to the client (like ``turn_index``, current ``player_clock``...) 2. Current player sends back a :ref:`req-CommitActionRequest` containing a new state in ``next_state``, and the list of the next players in sequence in ``next_players`` (it can start with the same current player to play again). It can also send a ``next_summary_data`` if needed. 3. Server acknowledges the commit with an :ref:`req-ActionCommitedRequest`. The client can check that the ``turn_index`` is correct. The server can also send an error at this stage (see below). 4. Server decides who’s playing next based on the list of next players sent in the :ref:`req-CommitActionRequest`, and starts back to point *1*. Here's what it gives graphically with a three players game: .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Player C"; "Server"; === 1st Turn === Server -> "Player A" [label = "ActionRequiredRequest(state=<>\nturn_index=1)"]; "Player A" -> Server [label = "CommitActionRequest(state=A,\nnext=[B, C, A],\nturn_index=1)"]; Server --> "Player A" [label = "ActionCommitedRequest(turn_index=2)"]; === 2nd Turn === Server -> "Player B" [label = "ActionRequiredRequest(state=A,\nturn_index=2)"]; "Player B" -> Server [label = "CommitActionRequest(state=B,\nnext=[C, A, B],\nturn_index=2)"]; Server --> "Player B" [label = "ActionCommitedRequest(turn_index=3)"]; === 3rd Turn === Server -> "Player C" [label = "ActionRequiredRequest(state=B,turn_index=3)"]; "Player C" -> Server [label = "CommitActionRequest(state=C,\nnext=[C, B, A],\nturn_index=4)"]; Server --> "Player C" [label = "ActionCommitedRequest(turn_index=4)"]; === 4th Turn === Server -> "Player C" [label = "ActionRequiredRequest(state=C,\nturn_index=4)"]; "Player C" -> Server [label = "CommitActionRequest(state=C1,\nnext=[A, B, C]\n,turn_index=5)"]; Server --> "Player C" [label = "ActionCommitedRequest(turn_index=5)"]; Server [color = yellow] } Notice how in the above example *Player C* is playing again in the *4th* turn. Note that the ``next_players`` is an array. This allows the server to know what would a possible turn look like, this also allows the server to select another player in case of robot hotswap. .. warning:: We recommend to put all possible players in the array ``next_players`` in order to support robot hotswap and avoid unwanted abort of the game. Also note that if the `broadcast` boolean parameter is set to `true`, the new game state will be broadcast to other active players and observers. This avoids using the :ref:`workflow-game-multicast` workflow. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Observer"; "Server" "Player A" -> Server [label="CommitActionRequest(next_state=\"toto\", broadcast=\"true\")"]; Server --> "Player A" [label="ActionCommitedRequest"]; Server -> "Player B" [label="GameStateUpdatedRequest(state=\"toto\")"]; Server -> "Observer" [label="GameStateUpdatedRequest(state=\"toto\")"]; Server [color = yellow] } .. _workflow-pause: Pausing Clock after commit ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. warning:: This feature is experimental, protocol might change in the future. Some games display the previous players turns animations at the start of the active player turn. While those animations run on the client, the active player clock is ticking on the server. Players might complain that their time has been consumed while they weren't able to play, which can be perceived as unfair. In order to mitigate this feeling, it is possible to specify a short `pause time` for the active player clock for this specific purpose. The clock will not be decreased for this delay, and then it will resume and tick normally. You have to fill the ``pause_time`` member of :ref:`req-CommitActionRequest` with the duration of the pause (in seconds) for the immediate ``next`` player. This time is given back in :ref:`req-ActionCommitedRequest`, and transmitted to the player through :ref:`req-ActionRequiredRequest`. Clock-related messages will be sent at appropriate moments: :ref:`req-ClockPausedRequest` when the clock is effectively paused (beginning of the turn), and :ref:`req-ClockResumedRequest` (providing clocks status) when the pause time is consumed. If for some reason, the player commits action before the pause is over, the server will cancel the resume order as it makes no sense. If the provided pause time is zero, no pause is done and the turn follows the regular protocol of a :ref:`workflow-turn`. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Server" "Player A" -> Server [label="CommitActionRequest(next=[B,A], pause_time=3)"]; Server --> "Player A" [label="ActionCommitedRequest(pause_time=3)"]; Server -> "Player B" [label="ActionRequiredRequest(pause_time=3)"]; Server -> "Player A" [label="ClockPausedRequest()", note="A is not paused but get the \ninformation that clock should be stopped"]; Server -> "Player B" [label="ClockPausedRequest()"]; === 3 seconds pass, the clock for B is not decreased === === everybody receives information that clock should resume === Server -> "Player A" [label="ClockResumedRequest(clock[A].clock.paused=false, \nclock[A].clock.armed=false)", note="also contains other clocks"]; Server -> "Player B" [label="ClockResumedRequest(clock[B].clock.paused=false, \nclock[B].clock.armed=true)"]; === pause time is exhausted, clock B has been resumed, turn continues as normal === "Player B" -> Server [label="CommitActionRequest(next=[A,B], pause_time=3)"]; Server --> "Player B" [label="ActionCommitedRequest(pause_time=3)"]; Server -> "Player A" [label="ActionRequiredRequest(pause_time=3)"]; Server -> "Player A" [label="ClockPausedRequest()"]; Server -> "Player B" [label="ClockPausedRequest()", note="B is not paused but get the \ninformation that clock should be stopped"]; "Player A" -> Server [label="CommitActionRequest(next=[B,A], pause_time=3)"]; === played but pause was not over: will not "resume" the clock === Server --> "Player A" [label="ActionCommitedRequest(pause_time=0)"]; Server -> "Player B" [label="ActionRequiredRequest(pause_time=0)"]; === pause time is 0: no pause === Server [color = yellow] } .. note:: Please note that a maximum duration for the pause is set in configuration, giving a pause time that breaks the limit will result in an error. .. warning:: The clock might resume at a moment where the player was not on the app. When the player comes back, she will notice that the clock is not paused. In fact, the "pause time" was already done server-side but the player just was not there to witness it. .. _workflow-idle-timer: Idle timer ^^^^^^^^^^ .. warning:: This feature is experimental, protocol might change in the future. The player clock ensures no malicious player can lock the game by not playing. Sometimes this might not be enough, as waiting for a whole player clock to expire can be quite long and cause frustration for other players. Besides the player clock, the |ss| offers an optional `idle timer`. Regular updates of the timer progress will be broadcast with :ref:`req-PlayerIdleProgressRequest`, filled with the array of players currently idle and the percentage of total time currently exhausted (at ``50%``, ``75%`` and ``100%``). When exhausted, the server will ask a robot to play for the currently idle players, as if they left, and the game will continue as normal (players are not kicked out of the game). To enable the idle timer at game creation, fill the ``idle_time`` field of :ref:`req-GameConfiguration` with a positive number of seconds. If the provided ``idle_time`` is zero or negative, the timer will not be used and the turn follows the regular protocol of a :ref:`workflow-turn`. .. warning:: We strongly advise to scale the ``idle_time`` duration with the total `player clock`, so that longer games will have a longer idle time, and shorter games will have a shorter idle time. .. note:: When idle timer is enabled at game creation, one can override the timer for the next turn in order to deal with asymetrical games or non linear thinking phase through ``idle_time`` in :ref:`req-CommitActionRequest`. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Server" "Player A" -> Server [label="LobbyCreateGameRequest(GameConfiguration(idle_time=40))"]; === all players join, the game starts === Server -> "Player B" [label="ActionRequiredRequest()"]; === player B should play, starting the idle timer immediately === === 20 seconds passed, 50% of the total idle time === Server -> "Player A" [label="PlayerIdleProgressRequest(progress=50, player_ids=[A])"]; Server -> "Player B" [label="PlayerIdleProgressRequest(progress=50, player_ids=[A])"]; === 30 seconds passed, 75% of the total idle time === Server -> "Player A" [label="PlayerIdleProgressRequest(progress=75, player_ids=[A])"]; Server -> "Player B" [label="PlayerIdleProgressRequest(progress=75, player_ids=[A])"]; === 40 seconds passed, 100% of the total idle time === Server -> "Player A" [label="PlayerIdleProgressRequest(progress=100, player_ids=[A])"]; Server -> "Player B" [label="PlayerIdleProgressRequest(progress=100, player_ids=[A])"]; Server -> "Player B" [label="PlayerTimeoutRequest(offender_id=[A], status=IDLE)"]; === B plays as a robot for A, game continues as normal === "Player B" -> Server [label="CommitActionRequest(next=[A,B])"]; Server --> "Player B" [label="ActionCommitedRequest()"]; Server -> "Player A" [label="ActionRequiredRequest()"]; === a new idle timer begins... === Server [color = yellow] } .. warning:: Even though we asked a robot to play, the original player still has ability to commit her action, while an other player will also try to commit action for her (as a robot). Depending on who shot first, the player or the robot handler will get an :ref:`req-ErrorRequest` with ``NOT_YOUR_TURN`` error. The game will continue as normal, as either way someone played for this turn. .. note:: When using a ``pause_time`` at commit, the `idle timer` will start only when the pause ends. Errors ^^^^^^ When a client commits data to the server, the following errors can be returned: +---------------------+-----------------------------------------------------------+ | Error Type | Meaning | +=====================+===========================================================+ | NOT_YOUR_TURN | player tried to play, server thinks she's not the current | | | player | +---------------------+-----------------------------------------------------------+ | UNKNOWN_PLAYER | ``next`` contains an invalid player | | +-----------------------------------------------------------+ | | player sending this commit is not part of this game | +---------------------+-----------------------------------------------------------+ | UNKNOWN_GAME | ``game_id`` is not part of on-going games of the player | +---------------------+-----------------------------------------------------------+ | YOU_FORFEITED | this player forfeited earlier, and thus can't | | | play anymore | +---------------------+-----------------------------------------------------------+ | YOU_RAN_OUT_OF_TIME | this player exhausted her player clock, and thus can't | | | play anymore | +---------------------+-----------------------------------------------------------+ | YOU_LEFT | this player has left a synchronous game, she needs to | | | resume the game first | +---------------------+-----------------------------------------------------------+ | INDEX_CONFLICT | this player provided a turn index, but it does not match | | | the server's own turn index | +---------------------+-----------------------------------------------------------+ | BAD_REQUEST | provided ``pause_time`` is incorrect | | +-----------------------------------------------------------+ | | ``simultaneous_players`` are invalid (see next) | | +-----------------------------------------------------------+ | | ``interruptible`` configuration is invalid (see next) | + +-----------------------------------------------------------+ | | any other problem the server detected | +---------------------+-----------------------------------------------------------+ .. _workflow-simultaneous: Simultaneous Game Turn ...................... Simultaneous turns allow to require multiple independent players actions during the same turn. To enter a simultaneous turn, one must send a :ref:`req-CommitActionRequest` with ``next_simultaneous`` as ``true``, and provide players required to act in ``simultaneous_players`` array. Once the action is validated, the game will enter in simultaneous turn and send :ref:`req-UserDataUpdateRequiredRequest` to all provided players (if present, otherwise :ref:`req-PlayerTimeoutRequest` is sent). For players to send data, use :ref:`req-UpdateUserDataRequest`. This should also be used when playing for a robot. The |ss| will send back :ref:`req-UserDataUpdatedRequest` for confirmation. Once all required players provided their UserData, the *next* player will be selected to merge all the actions. She receives :ref:`req-ActionRequiredRequest` with ``user_data`` populated with previously described requests. .. warning:: In case of *next* player is an invited robot, a :ref:`req-PlayerTimeoutRequest` is sent. .. note:: *merge* turn is seen as an administrative turn and will not update the *next* array of players. She sends a :ref:`req-CommitActionRequest` with the new state. If valid, the simultaneous turn is completed (after sending :ref:`req-ActionCommitedRequest` for confirmation). According to commit's ``next_simultaneous`` member, the next turn may be simultaneous or not... .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Player A"; "Player B"; "Server" "Player A" -> Server [label="CommitActionRequest(next_simultaneous=true, \nnext_state=\"toto\", \nsimultaneous_players=[A, B, C])"]; Server --> "Player A" [label="ActionCommitedRequest"]; === Entering simultaneous turn === Server -> "Player A" [label="UserDataUpdateRequiredRequest(state=\"toto\")"]; Server -> "Player B" [label="UserDataUpdateRequiredRequest(state=\"toto\")"]; Server -> "Player A" [label="PlayerTimeoutRequest(offender_id=C)", note="C is a robot"]; === Waiting for players actions === "Player A" -> Server [label="UpdateUserDataRequest(data=\"a\")"]; Server --> "Player A" [label="UserDataUpdatedRequest"]; "Player B" -> Server [label="UpdateUserDataRequest(data=\"b\")"]; Server --> "Player B" [label="UserDataUpdatedRequest"]; === Player A plays the robot for C === "Player A" -> Server [label="UpdateUserDataRequest(player_id=C, data=\"c\")"]; Server --> "Player A" [label="UserDataUpdatedRequest"]; === Ask a player to merge data === Server -> "Player A" [label="ActionRequiredRequest(user_data=[(A, \"a\"), (B, \"b\"), (C, \"c\")])"]; "Player A" -> Server [label="CommitActionRequest(next_state=\"abcfoobar\", next_simultaneous=false)"]; Server --> "Player A" [label="ActionCommitedRequest"]; Server [color = yellow] } If the player never responds to the :ref:`req-ActionRequiredRequest`, we will send a :ref:`req-PlayerTimeoutRequest` following the same model as regular turns, except the |ss| will provide ``user_data`` required to perform the merge as a robot. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 300; "Player A"; "Player B"; "Server" === Ask a player to merge data === Server -> "Player A" [label="ActionRequiredRequest(user_data=[(A, \"a\"), (B, \"b\"), (C, \"c\")])"]; === Player A forfeits / times out / quits the game, we ask a live player === Server -> "Player B" [label="PlayerTimeoutRequest(offender_id=1,\nuser_data=[(A, \"a\"), (B, \"b\"), (C, \"c\")])"]; "Player B" -> Server [label="CommitActionRequest(next_state=\"abcfoobar\")"]; Server --> "Player B" [label="ActionCommitedRequest"]; Server [color = yellow] } Also note that if the ``broadcast`` boolean parameter is set to ``true``, the user data will be broadcast to other active players and observers. This can be useful for other clients to acknowledge that *"Player A played and is waiting for you"*. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Observer"; "Server" "Player A" -> Server [label="UpdateUserData(user_data=[\"a\"], broadcast=true)"]; Server --> "Player A" [label="UserDataUpdatedRequest"]; Server -> "Player B" [label="GameUserDataUpdatedRequest(player=A,\nuser_data=\"a\")"]; Server -> "Observer" [label="GameUserDataUpdatedRequest(player=A,\nuser_data=\"a\")"]; Server [color = yellow] } .. warning:: *merger* selection: the player which should perform the merge of all the user data is by default the first member of the current *next* array if present or an invited robot. If she's not an invited robot and not present, the |ss| will select any other present player that was part of this simultaneous turn. To prevent waiting for the absent original merger player, the client logic must check and commit with the correct next array. If the simultaneous turn didn’t change the order of the next players, then the same next list should be used, in which case the original merger will be the next player to receive an ActionRequiredRequest. Pausing Clock in a simultaneous turn ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. warning:: This feature is experimental, protocol might change in the future. The same way clocks can be paused at the beginning of a standard turn, it is possible to pause the clocks at the beginning of a simultaneous turn. Besides setting ``next_simultaneous``, you have to fill the ``pause_time`` member of :ref:`req-CommitActionRequest` with the duration of the pause (in seconds) for the simultaneous players. This time is given back in :ref:`req-ActionCommitedRequest`, and transmitted to the player through :ref:`req-UserDataUpdateRequiredRequest`. All clocks of ``simultaneous_players`` will be paused, but only one :ref:`req-ClockPausedRequest` and :ref:`req-ClockResumedRequest` will be sent (to `all` players). If for some reason, a player updates its user-data before the pause is over, other players will still be paused. If the provided pause time is zero, no pause is done and the turn follows the regular protocol of a :ref:`workflow-simultaneous`. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Player C"; "Server" "Player A" -> Server [label="CommitActionRequest(next=[B,A], pause_time=3, next_simultaneous=true, simultaneous_players=[A,B])"]; Server --> "Player A" [label="ActionCommitedRequest(pause_time=3)"]; Server -> "Player A" [label="UserDataUpdateRequiredRequest(pause_time=3)"]; Server -> "Player B" [label="UserDataUpdateRequiredRequest(pause_time=3)"]; Server -> "Player A" [label="ClockPausedRequest()"]; Server -> "Player B" [label="ClockPausedRequest()"]; Server -> "Player C" [label="ClockPausedRequest()", note="C is not paused but will receive the \ninformation to freeze all clocks"]; === 3 seconds pass, clocks are not decreased === Server -> "Player A" [label="ClockResumedRequest(clock[A].clock.paused=false \nclock[A].clock.armed=true \nclock[B].clock.paused=false \nclock[B].clock.armed=true\n)"]; Server -> "Player B" [label="ClockResumedRequest()"]; Server -> "Player C" [label="ClockResumedRequest()", note="C is not resumed, just getting information\n for A and B"]; === pause time is exhausted, clocks A and B have been resumed, simturn continues as normal === "Player B" -> Server [label="UpdateUserDataRequest()"]; Server --> "Player B" [label="UserDataUpdateRequired(pause_time=3)"]; === B played, its clock will stop. A did not play, its time will decrease === Server [color = yellow] } .. note:: Please note that a maximum duration for the pause is set in configuration, giving a pause time that breaks the limit will result in an error. .. warning:: When the clock resumes, player might be absent and not see that the clock was paused. However, the time was indeed saved so globally this changes nothing. .. _workflow-interruption: Interruptions ............. When sending a :ref:`req-CommitActionRequest`, you can specify the action as `interruptible`: some other players will be asked by the |ss| if they want to interrupt the last action. For this, you have to specify the ``interruption_window_duration`` parameter with the duration you want the interruption to be possible for (in *milliseconds*). Players set in ``interruption_player`` are the `interrupters`: they will have the given amount of time to tell the server about the interruption, by sending an :ref:`req-InterruptActionRequest`, providing ``interrupt_data`` (please keep them small as they are transmitted to all players). It is important to provide a correct ``turn_index`` so the server knows exactly which action is being interrupted, in case of race conditions during consecutive interruptions. The correct value is given by :ref:`req-ActionCommitedRequest` and :ref:`req-GameStateUpdatedRequest`. .. note:: It is up to the game client's to acknowledge the game is in an interruption state. After sending the :ref:`req-CommitActionRequest`, the server will send a :ref:`req-GameStateUpdatedRequest` to all players. This request relays the ``interruption_window_duration`` given at commit, so if it is non-zero, you should be in an interruption turn. Once the time is exhausted, all players will receive an :ref:`req-InterruptionOverRequest` so they know the time is up. Any interruption received after the time delay will be discarded and return an :ref:`req-InterruptActionErrorRequest`. One player can also explicitly put a stop to the interruption phase by setting ``no_more_interruption``. A player will be selected to resolve the interruptions (as possibly there can be several interruptions) with an :ref:`req-ActionRequiredRequest` (or a :ref:`req-PlayerTimeoutRequest` if the merge should be performed by a robot). According to the result, the game will continue as normal. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Player B"; "Player C"; "Server" === Player A commits, asking for the next turn to be Interruptible === "Player A" -> Server [label="CommitActionRequest(interruption_window_duration=3000, \ninterruption_players=[B, C], \nnext=[B, C, A])"]; Server --> "Player A" [label="GameStateUpdatedRequest(interruption_window_duration=3000)"]; Server -> "Player B" [label="GameStateUpdatedRequest(interruption_window_duration=3000)"]; Server -> "Player C" [label="GameStateUpdatedRequest(interruption_window_duration=3000)"]; === Player B interrupts A === "Player B" -> Server [label="InterruptActionRequest(turn_index=2, \ninterruption_data=\"Not this time\", \nturn=A)"]; Server --> "Player B" [label="ActionInterruptedRequest(turn_index=2, \ninterruption_data=\"Not this time\", \nturn=A)"]; Server -> "Player A" [label="ActionInterruptedRequest(turn_index=2, \ninterruption_data=\"Not this time\", \nturn=A)"]; Server -> "Player C" [label="ActionInterruptedRequest(turn_index=2, \ninterruption_data=\"Not this time\", \nturn=A)"]; === Player C interrupts A === "Player C" -> Server [label="InterruptActionRequest(turn_index=2, \ninterruption_data=\"I protest too\", \nturn=A)"]; Server --> "Player C" [label="ActionInterruptedRequest(turn_index=2, \ninterruption_data=\"I protest too\", \nturn=A)"]; Server -> "Player A" [label="ActionInterruptedRequest(turn_index=2, \ninterruption_data=\"I protest too\", \nturn=A)"]; Server -> "Player B" [label="ActionInterruptedRequest(turn_index=2, \ninterruption_data=\"I protest too\", \nturn=A)"]; === Time is exhausted === Server -> "Player A" [label="InterruptionOverRequest(turn_index=2)"]; Server -> "Player B" [label="InterruptionOverRequest(turn_index=2)"]; Server -> "Player C" [label="InterruptionOverRequest(turn_index=2)"]; === Ask Player B has been selected to merge interruption data === Server -> "Player B" [label="ActionRequiredRequest(turn_index=3, \ninterruption_data=[(B, \"Not this time\"), \n(C, \"I protest too\")]"] Server [color = yellow] } .. warning:: Please note that during all interruption steps, the time will be decreased from the *original committer*'s clock (in the diagram, Player A). During the merge action, the time is decreased from the merger's clock (in the diagram, Player B). We strongly recommend to hide all this protocol under a visual animation for timing. For instance, once I played an interruptible card, the game would trigger an animation whose duration corresponds to the interruption duration, so: - other players do not have an inactive screen waiting for several seconds (there is always something happening to keep player's attention) - players have a visual indication that an interruption is possible, and how many time is left until the interruption window closes It is up to the game's client to set the interruption duration, thus we advise to use quite the same as animation duration. Also, the animation should start on all devices at the same time (or the closest possible) to minimize synchronization issues: - the interrupter could receive the message a little bit late, and the animation will still play as the interruption phase is over - the interruptee could start the animation too soon, so it thinks the interruption is over but the server still accepts interruption requests for a small period To achieve this, we recommend triggering the animation not after sending `CommitActionRequest` but only after receiving `GameStateUpdatedRequest`. It is sent to all clients at the same time so only network latency will come in play. .. warning:: Simultaneous turns do not support Interruptions. If you set both ``next_simultaneous`` and ``interruption_window_duration``, the server will send an :ref:`req-ErrorRequest`. .. _workflow-game-multicast: Multicast ......... The :ref:`req-MulticastDataRequest` allows to send arbitrary information to the other present players in the game, for instance to refresh the UI. .. warning:: Please note that this should not be used to relay the state change after an action. The correct way is to set the `broadcast` parameter of :ref:`req-CommitActionRequest` to ``true`` (or :ref:`req-UpdateUserDataRequest` in case of a simultaneous turn). The server will thus atomically send a :ref:`req-GameStateUpdatedRequest` exhibiting the new *state* (or *user data*) to other live players. If, for whatever reason, you can't use the `broadcast` parameter, you might have to use the :ref:`req-MulticastDataRequest` to send the new state data to other clients. However this is an anti-pattern and should be avoided: - the server will simply forward the information, it doesn't know whether it's related to state or not. It will not modify its state when receiving the request. You might inadvertently send data not compatible with the current state, resulting in a corrupted local state in the client. - :ref:`req-MulticastDataRequest` content is transient and will be lost for players leaving the game or in the rare event of a service restart, unlike the game state which is guaranteed available. - players may receive :ref:`req-MulticastDataRequest` before :ref:`req-ActionCommitedRequest`, possibly resulting in local conflicts. - worse, the next player may receive her :ref:`req-ActionRequiredRequest` before the :ref:`req-MulticastDataRequest`. The game engine would then compute the action to perform without having the last state. Now that you know the danger of using the multicast to transmit the game state, here is how to use it to send arbitrary information. Any present player can send a :ref:`req-MulticastDataRequest` to the |ss| with some binary ``data`` and possibly a list of ``recipients_ids``. The server will then send back a :ref:`req-ClientDataRequest` containing the binary ``data`` to all the ``recipients_ids``. If ``recipients_ids`` is empty, all the present players will receive the request. Non-present players can also be notified by a push notification, if ``notification`` is filled-in. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Server"; "Player B"; "Player C" "Player A" -> Server [label="MulticastDataRequest(data=A)"]; Server --> "Player A" [label="ClientDataRequest(data=A)"]; Server -> "Player B" [label="ClientDataRequest(data=A)"]; Server -> "Player C" [label="ClientDataRequest(data=A)"]; Server [color = yellow] } .. _workflow-forfeit: Forfeit ....... At any time a player can decide to withdraw from the game (and lose it). She does so by sending a :ref:`req-GameForfeitRequest`. In return the server will send back a :ref:`req-GameForfeitedRequest` to all the present players in the game. If the forfeiting player was the last *active* (*i.e.* all other players have already either forfeited, left or exhausted their Player Clock), then the game will :ref:`workflow-abort`. If the forfeiting player wasn't the last remaining *active* player, the |ss| will do the necessary to replace it with a robot as explained in :ref:`workflow-game-hotswap`. The |ss| will also send to all the present players a :ref:`req-PlayerReplacedRequest` so that they can display the hotswap information in the UI. .. _workflow-timeout: Player Timeout .............. When a player exhausts her own :ref:`concept-player-clock`, she can't play anymore and is replaced by a robot, as explained in :ref:`workflow-game-hotswap` section. The |ss| will also send to all the present players a :ref:`req-PlayerReplacedRequest` so that they can display the hotswap information in the UI. .. _workflow-game-hotswap: Robots ...... Since the |ss| Game instance has no knowledge of the game engine, it can't play robots directly. As such it asks one of the connected client to impersonate another player and play for her with an IA (which is what we call a *robot*). This can happen for the following reasons: * a player forfeited. * a player exhausted her :ref:`concept-player-clock` * a player left a :ref:`synchronous game ` * a robot had been invited In all those cases, at this player's turn, the |ss| will select another client (preferentially from the ones still connected to the server) and ask it to play for this defecting player. The game logic in the client thus must know how to play for a different player than the human using the UI. The robot workflow works like this: 1. *Player A* commits her action by sending a :ref:`req-CommitActionRequest` as usual 2. server notices the next player (*Player B*) needs to be played by a robot 3. if *Player A* is still present, the Server will send a :ref:`req-PlayerTimeoutRequest` to its client, for it to play for *Player B* 4. *Player A*'s client sends a :ref:`req-CommitActionRequest` as if it was *Player B* 5. server keeps going on with *Player C* This is best illustrated with this sequence diagram: .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 310; "Player A"; "Server"; "Player C" === 1st Turn === Server -> "Player A" [label = "ActionRequiredRequest(state=<>,\nturn_index=1)"]; "Player A" -> Server [label = "CommitActionRequest(state=A,\nnext=B,\nturn_index=1)"]; Server --> "Player A" [label = "ActionCommitedRequest(turn_index=2)"]; === 2nd Turn - B must be played by a robot === Server -> "Player A" [label = "PlayerTimeoutRequest(state=A,\noffender_id=B,\nturn_index=2)"]; "Player A" -> Server [label = "CommitActionRequest(state=B,\nnext=[C, B],\nplayer_id=B,\nturn_index=2)"]; Server --> "Player A" [label = "ActionCommitedRequest(turn_index=3)"]; === 3rd Turn === Server -> "Player C" [label = "ActionRequiredRequest(state=B,\nturn_index=3)"]; "Player C" -> Server [label = "CommitActionRequest(state=C,\nnext=A,\nturn_index=4)"]; Server --> "Player C" [label = "ActionCommitedRequest(turn_index=4)"]; Server [color = yellow] } If two or more players must be played by robots, the same process happens. If *Player A* is not present anymore when the server needs a robot to play, it will select one of the other connected clients of this game in order of appearance in the last ``next_players`` array. If none are connected at this time, the server will select the next non-robot in order of appearance in the last ``next_players`` array and send her a push notification as if it was her turn to play. .. warning:: We recommend to put all possible players in the array ``next_players``. Upon receiving this push notification, this client game logic (once the controlling human decides to play her turn) must check this game :ref:`req-StatusReport`, notice that ``turn`` and ``active_player`` are not the same (and ``active_player`` is this client's player) and use a robot to play for the ``turn`` player. .. note:: In a fully asynchronous game, any client can play for any player. To prevent any issue, the client that plays **must** check for the error ``NOT_YOUR_TURN`` after sending the :ref:`req-CommitActionRequest`. In case of error, it means another client already played, and the client should refresh its view of the :ref:`req-StatusReport` and possibly not play. As such, it is recommended to check the ``active_player`` value and only play when it matches the current player. .. _workflow-game-over: Game Over ......... At one point in the game, one of the player will trigger the end of game as implemented by the rules engine, this client will then send a :ref:`req-GameOverRequest` containing the ordering of the players in ``finalScores`` (including robots and timed out or forfeited players). Make sure the ``is_ranked`` is set to ``true`` if the |ss| needs to update the rankings of the players. The game status will then change to ``OUTCOME``, and all connected players will receive a :ref:`req-GameOutcomeRequest` containing ranking information (and the player's scores). The clients will then display this in a final score screen. This information can also be displayed from the :ref:`req-StatusReport` for asynchronous players. All players will have to confirm the outcome by sending a :ref:`req-GameOutcomeConfirmationRequest` before the game can reach the ``OVER`` status, and ultimately disappear. For asynchronous clients, the game logic can know if this player already saw the final scores by checking the :ref:`req-StatusReport` ``outcome_not_seen`` array which contains the list of players that have not yet confirmed the outcome. .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 302; "Player A"; "Server"; "Player B" === Last Turn === Server -> "Player A" [label = "ActionRequiredRequest(state=<>)"]; "Player A" -> Server [label = "CommitActionRequest(state=A,next=B)"]; Server --> "Player A" [label = "ActionCommitedRequest"]; === Game Over === "Player A" -> Server [label = "GameOverRequest(finalScores={A:[12,2],B:[23,1]}"]; Server -> "Player A" [label = "GameOutcomeRequest"]; Server -> "Player B" [label = "GameOutcomeRequest"]; === Player A & B confirm === "Player A" -> Server [label = "GameOutcomeConfirmationRequest"]; "Player B" -> Server [label = "GameOutcomeConfirmationRequest"]; Server [color = yellow] } .. warning:: No commits or user data updates will be processed after a :ref:`req-GameOverRequest` is sent to the server, be sure to provide the final score and final state of the game (including final bonuses) in the :ref:`req-GameOverRequest`. .. note:: In case of game over during a simultaneous turn, we recommend to send the :ref:`req-GameOverRequest` in place of a merge commit after receiving the :ref:`req-ActionRequiredRequest`. .. _workflow-abort: Abort ..... A game can abort at any time if no more players can play (all timed out or all withdrawn) and will try to notify connected players (and observers) with a :ref:`req-GameAbortedRequest` containing the applied ranking penalties. The game status also changes to ``ABORTING`` and then after a while to ``ABORTED`` (to let the players see the game status with :ref:`workflow-whatsnew` and then confirm with the message :ref:`req-GameAbortedConfirmationRequest`). .. seqdiag:: seqdiag { default_fontsize = 11; activation = none; edge_length = 200; "Player A"; "Server"; "Player B"; "Observer 1" === All players timeout or forfeited === Server -> "Player A" [label = "GameAbortedRequest"]; Server -> "Player B" [label = "GameAbortedRequest"]; Server -> "Observer 1" [label = "GameAbortedRequest"]; === Aborted confirmation === "Player A" -> Server [label = "GameAbortedConfirmationRequest"]; "Player B" -> Server [label = "GameAbortedConfirmationRequest"]; Server [color = yellow] } .. _workflow-game-observing: Game Observation ................ As explained in :ref:`concept-observation`, it is possible for multiple players to observe what happens in a given game. First the client needs to :ref:`start observing ` an observable game (from the observable game list). Once in the list of observers, the client will receive all events a normal player would normally receive, including all :ref:`req-ActionRequiredRequest` and :ref:`req-PlayerTimeoutRequest`. It is up to the client to display what happens in the game from this information and the game logic. .. _workflow-player-clock: Player Clocks Status .................... It is possible for a client to fetch all the player clocks status. It is hard to maintain a synchronized clock while displaying the UI, so from time to time, it is necessary to refresh the player clocks from the server. To do that, send a :ref:`req-GetClocksRequest` directed to a game, the server will send back a :ref:`req-ClocksStatusRequest`. .. note:: The playing player clock is always sent in the :ref:`req-ActionRequiredRequest`, so at least every start of a player turn, her local clock can be synchronized. Push Notifications ------------------ Definition .......... The |ss| is able to send native device push notification (to Steam users, Google android users and iOS/OSX users) when specific events happen: * an invitation is sent to a player * it is a player's turn (or a robot turn) * end of a game Prerequisites ............. To enable push notifications the |ss| needs to be provisioned with the correct configuration, as explained in the following table. The developer needs to contact the |dow| Administrators to enable the push notification feature. +---------------------+-----------------------------------------------------------+ | Platform | Material needed | +=====================+===========================================================+ | iOS or OSX | bundle id of the application (com.asmodee-digital.xxx) | | | from apple developper account provision profile | +---------------------+-----------------------------------------------------------+ | Google FCM | sender id | | +-----------------------------------------------------------+ | | key | +---------------------+-----------------------------------------------------------+ | Steam | app id | | +-----------------------------------------------------------+ | | api key | +---------------------+-----------------------------------------------------------+ Client side integration ....................... The |client|, when authenticating a player by sending an :ref:`req-AsyncAuthRequest`, must provide a :ref:`DeviceType ` structure containing: * the type of push system among (``IOS``, ``GCM``, ``FCM``, ``STEAM``, ``OSX``, ``IOS_SANDBOX``, ``OSX_SANDBOX``), see :ref:`Devices.Type ` * the opaque token provided by the client operating system. Apple allows to use a "sandbox" mode for Push Notifications, those are using their distinct codes because they use another endpoint of the APNS for developement/testing purpose (by default sandbox mode should be enabled). For more information about receiving Push Notifications on iOS see `Apple Push Notification Documentation `_. For more information about receiving Push Notifications on Android see `Firebase Cloud Messaging Documentation `_. .. note:: `GCM` is deprecated and not supported anymore by Google. Developers should use `FCM` instead. Format of a push notification ............................. When an event triggering a push notification happens, the |ss| will notify the Push Notification platform matching the recipient device which will send to the client the following data (which models the data available in an iOS notification): +---------------------+---------------------------------------------------------------+ | Key | Content | +=====================+===============================================================+ | ``action-loc-key`` | id of the action | +---------------------+---------------------------------------------------------------+ | ``loc-key`` | id of the localized string in the application | | | resources that will be displayed in the notification | +---------------------+---------------------------------------------------------------+ | ``loc-args`` | some arguments that depend on the notification type | +---------------------+---------------------------------------------------------------+ | ``async`` | notification payload | +---------------------+---------------------------------------------------------------+ The payload is a protobuf serialized data structure of :ref:`req-GameNotification` that contains the game id, the event type, the player id of the recipient and the current turn index. Apple Usage ^^^^^^^^^^^ On iOS, the client receives the notification as a dictionary. The above keys are used by the system to display the push notification if the app is not in foreground. For instance it will display a button whose string is searched in the app localized string whose key is the value of the ``action-loc-key``. The text content of the notification will be derived from the interpolation with ``loc-args`` of a string coming from the app localized string whose key is ``loc-key``. See `Apple Push Notification Payload Key Reference `_ for more information. Android Usage ^^^^^^^^^^^^^ On Android, the system is less automatic. The |ss| only sends `Data Messages `_. The client will receive a ``RemoteMessage`` object and can get access to the |ss| payload through the ``getData`` call which will return a ``Map`` containing the keys shown above. Once done, the client must build a notification (if in background) by looking at the ``loc-key`` string and interpolate it with ``loc-args`` as explained below, and display it as a notification (see `NotificationCompat.Builder `_ for this). String interpolation ^^^^^^^^^^^^^^^^^^^^ The interpolation of the ``loc-key`` matched string obeys to Apple format: every ``$@`` will be replaced by one of the value of the ``loc-args`` array, for instance:: if ``loc-key`` give this string: "You've been invited by $@" and ``log-args`` contains ``CaptnHook``, the result will be: "You've been invited by CaptnHook" Type of notifications ..................... +-------------------------+----------------+------------------------------------------+ | Type | Key | Value | +=========================+================+==========================================+ | ``YOUR_TURN`` | Sent to the current player in a game | | +----------------+------------------------------------------+ | | action_key | ``YOUR_TURN_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``YOUR_TURN`` | | +----------------+------------------------------------------+ | | localized_args | *none* | +-------------------------+----------------+------------------------------------------+ |``YOUR_TURN_INVITE`` | Sent to the current player of an invitation game | | +----------------+------------------------------------------+ | | action_key | ``YT_INVITE_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``YOUR_TURN_INVITE`` | | +----------------+------------------------------------------+ | | localized_args | name of the inviter | +-------------------------+----------------+------------------------------------------+ | | Sent to the player selected to play a robot for | | | another player | | +----------------+------------------------------------------+ |``YOUR_TURN_ROBOT`` | action_key | ``YT_ROBOT_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``YOUR_TURN_ROBOT`` | | +----------------+------------------------------------------+ | | localized_args | *none* | +-------------------------+----------------+------------------------------------------+ | | Sent to a player that has been invited to a game so that | | | she can confirm or deny the invitation | | +----------------+------------------------------------------+ |``CONFIRM_INVITATION`` | action_key | ``INVITATION_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``CONFIRM_INVITATION`` | | +----------------+------------------------------------------+ | | localized_args | name of the inviter | +-------------------------+----------------+------------------------------------------+ | | Sent to a player to inform her that an invitee confirmed | | | the invitation | | +----------------+------------------------------------------+ |``INVITEE_ACCEPTED`` | action_key | ``INVITEE_ACCEPTED_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``INVITEE_ACCEPTED`` | | +----------------+------------------------------------------+ | | localized_args | name of the invitee who accepted | +-------------------------+----------------+------------------------------------------+ | | Sent to a player to inform her that an invitee denied the | | | invitation | | +----------------+------------------------------------------+ |``INVITEE_DECLINED`` | action_key | ``INVITEE_DECLINED_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``INVITEE_DECLINED`` | | +----------------+------------------------------------------+ | | localized_args | name of the invitee who declined | +-------------------------+----------------+------------------------------------------+ | | Sent to a player to inform her that the game ended. When | | | all players saw the outcome, the game can be deleted | | +----------------+------------------------------------------+ |``GAME_OVER`` | action_key | ``GAME_OVER_ACTION`` | | +----------------+------------------------------------------+ | | localized_key | ``GAME_OVER`` | | +----------------+------------------------------------------+ | | localized_args | *none* | +-------------------------+----------------+------------------------------------------+