diff --git a/lib/netplay/netplay.cpp b/lib/netplay/netplay.cpp index b4f18602371..83f0d929429 100644 --- a/lib/netplay/netplay.cpp +++ b/lib/netplay/netplay.cpp @@ -4483,7 +4483,7 @@ bool NEThostGame(const char *SessionName, const char *PlayerName, bool spectator // Now switch player color of the host to what they normally use for MP games if (war_getMPcolour() >= 0) { - changeColour(NetPlay.hostPlayer, war_getMPcolour(), true); + changeColour(NetPlay.hostPlayer, war_getMPcolour(), realSelectedPlayer); } return true; } @@ -4541,7 +4541,7 @@ bool NEThostGame(const char *SessionName, const char *PlayerName, bool spectator // Now switch player color of the host to what they normally use for SP games if (NetPlay.hostPlayer < MAX_PLAYERS && war_getMPcolour() >= 0) { - changeColour(NetPlay.hostPlayer, war_getMPcolour(), true); + changeColour(NetPlay.hostPlayer, war_getMPcolour(), realSelectedPlayer); } NETregisterServer(WZ_SERVER_DISCONNECT); diff --git a/src/hci/quickchat.cpp b/src/hci/quickchat.cpp index 5627e81a7f9..79a8469f8d3 100644 --- a/src/hci/quickchat.cpp +++ b/src/hci/quickchat.cpp @@ -46,6 +46,7 @@ #include "../stats.h" #include "../qtscript.h" #include "../main.h" +#include "../faction.h" #include #include @@ -119,7 +120,7 @@ static bool isInternalMessage(WzQuickChatMessage msg) // MARK: - Helper functions -const char* to_output_string(WzQuickChatMessage msg); +std::string to_output_string(WzQuickChatMessage msg, const optional& messageData); static optional numberButtonPressed() { @@ -766,6 +767,13 @@ bool WzQuickChatTargeting::noTargets() const return !all && !humanTeammates && !aiTeammates && specificPlayers.empty(); } +WzQuickChatTargeting WzQuickChatTargeting::targetAll() +{ + WzQuickChatTargeting targeting; + targeting.all = true; + return targeting; +} + // MARK: - WzQuickChatSendToSelector std::shared_ptr WzQuickChatSendToSelector::make(bool teamOnly, const WzQuickChatTargeting& initialTargeting) @@ -2414,9 +2422,95 @@ std::shared_ptr WzQuickChatForm::createInGameEndGamePanel() return panel; } +// MARK: - + +namespace WzQuickChatDataContexts { + +// - INTERNAL_ADMIN_ACTION_NOTICE +namespace INTERNAL_ADMIN_ACTION_NOTICE { + WzQuickChatMessageData constructMessageData(Context ctx, uint32_t responsiblePlayerIdx, uint32_t targetPlayerIdx) + { + return WzQuickChatMessageData { static_cast(ctx), responsiblePlayerIdx, targetPlayerIdx }; + } + + std::string to_output_string(WzQuickChatMessageData messageData) + { + uint32_t responsiblePlayerIdx = messageData.dataA; + uint32_t targetPlayerIdx = messageData.dataB; + + if (responsiblePlayerIdx >= MAX_CONNECTED_PLAYERS) + { + return std::string(); + } + if (targetPlayerIdx >= MAX_CONNECTED_PLAYERS) + { + return std::string(); + } + + const char* responsiblePlayerName = NetPlay.players[responsiblePlayerIdx].name; + const char* targetPlayerName = NetPlay.players[targetPlayerIdx].name; + + const char* responsiblePlayerType = _("Player"); + if (responsiblePlayerIdx == NetPlay.hostPlayer) + { + responsiblePlayerType = _("Host"); + } + else if (NetPlay.players[responsiblePlayerIdx].isAdmin) + { + responsiblePlayerType = _("Admin"); + } + + switch (messageData.dataContext) + { + case static_cast(Context::Invalid): + return ""; + case static_cast(Context::Team): + return astringf(_("%s (%s) changed team of player (%s) to: %d"), responsiblePlayerType, responsiblePlayerName, targetPlayerName, NetPlay.players[targetPlayerIdx].team); + case static_cast(Context::Position): + return astringf(_("%s (%s) changed position of player (%s) to: %d"), responsiblePlayerType, responsiblePlayerName, targetPlayerName, NetPlay.players[targetPlayerIdx].position); + case static_cast(Context::Color): + return astringf(_("%s (%s) changed color of player (%s) to: %s"), responsiblePlayerType, responsiblePlayerName, targetPlayerName, getPlayerColourName(targetPlayerIdx)); + case static_cast(Context::Faction): + return astringf(_("%s (%s) changed faction of player (%s) to: %s"), responsiblePlayerType, responsiblePlayerName, targetPlayerName, to_localized_string(static_cast(NetPlay.players[targetPlayerIdx].faction))); + } + + return ""; // Silence compiler warning + } +} // namespace INTERNAL_ADMIN_ACTION_NOTICE + +} // namespace WzQuickChatDataContexts + // MARK: - Public functions -const char* to_output_string(WzQuickChatMessage msg) +bool quickChatMessageExpectsExtraData(WzQuickChatMessage msg) +{ + switch (msg) + { + // WZ-generated internal messages which require extra data + case WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE: + return true; + + default: + return false; + } +} + +int32_t to_output_sender(WzQuickChatMessage msg, uint32_t sender) +{ + // Certain internal messages override the sender for display purposes + switch (msg) + { + // WZ-generated internal messages - not for users to deliberately send + case WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE: + // override the "sender" to SYSTEM_MESSAGE type + return SYSTEM_MESSAGE; + + default: + return sender; + } +} + +std::string to_output_string(WzQuickChatMessage msg, const optional& messageData) { switch (msg) { @@ -2459,6 +2553,11 @@ const char* to_output_string(WzQuickChatMessage msg) case WzQuickChatMessage::TEAM_SUGGESTION_BUILD_CAPTURE_OILS: // TRANSLATORS: A suggestion to other player(s) return _("I suggest: Capturing oil resources"); + + // WZ-generated internal messages - not for users to deliberately send + case WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE: + return WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::to_output_string(messageData.value()); + default: return to_display_string(msg); } @@ -2622,6 +2721,8 @@ const char* to_display_string(WzQuickChatMessage msg) return _("Message delivery failure - try again"); case WzQuickChatMessage::INTERNAL_LOBBY_NOTICE_MAP_DOWNLOADED: return _("Map Downloaded"); + case WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE: + return _("Admin modified a setting"); // not a valid message case WzQuickChatMessage::MESSAGE_COUNT: @@ -2788,23 +2889,43 @@ static std::string formatReceivers(uint32_t senderIdx, const WzQuickChatTargetin return ss.str(); } -void addQuickChatMessageToConsole(WzQuickChatMessage message, uint32_t sender, const WzQuickChatTargeting& targeting) +void addQuickChatMessageToConsole(WzQuickChatMessage message, uint32_t sender, const WzQuickChatTargeting& targeting, const optional& messageData) { char formatted[MAX_CONSOLE_STRING_LENGTH]; - ssprintf(formatted, "[%s] %s (%s): %s", formatLocalDateTime("%H:%M").c_str(), getPlayerName(sender), formatReceivers(sender, targeting).c_str(), to_output_string(message)); + ssprintf(formatted, "[%s] %s (%s): %s", formatLocalDateTime("%H:%M").c_str(), getPlayerName(sender), formatReceivers(sender, targeting).c_str(), to_output_string(message, messageData).c_str()); bool teamSpecific = !targeting.all && (targeting.humanTeammates || targeting.aiTeammates); - addConsoleMessage(formatted, DEFAULT_JUSTIFY, sender, teamSpecific); + addConsoleMessage(formatted, DEFAULT_JUSTIFY, to_output_sender(message, sender), teamSpecific); } -void addLobbyQuickChatMessageToConsole(WzQuickChatMessage message, uint32_t sender, const WzQuickChatTargeting& targeting) +void addLobbyQuickChatMessageToConsole(WzQuickChatMessage message, uint32_t sender, const WzQuickChatTargeting& targeting, const optional& messageData) { bool teamSpecific = !targeting.all && (targeting.humanTeammates || targeting.aiTeammates); - addConsoleMessage(to_output_string(message), DEFAULT_JUSTIFY, sender, teamSpecific); + addConsoleMessage(to_output_string(message, messageData).c_str(), DEFAULT_JUSTIFY, to_output_sender(message, sender), teamSpecific); } -void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatTargeting targeting) +bool shouldHideQuickChatMessageFromLocalDisplay(WzQuickChatMessage message) +{ + if (!isInternalMessage(message)) + { + return false; + } + // special cases for internal messages that should be displayed locally + switch (message) + { + case WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE: + return false; + default: + break; + } + + // hide local display of this message + return true; +} + +void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatTargeting targeting, optional messageData /*= nullopt*/) { ASSERT_OR_RETURN(, myResponsibility(fromPlayer), "We are not responsible for player: %" PRIu32, fromPlayer); + ASSERT_OR_RETURN(, quickChatMessageExpectsExtraData(message) == messageData.has_value(), "Message [%d] unexpectedly %s", static_cast(message), messageData.has_value() ? "has message data" : "lacks message data"); bool internalMessage = isInternalMessage(message); @@ -2907,7 +3028,7 @@ void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatT if (recipient == selectedPlayer) { // Targetted at us - Output to console - addQuickChatMessageToConsole(message, fromPlayer, targeting); + addQuickChatMessageToConsole(message, fromPlayer, targeting, messageData); } else { @@ -2951,18 +3072,27 @@ void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatT { NETuint32_t(&playerIdx); } + if (quickChatMessageExpectsExtraData(message)) + { + if (messageData.has_value()) + { + NETuint32_t(&messageData.value().dataContext); + NETuint32_t(&messageData.value().dataA); + NETuint32_t(&messageData.value().dataB); + } + } NETend(); } - if (fromPlayer == selectedPlayer && (!recipients.empty() || !isInGame) && !internalMessage) + if (fromPlayer == selectedPlayer && (!recipients.empty() || !isInGame) && !shouldHideQuickChatMessageFromLocalDisplay(message)) { if (isInGame) { - addQuickChatMessageToConsole(message, fromPlayer, targeting); + addQuickChatMessageToConsole(message, fromPlayer, targeting, messageData); } else { - addLobbyQuickChatMessageToConsole(message, fromPlayer, targeting); + addLobbyQuickChatMessageToConsole(message, fromPlayer, targeting, messageData); } } @@ -2972,6 +3102,59 @@ void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatT } } +bool shouldProcessQuickChatMessage(const NETQUEUE& queue, bool isInGame, WzQuickChatMessage message, uint32_t sender, uint32_t recipient, const WzQuickChatTargeting& targeting, const optional& messageData) +{ + bool internalMessage = isInternalMessage(message); + + if (sender >= MAX_CONNECTED_PLAYERS || recipient >= MAX_CONNECTED_PLAYERS) + { + return false; + } + + if (whosResponsible(sender) != queue.index) + { + return false; + } + + if (!myResponsibility(recipient)) + { + return false; + } + + bool senderIsSpectator = NetPlay.players[sender].isSpectator; + if (isInGame && senderIsSpectator && !NetPlay.players[recipient].isSpectator) + { + // spectators can't talk to players in-game + return false; + } + + auto senderSpamMute = playerSpamMutedUntil(sender); + if (senderSpamMute.has_value() && !internalMessage) + { + // ignore message sent while player send was throttled + return false; + } + + // begin: message-specific checks + switch (message) + { + case WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE: + // should only ever be sent by the host + if (queue.index != NetPlay.hostPlayer) + { + return false; + } + break; + default: + // no special handling + break; + } + // end: message-specific checks + + // passes checks + return true; +} + bool recvQuickChat(NETQUEUE queue) { bool isInGame = (GetGameMode() == GS_NORMAL); @@ -2982,6 +3165,9 @@ bool recvQuickChat(NETQUEUE queue) uint32_t recipient = MAX_CONNECTED_PLAYERS; uint32_t messageValue = static_cast(WzQuickChatMessage::MESSAGE_COUNT); WzQuickChatTargeting targeting; + optional messageData; + + WzQuickChatMessage msgEnumVal; if (expectingSecuredMessage) { @@ -3012,45 +3198,27 @@ bool recvQuickChat(NETQUEUE queue) targeting.specificPlayers.insert(tmp_playerIdx); } } - NETend(); - - if (sender >= MAX_CONNECTED_PLAYERS || recipient >= MAX_CONNECTED_PLAYERS) - { - return false; - } - - if (whosResponsible(sender) != queue.index) + bool validMessageEnumValue = to_WzQuickChatMessage(messageValue, msgEnumVal); + if (validMessageEnumValue && quickChatMessageExpectsExtraData(msgEnumVal)) { - return false; + messageData = WzQuickChatMessageData(); + NETuint32_t(&messageData.value().dataContext); + NETuint32_t(&messageData.value().dataA); + NETuint32_t(&messageData.value().dataB); } + NETend(); - if (!myResponsibility(recipient)) + if (!validMessageEnumValue) { return false; } - bool senderIsSpectator = NetPlay.players[sender].isSpectator; - if (isInGame && senderIsSpectator && !NetPlay.players[recipient].isSpectator) + if (!shouldProcessQuickChatMessage(queue, isInGame, msgEnumVal, sender, recipient, targeting, messageData)) { - // spectators can't talk to players in-game return false; } - WzQuickChatMessage msgEnumVal; - if (!to_WzQuickChatMessage(messageValue, msgEnumVal)) - { - // invalid message enum value - return false; - } bool internalMessage = isInternalMessage(msgEnumVal); - - auto senderSpamMute = playerSpamMutedUntil(sender); - if (senderSpamMute.has_value() && !internalMessage) - { - // ignore message sent while player send was throttled - return false; - } - if (!internalMessage && recipient == selectedPlayer) { recordPlayerMessageSent(sender); @@ -3061,12 +3229,12 @@ bool recvQuickChat(NETQUEUE queue) // Output to the console if (isInGame) { - addQuickChatMessageToConsole(msgEnumVal, sender, targeting); + addQuickChatMessageToConsole(msgEnumVal, sender, targeting, messageData); audio_PlayTrack(ID_SOUND_MESSAGEEND); } else { - addLobbyQuickChatMessageToConsole(msgEnumVal, sender, targeting); + addLobbyQuickChatMessageToConsole(msgEnumVal, sender, targeting, messageData); audio_PlayTrack(FE_AUDIO_MESSAGEEND); } } diff --git a/src/hci/quickchat.h b/src/hci/quickchat.h index bce20f2edc2..88f082b00cb 100644 --- a/src/hci/quickchat.h +++ b/src/hci/quickchat.h @@ -105,7 +105,8 @@ /* FROM THIS POINT ON - ONLY INTERNAL MESSAGES! */ \ /* WZ-generated internal messages - not for users to deliberately send */ \ MSG(INTERNAL_MSG_DELIVERY_FAILURE_TRY_AGAIN) /* This should always be the first internal message! */ \ - MSG(INTERNAL_LOBBY_NOTICE_MAP_DOWNLOADED) + MSG(INTERNAL_LOBBY_NOTICE_MAP_DOWNLOADED) \ + MSG(INTERNAL_ADMIN_ACTION_NOTICE) #define GENERATE_ENUM(ENUM) ENUM, @@ -144,15 +145,43 @@ struct WzQuickChatTargeting bool aiTeammates = false; std::unordered_set specificPlayers; +public: + static WzQuickChatTargeting targetAll(); + public: void reset(); bool noTargets() const; }; +struct WzQuickChatMessageData +{ + uint32_t dataContext = 0; + uint32_t dataA = 0; + uint32_t dataB = 0; +}; + +// Begin: DataContext Enums for specific messages +namespace WzQuickChatDataContexts { + +// - INTERNAL_ADMIN_ACTION_NOTICE +namespace INTERNAL_ADMIN_ACTION_NOTICE { + enum class Context : uint32_t + { + Invalid = 0, + Team, + Position, + Color, + Faction + }; + WzQuickChatMessageData constructMessageData(Context ctx, uint32_t responsiblePlayerIdx, uint32_t targetPlayerIdx); +} // namespace INTERNAL_ADMIN_ACTION_NOTICE + +} // namespace WzQuickChatDataContexts + std::shared_ptr createQuickChatForm(WzQuickChatContext context, const std::function& onQuickChatSent, optional startingPanel = nullopt); void quickChatInitInGame(); struct NETQUEUE; -void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatTargeting targeting); +void sendQuickChat(WzQuickChatMessage message, uint32_t fromPlayer, WzQuickChatTargeting targeting, optional messageData = nullopt); bool recvQuickChat(NETQUEUE queue); diff --git a/src/multiint.cpp b/src/multiint.cpp index ee83bd73a69..83bd31dfe34 100644 --- a/src/multiint.cpp +++ b/src/multiint.cpp @@ -1448,6 +1448,34 @@ static bool isHostOrAdmin() return NetPlay.isHost || NetPlay.players[selectedPlayer].isAdmin; } +static bool isPlayerHostOrAdmin(uint32_t playerIdx) +{ + ASSERT_OR_RETURN(false, playerIdx < MAX_CONNECTED_PLAYERS, "Invalid player idx: %" PRIu32, playerIdx); + return (playerIdx == NetPlay.hostPlayer) || NetPlay.players[playerIdx].isAdmin; +} + +static bool shouldInformOfAdminAction(uint32_t targetPlayerIdx, uint32_t responsibleIdx) +{ + ASSERT_OR_RETURN(false, targetPlayerIdx < MAX_CONNECTED_PLAYERS, "Invalid targetPlayerIdx: %" PRIu32, targetPlayerIdx); + ASSERT_OR_RETURN(false, responsibleIdx < MAX_CONNECTED_PLAYERS, "Invalid responsibleIdx: %" PRIu32, responsibleIdx); + + if (responsibleIdx == targetPlayerIdx) + { + return false; // do not inform about self-action + } + if (!isPlayerHostOrAdmin(responsibleIdx)) + { + return false; + } + if (!NetPlay.players[targetPlayerIdx].allocated) + { + return false; // do not inform if target isn't a human player + } + + return true; +} + + // //////////////////////////////////////////////////////////////////////////// // Colour functions @@ -2472,19 +2500,51 @@ void WzMultiplayerOptionsTitleUI::closePositionChooser() closeAllChoosers(); } -static void changeTeam(UBYTE player, UBYTE team) +static void informIfAdminChangedOtherTeam(uint32_t targetPlayerIdx, uint32_t responsibleIdx) +{ + if (!shouldInformOfAdminAction(targetPlayerIdx, responsibleIdx)) + { + return; + } + + sendQuickChat(WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE, realSelectedPlayer, WzQuickChatTargeting::targetAll(), WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::constructMessageData(WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::Context::Team, responsibleIdx, targetPlayerIdx)); + + std::string senderPublicKeyB64 = base64Encode(getMultiStats(responsibleIdx).identity.toBytes(EcKey::Public)); + debug(LOG_INFO, "Admin %s (%s) changed team of player ([%u] %s) to: %d", NetPlay.players[responsibleIdx].name, senderPublicKeyB64.c_str(), NetPlay.players[targetPlayerIdx].position, NetPlay.players[targetPlayerIdx].name, NetPlay.players[targetPlayerIdx].team); +} + +static bool changeTeam(UBYTE player, UBYTE team, uint32_t responsibleIdx) { + if (team >= MAX_CONNECTED_PLAYERS) + { + return false; + } + + if (player >= MAX_CONNECTED_PLAYERS) + { + return false; + } + + if (NetPlay.players[player].team == team) + { + NETBroadcastPlayerInfo(player); // we do this regardless, in case of sync issues // FUTURE TODO: Doublecheck if this is still needed? + return false; // Nothing to do. + } + NetPlay.players[player].team = team; debug(LOG_WZ, "set %d as new team for player %d", team, player); NETBroadcastPlayerInfo(player); + informIfAdminChangedOtherTeam(player, responsibleIdx); + netPlayersUpdated = true; + return true; } static bool SendTeamRequest(UBYTE player, UBYTE chosenTeam) { if (NetPlay.isHost) // do or request the change. { - changeTeam(player, chosenTeam); // do the change, remember only the host can do this to avoid confusion. + changeTeam(player, chosenTeam, realSelectedPlayer); // do the change, remember only the host can do this to avoid confusion. } else { @@ -2539,9 +2599,7 @@ bool recvTeamRequest(NETQUEUE queue) resetReadyStatus(false); } debug(LOG_NET, "%s is now part of team: %d", NetPlay.players[player].name, (int) team); - changeTeam(player, team); // we do this regardless, in case of sync issues - - return true; + return changeTeam(player, team, queue.index); // we do this regardless, in case of sync issues } static bool SendReadyRequest(UBYTE player, bool bReady) @@ -2619,12 +2677,31 @@ bool changeReadyStatus(UBYTE player, bool bReady) return true; } -static bool changePosition(UBYTE player, UBYTE position) +static void informIfAdminChangedOtherPosition(uint32_t targetPlayerIdx, uint32_t responsibleIdx) +{ + if (!shouldInformOfAdminAction(targetPlayerIdx, responsibleIdx)) + { + return; + } + + sendQuickChat(WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE, realSelectedPlayer, WzQuickChatTargeting::targetAll(), WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::constructMessageData(WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::Context::Position, responsibleIdx, targetPlayerIdx)); + + std::string senderPublicKeyB64 = base64Encode(getMultiStats(responsibleIdx).identity.toBytes(EcKey::Public)); + debug(LOG_INFO, "Admin %s (%s) changed position of player (%s) to: %d", NetPlay.players[responsibleIdx].name, senderPublicKeyB64.c_str(), NetPlay.players[targetPlayerIdx].name, NetPlay.players[targetPlayerIdx].position); +} + +static bool changePosition(UBYTE player, UBYTE position, uint32_t responsibleIdx) { ASSERT_HOST_ONLY(return false); - ASSERT(player < MAX_PLAYERS, "Invalid player idx: %" PRIu8, player); + ASSERT_OR_RETURN(false, player < MAX_PLAYERS, "Invalid player idx: %" PRIu8, player); int i; + if (NetPlay.players[player].position == position) + { + // nothing to do + return false; + } + for (i = 0; i < MAX_PLAYERS; i++) { if (NetPlay.players[i].position == position) @@ -2634,6 +2711,7 @@ static bool changePosition(UBYTE player, UBYTE position) std::swap(NetPlay.players[i].position, NetPlay.players[player].position); std::swap(NetPlay.players[i].team, NetPlay.players[player].team); NETBroadcastTwoPlayerInfo(player, i); + informIfAdminChangedOtherPosition(player, responsibleIdx); netPlayersUpdated = true; return true; } @@ -2645,36 +2723,50 @@ static bool changePosition(UBYTE player, UBYTE position) // Positions were corrupted. Attempt to fix. NetPlay.players[player].position = position; NETBroadcastPlayerInfo(player); + informIfAdminChangedOtherPosition(player, responsibleIdx); netPlayersUpdated = true; return true; } return false; } -bool changeColour(unsigned player, int col, bool isHost) +static void informIfAdminChangedOtherColor(uint32_t targetPlayerIdx, uint32_t responsibleIdx) +{ + if (!shouldInformOfAdminAction(targetPlayerIdx, responsibleIdx)) + { + return; + } + + sendQuickChat(WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE, realSelectedPlayer, WzQuickChatTargeting::targetAll(), WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::constructMessageData(WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::Context::Color, responsibleIdx, targetPlayerIdx)); + + std::string senderPublicKeyB64 = base64Encode(getMultiStats(responsibleIdx).identity.toBytes(EcKey::Public)); + debug(LOG_INFO, "Admin %s (%s) changed color of player ([%u] %s) to: [%d] %s", NetPlay.players[responsibleIdx].name, senderPublicKeyB64.c_str(), NetPlay.players[targetPlayerIdx].position, NetPlay.players[targetPlayerIdx].name, NetPlay.players[targetPlayerIdx].colour, getPlayerColourName(targetPlayerIdx)); +} + +bool changeColour(unsigned player, int col, uint32_t responsibleIdx) { if (col < 0 || col >= MAX_PLAYERS_IN_GUI) { - return true; + return false; } if (player >= MAX_PLAYERS) { - return true; + return false; } if (getPlayerColour(player) == col) { - return true; // Nothing to do. + return false; // Nothing to do. } for (unsigned i = 0; i < MAX_PLAYERS; ++i) { if (getPlayerColour(i) == col) { - if (!isHost && NetPlay.players[i].allocated) + if (!isPlayerHostOrAdmin(responsibleIdx) && NetPlay.players[i].allocated) { - return true; // May not swap. + return false; // May not swap. } debug(LOG_NET, "Swapping colours between players %d(%d) and %d(%d)", @@ -2682,6 +2774,7 @@ bool changeColour(unsigned player, int col, bool isHost) setPlayerColour(i, getPlayerColour(player)); setPlayerColour(player, col); NETBroadcastTwoPlayerInfo(player, i); + informIfAdminChangedOtherColor(player, responsibleIdx); netPlayersUpdated = true; return true; } @@ -2693,6 +2786,7 @@ bool changeColour(unsigned player, int col, bool isHost) debug(LOG_NET, "corrupted colours: player (%u) new colour (%u) old colour (%d)", player, col, NetPlay.players[player].colour); setPlayerColour(player, col); NETBroadcastPlayerInfo(player); + informIfAdminChangedOtherColor(player, responsibleIdx); netPlayersUpdated = true; return true; } @@ -2703,7 +2797,7 @@ bool SendColourRequest(UBYTE player, UBYTE col) { if (NetPlay.isHost) // do or request the change { - return changeColour(player, col, true); + return changeColour(player, col, realSelectedPlayer); } else { @@ -2716,14 +2810,46 @@ bool SendColourRequest(UBYTE player, UBYTE col) return true; } +static void informIfAdminChangedOtherFaction(uint32_t targetPlayerIdx, uint32_t responsibleIdx) +{ + if (!shouldInformOfAdminAction(targetPlayerIdx, responsibleIdx)) + { + return; + } + + sendQuickChat(WzQuickChatMessage::INTERNAL_ADMIN_ACTION_NOTICE, realSelectedPlayer, WzQuickChatTargeting::targetAll(), WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::constructMessageData(WzQuickChatDataContexts::INTERNAL_ADMIN_ACTION_NOTICE::Context::Faction, responsibleIdx, targetPlayerIdx)); + + std::string senderPublicKeyB64 = base64Encode(getMultiStats(responsibleIdx).identity.toBytes(EcKey::Public)); + debug(LOG_INFO, "Admin %s (%s) changed faction of player ([%u] %s) to: %s", NetPlay.players[responsibleIdx].name, senderPublicKeyB64.c_str(), NetPlay.players[targetPlayerIdx].position, NetPlay.players[targetPlayerIdx].name, to_localized_string(static_cast(NetPlay.players[targetPlayerIdx].faction))); +} + +bool changeFaction(unsigned player, FactionID faction, uint32_t responsibleIdx) +{ + if (player >= MAX_PLAYERS) + { + return false; + } + + if (NetPlay.players[player].faction == faction) + { + return false; // Nothing to do. + } + + NetPlay.players[player].faction = static_cast(faction); + NETBroadcastPlayerInfo(player); + + informIfAdminChangedOtherFaction(player, responsibleIdx); + + return true; +} + static bool SendFactionRequest(UBYTE player, UBYTE faction) { // TODO: needs to be rewritten from scratch ASSERT_OR_RETURN(false, faction <= static_cast(MAX_FACTION_ID), "Invalid faction: %u", (unsigned int)faction); if (NetPlay.isHost) // do or request the change { - NetPlay.players[player].faction = static_cast(faction); - NETBroadcastPlayerInfo(player); + changeFaction(player, static_cast(faction), realSelectedPlayer); return true; } else @@ -2741,7 +2867,7 @@ static bool SendPositionRequest(UBYTE player, UBYTE position) { if (NetPlay.isHost) // do or request the change { - return changePosition(player, position); + return changePosition(player, position, realSelectedPlayer); } else { @@ -2788,9 +2914,7 @@ bool recvFactionRequest(NETQUEUE queue) resetReadyStatus(false, true); - NetPlay.players[player].faction = newFactionId.value(); - NETBroadcastPlayerInfo(player); - return true; + return changeFaction(player, newFactionId.value(), queue.index); } bool recvColourRequest(NETQUEUE queue) @@ -2819,7 +2943,7 @@ bool recvColourRequest(NETQUEUE queue) resetReadyStatus(false, true); - return changeColour(player, col, NetPlay.players[queue.index].isAdmin); + return changeColour(player, col, queue.index); } bool recvPositionRequest(NETQUEUE queue) @@ -2854,7 +2978,7 @@ bool recvPositionRequest(NETQUEUE queue) resetReadyStatus(false); - return changePosition(player, position); + return changePosition(player, position, queue.index); } static bool SendPlayerSlotTypeRequest(uint32_t player, bool isSpectator) @@ -6621,7 +6745,7 @@ class WzHostLobbyOperationsInterface : public HostLobbyOperationsInterface virtual ~WzHostLobbyOperationsInterface() { } public: - virtual bool changeTeam(uint32_t player, uint8_t team) override + virtual bool changeTeam(uint32_t player, uint8_t team, uint32_t responsibleIdx) override { ASSERT_HOST_ONLY(return false); ASSERT_OR_RETURN(false, player < MAX_PLAYERS, "Invalid player: %" PRIu32, player); @@ -6636,11 +6760,11 @@ class WzHostLobbyOperationsInterface : public HostLobbyOperationsInterface // no-op - nothing to do return true; } - ::changeTeam(player, team); + ::changeTeam(player, team, responsibleIdx); resetReadyStatus(false); return true; } - virtual bool changePosition(uint32_t player, uint8_t position) override + virtual bool changePosition(uint32_t player, uint8_t position, uint32_t responsibleIdx) override { ASSERT_HOST_ONLY(return false); ASSERT_OR_RETURN(false, player < MAX_PLAYERS, "Invalid player: %" PRIu32, player); @@ -6655,7 +6779,7 @@ class WzHostLobbyOperationsInterface : public HostLobbyOperationsInterface // no-op request - nothing to do return true; } - if (!::changePosition(player, position)) + if (!::changePosition(player, position, responsibleIdx)) { return false; } @@ -6818,7 +6942,7 @@ class WzHostLobbyOperationsInterface : public HostLobbyOperationsInterface resetReadyStatus(true); //reset and send notification to all clients return true; } - virtual bool autoBalancePlayers() override + virtual bool autoBalancePlayers(uint32_t responsibleIdx) override { ASSERT_HOST_ONLY(return false); if (!getAutoratingEnable()) @@ -6869,7 +6993,7 @@ class WzHostLobbyOperationsInterface : public HostLobbyOperationsInterface } sendRoomSystemMessage(astringf("Moving [%d]\"%s\" <%s> to pos %d", id, pl[i].name.c_str(), pl[i].elo.c_str(), toslot).c_str()); - changePosition(id, toslot); + changePosition(id, toslot, responsibleIdx); } sendRoomSystemMessage("Autobalance done"); return true; @@ -7554,7 +7678,7 @@ TITLECODE WzMultiplayerOptionsTitleUI::run() for (uint8_t slotInc = 0; slotInc < game.maxPlayers && playerInc < humans.size(); ++slotInc) { - changePosition(humans[playerInc], slotInc); + changePosition(humans[playerInc], slotInc, realSelectedPlayer); ++playerInc; } } @@ -8673,7 +8797,7 @@ static inline bool isSpectatorOnlySlot(UDWORD playerIdx) bool autoBalancePlayersCmd() { - return cmdInterface.autoBalancePlayers(); + return cmdInterface.autoBalancePlayers(realSelectedPlayer); } //// NOTE: Pass in NetPlay.players[i].position diff --git a/src/multiint.h b/src/multiint.h index 17036101f5c..cd721d2bd8a 100644 --- a/src/multiint.h +++ b/src/multiint.h @@ -85,7 +85,7 @@ std::shared_ptr addMultiBut(const std::shared_ptr &screen, U std::shared_ptr makeMultiBut(UDWORD id, UDWORD width, UDWORD height, const char *tipres, UDWORD norm, UDWORD down, UDWORD hi, unsigned tc, uint8_t alpha = 255); AtlasImage mpwidgetGetFrontHighlightImage(AtlasImage image); -bool changeColour(unsigned player, int col, bool isHost); +bool changeColour(unsigned player, int col, uint32_t responsibleIdx); extern char sPlayer[128]; extern bool multiintDisableLobbyRefresh; // gamefind diff --git a/src/multilobbycommands.cpp b/src/multilobbycommands.cpp index 3f312871973..34ed59e297f 100644 --- a/src/multilobbycommands.cpp +++ b/src/multilobbycommands.cpp @@ -295,7 +295,7 @@ bool processChatLobbySlashCommands(const NetworkTextMessage& message, HostLobbyO sendRoomNotifyMessage("Usage: " LOBBY_COMMAND_PREFIX "team "); return false; } - if (!cmdInterface.changeTeam(posToNetPlayer(s1), s2)) + if (!cmdInterface.changeTeam(posToNetPlayer(s1), s2, message.sender)) { std::string msg = astringf("Unable to change player %u team to %u", s1, s2); sendRoomNotifyMessage(msg.c_str()); @@ -387,7 +387,7 @@ bool processChatLobbySlashCommands(const NetworkTextMessage& message, HostLobbyO return false; } - if (!cmdInterface.changePosition(playerIdxA, s2)) + if (!cmdInterface.changePosition(playerIdxA, s2, message.sender)) { std::string msg = astringf("Unable to swap players %" PRIu8 " and %" PRIu8, s1, s2); sendRoomNotifyMessage(msg.c_str()); @@ -554,7 +554,7 @@ bool processChatLobbySlashCommands(const NetworkTextMessage& message, HostLobbyO sendRoomSystemMessage("Autobalance is available only for even player count."); return false; } - if (!cmdInterface.autoBalancePlayers()) + if (!cmdInterface.autoBalancePlayers(message.sender)) { // failure message logged by autoBalancePlayers() return false; diff --git a/src/multilobbycommands.h b/src/multilobbycommands.h index b7ba7b00177..944663c9f5c 100644 --- a/src/multilobbycommands.h +++ b/src/multilobbycommands.h @@ -35,8 +35,8 @@ class HostLobbyOperationsInterface virtual ~HostLobbyOperationsInterface(); public: - virtual bool changeTeam(uint32_t player, uint8_t team) = 0; - virtual bool changePosition(uint32_t player, uint8_t position) = 0; + virtual bool changeTeam(uint32_t player, uint8_t team, uint32_t responsibleIdx) = 0; + virtual bool changePosition(uint32_t player, uint8_t position, uint32_t responsibleIdx) = 0; virtual bool changeBase(uint8_t baseValue) = 0; virtual bool changeAlliances(uint8_t allianceValue) = 0; virtual bool changeScavengers(uint8_t scavsValue) = 0; @@ -44,7 +44,7 @@ class HostLobbyOperationsInterface virtual bool changeHostChatPermissions(uint32_t player_id, bool freeChatEnabled) = 0; virtual bool movePlayerToSpectators(uint32_t player_id) = 0; virtual bool requestMoveSpectatorToPlayers(uint32_t player_id) = 0; - virtual bool autoBalancePlayers() = 0; + virtual bool autoBalancePlayers(uint32_t responsibleIdx) = 0; virtual void quitGame(int exitCode) = 0; };