前回 で各 UI 要素の配置に関してざっと眺めました。
今回は AuthDialog
要素を見ていきます。
AuthDialog の構成要素
void FAuthDialogs::Create() { if (!ParentDialog) { return; } CreateLoginIDPasswordDialog(); CreateLoginExchangeCodeDialog(); CreateLoginDeviceCodeDialog(); CreateLoginDevAuthDialog(); CreateLoginAccountPortalDialog(); CreateDeviceCodeCancelLoginDialog(); CreateMFALoginDialog(); CreateUserLoggedIn(); SetStartLoginMode(); }
上記のように FAuthDialogs
では、多くの Dialog が生成されています。ですが起動すると、ログイン方法は 3 種類しか提供されていません。
FAuthDialogs::FAuthDialogs(DialogPtr Parent, std::wstring DialogLoginText, FontPtr DialogBoldSmallFont, FontPtr DialogSmallFont, FontPtr DialogTinyFont) : ParentDialog(Parent), LoginText(DialogLoginText), BoldSmallFont(DialogBoldSmallFont), SmallFont(DialogSmallFont), TinyFont(DialogTinyFont), SavedLoginMode(ELoginMode::AccountPortal), NewLoginMode(SavedLoginMode) { LoginMethods = { #ifdef DEV_BUILD FLoginMethodButtonData(L"Password", ELoginMode::IDPassword), FLoginMethodButtonData(L"Exchange", ELoginMode::ExchangeCode), FLoginMethodButtonData(L"Device Code", ELoginMode::DeviceCode), FLoginMethodButtonData(L"Dev Auth", ELoginMode::DevAuth), FLoginMethodButtonData(L"Account Portal", ELoginMode::AccountPortal) #else FLoginMethodButtonData(L"Device Code", ELoginMode::DeviceCode), FLoginMethodButtonData(L"Dev Auth", ELoginMode::DevAuth), FLoginMethodButtonData(L"Account Portal", ELoginMode::AccountPortal) #endif }; }
改めてコンストラクタを見ると、 DEV_BUILD
のコンパイラオプションで LoginMethods
が大きく異なっていることが分かります。
では DEV_BUILD
でビルドしてみます。
すると以下のように UI が変わることが分かります。
DEV_BUILD あり | DEV_BUILD なし |
---|---|
一応ボタンの配置に関するコードも見ておきます。
void FAuthDialogs::UpdateLoginMethodButtons(DialogPtr LoginDialog) { const Vector2 ParentPos = ParentDialog->GetPosition(); const Vector2 ParentSize = ParentDialog->GetSize(); const float PosX = 0.f; const float PosY = 140.f; const float SpX = 5.f; // spacing between each button const float NumLoginMethods = float(LoginMethods.size()); const float SizeX = (ParentDialog->GetSize().x - ((NumLoginMethods + 1) * SpX)) / NumLoginMethods; const Vector2 Size = Vector2(SizeX, 25.f); const float FirstButtonPosX = PosX + SpX; const size_t WidgetIndexOffset = 6; for (size_t MethodIndex = 0; MethodIndex < LoginMethods.size(); ++MethodIndex) { const float ButtonPosX = FirstButtonPosX + (SizeX + SpX) * MethodIndex; WidgetPtr MethodButton = LoginDialog->GetWidget(WidgetIndexOffset + MethodIndex); if (MethodButton) { MethodButton->SetSize(Size); MethodButton->SetPosition(Vector2(ButtonPosX, PosY) + ParentPos); } } }
前回、AuthDialog
は FriendsDialog
を親にして配置していると書きましたが、 ParentDialog
(実際には FriendsDialog) に対してサイズや配置が計算されていることが分かります。
Account Portal の処理を見る
以前の記事 で書きましたが Account Portal は、ブラウザでアカウントとの連携の許可をします。
FAuthDialogs::CreateLoginAccountPortalDialog
まずは Account Portal での UI 要素の作成部分を見ていきます。UI については、以下のようなモードがあります。
ログイン前 | ログイン後 | ログイン後に NEW をクリック |
---|---|---|
void FAuthDialogs::CreateLoginAccountPortalDialog() { LoginAccountPortalDialog = std::make_shared<FDialog>(Vector2(50, 50), Vector2(650, 700), ParentDialog->GetLayer() - 1); float PosX = 20.f; std::shared_ptr<FTextLabelWidget> LoginLabel = std::make_shared<FTextLabelWidget>( Vector2(PosX + 40.f, 10.f), Vector2(170.f, 30.f), LoginAccountPortalDialog->GetLayer() - 1, L"Log in to access " + LoginText + L".", L""); LoginLabel->Create(); LoginLabel->SetFont(BoldSmallFont); std::shared_ptr<FTextLabelWidget> AuthLabel = std::make_shared<FTextLabelWidget>( Vector2(PosX - 50.f, 50.f), Vector2(70.f, 30.f), LoginAccountPortalDialog->GetLayer() - 1, L"Account Portal", L""); AuthLabel->Create(); AuthLabel->SetFont(BoldSmallFont); std::shared_ptr<FTextLabelWidget> AuthLabel2 = std::make_shared<FTextLabelWidget>( Vector2(PosX - 50.f, 70.f), Vector2(180.f, 30.f), LoginAccountPortalDialog->GetLayer() - 1, L"You will be taken to the account portal.", L""); AuthLabel2->Create(); AuthLabel2->SetFont(TinyFont); std::shared_ptr<FTextLabelWidget> AuthLabel3 = std::make_shared<FTextLabelWidget>( Vector2(PosX - 50.f, 70.f), Vector2(100.f, 30.f), LoginDeviceCodeDialog->GetLayer() - 1, L"", L""); AuthLabel3->Create(); AuthLabel3->SetFont(TinyFont); std::shared_ptr<FButtonWidget> AuthButton = std::make_shared<FButtonWidget>( Vector2(PosX, 170.f), Vector2(100.f, 30.f), LoginAccountPortalDialog->GetLayer() - 1, L"LOG IN", assets::DefaultButtonAssets, BoldSmallFont, AuthButtonBackCol); AuthButton->Create(); AuthButton->SetOnPressedCallback([this]() { if (IsDialogReadyForInput()) { FGameEvent Event(EGameEventType::StartUserLogin, (int)ELoginMode::AccountPortal, L"", L""); FGame::Get().OnGameEvent(Event); } }); AuthButton->SetBackgroundColors(assets::DefaultButtonColors); std::shared_ptr<FButtonWidget> CancelButton = std::make_shared<FButtonWidget>( Vector2(0.f, 40.f), Vector2(80.f, 25.f), LoginAccountPortalDialog->GetLayer() - 1, L"CANCEL", assets::DefaultButtonAssets, SmallFont, AuthButtonBackCol); CancelButton->Create(); CancelButton->SetOnPressedCallback([this]() { if (IsDialogReadyForInput()) { FGameEvent Event(EGameEventType::CancelLogin); FGame::Get().OnGameEvent(Event); } }); CancelButton->SetBackgroundColors(assets::DefaultButtonColors); LoginAccountPortalDialog->AddWidget(LoginLabel); LoginAccountPortalDialog->AddWidget(AuthLabel); LoginAccountPortalDialog->AddWidget(AuthLabel2); LoginAccountPortalDialog->AddWidget(AuthLabel3); LoginAccountPortalDialog->AddWidget(AuthButton); LoginAccountPortalDialog->AddWidget(CancelButton); CreateLoginMethodWidgets(LoginAccountPortalDialog, ELoginMode::AccountPortal, PosX); AddDialog(LoginAccountPortalDialog); }
なんとなく UI に沿った要素が配置されているのが確認できます。一方で LOG OUT
と NEW
などのログイン後の要素がいくつか見当たりません。
ここに関しては、ログインメソッドに依らない要素として別途定義されています。ここでは一旦省きます。
AuthButton->SetOnPressedCallback
どうやらこれが LOG IN
ボタンをクリックした際の Callback のようです。
AuthButton->SetOnPressedCallback([this]() { if (IsDialogReadyForInput()) { FGameEvent Event(EGameEventType::StartUserLogin, (int)ELoginMode::AccountPortal, L"", L""); FGame::Get().OnGameEvent(Event); } });
IsDialogReadyForInput
に関しては ShowDialog
後に入力可能状態になっているかというものです。
では、続いて FGame::OnGameEvent
です。
FGame::OnGameEvent
2つのメソッドを呼び出しているようです。
void FGame::OnGameEvent(const FGameEvent& Event) { CustomInvites->OnGameEvent(Event); FBaseGame::OnGameEvent(Event); }
FCustomInvites::OnGameEvent
今回のイベントに合致するものはありません。
void FCustomInvites::OnGameEvent(const FGameEvent& Event) { if (Event.GetType() == EGameEventType::UserLoggedIn) { // 略 } else if (Event.GetType() == EGameEventType::UserLoggedOut) { // 略 } else if (Event.GetType() == EGameEventType::UserConnectLoggedIn) { // 略 } else if (Event.GetType() == EGameEventType::ShowPrevUser) { // 略 } else if (Event.GetType() == EGameEventType::ShowNextUser) { // 略 } else if (Event.GetType() == EGameEventType::CancelLogin) { } }
FBaseGame::OnGameEvent
かなり多くの箇所にイベントを伝搬しています。
void FBaseGame::OnGameEvent(const FGameEvent& Event) { PlayerManager->OnGameEvent(Event); Friends->OnGameEvent(Event); Menu->OnGameEvent(Event); Authentication->OnGameEvent(Event); Metrics->OnGameEvent(Event); Level->OnGameEvent(Event); EosUI->OnGameEvent(Event); }
上記のうち、今回の StartUserLogin
イベントが処理されるのは Menu
と Authentication
だけでした。
FBaseMenu::OnGameEvent
void FBaseMenu::OnGameEvent(const FGameEvent& Event) { if (Event.GetType() == EGameEventType::ShowPrevUser) { // 略 } else if (Event.GetType() == EGameEventType::ShowNextUser) { // 略 } else if (Event.GetType() == EGameEventType::CancelLogin) { // 略 } else if (Event.GetType() == EGameEventType::ToggleFPS) { // 略 } else if (Event.GetType() == EGameEventType::ExitGame) { // 略 } else if (Event.GetType() == EGameEventType::ShowPopupDialog) { // 略 } if (FriendsDialog) FriendsDialog->OnGameEvent(Event); if (AuthDialogs) AuthDialogs->OnGameEvent(Event); }
上記のうち AuthDialogs
のみが StartUserLogin
を受け付けます。
void FAuthDialogs::OnGameEvent(const FGameEvent& Event) { if (Event.GetType() == EGameEventType::StartUserLogin) { EnableLoginButtons(false); EnableLoginMethodButtons(false); } // 略 }
上記はいずれも LOG IN
ボタンや、各ログインメソッドのボタンを disable にするだけです。
FAuthentication::OnGameEvent
void FAuthentication::OnGameEvent(const FGameEvent& Event) { if (Event.GetType() == EGameEventType::CheckAutoLogin) { // 略 } else if (Event.GetType() == EGameEventType::StartUserLogin) { ELoginMode LoginMode = (ELoginMode)Event.GetFirstExtendedType(); ELoginExternalType LoginExternalType = (ELoginExternalType)Event.GetSecondExtendedType(); if (!Login(LoginMode, Event.GetFirstStr(), Event.GetSecondStr(), LoginExternalType)) { FGameEvent LoginFailedEvent(EGameEventType::UserLoginFailed, Event.GetUserId()); FGame::Get().OnGameEvent(LoginFailedEvent); } } // 略
処理中の FGameEvent
は以下です。GameEvent
のコンストラクタとの対応も記載します。
FGameEvent Event(EGameEventType::StartUserLogin, (int)ELoginMode::AccountPortal, L"", L""); // FGameEvent explicit FGameEvent(EGameEventType InType, int InFirstExtendedType, std::wstring InFirstStr, std::wstring InSecondStr) : Type(InType), FirstExtendedType(InFirstExtendedType), FirstStr(InFirstStr), SecondStr(InSecondStr) {}
よって、Login
メソッドの引数は以下のようになります。
Login((int)ELoginMode::AccountPortal, L"", L"", 0)
続いて、いよいよ Login
です。
FAuthentication::Login
bool FAuthentication::Login(ELoginMode LoginMode, std::wstring FirstStr, std::wstring SecondStr, ELoginExternalType ExternalType) { if (!FPlatform::IsInitialized()) return false; AuthHandle = EOS_Platform_GetAuthInterface(FPlatform::GetPlatformHandle()); assert(AuthHandle != nullptr); ConnectHandle = EOS_Platform_GetConnectInterface(FPlatform::GetPlatformHandle()); assert(ConnectHandle != nullptr); AddNotifyLoginStatusChanged(); EOS_Auth_Credentials Credentials = {}; Credentials.ApiVersion = EOS_AUTH_CREDENTIALS_API_LATEST; EOS_Auth_LoginOptions LoginOptions; memset(&LoginOptions, 0, sizeof(LoginOptions)); LoginOptions.ApiVersion = EOS_AUTH_LOGIN_API_LATEST; if (FCommandLine::Get().HasParam(CommandLineConstants::ScopesNoFlags)) { // 通らないため略 } else if (FCommandLine::Get().HasParam(CommandLineConstants::ScopesBasicProfile) || FCommandLine::Get().HasParam(CommandLineConstants::ScopesFriendsList) || FCommandLine::Get().HasParam(CommandLineConstants::ScopesPresence) || FCommandLine::Get().HasParam(CommandLineConstants::ScopesFriendsManagement) || FCommandLine::Get().HasParam(CommandLineConstants::ScopesEmail)) { // 通らないため略 } else { LoginOptions.ScopeFlags = EOS_EAuthScopeFlags::EOS_AS_BasicProfile | EOS_EAuthScopeFlags::EOS_AS_FriendsList | EOS_EAuthScopeFlags::EOS_AS_Presence; } static char FirstParamStr[256]; if (FirstStr.size() > sizeof(FirstParamStr) - 1) return false; sprintf_s(FirstParamStr, sizeof(FirstParamStr), "%s", FStringUtils::Narrow(FirstStr).c_str()); static char SecondParamStr[256]; if (SecondStr.size() > sizeof(SecondParamStr) - 1) return false; sprintf_s(SecondParamStr, sizeof(SecondParamStr), "%s", FStringUtils::Narrow(SecondStr).c_str()); switch (LoginMode) { case ELoginMode::IDPassword: { // 通らないため略 break; } case ELoginMode::ExchangeCode: { // 通らないため略 break; } case ELoginMode::DeviceCode: { // 通らないため略 break; } case ELoginMode::DevAuth: { // 通らないため略 break; } case ELoginMode::AccountPortal: { FDebugLog::Log(L"[EOS SDK] Logging In with Account Portal"); Credentials.Type = EOS_ELoginCredentialType::EOS_LCT_AccountPortal; break; } case ELoginMode::PersistentAuth: { // 通らないため略 break; } case ELoginMode::ExternalAuth: { // 通らないため略 break; } } LoginOptions.Credentials = &Credentials; CurrentLoginMode = LoginMode; EOS_Auth_Login(AuthHandle, &LoginOptions, this, LoginCompleteCallbackFn); AddConnectAuthExpirationNotification(); return true; }
- EOS_Platform_GetAuthInterface | Epic Online Services ドキュメンテーション
- EOS_Platform_GetConnectInterface | Epic Online Services ドキュメンテーション
- EOS_Auth_Credentials | Epic Online Services ドキュメンテーション
- AccountPortal でかつ今回は mobile ではないので
ApiVersion
以外の指定はありません EOS_LCT_AccountPortal - SystemAuthCredentialsOptions may be required if targeting mobile platforms. Otherwise N/A.
- AccountPortal でかつ今回は mobile ではないので
- EOS_Auth_LoginOptions | Epic Online Services ドキュメンテーション
- 今回指定している permission については以下に詳細があります
- EOS_EAuthScopeFlags | Epic Online Services ドキュメンテーション
- EOS_Auth_Login | Epic Online Services ドキュメンテーション
ざっとした流れとしては、最後の EOS_Auth_Login
を呼び出すための情報を構築している感じです。
ConnetHandle
このメソッド内では使っていないのですが、クラス内では利用している箇所もあるため、そのためだと思います。
EOS_Auth_Login
で Auth 用の URL の作成及び、ブラウザの起動まで行っていると推測されます。
ドキュメントには以下が記載されており、成功失敗に関わらずコールバックの実行を保証するとなっています。
This function is asynchronous; the EOS SDK guarantees that your callback (the CompletionDelegate parameter) will run when the operation completes, regardless of whether it succeeds or fails. Use the void* parameter to pass any contextual information the callback may need in order to react properly. Relevant information can be copied from the EOS SDK's cache while the callback function is running. You do not need to remove the callback function.
ではコールバックを見ていきます。
FAuthentication::LoginCompleteCallbackFn
void EOS_CALL FAuthentication::LoginCompleteCallbackFn(const EOS_Auth_LoginCallbackInfo* Data) { FAuthentication* ThisAuth = NULL; if (Data->ClientData) { ThisAuth = (FAuthentication*)Data->ClientData; if (ThisAuth) { FDebugLog::Log(L" - Login Mode: %d", ThisAuth->CurrentLoginMode); } } EOS_HAuth AuthHandle = EOS_Platform_GetAuthInterface(FPlatform::GetPlatformHandle()); assert(AuthHandle != nullptr); if (Data->ResultCode == EOS_EResult::EOS_Success) { const int32_t AccountsCount = EOS_Auth_GetLoggedInAccountsCount(AuthHandle); for (int32_t AccountIdx = 0; AccountIdx < AccountsCount; ++AccountIdx) { FEpicAccountId AccountId; AccountId = EOS_Auth_GetLoggedInAccountByIndex(AuthHandle, AccountIdx); EOS_ELoginStatus LoginStatus; LoginStatus = EOS_Auth_GetLoginStatus(AuthHandle, Data->LocalUserId); FDebugLog::Log(L"[EOS SDK] [%d] - Account ID: %ls, Status: %d", AccountIdx, FEpicAccountId(AccountId).ToString().c_str(), (int32_t)LoginStatus); } FGameEvent Event(EGameEventType::UserLoggedIn, Data->LocalUserId); FGame::Get().OnGameEvent(Event); } else if (Data->ResultCode == EOS_EResult::EOS_Auth_PinGrantCode) { // 今回は略 } else if (Data->ResultCode == EOS_EResult::EOS_Auth_MFARequired) { // 今回は略 } else if (Data->ResultCode == EOS_EResult::EOS_InvalidUser) { // 今回は略 } else if (Data->ResultCode == EOS_EResult::EOS_Auth_AccountFeatureRestricted) { // 今回は略 } else if (EOS_EResult_IsOperationComplete(Data->ResultCode)) { // 今回は略 } }
ここでは成功時、つまり EOS_Success
を受け取った場合のみ確認します。
といいつつ、成功時は AuthHandle
を使って、アカウント情報を取得しているだけですね。複数のユーザーでログインしている場合も扱えるようですね。
- EOS_Auth_OnLoginCallback | Epic Online Services ドキュメンテーション
- dev.epicgames.com
ClientData
やLocalUserId
はここから取得
- dev.epicgames.com
- EOS_EResult | Epic Online Services Developer
- かなりの種類がある
- EOS_Auth_GetLoggedInAccountsCount | Epic Online Services ドキュメンテーション
- EOS_Auth_GetLoggedInAccountByIndex | Epic Online Services ドキュメンテーション
- EOS_Auth_GetLoginStatus | Epic Online Services ドキュメンテーション
成功時にはさらに UserLoggedIn
の FGameEvent
を発行していますが、今回はこれ以上はいきません。
どのようにログイン状態を確認しているか
Callback を登録したのち、EOS SDK はどのようにログイン状態を確認しているか気になりましたが、ログの方には polling と出ていました。
[2022.01.15-00.34.18:014] [EOS SDK] LogEOSAuth: FInitiateDeviceFlowTask Success [2022.01.15-00.34.18:015] [EOS SDK] LogEOSAuth: Device code flow to be continued at https://www.epicgames.com/activate?userCode=xxx [2022.01.15-00.34.18:032] [EOS SDK] LogEOSAuth: Launching platform browser for account portal [2022.01.15-00.34.18:300] [EOS SDK] LogEOS: Error response received from backend. ServiceName=[OAuth], OperationName=[TokenGrant], Url=[<Redacted>], HttpStatus=[400], ErrorCode=[errors.com.epicgames.account.oauth.authorization_pending], NumericErrorCode=[1012], ErrorMessage=[The authorization server request is still pending as the end user has yet to visit and enter the verification code.], CorrId=[EOS-xxx] [2022.01.15-00.34.18:300] [EOS SDK] LogEOSAuth: Polling for token grant. Device code flow to be continued at https://www.epicgames.com/activate?userCode=xxx [2022.01.15-00.34.18:301] [EOS SDK] LogEOSAnalytics: Record Event: EOSSDK.HTTP.Complete <Redacted> [2022.01.15-00.34.28:314] [EOS SDK] LogEOSAuth: Next token grant poll [2022.01.15-00.34.28:531] [EOS SDK] LogEOS: Error response received from backend. ServiceName=[OAuth], OperationName=[TokenGrant], Url=[<Redacted>], HttpStatus=[400], ErrorCode=[errors.com.epicgames.account.oauth.authorization_pending], NumericErrorCode=[1012], ErrorMessage=[The authorization server request is still pending as the end user has yet to visit and enter the verification code.], CorrId=[EOS-xxx] [2022.01.15-00.34.28:532] [EOS SDK] LogEOSAuth: Polling for token grant. Device code flow to be continued at https://www.epicgames.com/activate?userCode=xxx [2022.01.15-00.34.28:532] [EOS SDK] LogEOSAnalytics: Record Event: EOSSDK.HTTP.Complete <Redacted> [2022.01.15-00.34.38:547] [EOS SDK] LogEOSAuth: Next token grant poll [2022.01.15-00.34.38:831] [EOS SDK] LogEOSAuth: FPollTokenGrantRequestTask Success [2022.01.15-00.34.38:831] [EOS SDK] LogEOSAnalytics: Record Event: EOSSDK.HTTP.Complete <Redacted> [2022.01.15-00.34.38:847] [EOS SDK] LogEOSAuth: FInitiateDeviceFlowTask Complete Success [2022.01.15-00.34.38:848] [EOS SDK] LogEOSAuth: NewUserToken: User ClientId: xyz...OaC AccountId: f87...b29 Access[Expires: 2022.01.15-02.34.41 Remaining: 7200.13] Refresh[Expires: 2022-07-14T00:34:41.128Z Remaining: 15552000.13] State: Valid
polling のコードまでは追えていませんが、 EOS_Platform_Tick が EOS SDK のループとして利用されていそうです。
先にイベントハンドルの方を見たので main
関数のイベントループの Main->Tick
部分を見ていなかったのですが、EOS_Platform_Tick
はそこで呼び出されています。(内部的には FMain::Update
-> FGame::Update
-> FPlatform::Update
の先なのですが)
まとめ
今回は AuthDialog
の UI の確認から、Account Portal を使ったログイン方法の流れをざっと確認しました。
サンプルコードの読み方すらもわからなかったので時間をかけて見ていきましたが、これでようやく他のサンプルも多少見ていけそうな気がします。