/// <summary>
        ///     Initialize a new MatchmakingRequest object
        /// </summary>
        /// <param name="client">
        ///     An already existing MatchmakingClient to use as the client for the request
        /// </param>
        /// <param name="request">
        ///     The ticket data to send with the matchmaking request.
        ///     Data will be copied internally on construction and treated as immutable.
        /// </param>
        /// <param name="timeoutMs">
        ///     The amount of time to wait (in ms) before aborting an incomplete MatchmakingRequest after it has been sent.
        ///     Match requests that time out on the client side will immediately be set to completed and stop listening for a match
        ///     assignment.
        /// </param>
        public MatchmakingRequest(MatchmakingClient client, CreateTicketRequest request, uint timeoutMs = 0)
        {
            m_Client = client
                       ?? throw new ArgumentNullException(nameof(client), logPre + $"Matchmaking {nameof(client)} must not be null");

            if (request == null)
            {
                throw new ArgumentNullException(nameof(request), logPre + $"{nameof(request)} must be a non-null, valid {nameof(CreateTicketRequest)} object");
            }

            // Try to immediately create and store the protobuf version of the CreateTicketRequest
            //  This allows us to fail fast, and also copies the data to prevent it from being mutable
            //  This may cause exceptions inside the protobuf code, which is fine since we're in the constructor
            var createTicketRequest = new Protobuf.CreateTicketRequest();


            string key = nameof(QosTicketInfo).ToLower();

            if (request.Properties != null && !request.Properties.ContainsKey(key))
            {
                QosTicketInfo results = QosConnector.Instance.Execute();
                if (results?.QosResults?.Count > 0)
                {
                    request.Properties = request.Properties ?? new Dictionary <string, string>();
                    request.Properties.Add(key, JsonUtility.ToJson(results));
                }
            }

            // Only set properties if not null
            // Request properties have to be massaged to be protobuf ByteString compatible
            if (request.Properties != null)
            {
                foreach (var kvp in request.Properties)
                {
                    var keyToLower = kvp.Key.ToLower();

#if UNITY_EDITOR || DEVELOPMENT_BUILD
                    if (!kvp.Key.Equals(keyToLower))
                    {
                        Debug.LogWarning(logPre + $"Ticket property with key {kvp.Key} must be all lowercase; changing in-place.");
                    }
#endif
                    createTicketRequest.Properties.Add(keyToLower, ByteString.CopyFrom(Encoding.UTF8.GetBytes(kvp.Value)));
                }
            }

            // Only add attributes if they exist
            if (request?.Attributes?.Count > 0)
            {
                createTicketRequest.Attributes.Add(request.Attributes);
            }

            m_CreateTicketRequest = createTicketRequest;

            State = MatchmakingRequestState.NotStarted;

            m_MatchRequestTimeoutMs = timeoutMs;
        }
        /// <summary>
        ///     <para>Dispose the MatchmakingRequest and release resources.  All in-flight web requests will be disposed regardless of state.
        ///     Object methods will no-op after disposal.</para>
        ///     <para>Best practice is to ensure that Cancel() has been called and completed before calling Dispose().</para>
        /// </summary>
        public void Dispose()
        {
            if (!m_Disposed)
            {
                // Dispose any leftover web requests; most of these should be disposed of elsewhere
                // This is just a catch-all in case we Dispose() in the middle of a request

                var createInFlight = false;
                var getInFlight    = false;
                var deleteInFlight = false;

                if (m_CreateTicketAsyncOperation != null)
                {
                    createInFlight = MatchmakingClient.IsWebRequestSent(m_CreateTicketAsyncOperation);
                    m_CreateTicketAsyncOperation.completed -= OnCreateTicketAsyncCompleted;
                    m_CreateTicketAsyncOperation.webRequest?.Dispose();
                    m_CreateTicketAsyncOperation = null;
                }

                if (m_GetTicketAsyncOperation != null)
                {
                    getInFlight = State == MatchmakingRequestState.Polling;
                    m_GetTicketAsyncOperation.completed -= OnGetTicketAsyncCompleted;
                    m_GetTicketAsyncOperation.webRequest?.Dispose();
                    m_GetTicketAsyncOperation = null;
                }

                if (m_DeleteTicketAsyncOperation != null)
                {
                    deleteInFlight = !m_DeleteTicketAsyncOperation.isDone;
                    m_DeleteTicketAsyncOperation.completed -= OnDeleteTicketAsyncCompleted;
                    m_DeleteTicketAsyncOperation.webRequest?.Dispose();
                    m_DeleteTicketAsyncOperation = null;
                }

                if ((createInFlight || getInFlight) && State != MatchmakingRequestState.Canceled)
                {
                    Debug.LogWarning(logPre +
                                     $"{nameof(MatchmakingRequest)} was terminated without being deleted." +
                                     "  This may cause ghost tickets in the matchmaker.");
                }

                if (deleteInFlight)
                {
                    Debug.LogWarning(logPre +
                                     $"{nameof(MatchmakingRequest)} was terminated while a Delete request was in flight" +
                                     "; Delete may not be processed by the matchmaker.");
                }

                if (!IsDone)
                {
                    State = MatchmakingRequestState.Disposed;
                    SetTerminalState();
                }

                m_Disposed = true;
            }
        }
        /// <summary>
        ///     <para>Cancel searching for a match, and de-register this request with the matchmaking service.</para>
        ///     <para>May only be called after Start() has been called.</para>
        ///     <para>
        ///         If a match assignment is found after Cancel() is called (but before it is processed by the matchmaker),
        ///         the request will still be canceled and the success handler will not trigger.
        ///     </para>
        ///     <para>Must be called from the main thread.</para>
        /// </summary>
        public IEnumerator CancelRequest()
        {
            // UnityWebRequest calls can only be made from the Main thread
            ThrowExceptionIfNotOnMainThread();

            if (!InState(MatchmakingRequestState.NotStarted, MatchmakingRequestState.Creating, MatchmakingRequestState.Polling))
            {
                Debug.LogError(logPre + $"Trying to call {nameof(CancelRequest)} in an invalid state ({State})");
                return(null);
            }

            Debug.Log(logPre + "Canceling matchmaking ticket");

            switch (State)
            {
            // If we're not started, we can just set the object state to cancelled immediately
            case MatchmakingRequestState.NotStarted:
                Debug.Log(logPre + $"Matchmaking aborted before {nameof(SendRequest)} was called");
                HandleDelete();
                break;

            // If we're in Polling and have a ticket ID, try to do a delete
            case MatchmakingRequestState.Polling:
                DeleteMatchRequestTicket();
                break;

            // If we're in Creating, try to cancel the Create request if it hasn't actually been sent yet
            //  If it has, then queue up a cancellation request for once we have a valid ticket id
            case MatchmakingRequestState.Creating:
                State = MatchmakingRequestState.Canceling;

                if (!MatchmakingClient.IsWebRequestSent(m_CreateTicketAsyncOperation))
                {
                    m_CreateTicketAsyncOperation?.webRequest?.Abort();
                    Debug.LogWarning(logPre + "Attempting to abort match request while Create call is still pending");
                }
                else
                {
                    Debug.LogWarning(logPre + "Match request does not have a TicketId yet; queuing cancel request");
                }

                break;

            default:
                throw new InvalidOperationException(logPre + $"Trying to call {nameof(CancelRequest)} in an invalid state ({State})");
            }

            return(WaitUntilCompleted);
        }
Ejemplo n.º 4
0
 internal MatchmakingController(string endpoint)
 {
     m_Client = new MatchmakingClient(endpoint);
 }
        // Callback for "Delete Ticket" UnityWebRequestAsyncOperation completion
        void OnDeleteTicketAsyncCompleted(AsyncOperation obj)
        {
            if (!(obj is UnityWebRequestAsyncOperation deleteTicketOp))
            {
                throw new ArgumentException(logPre + "Wrong AsyncOperation type in callback.");
            }

            // Every return statement in here will trigger finally{} cleanup
            try
            {
                // Short-circuit if we're in a terminal state or have no deletion registered
                if (IsDone || m_DeleteTicketAsyncOperation == null)
                {
                    return;
                }

                if (deleteTicketOp != m_DeleteTicketAsyncOperation)
                {
                    throw new InvalidOperationException(logPre + $"Wrong operation object received by {nameof(OnDeleteTicketAsyncCompleted)}.");
                }

                // If the request here was a success, we have successfully deleted a ticket
                if (MatchmakingClient.IsWebRequestFailed(deleteTicketOp.webRequest))
                {
                    Debug.LogError(logPre + $"Error deleting matchmaking ticket: {deleteTicketOp.webRequest.error}");
                    return;
                }

                // Everything after this is just to detect if there was additional information
                //  provided in the body of the DELETE response

                // If no body, we're done
                if (deleteTicketOp.webRequest?.downloadHandler?.data?.Length == 0)
                {
                    return;
                }

                // Try to parse body
                if (!MatchmakingClient.TryParseDeleteTicketResponse(deleteTicketOp.webRequest, out var result))
                {
                    // Delete succeeded but additional information could not be parsed; not a fatal error
                    Debug.LogError(logPre + $"Error parsing DELETE information for ticket");
                    return;
                }

                // Handle the status code returned inside the DELETE body
                if (result?.Status != null && result.Status.Code != 0)
                {
                    Debug.LogError(logPre + $"Error deleting matchmaking ticket. Code {result.Status.Code}: {result.Status.Message}");
                    return;
                }

                // Success!
                Debug.Log(logPre + $"Matchmaking ticket deleted successfully");
            }
            catch (Exception e)
            {
                Debug.LogError(logPre + $"Error deleting matchmaking ticket: {e.Message}");
            }
            finally
            {
                // Allow the operation to get garbage collected
                if (deleteTicketOp == m_DeleteTicketAsyncOperation)
                {
                    m_DeleteTicketAsyncOperation = null;
                }

                deleteTicketOp.webRequest?.Dispose();

                // Handle failed deletion cases as a successful cancel
                HandleDelete();
            }
        }
        // Callback for "Get Ticket" UnityWebRequestAsyncOperation completion
        void OnGetTicketAsyncCompleted(AsyncOperation obj)
        {
            if (!(obj is UnityWebRequestAsyncOperation getTicketOp))
            {
                throw new ArgumentException(logPre + "Wrong AsyncOperation type in callback.");
            }

            Protobuf.GetTicketResponse getResponse;

            // Every return statement in here will trigger finally{} cleanup
            try
            {
                // Short-circuit if we're in a terminal state or have no Get call registered
                if (IsDone || m_GetTicketAsyncOperation == null)
                {
                    return;
                }

                if (getTicketOp != m_GetTicketAsyncOperation)
                {
                    throw new InvalidOperationException(logPre + $"Wrong operation object received by {nameof(OnGetTicketAsyncCompleted)}.");
                }

                // No need to log on an expected abort state
                if (State == MatchmakingRequestState.Canceling || MatchmakingClient.IsWebRequestAborted(getTicketOp.webRequest))
                {
                    return;
                }

                // This helps to ensure that GET calls aborted during a delete are not processed
                if (State != MatchmakingRequestState.Polling)
                {
                    Debug.LogWarning(logPre + "Ignoring Matchmaking Get Ticket response while not in polling state.");
                    return;
                }

                if (MatchmakingClient.IsWebRequestFailed(getTicketOp.webRequest))
                {
                    HandleError($"Error getting matchmaking ticket: {getTicketOp.webRequest.error}");
                    return;
                }

                // Parse the body of the response - only try if we actually have a body
                // Successful responses w/o bodies are not considered failures
                if (getTicketOp.webRequest?.downloadHandler?.data?.Length == 0)
                {
                    return;
                }

                // Try to parse body
                if (!MatchmakingClient.TryParseGetTicketResponse(getTicketOp.webRequest, out getResponse))
                {
                    // Body was present but we couldn't parse it; this is probably a fatal error
                    HandleError($"Error parsing GET response for ticket");
                    return;
                }
            }
            catch (Exception e)
            {
                HandleError($"Error getting information for ticket: {e.Message}");
                return;
            }
            finally
            {
                // Allow the operation to get garbage collected
                if (getTicketOp == m_GetTicketAsyncOperation)
                {
                    m_GetTicketAsyncOperation = null;
                    m_LastPollTime            = Time.unscaledTime;
                }

                getTicketOp.webRequest?.Dispose();
            }

            // Consume the response

            if (getResponse.Status != null)
            {
                HandleError($"Error getting matchmaking ticket. Code {getResponse.Status.Code}: {getResponse.Status.Message}");
                return;
            }

            // Check to see if this GET call has an assignment
            //  If assignment is null, ticket hasn't completed matchmaking yet
            if (getResponse.Assignment != null)
            {
                var errorExists      = !string.IsNullOrEmpty(getResponse.Assignment.Error);
                var connectionExists = !string.IsNullOrEmpty(getResponse.Assignment.Connection);
                var propertiesExists = !string.IsNullOrEmpty(getResponse.Assignment.Properties);

                // Note that assignment can have null fields
                Assignment = new Assignment(getResponse.Assignment.Connection, getResponse.Assignment.Error, getResponse.Assignment.Properties);

                // Set to ErrorRequest state if assignment has no real data
                if (!errorExists && !connectionExists && !propertiesExists)
                {
                    HandleError("Error getting matchmaking ticket: assignment returned by service could not be processed");
                    return;
                }

                // Set to ErrorAssignment state if parsed assignment object contains an error entry
                if (errorExists)
                {
                    State = MatchmakingRequestState.ErrorAssignment;
                    Debug.LogError(logPre + $"Matchmaking completed with Assignment error: {Assignment.Error}");
                    SetTerminalState();
                    return;
                }

                // No error and valid connection and/or properties - set to AssignmentReceived
                State = MatchmakingRequestState.AssignmentReceived;
                Debug.Log(logPre + $"Matchmaking completed successfully; connection information received.");
                SetTerminalState();
            }
        }
        // Callback for "Create Ticket" UnityWebRequestAsyncOperation completion
        void OnCreateTicketAsyncCompleted(AsyncOperation obj)
        {
            if (!(obj is UnityWebRequestAsyncOperation createTicketOp))
            {
                throw new ArgumentException(logPre + "Wrong AsyncOperation type in callback.");
            }

            Protobuf.CreateTicketResponse result;

            // Every return statement in here will trigger finally{} cleanup
            try
            {
                // Short-circuit if we're in a terminal state or have no creation registered
                if (IsDone || m_CreateTicketAsyncOperation == null)
                {
                    return;
                }

                if (createTicketOp != m_CreateTicketAsyncOperation)
                {
                    throw new InvalidOperationException(logPre + $"Wrong operation object received by {nameof(OnCreateTicketAsyncCompleted)}.");
                }

                if (State != MatchmakingRequestState.Creating && State != MatchmakingRequestState.Canceling)
                {
                    Debug.LogWarning(logPre + "Ignoring Matchmaking Create Ticket response while not in creating state.");
                    return;
                }

                if (MatchmakingClient.IsWebRequestFailed(createTicketOp.webRequest))
                {
                    // If we tried to cancel while the Create call was being constructed or in flight,
                    //   the Create request will be set to an aborted state (counts as an "IsFailed" state)
                    if (State == MatchmakingRequestState.Canceling && MatchmakingClient.IsWebRequestAborted(createTicketOp.webRequest))
                    {
                        //if (MatchmakingClient.IsWebRequestSent(createTicketOp))
                        //Debug.LogWarning(logPre + "Matchmaking call was aborted, but ticket may have been created on the service end.");

                        Debug.Log(logPre + "Matchmaking was aborted during ticket creation");

                        HandleDelete();
                        return;
                    }

                    HandleError($"Error creating matchmaking ticket: {createTicketOp.webRequest.error}");
                    return;
                }

                // Parse the body of the response - only try if we actually have a body
                if (!MatchmakingClient.TryParseCreateTicketResponse(createTicketOp.webRequest, out result))
                {
                    // Could not parse the CREATE response; this is a fatal error
                    HandleError($"Error parsing CREATE response for ticket; could not get Ticket ID from service");
                }
            }
            catch (Exception e)
            {
                HandleError($"Error creating matchmaking ticket: {e.Message}");
                return;
            }
            finally
            {
                // Allow the operation to get garbage collected
                if (createTicketOp == m_CreateTicketAsyncOperation)
                {
                    m_CreateTicketAsyncOperation = null;
                }

                createTicketOp.webRequest?.Dispose();
            }

            // Try to consume the parsed response

            if (result == null)
            {
                HandleError("Error creating matchmaking ticket.");
                return;
            }

            if (result.Status != null)
            {
                HandleError($"Error creating matchmaking ticket. Code {result.Status.Code}: {result.Status.Message}");
                return;
            }

            if (string.IsNullOrEmpty(result.Id))
            {
                HandleError("Error creating matchmaking ticket. Id not set.");
                return;
            }

            // We were able to parse the Ticket Id
            TicketId = result.Id;

            // If a cancellation request is queued, send the cancellation instead of polling for assignment
            if (State == MatchmakingRequestState.Canceling)
            {
                DeleteMatchRequestTicket();
                return;
            }

            // Start polling for the assignment for the assigned Ticket Id
            Debug.Log(logPre + $"Ticket ID received; polling matchmaking for assignment");
            StartGetTicketRequest(TicketId);
        }