static funapi.Matchmaking.Server.MatchState CheckMatchRequirements(funapi.Matchmaking.Match match)
        {
            //
            // 매치 완료 조건 검사 핸들러 함수입니다.
            //

            long total_players_for_match = MatchmakingType.GetNumberOfMaxPlayers(match.MatchType);

            // 총 플레이어 수가 매치 완료 조건에 부합하는 지 검사합니다.
            if (match.Players.Count != total_players_for_match)
            {
                // 아직 더 많은 플레이어가 필요합니다.
                Log.Info("Waiting for more players: match_id={0}, match_type={1}, total_players_for_match={2}, current players={3}",
                         match.MatchId, match.MatchType, total_players_for_match, match.Players.Count);

                return(funapi.Matchmaking.Server.MatchState.kMatchNeedMorePlayer);
            }

            Log.Info("Matchmaking is done: match_id={0}, match_type={1}, total_players_for_match={2}, current players={3}",
                     match.MatchId, match.MatchType, total_players_for_match, match.Players.Count);

            // 매치메이킹이 끝났으니 이 정보를 토대로 데디케이티드 서버 생성을 요청합니다.
            DedicatedServerHelper.SpawnDedicatedServer(match);
            return(funapi.Matchmaking.Server.MatchState.kMatchComplete);
        }
        public static void ProcessSpawnOrMatchmaking(
            Session session,
            JObject message,
            SessionResponse.SessionResponseHandler handler)
        {
            Log.Assert(session != null);
            // 메시지 핸들러 함수는 세션 ID 를 이벤트 태그로 하는 이벤트 위에서 실행합니다.
            // 아래는 이 함수를 메시지 핸들러 위에서 실행하게 강제하게 검사하는 식입니다.
            // 세션 ID 별로 이 함수를 직렬화하지 않으면 동시에 서로 다른 곳에서
            // 이 세션에 접근할 수 있습니다.
            Log.Assert(Event.GetCurrentEventTag() == session.Id);

            //
            // 매치메이킹 + 데디케이티드 서버 스폰 요청 예제
            //
            // 유저 2명, 4명, 6명을 대상으로 새로운 데디케이티드 서버 프로세스를 생성합니다.
            //
            // 클라이언트는 다음 메시지 형태로 매치메이킹을 요청합니다.
            // {
            //   // Facebook ID 또는 구글+ ID 등 고유한 ID 를 사용해야 합니다.
            //   "account_id": "id",
            //   // 매치 타입, 이 파일에 있는 MatchType 정의 참조
            //   "match_type": 1
            //   "user_data": {
            //      "level": 70,
            //      "ranking_score": 1500,
            //      ...
            //   },
            // }

            if (message[kAccountId] == null || message[kAccountId].Type != JTokenType.String ||
                message[kMatchType] == null || message[kMatchType].Type != JTokenType.Integer ||
                message[kUserData] == null || message[kUserData].Type != JTokenType.Object)
            {
                Log.Error("Missing required fields: '{0}' / '{1}' / '{2}': session_id={3}, message={4}",
                          kAccountId, kMatchType, kUserData, session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Missing required fields.",
                                            new JObject()));
                return;
            }

            //
            // 매치메이킹 요청 인자 설정 가이드
            //

            // 1. 매치 타입
            // 매치 타입을 식별하는 용도로 사용합니다. 정해진 값이 없으므로
            // 서버에서 정의한 매치 타입과 일치하는지 확인할 필요가 있습니다.
            long match_type = message[kMatchType].Value <long>();

            if (!MatchmakingType.IsValidMatchType(match_type))
            {
                Log.Error("Invalid match_type: session_id={0}, message={1}", session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Invalid arguments.", new JObject()));
                return;
            }

            // 2. 계정(account) ID
            // AccountManager 로 로그인한 계정 ID 를 입력합니다.
            string account_id = message[kAccountId].Value <string>();

            // 요청한 계정과 로그인 중인 세션이 일치하는 지 검사합니다.
            // 이 검사를 통해 다른 유저 ID 로 매칭 요청하는 것을 방지할 수 있습니다.
            if (AccountManager.FindLocalSession(account_id).Id != session.Id)
            {
                Log.Info("ProcessMatchmaking denied: bad request: session_id={0}, message={1}",
                         session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Access denied for this account.", new JObject()));
                return;
            }

            // 3. user_data, 매치메이킹 및 생성한 데디케이티드 서버 안에서 사용할
            // 플레이어의 데이터 입니다. 서버는 클라이언트에서 보낸 user_data 를 복사한 후
            // 요청 시간을 추가합니다. 따라서 매치메이킹 시 사용할 데이터는 최종적으로
            // 다음과 같습니다.
            //   "user_data": {
            //      "level": 70,
            //      "mmr_score": 1500,
            //      "req_time": 1544167339,
            //      ...
            //   },
            JObject user_data = message[kUserData].ToJObject();

            if (user_data[MatchmakingType.kMatchLevel] == null ||
                user_data[MatchmakingType.kMatchLevel].Type != JTokenType.Integer ||
                user_data[MatchmakingType.kMMRScore] == null ||
                user_data[MatchmakingType.kMMRScore].Type != JTokenType.Integer)
            {
                // 매치메이킹 요청에 필요한 인자가 부족합니다.
                Log.Error("Missing required fields: session_id={0}, message={1}", session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Missing required fields.", new JObject()));
                return;
            }

            // 매치메이킹 없이 스폰을 진행하는 타입이면, 즉시 스폰을 시작합니다.
            // 매치메이킹은 2명 이상인 경우에만 동작한다는 점에 주의해주세요.
            // 1명이 매치메이킹 큐에 들어가면 매치메이킹 콜백을 호출하지 않습니다.
            // 연습 게임 또는 데디케이티드 서버 내부에서 사람을 채운 후 게임을 시작할 때
            // 이러한 방식을 사용할 수 있습니다.
            if (match_type == (long)MatchmakingType.MatchType.kNoMatching)
            {
                SpawnOrSendUser(account_id, user_data, match_type);
                return;
            }

            // 4. OnMatchCompleted 콜백
            // 매칭을 종료했을 때 결과를 받을 콜백 함수를 지정합니다.
            // 매치 결과는 콜백 함수의 funapi.Matchmaking.Client.MatchResult 타입으로
            // 확인할 수 있습니다.

            // 5. 서버 선택 방법
            //    - kRandom: 서버를 랜덤하게 선택합니다.
            //    - kMostNumberOfPlayers; 사람이 가장 많은 서버에서 매치를 수행합니다.
            //    - kLeastNumberOfPlayers; 사람이 가장 적은 서버에서 매치를 수행합니다.
            funapi.Matchmaking.Client.TargetServerSelection target_selection =
                funapi.Matchmaking.Client.TargetServerSelection.kRandom;

            // 6. OnMatchProgressUpdated 콜백
            // 매치 상태를 업데이트 할 때마다 결과를 받을 콜백 함수를 지정합니다.
            // 타임 아웃 (기본 값: default(TimeSpan))
            TimeSpan timeout = default(TimeSpan);

            // 이 세션에서 요청한 매치 타입을 기록해둡니다.
            LogMatchHistory(session, account_id, match_type);

            Log.Info("Requesting a matchmaking: session_id={0}, account_id={1}, match_type={2}, user_data={3}",
                     session.Id, account_id, match_type, user_data.ToString());

            funapi.Matchmaking.Client.MatchCallback match_cb = new funapi.Matchmaking.Client.MatchCallback(
                (string player_id, funapi.Matchmaking.Match match, funapi.Matchmaking.MatchResult result) => {
                OnMatchCompleted(player_id, match, result, session, handler);
            });
            funapi.Matchmaking.Client.Start2(
                match_type, account_id, user_data,
                match_cb,
                target_selection,
                OnMatchProgressUpdated,
                timeout);
        }
        public static void CancelMatchmaking(
            Session session, JObject message, SessionResponse.SessionResponseHandler handler)
        {
            // 메시지 핸들러 함수는 세션 ID 를 이벤트 태그로 하는 이벤트 위에서 실행합니다.
            // 아래는 이 함수를 메시지 핸들러 위에서 실행하게 강제하게 검사하는 식입니다.
            // 세션 ID 별로 이 함수를 직렬화하지 않으면 동시에 서로 다른 곳에서
            // 이 세션에 접근할 수 있습니다.
            Log.Assert(Event.GetCurrentEventTag() == session.Id);

            // 클라이언트는 다음 메시지 형태로 매치메이킹 취소를 요청합니다.
            // {
            //   "account_id": "id",
            //   "match_type": 1
            // }

            if (message[kAccountId] == null || message[kAccountId].Type != JTokenType.String ||
                message[kMatchType] == null || message[kMatchType].Type != JTokenType.Integer)
            {
                Log.Error("Missing required fields: '{0}' / '{1}': session_id={2}, message={3}",
                          kAccountId, kMatchType, session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Missing required fields.", new JObject()));
                return;
            }

            // 매치 타입
            long match_type = message[kMatchType].Value <long>();

            if (!MatchmakingType.IsValidMatchType(match_type))
            {
                Log.Error("Invalid match_type: session_id={0}, message={1}", session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Invalid arguments.", new JObject()));
                return;
            }

            if (match_type == (long)MatchmakingType.MatchType.kNoMatching)
            {
                // 매치메이킹 기능을 쓰지 않으므로 취소 처리한다.
                handler(SessionResponse.ResponseResult.OK,
                        new SessionResponse(session, 200, "OK.", new JObject()));
            }

            // 계정
            string account_id = message[kAccountId].Value <string>();

            // 요청한 계정과 로그인 중인 세션이 일치하는 지 검사합니다.
            // 이 검사를 통해 다른 유저 ID 로 매칭을 취소하는 행위를 방지할 수 있습니다.
            if (AccountManager.FindLocalSession(account_id).Id != session.Id)
            {
                Log.Info("CancelMatchmaking denied: bad request: session_id={0}, message={1}",
                         session.Id, message.ToString());
                handler(SessionResponse.ResponseResult.FAILED,
                        new SessionResponse(session, 400, "Access denied for this account.", new JObject()));
                return;
            }

            funapi.Matchmaking.Client.CancelCallback cancel_callback =
                new funapi.Matchmaking.Client.CancelCallback(
                    (string account_id2, funapi.Matchmaking.CancelResult result2) => {
                if (result2 != funapi.Matchmaking.CancelResult.kSuccess)
                {
                    // kCRNoRequest (요청하지 않은 매치) 또는
                    // kCRError (엔진 내부 에러)가 올 수 있습니다.
                    handler(SessionResponse.ResponseResult.FAILED,
                            new SessionResponse(session, 500, "Internal server error.",
                                                new JObject()));
                }
                else
                {
                    handler(SessionResponse.ResponseResult.OK,
                            new SessionResponse(session, 200, "OK.", new JObject()));
                }
            });
            Log.Info("Canceling matchmaking: session_id={0}, account_id={1}, match_type={2}",
                     session.Id, account_id, match_type);

            funapi.Matchmaking.Client.Cancel(match_type, account_id, cancel_callback);
        }
        public static void SendUser(
            long match_type,
            string account_id,
            JObject user_data,
            DedicatedServerHelper.SendUserCallback send_callback)
        {
            Log.Assert(MatchmakingType.IsValidMatchType(match_type));

            bool    found = false;
            Guid    target_match_id;
            JObject target_match_data = new JObject();

            // 매치 타입과 일치하는 플레이어 수를 선택합니다.
            long total_players_for_match = MatchmakingType.GetNumberOfMaxPlayers(match_type);

            do
            {
                lock (match_lock)
                {
                    // 현재 활성화된 매치를 검사하여 플레이어가 부족한 서버를 찾습니다.
                    // 순서는 UUID 를 따르므로 별다른 우선 순위가 없습니다.
                    // 조금 더 공정한 절차가 필요하다면 MyMatchInfo 에 모든 플레이어 없이 게임을 진행한
                    // 시간을 기록한 후, 이를 기준으로 우선순위를 부여할 수 있습니다.
                    //
                    // 1. 먼저 매치 타입으로 매치 ID 맵을 가져옵니다.
                    if (!match_type_map.ContainsKey(match_type))
                    {
                        break;
                    }

                    // 2. ID 에 해당하는 매치 목록을 순회하면서 필요한 플레이어보다 부족한
                    // 서버를 찾습니다.
                    foreach (Guid match_id in match_type_map[match_type])
                    {
                        Log.Assert(match_map.ContainsKey(match_id));
                        MyMatchInfo info = match_map[match_id];
                        if (info.players.Count < total_players_for_match)
                        {
                            // 서버를 찾았습니다.
                            target_match_id   = info.match_id;
                            target_match_data = info.match_data;
                            found             = true;
                            break;
                        }
                    }
                }
            }while (false);

            if (!found)
            {
                // 모든 서버가 매치에 필요한 플레이어를 확보했거나 서버가 없습니다.
                // 더 이상 진행할 수 없습니다.
                Log.Info("There's no available server to send user");
                send_callback(false);
            }
            else
            {
                // 난입을 요청합니다.
                List <string> account_ids = new List <string> {
                    account_id
                };
                List <JObject> user_data_list = new List <JObject> {
                    user_data
                };

                DedicatedServerManager.SendCallback send_cb =
                    new DedicatedServerManager.SendCallback(
                        (Guid match_id2, List <string> users2, bool success2) => {
                    OnUserSent(match_id2, users2, success2, target_match_data, match_type, send_callback);
                });
                DedicatedServerManager.SendUsers(
                    target_match_id, target_match_data, account_ids, user_data_list, send_cb);
            }
        }