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:
A possible personal chat interaction is as follows:
- After successful login, the personal chat entry icon shows the number of unread personal messages.
- Click on the personal chat icon to open the chat window.
- The window first displays a list of the most recent personal chat conversations.
- Select a conversation to display the chat content with the corresponding player.
- Chat with this player.
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 inpgos_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 ofchat_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:
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 isMsgTypeText
, and the text content of the message is stored incontent.text_content
. - SendPersonalCustomMsg: Used to send game custom messages. The
msg_type
of the message isMsgTypeCustom
, and the content of the message is stored incontent.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
Key Error Codes
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.
/**
* 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 Code | Relevant API | Handling Suggestion |
---|---|---|
kBackendIMChatFrequencyLimit | SendPersonalTextMsg SendPersonalCustomMsg | It means that the operation frequency exceeds limit. It's recommended to retry with exponentially expanded interval: 5s, 10s, 20s, etc. |
kBackendIMChatMsgIsTooLong | SendPersonalTextMsg SendPersonalCustomMsg | It 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." |
kBackendIMChatCustomDataIsTooLong | SendPersonalTextMsg SendPersonalCustomMsg | It 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. |
kBackendProfaneWords | SendPersonalTextMsg SendPersonalCustomMsg | It 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." |
kBackendErrPlayerIsPunished | SendPersonalTextMsg SendPersonalCustomMsg | The 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. |