Index: binaries/data/mods/public/gui/session/chat/Chat.js =================================================================== --- binaries/data/mods/public/gui/session/chat/Chat.js +++ binaries/data/mods/public/gui/session/chat/Chat.js @@ -82,18 +82,37 @@ */ submitChat(text, command = "") { - if (command.startsWith("/msg ")) + const pmPrefix = "/msg"; + const dmAddresseeID = command.startsWith(msgPrefix) ? g_Players.findIndex( + player => player.name === command.substring(msgPrefix.length + 1)) : null; + if (dmAddresseeID !== null) Engine.SetGlobalHotkey("privatechat", "Press", () => { this.openPage(command); }); let msg = command ? command + " " + text : text; - if (Engine.HasNetClient()) - Engine.SendNetworkChat(msg); - else + if (!Engine.HasNetClient()) + { this.ChatMessageHandler.handleMessage({ "type": "message", "guid": "local", "text": msg }); + return; + } + + const sender = g_Players[Engine.GetPlayerID()]; + const lookupCommand = dmAddresseeID === null ? command : msgPrefix; + const isAddressee = this.ChatAddressees.AddresseeTypes.find( + type => type.command === lookupCommand).isAddressee; + + const predicate = (receiver, receiverID) => + receiver.guid !== undefined && + (receiver === sender || isAddressee(receiver, receiverID, dmAddresseeID, sender)); + + const receivers = g_Players.filter(predicate); + + warn("Sending to: " + uneval(g_Players.map(player => player.name))); + + Engine.SendNetworkChat(msg, receivers.map(player => player.guid)); } } Index: binaries/data/mods/public/gui/session/chat/ChatAddressees.js =================================================================== --- binaries/data/mods/public/gui/session/chat/ChatAddressees.js +++ binaries/data/mods/public/gui/session/chat/ChatAddressees.js @@ -98,36 +98,30 @@ "isSelectable": () => !g_IsObserver, "label": markForTranslationWithContext("chat addressee", "Allies"), "context": markForTranslationWithContext("chat message context", "Ally"), - "isAddressee": - senderID => - g_Players[senderID] && - g_Players[Engine.GetPlayerID()] && - g_Players[senderID].isMutualAlly[Engine.GetPlayerID()], + "isAddressee": (receiver, receiverID, _, sender) => + !isPlayerObserver(receiverID) && sender.isMutualAlly[receiverID] }, { "command": "/enemies", "isSelectable": () => !g_IsObserver, "label": markForTranslationWithContext("chat addressee", "Enemies"), "context": markForTranslationWithContext("chat message context", "Enemy"), - "isAddressee": - senderID => - g_Players[senderID] && - g_Players[Engine.GetPlayerID()] && - g_Players[senderID].isEnemy[Engine.GetPlayerID()], + "isAddressee": (receiver, receiverID, _, sender) => + !isPlayerObserver(receiverID) && sender.isEnemy[receiverID] }, { "command": "/observers", "isSelectable": () => true, "label": markForTranslationWithContext("chat addressee", "Observers"), "context": markForTranslationWithContext("chat message context", "Observer"), - "isAddressee": senderID => g_IsObserver + "isAddressee": (_, receiverID) => isPlayerObserver(receiverID) }, { "command": "/msg", "isSelectable": () => false, "label": undefined, "context": markForTranslationWithContext("chat message context", "Private"), - "isAddressee": (senderID, addresseeGUID) => addresseeGUID == Engine.GetPlayerGUID() + "isAddressee": (_, receiverID, dmAddresseeID) => dmAddresseeID === receiverID } ]; Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js =================================================================== --- binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js +++ binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js @@ -124,7 +124,9 @@ (isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers")) return false; - let visible = isSender || addresseeType.isAddressee(senderID, addresseeGUID); + const playerID = Engine.GetPlayerID(); + const visible = isSender || addresseeType.isAddressee(g_Players[playerID], playerID, + addresseeIndex, g_Players[senderID]); msg.isVisiblePM = isPM && visible; return visible; Index: binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js =================================================================== --- binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js +++ binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js @@ -50,6 +50,7 @@ handleMessage(msg) { + warn("Receiving: " + uneval(msg)); let formatted = this.parseMessage(msg); if (!formatted) return; Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -27,6 +27,7 @@ #include #include +#include #include class CGame; @@ -230,7 +231,12 @@ void SendAssignPlayerMessage(const int playerID, const CStr& guid); - void SendChatMessage(const std::wstring& text); + /** + * @param receivers The username of the receiving clients. If empty send + * it to all clients. + */ + void SendChatMessage(const std::wstring& text, + const std::optional>& receivers); void SendReadyMessage(const int status); Index: source/network/NetClient.cpp =================================================================== --- source/network/NetClient.cpp +++ source/network/NetClient.cpp @@ -495,10 +495,18 @@ SendMessage(&assignPlayer); } -void CNetClient::SendChatMessage(const std::wstring& text) +void CNetClient::SendChatMessage(const std::wstring& text, + const std::optional>& receivers) { CChatMessage chat; chat.m_Message = text; + chat.m_SendToAll = receivers ? 0 : 1; + if (receivers) + std::transform(receivers->begin(), receivers->end(), std::back_inserter(chat.m_Receivers), + [](const std::string& receiver) + { + return CChatMessage::S_m_Receivers{receiver}; + }); SendMessage(&chat); } Index: source/network/NetMessages.h =================================================================== --- source/network/NetMessages.h +++ source/network/NetMessages.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -135,6 +135,10 @@ START_NMT_CLASS_(Chat, NMT_CHAT) NMT_FIELD(CStr, m_GUID) // ignored when client->server, valid when server->client NMT_FIELD(CStrW, m_Message) + NMT_FIELD_INT(m_SendToAll, i8, 1) + NMT_START_ARRAY(m_Receivers) + NMT_FIELD(CStr, m_GUID) + NMT_END_ARRAY() END_NMT_CLASS() START_NMT_CLASS_(Ready, NMT_READY) Index: source/network/NetServer.h =================================================================== --- source/network/NetServer.h +++ source/network/NetServer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -229,7 +230,8 @@ /** * Send a message to all clients who match one of the given states. */ - bool Broadcast(const CNetMessage* message, const std::vector& targetStates); + bool Multicast(const CNetMessage* message, const std::vector& targetStates, + const std::optional>& receivers = std::nullopt); private: friend class CNetServer; Index: source/network/NetServer.cpp =================================================================== --- source/network/NetServer.cpp +++ source/network/NetServer.cpp @@ -390,18 +390,36 @@ return CNetHost::SendMessage(message, peer, DebugName(session).c_str()); } -bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates) +bool CNetServerWorker::Multicast(const CNetMessage* message, + const std::vector& targetStates, + const std::optional>& receivers /* = std::nullopt */) { ENSURE(m_Host); + constexpr auto contains = [](const auto& range, const auto& value) + { + return std::any_of(range.begin(), range.end(), [&](const auto& elem) + { + return elem == value; + }); + }; + + const auto doWeHaveToSendTheMessageThroughThisSession = [&](const CNetServerSession& session) + { + if (!contains(targetStates, static_cast(session.GetCurrState()))) + return false; + if (!receivers) + return true; + return contains(*receivers, session.GetGUID()); + }; + bool ok = true; // TODO: this does lots of repeated message serialisation if we have lots // of remote peers; could do it more efficiently if that's a real problem for (CNetServerSession* session : m_Sessions) - if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() && - !session->SendMessage(message)) + if (doWeHaveToSendTheMessageThroughThisSession(*session) && !session->SendMessage(message)) ok = false; return ok; @@ -854,7 +872,7 @@ CKickedMessage kickedMessage; kickedMessage.m_Name = playerName; kickedMessage.m_Ban = ban; - Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); + Multicast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) @@ -893,7 +911,7 @@ { CPlayerAssignmentMessage message; ConstructPlayerAssignmentMessage(message); - Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); + Multicast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } const ScriptInterface& CNetServerWorker::GetScriptInterface() @@ -1197,7 +1215,7 @@ // Send it back to all clients that have finished // the loading screen (and the synchronization when rejoining) - server.Broadcast(message, { NSS_INGAME }); + server.Multicast(message, { NSS_INGAME }); // Save all the received commands if (server.m_SavedCommands.size() < message->m_Turn + 1) @@ -1246,7 +1264,17 @@ message->m_GUID = session->GetGUID(); - server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); + const std::vector receivingStates{NSS_PREGAME, NSS_INGAME}; + + if (message->m_SendToAll) + server.Multicast(message, receivingStates); + else + { + auto receivers = std::make_optional>(); + std::transform(message->m_Receivers.begin(), message->m_Receivers.end(), + std::back_inserter(*receivers), std::mem_fn(&CChatMessage::S_m_Receivers::m_GUID)); + server.Multicast(message, receivingStates, receivers); + } return true; } @@ -1265,7 +1293,7 @@ CReadyMessage* message = (CReadyMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); - server.Broadcast(message, { NSS_PREGAME }); + server.Multicast(message, { NSS_PREGAME }); server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status; @@ -1304,7 +1332,7 @@ if (session->GetGUID() == server.m_ControllerGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); - server.Broadcast(message, { NSS_PREGAME }); + server.Multicast(message, { NSS_PREGAME }); } return true; } @@ -1363,7 +1391,7 @@ // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet loadedSession->SendMessage(&message); - server.Broadcast(&message, { NSS_INGAME }); + server.Multicast(&message, { NSS_INGAME }); return true; } @@ -1432,7 +1460,7 @@ // Inform everyone of the client having rejoined CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); - server.Broadcast(message, { NSS_INGAME }); + server.Multicast(message, { NSS_INGAME }); // Send all pausing players to the rejoined client. for (const CStr& guid : server.m_PausingPlayers) @@ -1521,7 +1549,7 @@ loaded.m_CurrentTurn = 0; // Notice the changedSession is still in the NSS_PREGAME state - Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME }); + Multicast(&loaded, { NSS_PREGAME, NSS_INGAME }); m_State = SERVER_STATE_INGAME; return true; @@ -1561,7 +1589,7 @@ CGameStartMessage gameStart; gameStart.m_InitAttributes = initAttribs; - Broadcast(&gameStart, { NSS_PREGAME }); + Multicast(&gameStart, { NSS_PREGAME }); } CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) Index: source/network/NetServerTurnManager.cpp =================================================================== --- source/network/NetServerTurnManager.cpp +++ source/network/NetServerTurnManager.cpp @@ -98,7 +98,7 @@ CEndCommandBatchMessage msg; msg.m_TurnLength = m_TurnLength; msg.m_Turn = m_ReadyTurn; - m_NetServer.Broadcast(&msg, { NSS_INGAME }); + m_NetServer.Multicast(&msg, { NSS_INGAME }); ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn); m_SavedTurnLengths.push_back(m_TurnLength); @@ -172,7 +172,7 @@ h.m_Name = oosPlayername; msg.m_PlayerNames.push_back(h); } - m_NetServer.Broadcast(&msg, { NSS_INGAME }); + m_NetServer.Multicast(&msg, { NSS_INGAME }); break; } } Index: source/network/scripting/JSInterface_Network.cpp =================================================================== --- source/network/scripting/JSInterface_Network.cpp +++ source/network/scripting/JSInterface_Network.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -40,6 +40,8 @@ #include "third_party/encryption/pkcs5_pbkdf2.h" +#include + namespace JSI_Network { u16 GetDefaultPort() @@ -235,11 +237,25 @@ g_NetClient->SendKickPlayerMessage(playerName, ban); } -void SendNetworkChat(const CStrW& message) +void SendNetworkChat(const ScriptRequest& rq, const CStrW& message, JS::HandleValue handle) { ENSURE(g_NetClient); - g_NetClient->SendChatMessage(message); + if (handle.isNullOrUndefined()) + { + g_NetClient->SendChatMessage(message, std::nullopt); + return; + } + + auto receivers = std::make_optional>(); + if (!Script::FromJSVal(rq, handle, *receivers)) + { + ScriptException::Raise(rq, "The second argument to `SendNetworkChat` has to be either an Array " + "or a nullish value."); + return; + } + + g_NetClient->SendChatMessage(message, receivers); } void SendNetworkReady(int message)