diff --git a/lib/netplay/netplay.cpp b/lib/netplay/netplay.cpp index d3e3e1d1dd7..2a74a231660 100644 --- a/lib/netplay/netplay.cpp +++ b/lib/netplay/netplay.cpp @@ -412,6 +412,11 @@ void NET_setLobbyDisabled(const std::string& infoLinkURL) lobby_disabled_info_link_url = infoLinkURL; } +uint32_t NET_getCurrentHostedLobbyGameId() +{ + return gamestruct.gameId; +} + // Sets if the game is password protected or not void NETGameLocked(bool flag) { @@ -991,12 +996,19 @@ static void NETplayerLeaving(UDWORD index, bool quietSocketClose) sync_counter.left++; bool wasSpectator = NetPlay.players[index].isSpectator; MultiPlayerLeave(index); // more cleanup + bool resetReadyCalled = false; if (ingame.localJoiningInProgress) // Only if game hasn't actually started yet. { NET_DestroyPlayer(index); // sets index player's array to false if (!wasSpectator) { resetReadyStatus(false); // reset ready status for all players + resetReadyCalled = true; + } + + if (!resetReadyCalled) + { + wz_command_interface_output_room_status_json(); } } } @@ -1020,6 +1032,7 @@ static void NETplayerDropped(UDWORD index) sync_counter.drops++; bool wasSpectator = NetPlay.players[index].isSpectator; MultiPlayerLeave(id); // more cleanup + bool resetReadyCalled = false; if (ingame.localJoiningInProgress) // Only if game hasn't actually started yet. { // Send message type specifically for dropped / disconnects @@ -1031,6 +1044,12 @@ static void NETplayerDropped(UDWORD index) if (!wasSpectator) { resetReadyStatus(false); // reset ready status for all players + resetReadyCalled = true; + } + + if (!resetReadyCalled) + { + wz_command_interface_output_room_status_json(); } } @@ -4361,6 +4380,9 @@ static void NETallowJoining() { ASSERT(false, "wzFiles is uninitialized?? (Player: %" PRIu8 ")", index); } + + wz_command_interface_output_room_status_json(); + continue; // continue to next tmp_socket } diff --git a/lib/netplay/netplay.h b/lib/netplay/netplay.h index 04c56310fc6..6cfaf64846c 100644 --- a/lib/netplay/netplay.h +++ b/lib/netplay/netplay.h @@ -487,6 +487,7 @@ void NET_clearDownloadingWZFiles(); bool NET_getLobbyDisabled(); const std::string& NET_getLobbyDisabledInfoLinkURL(); void NET_setLobbyDisabled(const std::string& infoLinkURL); +uint32_t NET_getCurrentHostedLobbyGameId(); bool NETGameIsLocked(); void NETGameLocked(bool flag); diff --git a/src/multiint.cpp b/src/multiint.cpp index 69d84621be2..533c2856154 100644 --- a/src/multiint.cpp +++ b/src/multiint.cpp @@ -2606,7 +2606,19 @@ static bool SendReadyRequest(UBYTE player, bool bReady) { if (NetPlay.isHost) // do or request the change. { - return changeReadyStatus(player, bReady); + bool changedValue = changeReadyStatus(player, bReady); + if (changedValue && wz_command_interface_enabled()) + { + std::string playerPublicKeyB64 = base64Encode(getMultiStats(player).identity.toBytes(EcKey::Public)); + std::string playerIdentityHash = getMultiStats(player).identity.publicHashString(); + std::string playerVerifiedStatus = (ingame.VerifiedIdentity[player]) ? "V" : "?"; + std::string playerName = NetPlay.players[player].name; + std::string playerNameB64 = base64Encode(std::vector(playerName.begin(), playerName.end())); + wz_command_interface_output("WZEVENT: readyStatus=%d: %" PRIu32 " %s %s %s %s %s\n", bReady ? 1 : 0, player, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[player].IPtextAddress); + + wz_command_interface_output_room_status_json(); + } + return changedValue; } else { @@ -2662,11 +2674,24 @@ bool recvReadyRequest(NETQUEUE queue) return false; } - return changeReadyStatus((UBYTE)player, bReady); + bool changedValue = changeReadyStatus((UBYTE)player, bReady); + if (changedValue && wz_command_interface_enabled()) + { + std::string playerPublicKeyB64 = base64Encode(stats.identity.toBytes(EcKey::Public)); + std::string playerIdentityHash = stats.identity.publicHashString(); + std::string playerVerifiedStatus = (ingame.VerifiedIdentity[player]) ? "V" : "?"; + std::string playerName = NetPlay.players[player].name; + std::string playerNameB64 = base64Encode(std::vector(playerName.begin(), playerName.end())); + wz_command_interface_output("WZEVENT: readyStatus=%d: %" PRIu32 " %s %s %s %s %s\n", bReady ? 1 : 0, player, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[player].IPtextAddress); + + wz_command_interface_output_room_status_json(); + } + return changedValue; } bool changeReadyStatus(UBYTE player, bool bReady) { + bool changedValue = NetPlay.players[player].ready != bReady; NetPlay.players[player].ready = bReady; NETBroadcastPlayerInfo(player); netPlayersUpdated = true; @@ -2674,7 +2699,7 @@ bool changeReadyStatus(UBYTE player, bool bReady) // change PingTime to some value less than PING_LIMIT, so that multiplayPlayersReady // doesnt block ingame.PingTimes[player] = ingame.PingTimes[player] == PING_LIMIT ? 1 : ingame.PingTimes[player]; - return true; + return changedValue; } static void informIfAdminChangedOtherPosition(uint32_t targetPlayerIdx, uint32_t responsibleIdx) @@ -6889,7 +6914,7 @@ class WzHostLobbyOperationsInterface : public HostLobbyOperationsInterface std::string playerVerifiedStatus = (ingame.VerifiedIdentity[player]) ? "V" : "?"; std::string playerName = NetPlay.players[player].name; std::string playerNameB64 = base64Encode(std::vector(playerName.begin(), playerName.end())); - wz_command_interface_output("WZEVENT: hostChatPermissions=%s: %" PRIu32 " %" PRIu32 "%s %s %s %s %s\n", (freeChatEnabled) ? "Y" : "N", player, gameTime, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[player].IPtextAddress); + wz_command_interface_output("WZEVENT: hostChatPermissions=%s: %" PRIu32 " %" PRIu32 " %s %s %s %s %s\n", (freeChatEnabled) ? "Y" : "N", player, gameTime, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[player].IPtextAddress); } return true; @@ -7297,6 +7322,18 @@ void WzMultiplayerOptionsTitleUI::frontendMultiMessages(bool running) NETuint32_t(&player_id); NETend(); + if (player_id >= MAX_CONNECTED_PLAYERS) + { + debug(LOG_ERROR, "Bad NET_PLAYERRESPONDING received, ID is %d", (int)player_id); + break; + } + + if (whosResponsible(player_id) != queue.index && queue.index != NetPlay.hostPlayer) + { + HandleBadParam("NET_PLAYERRESPONDING given incorrect params.", player_id, queue.index); + break; + } + ingame.JoiningInProgress[player_id] = false; ingame.DataIntegrity[player_id] = false; break; diff --git a/src/multijoin.cpp b/src/multijoin.cpp index 8ee450cd032..fc6ec32be1b 100644 --- a/src/multijoin.cpp +++ b/src/multijoin.cpp @@ -449,6 +449,8 @@ void recvPlayerLeft(NETQUEUE queue) debug(LOG_INFO, "** player %u has dropped, in-game! (gameTime: %" PRIu32 ")", playerIndex, gameTime); ActivityManager::instance().updateMultiplayGameData(game, ingame, NETGameIsLocked()); + + wz_command_interface_output_room_status_json(); } // //////////////////////////////////////////////////////////////////////////// @@ -479,7 +481,7 @@ bool MultiPlayerLeave(UDWORD playerIndex) std::string playerVerifiedStatus = (ingame.VerifiedIdentity[playerIndex]) ? "V" : "?"; std::string playerName = NetPlay.players[playerIndex].name; std::string playerNameB64 = base64Encode(std::vector(playerName.begin(), playerName.end())); - wz_command_interface_output("WZEVENT: playerLeft: %" PRIu32 " %" PRIu32 "%s %s %s %s %s\n", playerIndex, gameTime, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[playerIndex].IPtextAddress); + wz_command_interface_output("WZEVENT: playerLeft: %" PRIu32 " %" PRIu32 " %s %s %s %s %s\n", playerIndex, gameTime, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[playerIndex].IPtextAddress); } if (ingame.localJoiningInProgress) diff --git a/src/multiplay.cpp b/src/multiplay.cpp index dda732db93d..dc0cab35fea 100644 --- a/src/multiplay.cpp +++ b/src/multiplay.cpp @@ -461,6 +461,7 @@ bool multiPlayerLoop() } ingame.lastPlayerDataCheck2 = std::chrono::steady_clock::now(); wz_command_interface_output("WZEVENT: allPlayersJoined\n"); + wz_command_interface_output_room_status_json(); } if (NetPlay.bComms) { @@ -1450,8 +1451,20 @@ bool recvMessage() if (ingame.JoiningInProgress[player_id]) { addKnownPlayer(NetPlay.players[player_id].name, getMultiStats(player_id).identity); + ingame.JoiningInProgress[player_id] = false; + + if (wz_command_interface_enabled()) + { + std::string playerPublicKeyB64 = base64Encode(getMultiStats(player_id).identity.toBytes(EcKey::Public)); + std::string playerIdentityHash = getMultiStats(player_id).identity.publicHashString(); + std::string playerVerifiedStatus = (ingame.VerifiedIdentity[player_id]) ? "V" : "?"; + std::string playerName = NetPlay.players[player_id].name; + std::string playerNameB64 = base64Encode(std::vector(playerName.begin(), playerName.end())); + wz_command_interface_output("WZEVENT: playerResponding: %" PRIu32 " %s %s %s %s %s\n", player_id, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[player_id].IPtextAddress); + + wz_command_interface_output_room_status_json(); + } } - ingame.JoiningInProgress[player_id] = false; break; } case GAME_ALLIANCE: @@ -2505,6 +2518,8 @@ void resetReadyStatus(bool bSendOptions, bool ignoreReadyReset) //Really reset ready status if (NetPlay.isHost && !ignoreReadyReset) { + wz_command_interface_output("WZEVENT: readyStatus=RESET\n"); + for (unsigned int i = 0; i < MAX_CONNECTED_PLAYERS; ++i) { //Ignore for autohost launch option. @@ -2518,6 +2533,8 @@ void resetReadyStatus(bool bSendOptions, bool ignoreReadyReset) changeReadyStatus(i, false); } } + + wz_command_interface_output_room_status_json(); } } diff --git a/src/stdinreader.cpp b/src/stdinreader.cpp index 8fba6f4db70..63087279948 100644 --- a/src/stdinreader.cpp +++ b/src/stdinreader.cpp @@ -1,6 +1,6 @@ /* This file is part of Warzone 2100. - Copyright (C) 2020-2021 Warzone 2100 Project + Copyright (C) 2020-2024 Warzone 2100 Project Warzone 2100 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ #include "multistat.h" #include "multilobbycommands.h" #include "clparse.h" +#include "main.h" #include #include @@ -578,7 +579,7 @@ static bool changeHostChatPermissionsForActivePlayerWithIdentity(const std::stri std::string playerVerifiedStatus = (ingame.VerifiedIdentity[i]) ? "V" : "?"; std::string playerName = NetPlay.players[i].name; std::string playerNameB64 = base64Encode(std::vector(playerName.begin(), playerName.end())); - wz_command_interface_output("WZEVENT: hostChatPermissions=%s: %" PRIu32 " %" PRIu32 "%s %s %s %s %s\n", (freeChatEnabled) ? "Y" : "N", i, gameTime, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[i].IPtextAddress); + wz_command_interface_output("WZEVENT: hostChatPermissions=%s: %" PRIu32 " %" PRIu32 " %s %s %s %s %s\n", (freeChatEnabled) ? "Y" : "N", i, gameTime, playerPublicKeyB64.c_str(), playerIdentityHash.c_str(), playerVerifiedStatus.c_str(), playerNameB64.c_str(), NetPlay.players[i].IPtextAddress); }); if (result) @@ -1061,6 +1062,12 @@ int cmdInputThreadFunc(void *) } }); } + else if(!strncmpl(line, "status")) + { + wzAsyncExecOnMainThread([] { + wz_command_interface_output_room_status_json(); + }); + } else if(!strncmpl(line, "shutdown now")) { inexit = true; @@ -1451,3 +1458,204 @@ void configSetCmdInterface(WZ_Command_Interface mode, std::string value) } wz_cmd_interface_param = value; } + +// MARK: - Output Room Status JSON + +static void WzCmdInterfaceDumpHumanPlayerVarsImpl(uint32_t player, bool gameHasFiredUp, nlohmann::ordered_json& j) +{ + PLAYER const &p = NetPlay.players[player]; + + j["name"] = p.name; + + if (!gameHasFiredUp) + { + // in lobby, output "ready" status + j["ready"] = static_cast(p.ready); + } + else + { + // once game has fired up, output loading / connection status + if (p.allocated) + { + if (ingame.JoiningInProgress[player]) + { + j["status"] = "loading"; + } + else + { + j["status"] = "active"; + } + if (ingame.PendingDisconnect[player]) + { + j["status"] = "pendingleave"; + } + } + else + { + j["status"] = "left"; + } + } + + const auto& identity = getMultiStats(player).identity; + if (!identity.empty()) + { + j["pk"] = base64Encode(identity.toBytes(EcKey::Public)); + } + else + { + j["pk"] = ""; + } + j["ip"] = NetPlay.players[player].IPtextAddress; + + if (ingame.PingTimes[player] != PING_LIMIT) + { + j["ping"] = ingame.PingTimes[player]; + } + else + { + j["ping"] = -1; // for "infinite" ping + } + + j["admin"] = static_cast(NetPlay.players[player].isAdmin || (player == NetPlay.hostPlayer)); + + if (player == NetPlay.hostPlayer) + { + j["host"] = 1; + } +} + +void wz_command_interface_output_room_status_json() +{ + if (!wz_command_interface_enabled()) + { + return; + } + + bool gameHasFiredUp = (GetGameMode() == GS_NORMAL); + + auto root = nlohmann::ordered_json::object(); + root["ver"] = 1; + + auto data = nlohmann::ordered_json::object(); + if (gameHasFiredUp) + { + if (ingame.TimeEveryoneIsInGame.has_value()) + { + data["state"] = "started"; + } + else + { + data["state"] = "starting"; + } + } + else + { + data["state"] = "lobby"; + } + if (NetPlay.isHost) + { + auto lobbyGameId = NET_getCurrentHostedLobbyGameId(); + if (lobbyGameId != 0) + { + data["lobbyid"] = lobbyGameId; + } + } + data["map"] = game.map; + + root["data"] = std::move(data); + + if (NetPlay.isHost) + { + auto players = nlohmann::ordered_json::array(); + for (uint8_t player = 0; player < game.maxPlayers; ++player) + { + PLAYER const &p = NetPlay.players[player]; + auto j = nlohmann::ordered_json::object(); + + j["pos"] = p.position; + j["team"] = p.team; + j["col"] = p.colour; + j["fact"] = static_cast(p.faction); + + if (p.ai == AI_CLOSED) + { + // closed slot + j["type"] = "closed"; + } + else if (p.ai == AI_OPEN) + { + if (!gameHasFiredUp && !p.allocated) + { + // available / open slot (in lobby) + j["type"] = "open"; + } + else + { + if (!p.allocated) + { + // if game has fired up and this slot is no longer allocated, skip it entirely if it wasn't initially a human player + if (p.difficulty != AIDifficulty::HUMAN) + { + continue; + } + } + + // human (or host) slot + j["type"] = (p.isSpectator) ? "spec" : "player"; + + WzCmdInterfaceDumpHumanPlayerVarsImpl(player, gameHasFiredUp, j); + } + } + else + { + // bot player + j["type"] = "bot"; + + j["name"] = getAIName(player); + j["difficulty"] = static_cast(NetPlay.players[player].difficulty); + } + + players.push_back(std::move(j)); + } + root["players"] = std::move(players); + + auto spectators = nlohmann::ordered_json::array(); + for (uint32_t i = MAX_PLAYER_SLOTS; i < MAX_CONNECTED_PLAYERS; ++i) + { + PLAYER const &p = NetPlay.players[i]; + if (p.ai == AI_CLOSED) + { + continue; + } + + auto j = nlohmann::ordered_json::object(); + if (!p.allocated) + { + if (!gameHasFiredUp) + { + // available / open spectator slot + j["type"] = "open"; + } + else + { + // no spectator connected to this slot - skip + continue; + } + } + else + { + // human (or host) slot + j["type"] = (p.isSpectator) ? "spec" : "player"; + + WzCmdInterfaceDumpHumanPlayerVarsImpl(i, gameHasFiredUp, j); + } + + spectators.push_back(std::move(j)); + } + root["specs"] = std::move(spectators); + } + + std::string statusJSONStr = std::string("__WZROOMSTATUS__") + root.dump(-1, ' ', false, nlohmann::ordered_json::error_handler_t::replace) + "__ENDWZROOMSTATUS__"; + statusJSONStr.append("\n"); + wz_command_interface_output_str(statusJSONStr.c_str()); +} diff --git a/src/stdinreader.h b/src/stdinreader.h index 0f53a1e9950..8e9331a3fed 100644 --- a/src/stdinreader.h +++ b/src/stdinreader.h @@ -45,3 +45,5 @@ void wz_command_interface_output(const char *str, ...) WZ_DECL_FORMAT(printf, 1, #endif void wz_command_interface_output_str(const char *str); + +void wz_command_interface_output_room_status_json();