Epic Online Services サンプルの P2PNAT のコードリーディング ②

前回 P2PNAT サンプルの初期化部分を中心にみていきました。

今回は以下の EOS のドキュメントを追いながらコードも見ていきたいと思います。

NAT P2P インターフェース | Epic Online Services ドキュメンテーション

接続をリクエストする

ローカル ユーザーが EOS_P2P_SendPacket 関数などでリモート ユーザーに情報を送信する場合、P2P インターフェースはこの 2 人のユーザー間の接続開始リクエストを自動で行い、その際に EOS_P2P_SocketId はこの接続の識別子として機能します。情報を送信するユーザーは SocketId に対して自分のリクエストを自動で承認しますが、情報を受信するユーザーはそれを承認する必要があり、通常は受信接続リクエストをリッスンして EOS_P2P_AcceptConnection 関数を使用します。

EOS_P2P_SendPacket

パケットの送信は、サンプルでは Chat メッセージの送信と同時に行われます。以下は、サンプルの Chat ウィンドウである FP2PNATDialog において、Chat の文字列を送信 (Enter を押した) 場合のイベントハンドラです。
FP2PNAT::SendMessage を呼び出しているのが分かります。 CurrentChat は前回の最後で設定している部分を見ましたが、Chat 相手の FProductUserId です。これは実態としては EOS_ProductUserId なのですが、ドキュメントにも以下の記述があります。

通常、有効な EOS_ProductUserId は両方のユーザーに必要で、データの送信で自分が誰かを示し、またデータの送信先のユーザーを指定します。

void FP2PNATDialog::OnSendMessage(const std::wstring& Value)
{
    if (CurrentChat.IsValid())
    {
        FChatWithFriendData& Chat = GetChat(CurrentChat);

        std::vector<std::wstring> NewLines;
        SplitMessageIntoLines(Value, NewLines);
        for (const std::wstring& NextLine : NewLines)
        {
            if (!NextLine.empty())
            {
                Chat.ChatLines.push_back(std::make_pair(true, NextLine));
            }
        }
        
        bChatViewDirty = true;

        FGame::Get().GetP2PNAT()->SendMessage(CurrentChat, Value);
    }

    ChatInputField->Clear();
}

続いて呼び出し先の FP2PNAT::SendMessage です。

void FP2PNAT::SendMessage(FProductUserId FriendId, const std::wstring& Message)
{
    if (!FriendId.IsValid() || Message.empty())
    {
        FDebugLog::LogError(L"EOS P2PNAT SendMessage: bad input data (account id is wrong or message is empty).");
        return;
    }

    PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser());
    if (Player == nullptr)
    {
        FDebugLog::LogError(L"EOS P2PNAT SendMessage: error user not logged in.");
        return;
    }

    EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());

    EOS_P2P_SocketId SocketId;
    SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST;
    strncpy_s(SocketId.SocketName, "CHAT", 5);

    EOS_P2P_SendPacketOptions Options;
    Options.ApiVersion = EOS_P2P_SENDPACKET_API_LATEST;
    Options.LocalUserId = Player->GetProductUserID();
    Options.RemoteUserId = FriendId;
    Options.SocketId = &SocketId;
    Options.bAllowDelayedDelivery = EOS_TRUE;
    Options.Channel = 0;
    Options.Reliability = EOS_EPacketReliability::EOS_PR_ReliableOrdered;

    std::string MessageNarrow = FStringUtils::Narrow(Message);

    Options.DataLengthBytes = static_cast<uint32_t>(MessageNarrow.size());
    Options.Data = MessageNarrow.data();

    EOS_EResult Result = EOS_P2P_SendPacket(P2PHandle, &Options);
    if (Result != EOS_EResult::EOS_Success)
    {
        FDebugLog::LogError(L"EOS P2PNAT SendMessage: error while sending data, code: %ls.", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str());
    }
}

FPlayerManager に関しては、ここまでで見ていないので飛ばします。ローカルのユーザーの情報を取得しているのだと思います。

まず EOS_P2P_SocketId に関してですが、socket name は application-defined となっているので、このサンプルのように CHAT のようなシンプルな名前をつけることは通常ないのかもしれません。そもそも設定用のプロパティも公開されていません。この場合だと、SocketId を保持していないからでしょうか。

続いて EOS_P2P_SendPacketOptions ですが、LocalUserIdRemoteUserId に Chat 相手と自身の FProductUserId を指定しています。Channel に関してはわかりませんでした。

接続リクエスト通知を受信する

EOS_P2P_AddNotifyPeerConnectionRequestFP2PNAT::OnGameEventUserConnectLoggedIn をハンドルする際に使われます。
この UserConnectLoggedInUserLoggedIn とは異なり、Connect インターフェース を利用した場合のイベントですが、サンプルアプリケーションは UserLoggedIn の場合に必ず Connect インターフェースでのログインも行われています。(ちょっとこの部分理解が浅いので後日調べたいと思います)

void FP2PNAT::OnGameEvent(const FGameEvent& Event)
{
    if (Event.GetType() == EGameEventType::UserConnectLoggedIn)
    {
        FProductUserId UserId = Event.GetProductUserId();
        OnUserConnectLoggedIn(UserId);
    }
    else if (Event.GetType() == EGameEventType::UserLoggedOut)
    {
        // 略
    }
}

void FP2PNAT::OnUserConnectLoggedIn(FProductUserId UserId)
{
    RefreshNATType();

    //subscribe to connection requests
    SubscribeToConnectionRequests();
}

void FP2PNAT::SubscribeToConnectionRequests()
{
    PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser());
    if (Player == nullptr || !Player->GetProductUserID().IsValid())
    {
        FDebugLog::LogError(L"EOS P2PNAT SubscribeToConnectionRequests: error user not logged in.");
        return;
    }

    if (ConnectionNotificationId == EOS_INVALID_NOTIFICATIONID)
    {
        EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());

        EOS_P2P_SocketId SocketId;
        SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST;
        strncpy_s(SocketId.SocketName, "CHAT", 5);

        EOS_P2P_AddNotifyPeerConnectionRequestOptions Options;
        Options.ApiVersion = EOS_P2P_ADDNOTIFYPEERCONNECTIONREQUEST_API_LATEST;
        Options.LocalUserId = Player->GetProductUserID();
        Options.SocketId = &SocketId;

        ConnectionNotificationId = EOS_P2P_AddNotifyPeerConnectionRequest(P2PHandle, &Options, nullptr, OnIncomingConnectionRequest);
        if (ConnectionNotificationId == EOS_INVALID_NOTIFICATIONID)
        {
            FDebugLog::LogError(L"EOS P2PNAT SubscribeToConnectionRequests: could not subscribe, bad notification id returned.");
        }
    }
}

void FP2PNAT::UnsubscribeFromConnectionRequests()
{
    EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());
    EOS_P2P_RemoveNotifyPeerConnectionRequest(P2PHandle, ConnectionNotificationId);
    ConnectionNotificationId = EOS_INVALID_NOTIFICATIONID;
}

ConnectionNotificationId は NAT P2P インターフェースのページに以下がある通り、通知受信の解除にも使われるため保持されています。

以下のパラメータを渡して EOS_P2P_RemoveNotifyPeerConnectionRequest 関数を使用し、ピア接続リクエスト ハンドラを削除できます。

接続を承認する

EOS_P2P_AcceptConnection は上述の EOS_P2P_AddNotifyPeerConnectionRequest の呼び出しに渡した callback 関数である OnIncomingConnectionRequest で使われます。つまり、通知を受信した際に承認するという流れのようです。

void EOS_CALL FP2PNAT::OnIncomingConnectionRequest(const EOS_P2P_OnIncomingConnectionRequestInfo* Data)
{
    if (Data)
    {
        std::string SocketName = Data->SocketId->SocketName;
        if (SocketName != "CHAT")
        {
            FDebugLog::LogError(L"EOS P2PNAT OnIncomingConnectionRequest: bad socket id.");
            return;
        }

        PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser());
        if (Player == nullptr || !Player->GetProductUserID().IsValid())
        {
            FDebugLog::LogError(L"EOS P2PNAT OnIncomingConnectionRequest: error user not logged in.");
            return;
        }
        
        EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());
        EOS_P2P_AcceptConnectionOptions Options;
        Options.ApiVersion = EOS_P2P_ACCEPTCONNECTION_API_LATEST;
        Options.LocalUserId = Player->GetProductUserID();
        Options.RemoteUserId = Data->RemoteUserId;

        EOS_P2P_SocketId SocketId;
        SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST;
        strncpy_s(SocketId.SocketName, "CHAT", 5);
        Options.SocketId = &SocketId;

        EOS_EResult Result = EOS_P2P_AcceptConnection(P2PHandle, &Options);
        if (Result != EOS_EResult::EOS_Success)
        {
            FDebugLog::LogError(L"EOS P2PNAT OnIncomingConnectionRequest: error while accepting connection, code: %ls.", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str());
        }
    }
    else
    {
        FDebugLog::LogError(L"EOS P2PNAT SubscribeToConnectionRequests: EOS_P2P_OnIncomingConnectionRequestInfo is NULL.");
    }
}

EOS_P2P_SendPacket と似たインターフェースになっています。

接続を終了する

ここに関しては、サンプルでは利用されていませんでした。

データを受信する

void FP2PNAT::Update()
{
    HandleReceivedMessages();
}

void FP2PNAT::HandleReceivedMessages()
{
    PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser());
    if (Player == nullptr || !Player->GetProductUserID().IsValid()) return;

    EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle());

    EOS_P2P_ReceivePacketOptions Options;
    Options.ApiVersion = EOS_P2P_RECEIVEPACKET_API_LATEST;
    Options.LocalUserId = Player->GetProductUserID();
    Options.MaxDataSizeBytes = 4096;
    Options.RequestedChannel = nullptr;

    //Packet params
    FProductUserId FriendId;

    EOS_P2P_SocketId SocketId;
    SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST;
    uint8_t Channel = 0;

    std::vector<char> MessageData;
    MessageData.resize(Options.MaxDataSizeBytes);
    uint32_t BytesWritten = 0;

    EOS_EResult Result = EOS_P2P_ReceivePacket(P2PHandle, &Options, &FriendId.AccountId, &SocketId, &Channel, MessageData.data(), &BytesWritten);
    if (Result == EOS_EResult::EOS_NotFound)
    {
        //no more packets, just end
        return;
    }
    else if (Result == EOS_EResult::EOS_Success)
    {
        auto P2PDialog = static_cast<FMenu&>(*FGame::Get().GetMenu()).GetP2PNATDialog();
        if (P2PDialog)
        {
            std::wstring MessageWide;
            std::string MessageNarrow(MessageData.data(), BytesWritten);
            MessageWide = FStringUtils::Widen(MessageNarrow);
            P2PDialog->OnMessageReceived(MessageWide, FriendId);
        }
    }
    else
    {
        FDebugLog::LogError(L"EOS P2PNAT HandleReceivedMessages: error while reading data, code: %ls.", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str());
    }
}

引き続き Channel の用途は分かっていないのですが、Chat に利用する SocketId と、相手 Friend の id を使って受信をしています。
ただ、ここまでは ProductUserId であったのが、ここでは EOS_EpicAccountId に相当する Id が使われているようです。(ドキュメントでは EOS_ProductUserId になっているので、少し謎です)

その後 P2PDialogOnMessageReceived に受信したデータを渡しています。

まとめ

今回は公式の NAT P2P インターフェースのドキュメントをなぞる形でサンプルコードを追っていきました。

今回以下の疑問が出ました。

  • EOS Connect インターフェースでのログインはどういうことか
  • EOS_EpicAccountIdEOS_ProductUserId はどのように違うのか

おそらくこの 2 つは関連すると考えられるので、引き続きここを理解できればと思います。