diff --git a/dep/ggpo-x/src/backends/p2p.cpp b/dep/ggpo-x/src/backends/p2p.cpp index b02fc3ad3..9131b13b1 100644 --- a/dep/ggpo-x/src/backends/p2p.cpp +++ b/dep/ggpo-x/src/backends/p2p.cpp @@ -294,7 +294,7 @@ Peer2PeerBackend::AddPlayer(GGPOPlayer *player, } // no other players in this session? - if (player->type == GGPO_PLAYERTYPE_LOCAL && _num_players == 1) + if (player->type == GGPO_PLAYERTYPE_LOCAL && _num_players == 1 && _num_spectators == 0) _synchronizing = false; return GGPO_OK; diff --git a/dep/ggpo-x/src/backends/spectator.cpp b/dep/ggpo-x/src/backends/spectator.cpp index e69661cd2..d93cc1bfe 100644 --- a/dep/ggpo-x/src/backends/spectator.cpp +++ b/dep/ggpo-x/src/backends/spectator.cpp @@ -21,13 +21,13 @@ SpectatorBackend::SpectatorBackend(GGPOSessionCallbacks* cb, int num_players, in * Initialize the UDP port */ // FIXME - abort(); + //abort(); //_udp.Init(localport, &_poll, this); /* * Init the host endpoint */ - //_host.Init(&_udp, _poll, 0, hostip, hostport, NULL); + _host.Init(peer, 0, NULL); _host.Synchronize(); } diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp index 44e1c9ee2..10c554b25 100644 --- a/src/core/netplay.cpp +++ b/src/core/netplay.cpp @@ -90,6 +90,12 @@ static void ShowChatMessage(s32 player_id, const std::string_view& message); static void RequestReset(ResetRequestMessage::Reason reason, s32 causing_player_id = 0); static void SendConnectRequest(); +// Spectators +static bool IsSpectator(const ENetPeer* peer); +static s32 GetFreeSpectatorSlot(); +static s32 GetSpectatorSlotForPeer(const ENetPeer* peer); +static void DropSpectator(s32 slot_id, DropPlayerReason reason); + // Controlpackets static void HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt); static void HandleControlMessage(s32 player_id, const ENetPacket* pkt); @@ -159,6 +165,12 @@ static std::bitset s_reset_players; static Common::Timer s_reset_start_time; static Common::Timer s_last_host_connection_attempt; +// Spectators +static std::array s_spectators; +static std::bitset s_reset_spectators; +static s32 s_num_spectators = 0; +static bool s_local_spectating; + /// GGPO static std::string s_local_nickname; static std::string s_local_session_password; @@ -238,26 +250,17 @@ static bool SendControlPacket(s32 player_id, const PacketWrapper& pkt) return SendControlPacket(s_peers[player_id].peer, pkt); } template -static void SendControlPacketToAll(const PacketWrapper& pkt) +static void SendControlPacketToAll(const PacketWrapper& pkt, bool send_to_spectators) { - for (s32 i = 0; i < MAX_PLAYERS; i++) + const s32 total_peers = send_to_spectators ? MAX_PLAYERS + MAX_SPECTATORS : MAX_PLAYERS; + for (s32 i = 0; i < total_peers; i++) { - if (!s_peers[i].peer) + ENetPeer* peer_to_send = i < MAX_PLAYERS ? s_peers[i].peer : s_spectators[i - MAX_PLAYERS].peer; + if (!peer_to_send) continue; - // last one? - bool last = true; - for (s32 j = i + 1; j < MAX_PLAYERS; j++) - { - if (s_peers[j].peer) - { - last = false; - break; - } - } - - ENetPacket* pkt_to_send = last ? pkt.pkt : enet_packet_create(pkt.pkt->data, pkt.pkt->dataLength, pkt.pkt->flags); - const int rc = enet_peer_send(s_peers[i].peer, ENET_CHANNEL_CONTROL, pkt_to_send); + ENetPacket* pkt_to_send = enet_packet_create(pkt.pkt->data, pkt.pkt->dataLength, pkt.pkt->flags); + const int rc = enet_peer_send(peer_to_send, ENET_CHANNEL_CONTROL, pkt_to_send); if (rc != 0) { Log_ErrorPrintf("enet_peer_send() to player %d failed: %d", i, rc); @@ -339,7 +342,7 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re ENetAddress server_address; server_address.host = ENET_HOST_ANY; server_address.port = is_hosting ? static_cast(port) : ENET_PORT_ANY; - s_enet_host = enet_host_create(&server_address, MAX_PLAYERS - 1, NUM_ENET_CHANNELS, 0, 0); + s_enet_host = enet_host_create(&server_address, MAX_PLAYERS + MAX_SPECTATORS - 1, NUM_ENET_CHANNELS, 0, 0); if (!s_enet_host) { Log_ErrorPrintf("Failed to create enet host."); @@ -351,6 +354,7 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re s_local_delay = ldelay; s_reset_cookie = 0; s_reset_players.reset(); + s_reset_spectators.reset(); // If we're the host, we can just continue on our merry way, the others will join later. if (is_hosting) @@ -358,7 +362,9 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re // Starting session with a single player. s_player_id = 0; s_num_players = 1; + s_num_spectators = 0; s_reset_players = 1; + s_reset_spectators = 0; s_peers[s_player_id].nickname = s_local_nickname; CreateGGPOSession(); s_state = SessionState::Running; @@ -451,10 +457,16 @@ void Netplay::RequestCloseSession(CloseSessionMessage::Reason reason) // Notify everyone auto pkt = NewControlPacket(); pkt->reason = reason; - SendControlPacketToAll(pkt); + SendControlPacketToAll(pkt, true); + // close spectator connections + for (s32 i = 0; i < MAX_SPECTATORS; i++) + { + if (s_spectators[i].peer) + enet_peer_disconnect(s_spectators[i].peer, 0); + } } - // Close all connections + // Close player connections DestroyGGPOSession(); for (s32 i = 0; i < MAX_PLAYERS; i++) { @@ -486,7 +498,14 @@ void Netplay::RequestCloseSession(CloseSessionMessage::Reason reason) { const s32 player_id = GetPlayerIdForPeer(event.peer); if (player_id >= 0) + { s_peers[player_id].peer = nullptr; + return; + } + + const s32 spectator_slot = GetSpectatorSlotForPeer(event.peer); + if (spectator_slot >= 0) + s_spectators[spectator_slot].peer = nullptr; } break; @@ -542,6 +561,14 @@ void Netplay::ShutdownEnetHost() s_peers[i] = {}; } + for (u32 i = 0; i < MAX_SPECTATORS; i++) + { + if (s_spectators[i].peer) + enet_peer_reset(s_spectators[i].peer); + + s_spectators[i] = {}; + } + enet_host_destroy(s_enet_host); s_enet_host = nullptr; } @@ -572,7 +599,9 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) case ENET_EVENT_TYPE_DISCONNECT: { + const s32 spectator_slot = GetSpectatorSlotForPeer(event->peer); const s32 player_id = GetPlayerIdForPeer(event->peer); + if (s_state == SessionState::Connecting) { Assert(player_id == s_host_player_id); @@ -586,6 +615,12 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) return; } + if (spectator_slot >= 0) + { + DropSpectator(spectator_slot, DropPlayerReason::DisconnectedFromHost); + return; + } + Log_WarningPrintf("ENet player %d disconnected", player_id); if (IsValidPlayerId(player_id)) { @@ -599,8 +634,10 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) case ENET_EVENT_TYPE_RECEIVE: { - const s32 player_id = GetPlayerIdForPeer(event->peer); - if (player_id < 0) + s32 player_id = GetPlayerIdForPeer(event->peer); + const s32 spectator_slot = GetSpectatorSlotForPeer(event->peer); + + if (player_id < 0 && spectator_slot < 0) { // If it's a new connection, we need to handle connection request messages. if (event->channelID == ENET_CHANNEL_CONTROL && IsHost()) @@ -611,6 +648,9 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) if (event->channelID == ENET_CHANNEL_CONTROL) { + if (player_id < 0) + player_id = spectator_slot + MAX_PLAYERS + 1; + HandleControlMessage(player_id, event->packet); } else if (event->channelID == ENET_CHANNEL_GGPO) @@ -736,8 +776,36 @@ void Netplay::CreateGGPOSession() cb.free_buffer = NpFreeBuffCb; cb.on_event = NpOnEventCb; + if (s_local_spectating) + { + ggpo_start_spectating(&s_ggpo, &cb, s_num_players, sizeof(Netplay::Input), s_peers[s_host_player_id].peer); + return; + } + ggpo_start_session(&s_ggpo, &cb, s_num_players, sizeof(Netplay::Input), MAX_ROLLBACK_FRAMES); + // if you are the host be sure to add the needed spectators to the session before the players + // this way we prevent the session finishing to synchronize before adding the spectators. + if (IsHost()) + { + for (s32 i = 0; i < MAX_SPECTATORS; i++) + { + // slot filled? + if (!s_spectators[i].peer) + continue; + + GGPOErrorCode result; + GGPOPlayer player = {sizeof(GGPOPlayer)}; + + player.type = GGPO_PLAYERTYPE_SPECTATOR; + player.u.remote.peer = s_spectators[i].peer; + result = ggpo_add_player(s_ggpo, &player, &s_spectators[i].ggpo_handle); + + // It's a new session, this should always succeed... + Assert(GGPO_SUCCEEDED(result)); + } + } + int player_num = 1; for (s32 i = 0; i < MAX_PLAYERS; i++) { @@ -890,7 +958,8 @@ void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt) Log_InfoPrintf("Received session details from host: "); Log_InfoPrintf(" Console Region: %s", Settings::GetConsoleRegionDisplayName(msg->settings.console_region)); - Log_InfoPrintf(" BIOS Hash: %s%s", msg->bios_hash.ToString().c_str(), msg->settings.was_fast_booted ? " (fast booted)" : ""); + Log_InfoPrintf(" BIOS Hash: %s%s", msg->bios_hash.ToString().c_str(), + msg->settings.was_fast_booted ? " (fast booted)" : ""); Log_InfoPrintf(" Game Serial: %.*s", msg->game_serial_length, msg->GetGameSerial().data()); Log_InfoPrintf(" Game Title: %.*s", msg->game_title_length, msg->GetGameTitle().data()); Log_InfoPrintf(" Game Hash: %" PRIX64, msg->game_hash); @@ -947,10 +1016,11 @@ void Netplay::SendConnectRequest() { DebugAssert(!IsHost()); - Log_DevPrintf("Sending connect request to host with player id %d", s_player_id); + std::string req = s_local_spectating ? "as a spectator" : fmt::format("with player id {}", s_player_id); + Log_DevPrintf("Sending connect request to host %s", req.c_str()); auto pkt = NewControlPacket(); - pkt->mode = JoinRequestMessage::Mode::Player; + pkt->mode = s_local_spectating ? JoinRequestMessage::Mode::Spectator : JoinRequestMessage::Mode::Player; pkt->requested_player_id = s_player_id; std::memset(pkt->nickname, 0, sizeof(pkt->nickname)); std::memset(pkt->session_password, 0, sizeof(pkt->session_password)); @@ -959,6 +1029,55 @@ void Netplay::SendConnectRequest() SendControlPacket(s_peers[s_host_player_id].peer, pkt); } +bool Netplay::IsSpectator(const ENetPeer* peer) +{ + for (s32 i = 0; i < MAX_SPECTATORS; i++) + { + if (s_spectators[i].peer == peer) + return true; + } + return false; +} + +s32 Netplay::GetFreeSpectatorSlot() +{ + for (s32 i = 0; i < MAX_SPECTATORS; i++) + { + if (!s_spectators[i].peer) + return i; + } + return -1; +} + +s32 Netplay::GetSpectatorSlotForPeer(const ENetPeer* peer) +{ + for (s32 i = 0; i < MAX_SPECTATORS; i++) + { + if (s_spectators[i].peer == peer) + return i; + } + return -1; +} + +void Netplay::DropSpectator(s32 slot_id, DropPlayerReason reason) +{ + Assert(IsHost()); + DebugAssert(s_spectators[slot_id].peer); + Log_InfoPrintf("Dropping Spectator %d: %s", slot_id, s_spectators[slot_id].nickname.c_str()); + + Host::OnNetplayMessage( + fmt::format(Host::TranslateString("Netplay", "Spectator {} left the session: {}").GetCharArray(), slot_id, + s_spectators[slot_id].nickname, DropPlayerReasonToString(reason))); + + enet_peer_disconnect_now(s_spectators[slot_id].peer, 0); + s_spectators[slot_id] = {}; + s_num_spectators--; + // sadly we have to reset here. this really sucks for the active players since you dont really want to halt for a spectator. + // not resetting seems to be creating index out of bounds errors in the ringbuffer. + // TODO ? + Reset(); +} + void Netplay::UpdateConnectingState() { if (s_reset_start_time.GetTimeSeconds() >= MAX_CONNECT_TIME) @@ -1011,11 +1130,34 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt) return; } - // TODO: Spectators shouldn't get assigned a real player ID, they should go into a separate peer list. - if (msg->mode != JoinRequestMessage::Mode::Player) + if (msg->mode == JoinRequestMessage::Mode::Spectator) { - response->result = JoinResponseMessage::Result::SessionClosed; + // something is really wrong if this isn't the host. + Assert(IsHost()); + + const s32 spectator_slot = GetFreeSpectatorSlot(); + // no free slots? notify the peer. + if (spectator_slot < 0) + { + response->result = JoinResponseMessage::Result::ServerFull; + SendControlPacket(peer, response); + return; + } + + Assert(s_num_spectators < MAX_SPECTATORS); + response->result = JoinResponseMessage::Result::Success; + response->player_id = MAX_PLAYERS + 1 + spectator_slot; SendControlPacket(peer, response); + // continue and add peer to the list. + s_spectators[spectator_slot].peer = peer; + s_spectators[spectator_slot].nickname = msg->GetNickname(); + s_num_spectators++; + // Force everyone to resync for now. sadly since ggpo currently only supports adding spectators during setup.. + Reset(); + // notify host that the spectator joined + Host::OnNetplayMessage( + fmt::format(Host::TranslateString("Netplay", "{} is joining the session as a Spectator.").GetCharArray(), + msg->GetNickname())); return; } @@ -1123,6 +1265,7 @@ void Netplay::HandleJoinResponseMessage(s32 player_id, const ENetPacket* pkt) s_player_id = msg->player_id; s_state = SessionState::Resetting; s_reset_players.reset(); + s_reset_spectators.reset(); s_reset_start_time.Reset(); } @@ -1197,9 +1340,10 @@ void Netplay::Reset() // Any GGPO packets will get dropped, since the session's gone temporarily. DestroyGGPOSession(); - for (s32 i = 0; i < MAX_PLAYERS; i++) + for (s32 i = 0; i < MAX_PLAYERS + MAX_SPECTATORS; i++) { - if (!s_peers[i].peer) + ENetPeer* peer_to_send = i < MAX_PLAYERS ? s_peers[i].peer : s_spectators[i - MAX_PLAYERS].peer; + if (!peer_to_send) continue; ENetPacket* pkt = enet_packet_create(nullptr, sizeof(header) + state_data_size, ENET_PACKET_FLAG_RELIABLE); @@ -1207,7 +1351,7 @@ void Netplay::Reset() std::memcpy(pkt->data + sizeof(header), state.GetMemoryPointer(), state_data_size); // This should never fail, we get errors back later.. - const int rc = enet_peer_send(s_peers[i].peer, ENET_CHANNEL_CONTROL, pkt); + const int rc = enet_peer_send(peer_to_send, ENET_CHANNEL_CONTROL, pkt); if (rc != 0) Log_ErrorPrintf("enet_peer_send() for synchronization request failed: %d", rc); } @@ -1222,6 +1366,7 @@ void Netplay::Reset() s_state = SessionState::Resetting; s_reset_players.reset(); + s_reset_spectators.reset(); s_reset_players.set(s_player_id); s_reset_start_time.Reset(); } @@ -1246,11 +1391,15 @@ void Netplay::HandleResetMessage(s32 player_id, const ENetPacket* pkt) DestroyGGPOSession(); // Make sure we're connected to all players. - Assert(msg->num_players > 1); + Assert(msg->num_players > 1 || s_local_spectating); Log_DevPrintf("Checking connections"); s_num_players = msg->num_players; for (s32 i = 0; i < MAX_PLAYERS; i++) { + // We are already connected to the host as a spectator we dont need any other connections + if (s_local_spectating) + continue; + Peer& p = s_peers[i]; if (msg->players[i].controller_port < 0) { @@ -1314,6 +1463,15 @@ void Netplay::HandleResetMessage(s32 player_id, const ENetPacket* pkt) s_state = SessionState::Resetting; s_reset_cookie = msg->cookie; s_reset_players.reset(); + s_reset_spectators.reset(); + + if (s_local_spectating) + { + s_reset_spectators.set(s_player_id - (MAX_PLAYERS + 1)); + s_reset_start_time.Reset(); + return; + } + s_reset_players.set(s_player_id); s_reset_start_time.Reset(); } @@ -1329,6 +1487,13 @@ void Netplay::HandleResetCompleteMessage(s32 player_id, const ENetPacket* pkt) Log_ErrorPrintf("Received unexpected reset complete from player %d", player_id); return; } + else if (player_id > MAX_PLAYERS && player_id <= MAX_PLAYERS + MAX_SPECTATORS) + { + const s32 spectator_slot = player_id - (MAX_PLAYERS + 1); + s_reset_spectators.set(spectator_slot); + Log_DevPrintf("Spectator %d is now reset and ready", spectator_slot); + return; + } else if (s_reset_players.test(player_id)) { Log_ErrorPrintf("Received double reset from player %d", player_id); @@ -1363,12 +1528,14 @@ void Netplay::HandleResumeSessionMessage(s32 player_id, const ENetPacket* pkt) void Netplay::UpdateResetState() { + const s32 num_players = (s_local_spectating ? 1 : s_num_players); if (IsHost()) { - if (static_cast(s_reset_players.count()) == s_num_players) + if (static_cast(s_reset_players.count()) == num_players && + static_cast(s_reset_spectators.count()) == s_num_spectators) { - Log_VerbosePrintf("All players synchronized, resuming!"); - SendControlPacketToAll(NewControlPacket()); + Log_VerbosePrintf("All players and spectators synchronized, resuming!"); + SendControlPacketToAll(NewControlPacket(), true); CreateGGPOSession(); s_state = SessionState::Running; return; @@ -1388,11 +1555,21 @@ void Netplay::UpdateResetState() Log_DevPrintf("Dropping player %d because they didn't connect in time", i); DropPlayer(i, DropPlayerReason::ConnectTimeout); } + + for (s32 i = 0; i < MAX_SPECTATORS; i++) + { + if (s_reset_spectators.test(i)) + continue; + + // we'll check if we're done again next loop + Log_DevPrintf("Dropping Spectator %d because they didn't connect in time", i); + DropSpectator(i, DropPlayerReason::ConnectTimeout); + } } } else { - if (static_cast(s_reset_players.count()) != s_num_players) + if (static_cast(s_reset_players.count()) != num_players) { for (s32 i = 0; i < MAX_PLAYERS; i++) { @@ -1403,7 +1580,7 @@ void Netplay::UpdateResetState() s_reset_players.set(i); } - if (static_cast(s_reset_players.count()) == s_num_players) + if (static_cast(s_reset_players.count()) == num_players) { // now connected to all! Log_InfoPrintf("Connected to %d players, waiting for host...", s_num_players); @@ -1421,8 +1598,14 @@ void Netplay::UpdateResetState() } } + // Log_InfoPrintf("p:%d/s:%d", num_players, s_num_spectators); + + const s32 min_progress = IsHost() ? static_cast(s_reset_players.count() + s_reset_spectators.count()) : + static_cast(s_reset_players.count()); + const s32 max_progress = IsHost() ? s_num_players + s_num_spectators : num_players; + PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16)); - Host::DisplayLoadingScreen("Netplay synchronizing", 0, static_cast(s_reset_players.count()), s_num_players); + Host::DisplayLoadingScreen("Netplay synchronizing", 0, min_progress, max_progress); Host::PumpMessagesOnCPUThread(); } @@ -1454,7 +1637,7 @@ void Netplay::NotifyPlayerJoined(s32 player_id) { auto pkt = NewControlPacket(); pkt->player_id = player_id; - SendControlPacketToAll(pkt); + SendControlPacketToAll(pkt, false); } Host::OnNetplayMessage( @@ -1498,7 +1681,7 @@ void Netplay::DropPlayer(s32 player_id, DropPlayerReason reason) auto pkt = NewControlPacket(); pkt->reason = reason; pkt->player_id = player_id; - SendControlPacketToAll(pkt); + SendControlPacketToAll(pkt, false); // resync with everyone who's left Reset(); @@ -1830,7 +2013,8 @@ void Netplay::SendChatMessage(const std::string_view& msg) auto pkt = NewControlPacket(sizeof(ChatMessage) + static_cast(msg.length())); std::memcpy(pkt.pkt->data + sizeof(ChatMessage), msg.data(), msg.length()); - SendControlPacketToAll(pkt); + // TODO: turn chat on for spectators? it's kind of weird to handle. probably has to go through the host and be relayed to the players. + SendControlPacketToAll(pkt, false); // add own netplay message locally to netplay messages ShowChatMessage(s_player_id, msg); @@ -1876,7 +2060,7 @@ bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std // TODO: This is going to blow away our memory cards, because for sync purposes we want all clients // to have the same data, and we don't want to trash their local memcards. We should therefore load // the memory cards for this game (based on game/global settings), and copy that to the temp card. - + // TODO: input delay. GGPO Should support changing it on the fly. const s32 input_delay = 1; @@ -1894,9 +2078,11 @@ bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std return true; } -bool Netplay::JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password) +bool Netplay::JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password, + bool spectating) { s_local_session_password = std::move(password); + s_local_spectating = spectating; // TODO: input delay. GGPO Should support changing it on the fly. const s32 input_delay = 1; diff --git a/src/core/netplay.h b/src/core/netplay.h index 082b3e60b..f72d79c86 100644 --- a/src/core/netplay.h +++ b/src/core/netplay.h @@ -10,6 +10,9 @@ enum : s32 // Maximum number of emulated controllers. MAX_PLAYERS = 2, + // Maximum number of spectators allowed to watch the session. + MAX_SPECTATORS = 4, + // Maximum netplay prediction frames MAX_ROLLBACK_FRAMES = 8, @@ -29,7 +32,7 @@ enum : u8 }; bool CreateSession(std::string nickname, s32 port, s32 max_players, std::string password); -bool JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password); +bool JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password, bool spectating); bool IsActive(); diff --git a/src/duckstation-qt/joinnetplaysessiondialog.ui b/src/duckstation-qt/joinnetplaysessiondialog.ui index 603884899..0a2e8d97e 100644 --- a/src/duckstation-qt/joinnetplaysessiondialog.ui +++ b/src/duckstation-qt/joinnetplaysessiondialog.ui @@ -10,7 +10,7 @@ 0 0 480 - 220 + 226 @@ -146,11 +146,28 @@ - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - + + + + + Qt::LeftToRight + + + Enable Spectator Mode + + + true + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + diff --git a/src/duckstation-qt/netplaydialogs.cpp b/src/duckstation-qt/netplaydialogs.cpp index 3010dc218..8b767bc14 100644 --- a/src/duckstation-qt/netplaydialogs.cpp +++ b/src/duckstation-qt/netplaydialogs.cpp @@ -82,9 +82,10 @@ void JoinNetplaySessionDialog::accept() const QString& nickname = m_ui.nickname->text(); const QString& hostname = m_ui.hostname->text(); const QString& password = m_ui.password->text(); + const bool spectating = m_ui.spectating->isChecked(); QDialog::accept(); - g_emu_thread->joinNetplaySession(nickname.trimmed(), hostname.trimmed(), port, password); + g_emu_thread->joinNetplaySession(nickname.trimmed(), hostname.trimmed(), port, password, spectating); } bool JoinNetplaySessionDialog::validate() diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 843b25791..c81861e1a 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1092,16 +1092,17 @@ void EmuThread::createNetplaySession(const QString& nickname, qint32 port, qint3 } void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port, - const QString& password) + const QString& password, bool spectating) { if (!isOnThread()) { QMetaObject::invokeMethod(this, "joinNetplaySession", Qt::QueuedConnection, Q_ARG(const QString&, nickname), - Q_ARG(const QString&, hostname), Q_ARG(qint32, port), Q_ARG(const QString&, password)); + Q_ARG(const QString&, hostname), Q_ARG(qint32, port), Q_ARG(const QString&, password), + Q_ARG(bool, spectating)); return; } - if (!Netplay::JoinSession(nickname.toStdString(), hostname.toStdString(), port, password.toStdString())) + if (!Netplay::JoinSession(nickname.toStdString(), hostname.toStdString(), port, password.toStdString(), spectating)) { errorReported(tr("Netplay Error"), tr("Failed to join netplay session. The log may contain more information.")); return; @@ -2246,7 +2247,7 @@ int main(int argc, char* argv[]) } else { - g_emu_thread->joinNetplaySession(nickname, remote, port, QString()); + g_emu_thread->joinNetplaySession(nickname, remote, port, QString(), false); } }); } diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index b64c66e8f..cfebfe7ac 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -188,7 +188,8 @@ public Q_SLOTS: void applyCheat(quint32 index); void reloadPostProcessingShaders(); void createNetplaySession(const QString& nickname, qint32 port, qint32 max_players, const QString& password); - void joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port, const QString& password); + void joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port, const QString& password, + bool spectating); private Q_SLOTS: void stopInThread();