public static void Install(SessionResponse.SessionResponseHandler handler) { response_handler = handler; DedicatedServerManager.RegisterMatchResultCallback(OnMatchResultPosted); DedicatedServerManager.RegisterUserEnteredCallback(OnJoinedCallbackPosted); DedicatedServerManager.RegisterUserLeftCallback(OnLeftCallbackPosted); DedicatedServerManager.RegisterCustomCallback(OnCustomCallbackPosted); }
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); }
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())); }
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); }
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); }
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); }
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)); }
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); }