Skip to main content

Personal Chat

1. Overview

This document introduces the use of the in-game Personal Chat feature for one-on-one chat with other players. It is initiated by providing the Player ID of the other player.

After completing this tutorial, developers will know:

  • The basic concepts of the PGOS personal chat.
  • Typical use cases for the personal chat.
  • How to send and receive personal chat messages.
  • How to get a list of recent chats and how to get historical messages with certain players.
  • Personal chat events introduction and how to monitor them.

2. General Sequence

Here is a general sequence for using the personal chat:

img

A possible personal chat interaction is as follows:

  1. After successful login, the personal chat entry icon shows the number of unread personal messages.
  2. Click on the personal chat icon to open the chat window.
  3. The window first displays a list of the most recent personal chat conversations.
  4. Select a conversation to display the chat content with the corresponding player.
  5. Chat with this player.

img

3. Data Structure

Personal chat uses the FClientPersonalMsgInfo and FClientPersonalChatItem data structures. FClientPersonalMsgInfo is used to display message content, FClientPersonalChatItem is used to display player conversations.

struct FClientPersonalMsgInfo 
{
/** The player who sent the message */
FPlayerInfo sender;
/** The time the message was sent */
int64 sent_time;
/** The type of the message */
EClientChatMsgType msg_type;
/** The message to be sent */
FClientPersonalMsgContent content;
/** The sequence number of the message */
int64 seq;
/** Additional information added by the game */
FString custom_data;
};

struct FClientPersonalChatItem
{
/** The peer player info */
FPlayerInfo peer_player_info;
/** Unread message count */
int32 unread_count;
/** Recent chat message info */
FClientPersonalMsgInfo msg_latest;
/** Is the player my friend */
bool is_friend;
};

enum class EClientChatMsgType : uint8
{
MsgTypeText = 0 UMETA(DisplayName = "MsgTypeText"),
MsgTypeCustom = 1 UMETA(DisplayName = "MsgTypeCustom")
};

struct FClientPersonalMsgContent
{
/** Take the value from here when msg type is MsgTypeText */
FString text_content;
/** Take the value from here when msg type is MsgTypeCustom */
FString custom_content;
};

The EClientChatMsgType type corresponds to the fields in FClientPersonalMsgContent. Currently, EClientChatMsgType includes MsgTypeText and MsgTypeCustom. When EClientChatMsgType is MsgTypeText, the message content is in text_content of FClientPersonalMsgContent. When EClientChatMsgType is MsgTypeCustom, the message content is in custom_content of FClientPersonalMsgContent.

In addition to the above data structures, you may also want to understand the following concepts:

  • Personal chat storage:
    • Backend Storage: The backend will temporarily store up to 50 unread messages for each player, and if there are more than 50 unread messages, the oldest messages will be cleared. The temporary storage duration for unread messages is a maximum of 7 days; however, when new unread messages arrive, the countdown for cleanup will be reset, meaning that if there are new unread messages, the temporary storage will be retained for another 7 days.
    • Local Storage: The messages sent and received in a personal chat are stored in the client's local database. Each account uses a separate database file to save its messages in personal chat. You can configure the chat_db_max_bytes value in pgos_config.ini to limit the max size of a single database file. We also provide the GetLocalCacheSize API to obtain the database file size and the ClearLocalCache API to clear chat records. You can use these APIs to prompt the user to clear chat records when the database file is too large.
  • Personal chat conversation: Tracks the personal chat interaction with other players.
  • seq: The sequence number of the message increases as players receive more messages from a given player.
  • Unread personal chat messages: Personal chat messages that are not tagged as read.

Note: The minimum value of chat_db_max_bytes is 1 MB (1048576). We do not recommend setting a value over 100 MB. If you do not specify a value, it takes the default value of 100 MB. When a single database file exceeds the value of chat_db_max_bytes, PGOS will automatically delete older messages to free up space.

4. Using Personal Chat

4.1 Obtain recent personal chat conversation list

When players are preparing to initiate a personal chat, they may need to use the GetMyPersonalChatList API to view the list of recent personal chat conversations.

/**
* Get my personal chat list
*/
void GetMyPersonalChatList(TFunction<void(const FPgosResult& Ret, const FClientGetMyPersonalChatListResult* Data)> Callback) const;

The result returned is saved in the FClientGetMyPersonalChatListResult structure. The returned value list uses the following priority: Number of unread messages > time of the last message.

struct PGOSSDKCPP_API FClientGetMyPersonalChatListResult 
{
/** Personal chat list */
TArray<FClientPersonalChatItem> personal_chat_list;
};

struct PGOSSDKCPP_API FClientPersonalChatItem
{
/** The peer player info */
FPlayerInfo peer_player_info;
/** Unread message count */
int32 unread_count;
/** Recent chat message info */
FClientPersonalMsgInfo msg_latest;
/** Is the player my friend */
bool is_friend;
};

Call GetMyPersonalChatList from PersonalChat module as follows:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeClass::GetMyPersonalChatList()
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->GetMyPersonalChatList([](const FPgosResult& Ret, const FClientGetMyPersonalChatListResult* Result)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("GetMyPersonalChatList Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("GetMyPersonalChatList Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

4.2 Obtain personal chat message list

PGOS stores the personal chat messages locally. You can use the GetPersonalMsgList paginated query function to obtain chat records of the specified player.

/**
* Get chat message list with a player
*
* @param PeerPlayerId The peer player ID
* @param StartSeq The start sequence number of the message to query, 0 means start from the latest message.
* @param Count The count of message to get
*/
void GetPersonalMsgList(
const FString& PeerPlayerId,
int64 StartSeq,
int32 Count,
TFunction<void(const FPgosResult& Ret, const FClientGetPersonalChatMsgListResult* Data)> Callback) const;

The returned results are saved in the FClientGetPersonalChatMsgListResult structure:

struct PGOSSDKCPP_API FClientGetPersonalChatMsgListResult 
{
/** Chat message info list */
TArray<FClientPersonalMsgInfo> msg_list;
/** Whether has more message */
bool has_more;
/** The starting sequence for the next query */
int64 next_seq;
};

The mechanism to locally store chat records and obtain chat records is shown in the image below:

img

Call GetPersonalMsgList from PersonalChat module as follows:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeClass::GetPersonalMsgList(const FString& PlayerId, int64 StartSeq)
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->GetPersonalMsgList(PlayerId, StartSeq, 20, [](const FPgosResult& Ret, const FClientGetPersonalChatMsgListResult* Result)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("GetPersonalMsgList Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("GetPersonalMsgList Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

4.3 Send personal chat message

All players can use a Player ID to initiate a personal chat with the corresponding player. After they send a personal chat message, the other players will receive it immediately if they are online.

There are two ways to send messages:

  • SendPersonalTextMsg: Send a simple text message. The msg_type of the message is MsgTypeText, and the text content of the message is stored in content.text_content.
  • SendPersonalCustomMsg: Used to send game custom messages. The msg_type of the message is MsgTypeCustom, and the content of the message is stored in content.custom_content.
/**
* Send personal chat text message
*
* @param TextMsgReq Request struct for sending text message
*/
void SendPersonalTextMsg(
const FClientSendPersonalMsgParams& Params,
TFunction<void(const FPgosResult& Ret, const FClientPersonalMsgInfo* Data)> Callback) const;

/**
* Send personal chat custom message
*
* @param CustomMsgReq Request struct for sending custom message
*/
void SendPersonalCustomMsg(
const FClientSendPersonalMsgParams& Params,
TFunction<void(const FPgosResult& Ret, const FClientPersonalMsgInfo* Data)> Callback) const;

Here you need to pay attention to the input parameters. Take the parameter FClientPersonalTextMsgReq of the SendPersonalTextMsg interface as an example:

struct FClientSendPersonalMsgParams 
{
/** Peer player's ID */
FString peer_player_id;
/** content */
FString content;
/** Additional information added by the game */
FString custom_data;
};

content is generally the input content of the user, which will be filtered by content moderation. custom_data is used for additional information added by game developers and will not be filtered by content moderation. The caveat here is not to store user input in custom_data.

The returned results are saved in the FClientPersonalMsgInfo structure, which is described in detail above.

Call SendPersonalTextMsg/SendPersonalCustomMsg from PersonalChat module as follows:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeClass::SendPersonalTextMsg(const FString& PlayerId, const FString& Message)
{
FClientSendPersonalMsgParams Params;
Params.player_id = PlayerId;
Params.content = Message;
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->SendPersonalTextMsg(Params, [](const FPgosResult& Ret, const FClientPersonalMsgInfo* Info)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalTextMsg Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalTextMsg Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

void SomeClass::SendPersonalCustomMsg(const FString& PlayerId, const FString& CustomMessage)
{
FClientSendPersonalMsgParams Params;
Params.player_id = PlayerId;
Params.content = Message;
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->SendPersonalCustomMsg(Params, [](const FPgosResult& Ret, const FClientPersonalMsgInfo* Info)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalCustomMsg Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("SendPersonalCustomMsg Failed: err_code=(%d), err_msg=(%s)"), Ret.err_code,
*Ret.msg);
}
});
}
}

Note: Players can allow or not allow non-friends to send personal chat messages to them. See the Privacy Settings Document

4.4 Activate a personal chat

Players may activates a personal chat with a specified player via the ActivePersonalChat API.

Under normal circumstances, a player will constantly receive messages when chatting with another player. Each time they receive a message, the unread message count will change, triggering an OnUnreadPersonalMsgCountNotify event and OnReceivePersonalMsg event, notifying changes in total unread messages and unread messages from this player. The player is in the chat interface with another player, so new messages should be automatically marked as read. To avoid repeated changes to the total unread count triggering OnUnreadPersonalMsgCountNotify event again, we provide the ActivePersonalChat API, which activates a chat with a specified player.

  • Before activation, each new message changes the unread message count and triggers an OnUnreadPersonalMsgCountNotify event.
  • After activation, the unread message count for the specified player will remain at 0, so new messages will not trigger OnUnreadPersonalMsgCountNotify events.

As the example shown in the image below, you are currently chatting with Jerry. When you receive messages from Jerry, they are marked as read by default and do not change the total unread message count. When you are in the chat interface with Jerry, the ActivePersonalChat API activates the chat and Jerry's unread message count remains 0, so when you receive personal chat messages from Jerry, it does not trigger OnUnreadPersonalMsgCountNotify event. When you leave the chat interface, a null PlayerId parameter is passed to the API so that no personal chat is activated.

img

/**
* Set the current personal chat. When setting, OnUnreadPersonalMsgCountNotify will be pushed, and subsequent personal chat messages currently in progress are automatically marked as read
*
* @param PlayerId The player to set as active
*/
void ActivePersonalChat(const FString& PlayerId) const;

Call ActivePersonalChat from PersonalChat module as follows:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeClass::ActivePersonalChat(const FString& PlayerId)
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->ActivePersonalChat(PlayerId);
}
}

Note:

  • Only one chat can be activated at a time.
  • When the PlayerId is null, no conversation is activated.

4.5 Clear Unread Personal Chat Messages Count

If all the personal chat messages from a specified player are read, you can use the ClearPersonalUnreadCount API to set the unread message count for this player to zero.

/**
* Mark messages with someone as all read
*
* @param PlayerId is the ID of the specified player. If it is null, the unread message counts for all current players are set to zero.
*/
void ClearPersonalUnreadCount(
const FString& PlayerId,
TFunction<void(const FPgosResult& Ret) const;

Call ClearPersonalUnreadCount from PersonalChat module as follows:#### Zero unread personal chat message count

If all the personal chat messages from a specified player are read, you can use the ClearPersonalUnreadCount API to set the unread message count for this player to zero.

/**
* Mark messages with someone as all read
*
* @param PlayerId is the ID of the specified player. If it is null, the unread message counts for all current players are set to zero.
*/
void ClearPersonalUnreadCount(
const FString& PlayerId,
TFunction<void(const FPgosResult& Ret, const FClientClearPersonalUnreadCountResult* Data)> Callback) const;

Call ClearPersonalUnreadCount from PersonalChat module as follows:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeClass::ClearPersonalUnreadCount(const FString& PlayerId)
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->ClearPersonalUnreadCount(PlayerId, [](const FPgosResult& Ret, const FClientClearPersonalUnreadCountResult* Result)
{
if (Ret.err_code == 0)
{
UE_LOG(LogTemp, Log, TEXT("ClearPersonalUnreadCount Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("ClearPersonalUnreadCount Failed: err_code=(%d), err_msg=%s]"), Ret.err_code,
*Ret.msg);
}
});
}
}

4.6 Monitor Personal Chat Events

Some events will be triggered when using the personal chat:

  • OnUnreadPersonalMsgCountNotify: Immediately triggered after login and when the total unread count changes. The event results are saved in FClientUnreadMsgCountNotifyEvt.
  • OnReceivePersonalMsg: Triggered when receiving a personal chat message. The event results are saved in FClientReceivePersonalMsgEvt.
struct PGOSSDKCPP_API FClientUnreadMsgCountNotifyEvt 
{
/** Total unread count of personal chat or group chat */
int32 total_unread_count;
};

struct PGOSSDKCPP_API FClientReceivePersonalMsgEvt
{
/** Personal chat message received */
FClientPersonalMsgInfo msg;
/** Count of unread message from sender */
int32 unread_count;
};

Monitor personal chat events from PersonalChat module as follows:

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeClass::MonitorPersonalChatEvents()
{
auto PersonalChat = IPgosSDKCpp::Get().GetClientPersonalChatAPI();
if (PersonalChat)
{
PersonalChat->OnUnreadPersonalMsgCountNotify().AddUObject(this, &SomeClass::OnUnreadPersonalMsgCountNotify);
PersonalChat->OnReceivePersonalMsg().AddUObject(this, &SomeClass::OnReceivePersonalMsg);
}
}

void SomeClass::OnUnreadPersonalMsgCountNotify(const FClientUnreadMsgCountNotifyEvt& Event)
{
UE_LOG(LogTemp, Log, TEXT("OnUnreadPersonalMsgCountNotify"));
}

void SomeClass::OnReceivePersonalMsg(const FClientReceivePersonalMsgEvt& Event)
{
UE_LOG(LogTemp, Log, TEXT("OnReceivePersonalMsg"));
const auto MsgType = Event.msg.msg_type;
if (MsgType == EClientChatMsgType::MsgTypeText)
{
UE_LOG(LogTemp, Log, TEXT("receive text msg, content=(%s)"), *Event.msg.text_content);
} else if (MsgType == EClientChatMsgType::MsgTypeCustom) {
UE_LOG(LogTemp, Log, TEXT("receive custom msg, content=(%s)"), *Event.msg.custom_content);
}
}

5. Key Error Handling

Error CodeRelevant APIHandling Suggestion
kBackendIMChatFrequencyLimitSendPersonalTextMsg SendPersonalCustomMsgIt means that the operation frequency exceeds limit. It's recommended to retry with exponentially expanded interval: 5s, 10s, 20s, etc.
kBackendIMChatMsgIsTooLongSendPersonalTextMsg SendPersonalCustomMsgIt means that the message content is too long. The limit is 1000 characters. The game should prompt player with a message like "Message too long, please shorten it or split it in multiple messages."
kBackendIMChatCustomDataIsTooLongSendPersonalTextMsg SendPersonalCustomMsgIt means that the custom data is too long. The limit is 8000 bytes. The game catch this error and avoid using super long custom data.
kBackendProfaneWordsSendPersonalTextMsg SendPersonalCustomMsgIt means the message content contains profane words and cannot be masked. The game should prompt the player with a message like "Please remove profane words to send message."
kBackendErrPlayerIsPunishedSendPersonalTextMsg SendPersonalCustomMsgThe player is punished. msg is the json serialized string of deadline(unix timestamp (in seconds) at the end of the punishment, <= 0 means permanent punishment) and reason.