Epic Online Services サンプルの AuthAndFriends のコードリーディング ⑤

前回 で各 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 でビルドしてみます。

f:id:you1dan:20220115075220p:plain

すると以下のように UI が変わることが分かります。

DEV_BUILD あり DEV_BUILD なし
f:id:you1dan:20220115075404p:plain f:id:you1dan:20220115075415p:plain

一応ボタンの配置に関するコードも見ておきます。

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);
        }
    }
}

前回、AuthDialogFriendsDialog を親にして配置していると書きましたが、 ParentDialog (実際には FriendsDialog) に対してサイズや配置が計算されていることが分かります。

Account Portal の処理を見る

以前の記事 で書きましたが Account Portal は、ブラウザでアカウントとの連携の許可をします。

FAuthDialogs::CreateLoginAccountPortalDialog

まずは Account Portal での UI 要素の作成部分を見ていきます。UI については、以下のようなモードがあります。

ログイン前 ログイン後 ログイン後に NEW をクリック
f:id:you1dan:20220115083244p:plain f:id:you1dan:20220115083637p:plain f:id:you1dan:20220115083649p:plain
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 OUTNEW などのログイン後の要素がいくつか見当たりません。
ここに関しては、ログインメソッドに依らない要素として別途定義されています。ここでは一旦省きます。

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 イベントが処理されるのは MenuAuthentication だけでした。

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_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 を使って、アカウント情報を取得しているだけですね。複数のユーザーでログインしている場合も扱えるようですね。

成功時にはさらに UserLoggedInFGameEvent を発行していますが、今回はこれ以上はいきません。

どのようにログイン状態を確認しているか

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 を使ったログイン方法の流れをざっと確認しました。

サンプルコードの読み方すらもわからなかったので時間をかけて見ていきましたが、これでようやく他のサンプルも多少見ていけそうな気がします。

参考にさせていただいた記事