mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Multicast chat messages
Only the sender and the recipients receive the chat messages. This commit only has an affecto on messages where the addressee(s) are selected through the dropdown. Addressee(s) selected with a "/" command are still sent to evevyone and filteret by the receiver.
This commit is contained in:
parent
023527e56e
commit
e04506814a
11 changed files with 207 additions and 53 deletions
|
|
@ -78,7 +78,7 @@ class Chat
|
|||
}
|
||||
|
||||
/**
|
||||
* Send the given chat message.
|
||||
* Send the given chat message to the addressees.
|
||||
*/
|
||||
submitChat(text, command = "")
|
||||
{
|
||||
|
|
@ -88,7 +88,7 @@ class Chat
|
|||
let msg = command ? command + " " + text : text;
|
||||
|
||||
if (Engine.HasNetClient())
|
||||
Engine.SendNetworkChat(msg);
|
||||
Engine.SendNetworkChat(msg, this.getReceiverGUIDs(text, command));
|
||||
else
|
||||
this.ChatMessageHandler.handleMessage({
|
||||
"type": "message",
|
||||
|
|
@ -96,4 +96,39 @@ class Chat
|
|||
"text": msg
|
||||
});
|
||||
}
|
||||
|
||||
getReceiverGUIDs(text, command)
|
||||
{
|
||||
const senderGUID = Engine.GetPlayerGUID();
|
||||
|
||||
if (command.startsWith("/msg "))
|
||||
{
|
||||
const receiverGUID =
|
||||
this.ChatMessageFormatPlayer.matchUsername(
|
||||
command.substr("/msg ".length));
|
||||
|
||||
if (!receiverGUID)
|
||||
{
|
||||
warn("Unknown chat addressee: " + text);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [senderGUID, receiverGUID];
|
||||
}
|
||||
|
||||
const isAddressee = this.ChatAddressees.AddresseeTypes.find(
|
||||
type => type.command === command)?.isAddressee;
|
||||
|
||||
if (!isAddressee)
|
||||
{
|
||||
warn("Unknown chat command " + command);
|
||||
return [];
|
||||
}
|
||||
|
||||
const senderID = Engine.GetPlayerID();
|
||||
return Object.keys(g_PlayerAssignments).filter(potentialReceiverGUID => {
|
||||
return potentialReceiverGUID === senderGUID ||
|
||||
isAddressee(senderID, g_PlayerAssignments[potentialReceiverGUID].player);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,16 @@ class ChatAddressees
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* isAddressee is used to determine the receiverGUIDs when sending and
|
||||
* when displaying deciding whether to display network chat.
|
||||
*
|
||||
* The code may assume sender != receiver.
|
||||
*
|
||||
* The function must return true when the message should be sent from X to Y and
|
||||
* it must return true when the message should be received by Y if sent by X and
|
||||
* return false otherwise.
|
||||
*/
|
||||
ChatAddressees.prototype.AddresseeTypes = [
|
||||
{
|
||||
"command": "",
|
||||
|
|
@ -98,36 +108,35 @@ ChatAddressees.prototype.AddresseeTypes = [
|
|||
"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": (senderID, receiverID) =>
|
||||
g_Players[senderID] &&
|
||||
g_Players[receiverID] &&
|
||||
g_Players[senderID].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": (senderID, receiverID) =>
|
||||
g_Players[senderID] &&
|
||||
g_Players[receiverID] &&
|
||||
g_Players[senderID].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": (senderID, receiverID) =>
|
||||
!isPlayerObserver(senderID) || isPlayerObserver(receiverID)
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -89,24 +89,17 @@ class ChatMessageFormatPlayer
|
|||
// Parse private message
|
||||
let isPM = msg.cmd == "/msg";
|
||||
let addresseeGUID;
|
||||
let addresseeIndex;
|
||||
if (isPM)
|
||||
{
|
||||
addresseeGUID = this.matchUsername(msg.text);
|
||||
let addressee = g_PlayerAssignments[addresseeGUID];
|
||||
if (!addressee)
|
||||
{
|
||||
if (isSender)
|
||||
warn("Couldn't match username: " + msg.text);
|
||||
warn("Couldn't find chat message receiver: " + msg.text);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prohibit PM if addressee and sender are identical
|
||||
if (isSender && addresseeGUID == Engine.GetPlayerGUID())
|
||||
return false;
|
||||
|
||||
msg.text = msg.text.substr(addressee.name.length + 1);
|
||||
addresseeIndex = addressee.player;
|
||||
}
|
||||
|
||||
// Set context string
|
||||
|
|
@ -120,11 +113,30 @@ class ChatMessageFormatPlayer
|
|||
msg.context = addresseeType.context;
|
||||
|
||||
// For observers only permit public- and observer-chat and PM to observers
|
||||
if (isPlayerObserver(senderID) &&
|
||||
(isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers"))
|
||||
return false;
|
||||
if (isPlayerObserver(senderID))
|
||||
{
|
||||
if (isPM && !g_IsObserver)
|
||||
{
|
||||
warn("Received unexpected private chat message from observer " +
|
||||
g_PlayerAssignments[msg.guid]?.name +
|
||||
" to active player " +
|
||||
g_PlayerAssignments[addresseeGUID]?.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
let visible = isSender || addresseeType.isAddressee(senderID, addresseeGUID);
|
||||
if (!isPM && msg.cmd != "/observers")
|
||||
{
|
||||
warn("Received unexpected chat message from observer " +
|
||||
g_PlayerAssignments[msg.guid]?.name + " to " + msg.cmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We already should only be receiving messages that were meant for us. The following
|
||||
// consistency check rejects a message only if it was manipulated by the sender or if it was
|
||||
// sent before a relevant simulation change occurred.
|
||||
const visible = isSender || (addresseeType.isAddressee(senderID, Engine.GetPlayerID()) &&
|
||||
(!isPM || addresseeGUID === Engine.GetPlayerGUID()));
|
||||
msg.isVisiblePM = isPM && visible;
|
||||
|
||||
return visible;
|
||||
|
|
@ -143,7 +155,7 @@ class ChatMessageFormatPlayer
|
|||
for (let guid in g_PlayerAssignments)
|
||||
{
|
||||
let pName = g_PlayerAssignments[guid].name;
|
||||
if (text.indexOf(pName + " ") == 0 && pName.length > match.length)
|
||||
if (text.startsWith(pName) && pName.length > match.length)
|
||||
{
|
||||
match = pName;
|
||||
playerGUID = guid;
|
||||
|
|
|
|||
|
|
@ -465,10 +465,17 @@ void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid)
|
|||
SendMessage(&assignPlayer);
|
||||
}
|
||||
|
||||
void CNetClient::SendChatMessage(const std::wstring& text)
|
||||
void CNetClient::SendChatMessage(const std::wstring& text,
|
||||
std::optional<std::vector<std::string>> receivers)
|
||||
{
|
||||
CChatMessage chat;
|
||||
chat.m_Message = text;
|
||||
if (receivers)
|
||||
std::transform(receivers->begin(), receivers->end(), std::back_inserter(chat.m_Receivers),
|
||||
[](std::string& receiver)
|
||||
{
|
||||
return CChatMessage::S_m_Receivers{std::move(receiver)};
|
||||
});
|
||||
SendMessage(&chat);
|
||||
}
|
||||
|
||||
|
|
@ -732,7 +739,7 @@ bool CNetClient::OnChat(CNetClient* client, CFsmEvent* event)
|
|||
|
||||
client->PushGuiMessage(
|
||||
"type", "chat",
|
||||
"guid", message->m_GUID,
|
||||
"guid", message->m_SenderGUID,
|
||||
"text", message->m_Message);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
class CGame;
|
||||
|
|
@ -229,7 +230,12 @@ public:
|
|||
|
||||
void SendAssignPlayerMessage(const int playerID, const CStr& guid);
|
||||
|
||||
void SendChatMessage(const std::wstring& text);
|
||||
/**
|
||||
* @param text The message to send.
|
||||
* @param receivers The GUID of the receiving clients. If empty send it to
|
||||
* all clients.
|
||||
*/
|
||||
void SendChatMessage(const std::wstring& text, std::optional<std::vector<std::string>> receivers);
|
||||
|
||||
void SendReadyMessage(const int status);
|
||||
|
||||
|
|
|
|||
|
|
@ -136,8 +136,11 @@ START_NMT_CLASS_(AuthenticateResult, NMT_AUTHENTICATE_RESULT)
|
|||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(Chat, NMT_CHAT)
|
||||
NMT_FIELD(CStr, m_GUID) // ignored when client->server, valid when server->client
|
||||
NMT_FIELD(CStr, m_SenderGUID) // ignored when client->server
|
||||
NMT_FIELD(CStrW, m_Message)
|
||||
NMT_START_ARRAY(m_Receivers) // send to all when empty
|
||||
NMT_FIELD(CStr, m_ReceiverGUID) // ignored when server->client
|
||||
NMT_END_ARRAY()
|
||||
END_NMT_CLASS()
|
||||
|
||||
START_NMT_CLASS_(Ready, NMT_READY)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
#include "lib/external_libraries/enet.h"
|
||||
#include "lib/types.h"
|
||||
#include "network/StunClient.h"
|
||||
#include "ps/algorithm.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/GUID.h"
|
||||
|
|
@ -355,18 +356,31 @@ bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
|
|||
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
|
||||
}
|
||||
|
||||
bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates)
|
||||
bool CNetServerWorker::Multicast(const CNetMessage* message,
|
||||
const std::vector<NetServerSessionState>& targetStates,
|
||||
const std::optional<std::vector<std::string>>& receivers /* = std::nullopt */)
|
||||
{
|
||||
ENSURE(m_Host);
|
||||
|
||||
const auto isReceiver = [&](const CNetServerSession& session)
|
||||
{
|
||||
if (!PS::contains(targetStates,
|
||||
static_cast<NetServerSessionState>(session.GetCurrState())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!receivers)
|
||||
return true;
|
||||
return PS::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<NetServerSessionState>(session->GetCurrState())) != targetStates.end() &&
|
||||
!session->SendMessage(message))
|
||||
if (isReceiver(*session) && !session->SendMessage(message))
|
||||
ok = false;
|
||||
|
||||
return ok;
|
||||
|
|
@ -822,7 +836,7 @@ void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
|
|||
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)
|
||||
|
|
@ -861,7 +875,7 @@ void CNetServerWorker::SendPlayerAssignments()
|
|||
{
|
||||
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()
|
||||
|
|
@ -1191,7 +1205,7 @@ bool CNetServerWorker::OnSimulationCommand(CNetServerSession* session, CFsmEvent
|
|||
|
||||
// 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)
|
||||
|
|
@ -1209,7 +1223,7 @@ bool CNetServerWorker::OnFlare(CNetServerSession* session, CFsmEvent* event)
|
|||
CNetServerWorker& server = session->GetServer();
|
||||
CFlareMessage* message = (CFlareMessage*)event->GetParamRef();
|
||||
message->m_GUID = session->GetGUID();
|
||||
server.Broadcast(message, { NSS_INGAME });
|
||||
server.Multicast(message, { NSS_INGAME });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1247,9 +1261,20 @@ bool CNetServerWorker::OnChat(CNetServerSession* session, CFsmEvent* event)
|
|||
|
||||
CChatMessage* message = (CChatMessage*)event->GetParamRef();
|
||||
|
||||
message->m_GUID = session->GetGUID();
|
||||
message->m_SenderGUID = session->GetGUID();
|
||||
|
||||
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
|
||||
const std::vector<NetServerSessionState> receivingStates{NSS_PREGAME, NSS_INGAME};
|
||||
const std::vector messageReceivers{std::exchange(message->m_Receivers, {})};
|
||||
|
||||
if (messageReceivers.empty())
|
||||
server.Multicast(message, receivingStates);
|
||||
else
|
||||
{
|
||||
auto receivers = std::make_optional<std::vector<std::string>>();
|
||||
std::transform(messageReceivers.begin(), messageReceivers.end(), std::back_inserter(*receivers),
|
||||
std::mem_fn(&CChatMessage::S_m_Receivers::m_ReceiverGUID));
|
||||
server.Multicast(message, receivingStates, std::move(receivers));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1267,7 +1292,7 @@ bool CNetServerWorker::OnReady(CNetServerSession* session, CFsmEvent* event)
|
|||
|
||||
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 +1329,7 @@ bool CNetServerWorker::OnGameSetup(CNetServerSession* session, CFsmEvent* event)
|
|||
if (session->GetGUID() == server.m_ControllerGUID)
|
||||
{
|
||||
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
|
||||
server.Broadcast(message, { NSS_PREGAME });
|
||||
server.Multicast(message, { NSS_PREGAME });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1383,7 +1408,7 @@ bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent*
|
|||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -1450,7 +1475,7 @@ bool CNetServerWorker::OnRejoined(CNetServerSession* session, CFsmEvent* event)
|
|||
// 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)
|
||||
|
|
@ -1536,7 +1561,7 @@ bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
|
|||
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;
|
||||
|
|
@ -1581,7 +1606,7 @@ void CNetServerWorker::StartGame(const CStr& initAttribs)
|
|||
|
||||
CGameStartMessage gameStart;
|
||||
gameStart.m_InitAttributes = initAttribs;
|
||||
Broadcast(&gameStart, { NSS_PREGAME });
|
||||
Multicast(&gameStart, { NSS_PREGAME });
|
||||
}
|
||||
|
||||
void CNetServerWorker::StartSavedGame(const CStr& initAttribs)
|
||||
|
|
@ -1590,7 +1615,7 @@ void CNetServerWorker::StartSavedGame(const CStr& initAttribs)
|
|||
|
||||
CGameSavedStartMessage gameSavedStart;
|
||||
gameSavedStart.m_InitAttributes = initAttribs;
|
||||
Broadcast(&gameSavedStart, { NSS_PREGAME });
|
||||
Multicast(&gameSavedStart, { NSS_PREGAME });
|
||||
}
|
||||
|
||||
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
|
|
@ -229,7 +230,8 @@ public:
|
|||
/**
|
||||
* Send a message to all clients who match one of the given states.
|
||||
*/
|
||||
bool Broadcast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates);
|
||||
bool Multicast(const CNetMessage* message, const std::vector<NetServerSessionState>& targetStates,
|
||||
const std::optional<std::vector<std::string>>& receivers = std::nullopt);
|
||||
|
||||
private:
|
||||
friend class CNetServer;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2024 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
|
|
@ -98,7 +98,7 @@ void CNetServerTurnManager::CheckClientsReady()
|
|||
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 @@ void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& sessio
|
|||
h.m_Name = oosPlayername;
|
||||
msg.m_PlayerNames.push_back(h);
|
||||
}
|
||||
m_NetServer.Broadcast(&msg, { NSS_INGAME });
|
||||
m_NetServer.Multicast(&msg, { NSS_INGAME });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@
|
|||
|
||||
#include "third_party/encryption/pkcs5_pbkdf2.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace JSI_Network
|
||||
{
|
||||
u16 GetDefaultPort()
|
||||
|
|
@ -237,11 +239,25 @@ void KickPlayer(const CStrW& playerName, bool ban)
|
|||
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<std::vector<std::string>>();
|
||||
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, std::move(receivers));
|
||||
}
|
||||
|
||||
void SendNetworkReady(int message)
|
||||
|
|
|
|||
39
source/ps/algorithm.h
Normal file
39
source/ps/algorithm.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/* Copyright (C) 2024 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 0 A.D. is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef ALGORITHM_H
|
||||
#define ALGORITHM_H
|
||||
|
||||
namespace PS
|
||||
{
|
||||
/**
|
||||
* Simplifed version of std::ranges::contains (C++23) as we don't support the
|
||||
* original one yet. The naming intentionally follows the STL version to make
|
||||
* the future replacement easier with less changing.
|
||||
* It supports only a subset of std::ranges::contains functionality.
|
||||
*/
|
||||
template<typename Range, typename T = typename Range::value_type>
|
||||
bool contains(Range&& range, const T& value)
|
||||
{
|
||||
return std::any_of(range.begin(), range.end(), [&](const auto& elem)
|
||||
{
|
||||
return elem == value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ALGORITHM_H
|
||||
Loading…
Reference in a new issue