diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp
index 357fdca39..ff12b3f1a 100644
--- a/src/citra_qt/multiplayer/chat_room.cpp
+++ b/src/citra_qt/multiplayer/chat_room.cpp
@@ -70,12 +70,11 @@ public:
}
QString GetSystemChatMessage() const {
- return QString("[%1] %3")
- .arg(timestamp, system_color, message);
+ return QString("[%1] * %3").arg(timestamp, system_color, message);
}
private:
- static constexpr const char system_color[] = "#888888";
+ static constexpr const char system_color[] = "#FF8C00";
QString timestamp;
QString message;
};
@@ -133,6 +132,7 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique();
+ qRegisterMetaType();
qRegisterMetaType();
qRegisterMetaType();
@@ -140,7 +140,12 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_uniqueBindOnChatMessageRecieved(
[this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
+ member->BindOnStatusMessageReceived(
+ [this](const Network::StatusMessageEntry& status_message) {
+ emit StatusMessageReceived(status_message);
+ });
connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive);
+ connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive);
} else {
// TODO (jroweboy) network was not initialized?
}
@@ -220,6 +225,27 @@ void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
}
}
+void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) {
+ QString name;
+ if (status_message.username.empty() || status_message.username == status_message.nickname) {
+ name = QString::fromStdString(status_message.nickname);
+ } else {
+ name = QString("%1 (%2)").arg(QString::fromStdString(status_message.nickname),
+ QString::fromStdString(status_message.username));
+ }
+ QString message;
+ switch (status_message.type) {
+ case Network::IdMemberJoin:
+ message = tr("%1 has joined").arg(name);
+ break;
+ case Network::IdMemberLeave:
+ message = tr("%1 has left").arg(name);
+ break;
+ }
+ if (!message.isEmpty())
+ AppendStatusMessage(message);
+}
+
void ChatRoom::OnSendChat() {
if (auto room = Network::GetRoomMember().lock()) {
if (room->GetState() != Network::RoomMember::State::Joined) {
diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/citra_qt/multiplayer/chat_room.h
index d76f995b8..7a7c84b48 100644
--- a/src/citra_qt/multiplayer/chat_room.h
+++ b/src/citra_qt/multiplayer/chat_room.h
@@ -39,6 +39,7 @@ public:
public slots:
void OnRoomUpdate(const Network::RoomInformation& info);
void OnChatReceive(const Network::ChatEntry&);
+ void OnStatusMessageReceive(const Network::StatusMessageEntry&);
void OnSendChat();
void OnChatTextChanged();
void PopupContextMenu(const QPoint& menu_location);
@@ -47,6 +48,7 @@ public slots:
signals:
void ChatReceived(const Network::ChatEntry&);
+ void StatusMessageReceived(const Network::StatusMessageEntry&);
private:
static constexpr u32 max_chat_lines = 1000;
@@ -61,5 +63,6 @@ private:
};
Q_DECLARE_METATYPE(Network::ChatEntry);
+Q_DECLARE_METATYPE(Network::StatusMessageEntry);
Q_DECLARE_METATYPE(Network::RoomInformation);
Q_DECLARE_METATYPE(Network::RoomMember::State);
diff --git a/src/network/room.cpp b/src/network/room.cpp
index cae4a7258..ee62df220 100644
--- a/src/network/room.cpp
+++ b/src/network/room.cpp
@@ -127,6 +127,12 @@ public:
*/
void SendCloseMessage();
+ /**
+ * Sends a system message to all the connected clients.
+ */
+ void SendStatusMessage(StatusMessageTypes type, const std::string& nickname,
+ const std::string& username);
+
/**
* Sends the information about the room, along with the list of members
* to every connected client in the room.
@@ -290,6 +296,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) {
}
member.user_data = verify_backend->LoadUserData(uid, token);
+ // Notify everyone that the user has joined.
+ SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username);
+
{
std::lock_guard lock(member_mutex);
members.push_back(std::move(member));
@@ -415,6 +424,24 @@ void Room::RoomImpl::SendCloseMessage() {
}
}
+void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname,
+ const std::string& username) {
+ Packet packet;
+ packet << static_cast(IdStatusMessage);
+ packet << static_cast(type);
+ packet << nickname;
+ packet << username;
+ std::lock_guard lock(member_mutex);
+ if (!members.empty()) {
+ ENetPacket* enet_packet =
+ enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+ for (auto& member : members) {
+ enet_peer_send(member.peer, 0, enet_packet);
+ }
+ }
+ enet_host_flush(server);
+}
+
void Room::RoomImpl::BroadcastRoomInformation() {
Packet packet;
packet << static_cast(IdRoomInformation);
@@ -571,16 +598,23 @@ void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) {
void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) {
// Remove the client from the members list.
+ std::string nickname, username;
{
std::lock_guard lock(member_mutex);
- members.erase(
- std::remove_if(members.begin(), members.end(),
- [client](const Member& member) { return member.peer == client; }),
- members.end());
+ auto member = std::find_if(members.begin(), members.end(), [client](const Member& member) {
+ return member.peer == client;
+ });
+ if (member != members.end()) {
+ nickname = member->nickname;
+ username = member->user_data.username;
+ members.erase(member);
+ }
}
// Announce the change to all clients.
enet_peer_disconnect(client, 0);
+ if (!nickname.empty())
+ SendStatusMessage(IdMemberLeave, nickname, username);
BroadcastRoomInformation();
}
diff --git a/src/network/room.h b/src/network/room.h
index d1a26e62d..a3d93eea9 100644
--- a/src/network/room.h
+++ b/src/network/room.h
@@ -61,6 +61,13 @@ enum RoomMessageTypes : u8 {
IdCloseRoom,
IdRoomIsFull,
IdConsoleIdCollision,
+ IdStatusMessage,
+};
+
+/// Types of system status messages
+enum StatusMessageTypes : u8 {
+ IdMemberJoin = 1, ///< Member joining
+ IdMemberLeave, ///< Member leaving
};
/// This is what a server [person creating a server] would use.
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
index 8fe67c086..79ba71da8 100644
--- a/src/network/room_member.cpp
+++ b/src/network/room_member.cpp
@@ -58,6 +58,7 @@ public:
private:
CallbackSet callback_set_wifi_packet;
CallbackSet callback_set_chat_messages;
+ CallbackSet callback_set_status_messages;
CallbackSet callback_set_room_information;
CallbackSet callback_set_state;
};
@@ -109,6 +110,13 @@ public:
*/
void HandleChatPacket(const ENetEvent* event);
+ /**
+ * Extracts a system message entry from a received ENet packet and adds it to the system message
+ * queue.
+ * @param event The ENet event that was received.
+ */
+ void HandleStatusMessagePacket(const ENetEvent* event);
+
/**
* Disconnects the RoomMember from the Room
*/
@@ -148,6 +156,9 @@ void RoomMember::RoomMemberImpl::MemberLoop() {
case IdChatMessage:
HandleChatPacket(&event);
break;
+ case IdStatusMessage:
+ HandleStatusMessagePacket(&event);
+ break;
case IdRoomInformation:
HandleRoomInformationPacket(&event);
break;
@@ -317,6 +328,22 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
Invoke(chat_entry);
}
+void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) {
+ Packet packet;
+ packet.Append(event->packet->data, event->packet->dataLength);
+
+ // Ignore the first byte, which is the message id.
+ packet.IgnoreBytes(sizeof(u8));
+
+ StatusMessageEntry status_message_entry{};
+ u8 type{};
+ packet >> type;
+ status_message_entry.type = static_cast(type);
+ packet >> status_message_entry.nickname;
+ packet >> status_message_entry.username;
+ Invoke(status_message_entry);
+}
+
void RoomMember::RoomMemberImpl::Disconnect() {
member_information.clear();
room_information.member_slots = 0;
@@ -367,6 +394,12 @@ RoomMember::RoomMemberImpl::CallbackSet& RoomMember::RoomMemberImpl::
return callback_set_chat_messages;
}
+template <>
+RoomMember::RoomMemberImpl::CallbackSet&
+RoomMember::RoomMemberImpl::Callbacks::Get() {
+ return callback_set_status_messages;
+}
+
template
void RoomMember::RoomMemberImpl::Invoke(const T& data) {
std::lock_guard lock(callback_mutex);
@@ -519,6 +552,11 @@ RoomMember::CallbackHandle RoomMember::BindOnChatMessageRecieved(
return room_member_impl->Bind(callback);
}
+RoomMember::CallbackHandle RoomMember::BindOnStatusMessageReceived(
+ std::function callback) {
+ return room_member_impl->Bind(callback);
+}
+
template
void RoomMember::Unbind(CallbackHandle handle) {
std::lock_guard lock(room_member_impl->callback_mutex);
@@ -538,5 +576,6 @@ template void RoomMember::Unbind(CallbackHandle);
template void RoomMember::Unbind(CallbackHandle);
template void RoomMember::Unbind(CallbackHandle);
template void RoomMember::Unbind(CallbackHandle);
+template void RoomMember::Unbind(CallbackHandle);
} // namespace Network
diff --git a/src/network/room_member.h b/src/network/room_member.h
index 4329263aa..5062b225e 100644
--- a/src/network/room_member.h
+++ b/src/network/room_member.h
@@ -40,6 +40,14 @@ struct ChatEntry {
std::string message; ///< Body of the message.
};
+/// Represents a system status message.
+struct StatusMessageEntry {
+ StatusMessageTypes type; ///< Type of the message
+ /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc.
+ std::string nickname;
+ std::string username;
+};
+
/**
* This is what a client [person joining a server] would use.
* It also has to be used if you host a game yourself (You'd create both, a Room and a
@@ -192,6 +200,16 @@ public:
CallbackHandle BindOnChatMessageRecieved(
std::function callback);
+ /**
+ * Binds a function to an event that will be triggered every time a StatusMessage is
+ * received. The function will be called every time the event is triggered. The callback
+ * function must not bind or unbind a function. Doing so will cause a deadlock
+ * @param callback The function to call
+ * @return A handle used for removing the function from the registered list
+ */
+ CallbackHandle BindOnStatusMessageReceived(
+ std::function callback);
+
/**
* Leaves the current room.
*/