public static void Install(SessionResponse.SessionResponseHandler handler)
        {
            response_handler = handler;

            DedicatedServerManager.RegisterMatchResultCallback(OnMatchResultPosted);
            DedicatedServerManager.RegisterUserEnteredCallback(OnJoinedCallbackPosted);
            DedicatedServerManager.RegisterUserLeftCallback(OnLeftCallbackPosted);
            DedicatedServerManager.RegisterCustomCallback(OnCustomCallbackPosted);
        }
Beispiel #2
0
        public static void OnLogoutRequest(Session session, JObject message)
        {
            Guid    session_id      = session.Id;
            JObject session_context = session.Context;

            Log.Info("OnLogoutRequest: session={0}, context={1}, message={2}",
                     session_id, session_context.ToString(), message.ToString());

            // 이후 과정은 authentication_helper.cs 를 참고하세요.
            SessionResponse.SessionResponseHandler on_logout =
                new SessionResponse.SessionResponseHandler(
                    (SessionResponse.ResponseResult error, SessionResponse response) => {
                OnLogout(error, response, false /* not caused by session close */);
            });
            AuthenticationHelper.Logout(session, on_logout);
        }
Beispiel #3
0
        public static void OnLoggedOut(
            string account_id, Session session, bool logged_out,
            SessionResponse.SessionResponseHandler logout_handler)
        {
            if (!logged_out)
            {
                // logged_out=false 면 로그아웃하지 않은 사용자가 로그아웃을 시도한 경우입니다.
                Log.Info("Not logged in: session_id={0}, account_id={1}", session.Id, account_id);
                logout_handler(SessionResponse.ResponseResult.FAILED,
                               new SessionResponse(session, 400, "The user did not login.", new JObject()));
                return;
            }

            Log.Info("Logged out: session_id={0}, account_id={1}", session.Id, account_id);

            logout_handler(SessionResponse.ResponseResult.OK, new SessionResponse(session, 200, "OK", new JObject()));
        }
Beispiel #4
0
        public static void OnCancelMatchRequest(Session session, JObject message)
        {
            Guid    session_id      = session.Id;
            JObject session_context = session.Context;

            Log.Info("OnCancelMatchRequest: session={0}, context={1}, message={2}",
                     session_id, session_context.ToString(), message.ToString());

            SessionResponse.SessionResponseHandler response_handler =
                new SessionResponse.SessionResponseHandler(
                    (SessionResponse.ResponseResult error, SessionResponse response) => {
                SendMyMessage(response.session, kCancelMatchMessage,
                              response.error_code, response.error_message, response.data);
            });

            // 이후 과정은 matchmaking_helper.cs 를 참고하세요.
            MatchmakingHelper.CancelMatchmaking(session, message, response_handler);
        }
Beispiel #5
0
        public static void OnLoginRequest(Session session, JObject message)
        {
            Guid    session_id      = session.Id;
            JObject session_context = session.Context;

            Log.Info("OnLoginRequest: session={0}, context={1}, message={2}",
                     session_id, session_context.ToString(), message.ToString());

            // 로그인을 시도합니다.
            // OnLogin 함수는 이 세션이 로그인을 성공/실패한 결과를 보낼 때 사용합니다.
            // OnLogout 함수는 중복 로그인 처리(로그인한 다른 세션을 로그아웃) 시 사용합니다.
            // 이후 과정은 authentication_helper.cs 를 참고하세요.
            SessionResponse.SessionResponseHandler on_logout =
                new SessionResponse.SessionResponseHandler(
                    (SessionResponse.ResponseResult error, SessionResponse response) => {
                OnLogout(error, response, false /* not caused by session close */);
            });
            AuthenticationHelper.Login(session, message, OnLogin, on_logout);
        }
Beispiel #6
0
        public static void Logout(Session session, SessionResponse.SessionResponseHandler logout_handler)
        {
            Log.Assert(session != null);
            Log.Assert(logout_handler != null);

            string account_id = AccountManager.FindLocalAccount(session);

            if (String.IsNullOrEmpty(account_id))
            {
                // 이 세션으로 로그인한 적이 없습니다.
                Log.Info("This session was not used for login: session_id={0}", session.Id);
                return;
            }

            // 분산 환경이라면 SetLoggedOutGlobalAsync() 함수를 사용해주세요.
            AccountManager.LogoutCallback logout_cb = new AccountManager.LogoutCallback(
                (string account_id2, Session session2, bool logged_out2) => {
                OnLoggedOut(account_id2, session2, logged_out2, logout_handler);
            });

            AccountManager.SetLoggedOutAsync(account_id, logout_cb);
        }
Beispiel #7
0
        public static void OnLoggedIn(
            string account_id, Session session, bool logged_in, string platform, long try_count,
            SessionResponse.SessionResponseHandler login_handler,
            SessionResponse.SessionResponseHandler logout_handler)
        {
            if (!logged_in)
            {
                if (try_count == 0)
                {
                    // 로그인에 실패했습니다. 누군가 먼저 로그인 하거나, 시스템 장애일 수 있습니다.
                    // 처음에는 강제로 로그아웃 후 재시도합니다.
                    Log.Info("Already logged in: session_id={0}, account_id={1}, platform={2}, try_count={3}",
                             session.Id, account_id, platform, try_count);

                    AccountManager.LogoutCallback on_logged_out =
                        new AccountManager.LogoutCallback((string account_id2, Session session_logged_out2, bool logged_out2) => {
                        AccountManager.LoginCallback on_logged_in =
                            new AccountManager.LoginCallback((string account_id3, Session session3, bool logged_in3) => {
                            OnLoggedIn(account_id3, session3, logged_in3, platform, try_count, login_handler, logout_handler);
                        });
                        AccountManager.CheckAndSetLoggedInAsync(account_id2, session, on_logged_in);

                        // 기존 세션에 로그아웃 메시지를 보냅니다.
                        if (logged_out2)
                        {
                            logout_handler(SessionResponse.ResponseResult.OK,
                                           new SessionResponse(session_logged_out2, 200, "Duplicated login", new JObject()));
                        }
                    });

                    // 분산 환경이라면 SetLoggedOutGlobalAsync() 함수를 사용해주세요.
                    AccountManager.SetLoggedOutAsync(account_id, on_logged_out);
                    return;
                }
                else
                {
                    // 로그인을 두 번 이상 실패했습니다.
                    // 만약 SetLoggedOutGlobalAsync() 함수를 사용 중이라면 분산 환경을
                    // 구성하는 Zookeeper / Redis 서비스가 제대로 동작하지 않을 가능성도
                    // 있습니다. 그러나 이 경우 엔진 내부적으로 조치한 후 에러를 출력하기
                    // 때문에 여기서는 클라이언트 처리만 하는 게 좋습니다.
                    Log.Error("Login failed: session_id={0}, account_id={1}, platform={2}, try_count={3}",
                              session.Id, account_id, platform, try_count);
                    login_handler(SessionResponse.ResponseResult.FAILED,
                                  new SessionResponse(session, 500, "Internal server error.", new JObject()));
                    return;
                }
            }              // if (not logged_in)

            // 로그인 성공
            Log.Info("Login succeed: session_id={0}, account_id={1}, platform={2}, try_count={3}",
                     session.Id, account_id, platform, try_count);

            // 클라이언트에게 보낼 응답은 이 곳에 설정합니다.
            JObject response_data = new JObject();

            response_data["key1"] = "value1";
            response_data["key2"] = "value2";
            response_data["key3"] = "value3";

            login_handler(SessionResponse.ResponseResult.OK, new SessionResponse(session, 200, "OK", response_data));
        }
Beispiel #8
0
        public static void Login(
            Session session, JObject message,
            SessionResponse.SessionResponseHandler login_handler,
            SessionResponse.SessionResponseHandler logout_handler)
        {
            Log.Assert(session != null);
            Log.Assert(message != null);
            Log.Assert(login_handler != null);
            Log.Assert(logout_handler != null);

            //
            // 로그인 요청 예제
            //
            // 클라이언트는 다음 메시지 형태로 로그인을 요청해야 합니다.
            // {
            //   // Facebook ID 또는 구글+ ID 등 고유한 ID 를 사용해야 합니다.
            //   "account_id": "id",
            //   "platform": "facebook"
            //   "access_token": "account's access token"
            // }

            // 메시지 안에 필수 파라메터가 있는지 확인합니다.
            if (message[kAccounId] == null || message[kAccounId].Type != JTokenType.String ||
                message[kPlatformName] == null || message[kPlatformName].Type != JTokenType.String)
            {
                Log.Error("The message does not have '{0}' or '{1}': session={2}, message={3}",
                          kAccounId, kPlatformName, session.Id, message.ToString());
                login_handler(SessionResponse.ResponseResult.FAILED,
                              new SessionResponse(session, 400, "Missing required fields.", new JObject()));
                return;
            }

            string account_id = message[kAccounId].Value <string>();
            string platform   = message[kPlatformName].Value <string>();

            if (platform == "facebook")
            {
                // Facebook 플랫폼 사용자의 경우, 올바른 사용자인지 검증합니다.
                if (message[kPlatformAccessToken] == null || message[kPlatformAccessToken].Type != JTokenType.String)
                {
                    Log.Error("The message does not have {0}: session={1}, message={2}",
                              kPlatformAccessToken, session.Id, message.ToString());
                    login_handler(SessionResponse.ResponseResult.FAILED,
                                  new SessionResponse(session, 400, "Missing required fields.", new JObject()));
                    return;
                }

                string access_token = message[kPlatformAccessToken].Value <string>();
                FacebookAuthentication.AuthenticationRequest request =
                    new FacebookAuthentication.AuthenticationRequest(access_token);

                FacebookAuthentication.AuthenticationResponseHandler on_authenticated =
                    new FacebookAuthentication.AuthenticationResponseHandler(
                        (FacebookAuthentication.AuthenticationRequest request2,
                         FacebookAuthentication.AuthenticationResponse response2,
                         bool error) => {
                    if (error)
                    {
                        // Facebook 서버 오류 또는 올바르지 않은 사용자인 경우
                        Log.Warning("Failed to authenticate Facebook account: session={0}, code={1}, message={2}",
                                    session.Id, response2.Error.Code, response2.Error.Message);
                        login_handler(SessionResponse.ResponseResult.FAILED,
                                      new SessionResponse(session, 400, "Missing required fields.", new JObject()));
                        return;
                    }

                    Log.Info("Facebook authentication succeed: session={0}, account_id={1}", session.Id, account_id);

                    // 이 예제에서는 로그인 시도를 기록합니다.
                    long try_count = 0;

                    // 분산 환경이라면 CheckAndSetLoggedInGlobalAsync() 함수를 사용해주세요.
                    AccountManager.LoginCallback on_logged_in =
                        new AccountManager.LoginCallback((string account_id2, Session session2, bool logged_in2) => {
                        OnLoggedIn(account_id2, session2, logged_in2, platform, try_count, login_handler, logout_handler);
                    });
                    AccountManager.CheckAndSetLoggedInAsync(account_id, session, on_logged_in);
                });

                // Facebook 인증을 요청합니다.
                FacebookAuthentication.Authenticate(request, on_authenticated);
            }
            else
            {
                //
                // 로그인 시도
                //
                // 요청한 세션으로 로그인을 시도합니다. 이 서버의 로그인 정책은 로그인을 시도하되,
                // 이미 다른 곳에서 로그인한 경우, 로그아웃 후 재시도합니다.
                long try_count = 0;

                // 분산 환경이라면 CheckAndSetLoggedInGlobalAsync() 함수를 사용해주세요.
                AccountManager.LoginCallback on_logged_in =
                    new AccountManager.LoginCallback((string account_id2, Session session2, bool logged_in2) => {
                    OnLoggedIn(account_id2, session2, logged_in2, platform, try_count, login_handler, logout_handler);
                });
                AccountManager.CheckAndSetLoggedInAsync(account_id, session, on_logged_in);
            }
        }
        static void OnMatchCompleted(string account_id,
                                     funapi.Matchmaking.Match match,
                                     funapi.Matchmaking.MatchResult result,
                                     Session session,
                                     SessionResponse.SessionResponseHandler handler)
        {
            //
            // 매치 결과를 받는 콜백 핸들러입니다. 매치를 요청한 각 플레이어를 대상으로
            // 핸들러를 호출합니다.
            // 매치를 정상적으로 성사한 경우: match_id 로 직렬화 한 이벤트에서 실행합니다.
            // 매치 성사에 실패한 경우: 직렬화하지 않은 이벤트에서 실행합니다.
            //
            // 매치메이킹에 참여하는 사람이 많지 않거나 matchmaking_server_wrapper.cs 에서
            // 정의한 매치 조건이 까다로운 경우, 클라이언트가 매치메이킹을 요청하는 시점과
            // 매치가 성사되는 이 시점의 차가 커질 수 있습니다.
            //
            // 따라서 클라이언트는 매치메이킹 요청을 보낸 후 이 핸들러에서 메시지를 보내기
            // 전까지 다른 행동을 할 수 있도록 만들어야 합니다.
            //

            Log.Info("OnMatchCompleted: account_id={0}, result={1}", account_id, result);

            if (result != funapi.Matchmaking.MatchResult.kSuccess)
            {
                // funapi.Matchmaking.Client.MatchResult 결과에 따라 어떻게 처리할 지 결정합니다.
                if (result == funapi.Matchmaking.MatchResult.kError)
                {
                    // 엔진 내부 에러입니다.
                    // 일반적으로 RPC 서비스를 사용할 수 없을 경우 발생합니다.
                    handler(SessionResponse.ResponseResult.FAILED,
                            new SessionResponse(session, 500, "Internal server error.",
                                                new JObject()));
                }
                else if (result == funapi.Matchmaking.MatchResult.kAlreadyRequested)
                {
                    // 이미 이 account_id 로 매치메이킹 요청을 했습니다. 매치 타입이 다르더라도
                    // ID 가 같다면 실패합니다.
                    handler(SessionResponse.ResponseResult.FAILED,
                            new SessionResponse(session, 400, "Already requested.", new JObject()));
                }
                else if (result == funapi.Matchmaking.MatchResult.kTimeout)
                {
                    // 매치메이킹 요청을 지정한 시간 안에 수행하지 못했습니다.
                    // 더 넓은 범위의 매치 재시도 또는 클라이언트에게 단순 재시도를 요구할 수 있습니다.
                    handler(SessionResponse.ResponseResult.FAILED,
                            new SessionResponse(session, 400, "Timed out.", new JObject()));
                }
                else
                {
                    // 아이펀 엔진에서 추후 MatchResult 를 더 추가할 경우를 대비해
                    // 이곳에 로그를 기록합니다.
                    Log.Warning("not supported error: result={0}", result);
                    handler(SessionResponse.ResponseResult.FAILED,
                            new SessionResponse(session, 500, "Unknown error.", new JObject()));
                }
                return;
            }

            // 매치 결과 ID 입니다.
            Guid match_id = match.MatchId;
            // 매치메이킹 서버에서 정의한 JSON 컨텍스트 입니다.
            // 매치메이킹 요청(StartMatchmaking2) 시 인자로 넣는 context 와는 다른
            // context 라는 점에 주의하세요.
            JObject match_context = match.Context;
            // 매치메이킹 요청 시 지정한 매치 타입입니다.
            long match_type = match.MatchType;
            // 이 매치에 참여한 플레이어 정보입니다.
            string player_ss = "[";

            for (int i = 0; i < match.Players.Count; ++i)
            {
                if (i != 0)
                {
                    player_ss += ", ";
                }
                player_ss += String.Format("account_id={0}, user_data={1}",
                                           match.Players[i].Id,
                                           match.Players[i].Context.ToString());
            }
            player_ss += "]";

            Log.Info("Matchmaking succeed: match_id={0}, match_context={1}, match_type={2}, players={3}",
                     match_id, match_context.ToString(), match_type, player_ss);

            // 매치메이킹에 성공했습니다. 매치메이킹 서버(matchmaking_server_wrapper.cc)에서
            // 매치에 성공한 사람들을 모아 데디케이티드 서버 생성을 시작합니다.
            // (CheckMatchRequirements 함수에서 kMatchComplete 를 반환한 후입니다)
            // 이 시점에서는 단순히 히스토리만 초기화하고 메시지는 보내지 않습니다.
            // (데디케이티드 서버 생성 후 최종적으로 성공 메시지를 보냅니다)

            Event.EventFunction event_fn = new Event.EventFunction(() => {
                ClearMatchHistory(session);
            });
            Event.Invoke(event_fn, session.Id);
        }
        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 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);
        }