From a7e787456c075221903d95059dbfc82e4305e8e1 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 11 May 2023 00:07:18 +1000 Subject: [PATCH] Netplay: Improve disconnection handling --- src/common/windows_headers.h | 3 + src/core/netplay.cpp | 959 +++++++++++++++++--------- src/core/netplay.h | 11 +- src/core/netplay_packets.h | 241 +++++++ src/core/system.cpp | 2 +- src/duckstation-qt/qthost.cpp | 3 + src/frontend-common/imgui_netplay.cpp | 23 +- 7 files changed, 908 insertions(+), 334 deletions(-) create mode 100644 src/core/netplay_packets.h diff --git a/src/common/windows_headers.h b/src/common/windows_headers.h index 1830d22b4..1587fae6d 100644 --- a/src/common/windows_headers.h +++ b/src/common/windows_headers.h @@ -27,3 +27,6 @@ #if defined(DeleteFile) #undef DeleteFile #endif +#if defined(GetMessage) +#undef GetMessage +#endif diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp index aa9728b17..a86133f32 100644 --- a/src/core/netplay.cpp +++ b/src/core/netplay.cpp @@ -10,6 +10,7 @@ #include "digital_controller.h" #include "host.h" #include "host_settings.h" +#include "netplay_packets.h" #include "pad.h" #include "save_state_version.h" #include "spu.h" @@ -31,81 +32,6 @@ Log_SetChannel(Netplay); namespace Netplay { -// TODO: Put this in a header.. -enum class SessionState -{ - Inactive, - Initializing, - Connecting, - Synchronizing, - Running, -}; - -enum class ControlMessage : u32 -{ - ConnectResponse, - SynchronizeSession, - SynchronizeComplete, - ChatMessage, -}; - -#pragma pack(push, 1) -struct ControlMessageHeader -{ - ControlMessage type; - u32 size; -}; - -struct ControlConnectResponseMessage -{ - enum class Result : u32 - { - Success = 0, - ServerFull, - PlayerIDInUse, - }; - - Result result; - s32 player_id; - - static ControlMessage MessageType() { return ControlMessage::ConnectResponse; } -}; - -struct ControlSynchronizeSessionMessage -{ - struct PlayerAddress - { - u32 host; - u16 port; - s16 controller_port; // -1 if not present - }; - - ControlMessageHeader header; - s32 num_players; - PlayerAddress players[MAX_PLAYERS]; - u32 state_data_size; - // state_data_size bytes of state data follows - - static ControlMessage MessageType() { return ControlMessage::SynchronizeSession; } -}; - -struct ControlSynchronizeCompleteMessage -{ - ControlMessageHeader header; - - static ControlMessage MessageType() { return ControlMessage::SynchronizeComplete; } -}; - -struct ControlChatMessage -{ - ControlMessageHeader header; - - u32 chat_message_size; - - static ControlMessage MessageType() { return ControlMessage::ChatMessage; } -}; -#pragma pack(pop) - using SaveStateBuffer = std::unique_ptr; struct Input @@ -113,6 +39,10 @@ struct Input u32 button_data; }; +// TODO: Might be a bit generous... should we move this to config? +static constexpr float MAX_CONNECT_TIME = 15.0f; +static constexpr float MAX_CLOSE_TIME = 3.0f; + static bool NpAdvFrameCb(void* ctx, int flags); static bool NpSaveFrameCb(void* ctx, unsigned char** buffer, int* len, int* checksum, int frame); static bool NpLoadFrameCb(void* ctx, unsigned char* buffer, int len, int rb_frames, int frame_to_load); @@ -127,9 +57,9 @@ static void SetInputs(Input inputs[2]); static void SetSettings(); -static bool CreateSystem(std::string game_path); - -// TODO: Fatal error and shutdown helper +static bool CreateSystem(std::string game_path, bool hosting); +static void CloseSessionWithError(const std::string_view& message); +static void RequestCloseSession(CloseSessionMessage::Reason reason); // ENet static bool InitializeEnet(); @@ -144,28 +74,41 @@ static bool IsValidPlayerId(s32 player_id); static s32 GetFreePlayerId(); static ENetPeer* GetPeerForPlayer(s32 player_id); static s32 GetPlayerIdForPeer(const ENetPeer* peer); -static bool WaitForPeerConnections(); +std::string_view GetNicknameForPlayer(s32 player_id); +static void NotifyPlayerJoined(s32 player_id); +static void DropPlayer(s32 player_id, DropPlayerReason reason); +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(); // Controlpackets +static void HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt); static void HandleControlMessage(s32 player_id, const ENetPacket* pkt); static void HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt); -static void HandleSynchronizeSessionMessage(s32 player_id, const ENetPacket* pkt); -static void HandleSynchronizeCompleteMessage(s32 player_id, const ENetPacket* pkt); -static void HandleControlChatMessage(s32 player_id, const ENetPacket* pkt); +static void HandleResetMessage(s32 player_id, const ENetPacket* pkt); +static void HandleResetCompleteMessage(s32 player_id, const ENetPacket* pkt); +static void HandleResumeSessionMessage(s32 player_id, const ENetPacket* pkt); +static void HandleResetRequestMessage(s32 player_id, const ENetPacket* pkt); +static void HandlePlayerJoinedMessage(s32 player_id, const ENetPacket* pkt); +static void HandleDropPlayerMessage(s32 player_id, const ENetPacket* pkt); +static void HandleCloseSessionMessage(s32 player_id, const ENetPacket* pkt); +static void HandleChatMessage(s32 player_id, const ENetPacket* pkt); // l = local, r = remote -static bool CreateGGPOSession(); +static void CreateGGPOSession(); static void DestroyGGPOSession(); static bool Start(s32 lhandle, u16 lport, const std::string& raddr, u16 rport, s32 ldelay, u32 pred, std::string game_path); +static void CloseSession(); // Host functions. -static void HandlePeerConnectionAsHost(ENetPeer* peer, s32 claimed_player_id); +static void HandlePeerConnectionAsHost(ENetPeer* peer); static void HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_id); static void HandlePeerDisconnectionAsHost(s32 player_id); static void HandlePeerDisconnectionAsNonHost(s32 player_id); -static void Resynchronize(); -static void CheckForCompleteResynchronize(); +static void Reset(); +static void UpdateResetState(); +static void UpdateConnectingState(); static void AdvanceFrame(); static void RunFrame(); @@ -193,6 +136,7 @@ static SessionState s_state; struct Peer { ENetPeer* peer; + std::string nickname; GGPOPlayerHandle ggpo_handle; }; static ENetHost* s_enet_host = nullptr; @@ -200,8 +144,12 @@ static std::array s_peers; static s32 s_host_player_id = 0; static s32 s_player_id = 0; static s32 s_num_players = 0; -static s32 s_synchronized_players = 0; // only valid on host +static u32 s_reset_cookie = 0; +static std::bitset s_reset_players; +static Common::Timer s_reset_start_time; +/// GGPO +static std::string s_local_nickname; static GGPOPlayerHandle s_local_handle = GGPO_INVALID_HANDLE; static s32 s_local_delay = 0; static GGPONetworkStats s_last_net_stats{}; @@ -221,6 +169,19 @@ static s32 s_next_timesync_recovery_frame = -1; // Packet helpers ////////////////////////////////////////////////////////////////////////// +static std::string DropPlayerReasonToString(DropPlayerReason reason) +{ + switch (reason) + { + case DropPlayerReason::ConnectTimeout: + return Host::TranslateStdString("Netplay", "Connection timeout"); + case DropPlayerReason::DisconnectedFromHost: + return Host::TranslateStdString("Netplay", "Disconnected from host"); + default: + return "Unknown"; + } +} + template struct PacketWrapper { @@ -262,6 +223,54 @@ static bool SendControlPacket(s32 player_id, const PacketWrapper& pkt) DebugAssert(player_id >= 0 && player_id < MAX_PLAYERS && s_peers[player_id].peer); return SendControlPacket(s_peers[player_id].peer, pkt); } +template +static void SendControlPacketToAll(const PacketWrapper& pkt) +{ + for (s32 i = 0; i < MAX_PLAYERS; i++) + { + if (!s_peers[i].peer) + 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); + if (rc != 0) + { + Log_ErrorPrintf("enet_peer_send() to player %d failed: %d", i, rc); + enet_packet_destroy(pkt.pkt); + } + } +} +template +static const T* CheckReceivedPacket(s32 player_id, const ENetPacket* pkt) +{ + if (pkt->dataLength < sizeof(T)) + { + Log_ErrorPrintf("Received too-short control packet %u from player %d", static_cast(T::MessageType()), + player_id); + return nullptr; + } + + const ControlMessageHeader* hdr = reinterpret_cast(pkt->data); + if (hdr->size < sizeof(T)) + { + Log_ErrorPrintf("Received too-short control packet %u from player %d [inner field]", + static_cast(T::MessageType()), player_id); + return nullptr; + } + + return reinterpret_cast(pkt->data); +} } // namespace Netplay // Netplay Impl @@ -269,11 +278,12 @@ static bool SendControlPacket(s32 player_id, const PacketWrapper& pkt) bool Netplay::Start(s32 lhandle, u16 lport, const std::string& raddr, u16 rport, s32 ldelay, u32 pred, std::string game_path) { + const bool is_hosting = (lhandle == 1); s_state = SessionState::Initializing; SetSettings(); - if (!CreateSystem(std::move(game_path))) + if (!CreateSystem(std::move(game_path), is_hosting)) { Log_ErrorPrintf("Failed to create system."); return false; @@ -297,24 +307,21 @@ bool Netplay::Start(s32 lhandle, u16 lport, const std::string& raddr, u16 rport, } s_host_player_id = 0; + s_local_nickname = fmt::format("NICKNAME{}", lhandle); s_local_delay = ldelay; + s_reset_cookie = 0; + s_reset_players.reset(); // If we're the host, we can just continue on our merry way, the others will join later. - if (lhandle == 1) + if (is_hosting) { // Starting session with a single player. s_player_id = 0; s_num_players = 1; - s_synchronized_players = 1; - - if (!CreateGGPOSession()) - { - Log_ErrorPrintf("Failed to create GGPO session for host."); - return false; - } - - Log_InfoPrintf("Netplay session started as host."); + s_reset_players = 1; + CreateGGPOSession(); s_state = SessionState::Running; + Log_InfoPrintf("Netplay session started as host."); return true; } @@ -340,12 +347,21 @@ bool Netplay::Start(s32 lhandle, u16 lport, const std::string& raddr, u16 rport, // Wait until we're connected to the main host. They'll send us back state to load and a full player list. s_state = SessionState::Connecting; + s_reset_start_time.Reset(); return true; } +void Netplay::SystemDestroyed() +{ + // something tried to shut us down.. + RequestCloseSession(CloseSessionMessage::Reason::HostShutdown); +} + void Netplay::CloseSession() { - Assert(IsActive()); + Assert(IsActive() || s_state == SessionState::ClosingSession); + + const bool was_host = IsHost(); DestroyGGPOSession(); ShutdownEnetHost(); @@ -353,14 +369,20 @@ void Netplay::CloseSession() // Restore original settings. Host::Internal::SetNetplaySettingsLayer(nullptr); System::ApplySettings(false); + + s_state = SessionState::Inactive; + + // Shut down the VM too, if we're not the host. + if (!was_host) + System::ShutdownSystem(false); } bool Netplay::IsActive() { - return (s_state != SessionState::Inactive); + return (s_state >= SessionState::Initializing && s_state <= SessionState::Running); } -bool Netplay::CreateSystem(std::string game_path) +bool Netplay::CreateSystem(std::string game_path, bool is_hosting) { // close system if its already running if (System::IsValid()) @@ -372,17 +394,90 @@ bool Netplay::CreateSystem(std::string game_path) if (!System::BootSystem(param)) return false; -#if 1 - // Fast Forward to Game Start if needed. - SPU::SetAudioOutputMuted(true); - while (System::GetInternalFrameNumber() < 2) - System::RunFrame(); - SPU::SetAudioOutputMuted(false); -#endif + if (is_hosting) + { + // Fast Forward to Game Start if needed. + SPU::SetAudioOutputMuted(true); + while (System::GetInternalFrameNumber() < 2) + System::RunFrame(); + SPU::SetAudioOutputMuted(false); + } return true; } +void Netplay::CloseSessionWithError(const std::string_view& message) +{ + Host::ReportErrorAsync(Host::TranslateString("Netplay", "Netplay Error"), message); + s_state = SessionState::ClosingSession; +} + +void Netplay::RequestCloseSession(CloseSessionMessage::Reason reason) +{ + if (IsHost()) + { + // Notify everyone + auto pkt = NewControlPacket(); + pkt->reason = reason; + SendControlPacketToAll(pkt); + } + + // Close all connections + DestroyGGPOSession(); + for (s32 i = 0; i < MAX_PLAYERS; i++) + { + if (s_peers[i].peer) + { + if (IsHost()) + enet_peer_disconnect_later(s_peers[i].peer, 0); + else + enet_peer_disconnect(s_peers[i].peer, 0); + } + } + + // but wait for them to actually drop + s_state = SessionState::ClosingSession; + s_reset_start_time.Reset(); + + // if we have a system, we can display the visual, otherwise just get out of here + // that might happen if they click shutdown, then shutdown again and don't wait + while (System::IsValid() && s_reset_start_time.GetTimeSeconds() < MAX_CLOSE_TIME) + { + // Just check that all players have disconnected. + // We don't want to handle any requests here. + ENetEvent event; + if (enet_host_service(s_enet_host, &event, 1) >= 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_DISCONNECT: + { + const s32 player_id = GetPlayerIdForPeer(event.peer); + if (player_id >= 0) + s_peers[player_id].peer = nullptr; + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + // Discard all packets. + enet_packet_destroy(event.packet); + } + break; + + default: + break; + } + } + + if (std::none_of(s_peers.begin(), s_peers.end(), [](const Peer& p) { return p.peer != nullptr; })) + return; + + Host::DisplayLoadingScreen("Closing session"); + Host::PumpMessagesOnCPUThread(); + } +} + bool Netplay::InitializeEnet() { static bool enet_initialized = false; @@ -400,17 +495,16 @@ bool Netplay::InitializeEnet() void Netplay::ShutdownEnetHost() { - Log_DevPrint("Disconnecting all peers"); + if (!s_enet_host) + return; - // forcefully disconnect all peers - // TODO: do we want to send disconnect requests and wait a bit? + Log_DevPrint("Shutting down Enet host"); for (u32 i = 0; i < MAX_PLAYERS; i++) { if (s_peers[i].peer) - { enet_peer_reset(s_peers[i].peer); - s_peers[i].peer = nullptr; - } + + s_peers[i] = {}; } enet_host_destroy(s_enet_host); @@ -433,7 +527,7 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) case ENET_EVENT_TYPE_CONNECT: { if (IsHost()) - HandlePeerConnectionAsHost(event->peer, static_cast(event->data)); + HandlePeerConnectionAsHost(event->peer); else HandlePeerConnectionAsNonHost(event->peer, static_cast(event->data)); @@ -450,7 +544,7 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) Panic("Failed to connect to host"); return; } - else if (s_state == SessionState::Synchronizing) + else if (s_state == SessionState::Resetting) { // let the timeout deal with it Log_DevPrintf("Ignoring disconnection from %d while synchronizing", player_id); @@ -458,13 +552,13 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) } Log_WarningPrintf("ENet player %d disconnected", player_id); - Host::OnNetplayMessage(fmt::format("*** DISCONNECTED PLAYER {} ***", player_id)); - - Assert(IsValidPlayerId(player_id)); - if (IsHost()) - HandlePeerDisconnectionAsHost(player_id); - else - HandlePeerDisconnectionAsNonHost(player_id); + if (IsValidPlayerId(player_id)) + { + if (IsHost()) + HandlePeerDisconnectionAsHost(player_id); + else + HandlePeerDisconnectionAsNonHost(player_id); + } } break; @@ -473,7 +567,10 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) const s32 player_id = GetPlayerIdForPeer(event->peer); if (player_id < 0) { - Log_WarningPrintf("Received packet from unknown player"); + // If it's a new connection, we need to handle connection request messages. + if (event->channelID == ENET_CHANNEL_CONTROL) + HandleMessageFromNewPeer(event->peer, event->packet); + enet_packet_destroy(event->packet); return; } @@ -492,6 +589,8 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) { Log_ErrorPrintf("Unexpected packet channel %u", event->channelID); } + + enet_packet_destroy(event->packet); } break; @@ -509,7 +608,7 @@ void Netplay::PollEnet(Common::Timer::Value until_time) u64 current_time = Common::Timer::GetCurrentValue(); - for (;;) + while (IsActive()) { const u32 enet_timeout = (current_time >= until_time) ? 0 : @@ -578,45 +677,14 @@ s32 Netplay::GetFreePlayerId() return -1; } -bool Netplay::WaitForPeerConnections() +std::string_view Netplay::GetNicknameForPlayer(s32 player_id) { - static constexpr float MAX_CONNECT_TIME = 30.0f; - Common::Timer timeout; - - const s32 clients_to_connect = s_num_players - 1; - Log_VerbosePrintf("Waiting for connection to %d peers", clients_to_connect); - - for (;;) - { - // TODO: Handle early shutdown/cancel request. - s32 num_connected_peers = 0; - for (s32 i = 0; i < MAX_PLAYERS; i++) - { - if (i != s_player_id && s_peers[i].peer && s_peers[i].peer->state == ENET_PEER_STATE_CONNECTED) - num_connected_peers++; - } - if (num_connected_peers == clients_to_connect) - break; - - if (timeout.GetTimeSeconds() >= MAX_CONNECT_TIME) - { - Log_ErrorPrintf("Peer connection timeout"); - return false; - } - - Host::PumpMessagesOnCPUThread(); - Host::DisplayLoadingScreen("Connected to netplay peers", 0, clients_to_connect, num_connected_peers); - - const Common::Timer::Value poll_end_time = - Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16); - PollEnet(poll_end_time); - } - - Log_InfoPrint("Peer connection complete."); - return true; + return IsValidPlayerId(player_id) ? + std::string_view(player_id == s_player_id ? s_local_nickname : s_peers[player_id].nickname) : + std::string_view(); } -bool Netplay::CreateGGPOSession() +void Netplay::CreateGGPOSession() { /* TODO: since saving every frame during rollback loses us time to do actual gamestate iterations it might be better to @@ -655,16 +723,12 @@ bool Netplay::CreateGGPOSession() result = ggpo_add_player(s_ggpo, &player, &s_peers[i].ggpo_handle); } - if (!GGPO_SUCCEEDED(result)) - { - Log_ErrorPrintf("Failed to add player %d", i); - return false; - } + // It's a new session, this should always succeed... + Assert(GGPO_SUCCEEDED(result)); } ggpo_set_frame_delay(s_ggpo, s_local_handle, s_local_delay); InitializeFramePacing(); - return true; } void Netplay::DestroyGGPOSession() @@ -697,16 +761,36 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt) HandleConnectResponseMessage(player_id, pkt); break; - case ControlMessage::SynchronizeSession: - HandleSynchronizeSessionMessage(player_id, pkt); + case ControlMessage::Reset: + HandleResetMessage(player_id, pkt); break; - case ControlMessage::SynchronizeComplete: - HandleSynchronizeCompleteMessage(player_id, pkt); + case ControlMessage::ResetComplete: + HandleResetCompleteMessage(player_id, pkt); + break; + + case ControlMessage::ResumeSession: + HandleResumeSessionMessage(player_id, pkt); + break; + + case ControlMessage::PlayerJoined: + HandlePlayerJoinedMessage(player_id, pkt); + break; + + case ControlMessage::DropPlayer: + HandleDropPlayerMessage(player_id, pkt); + break; + + case ControlMessage::ResetRequest: + HandleResetRequestMessage(player_id, pkt); + break; + + case ControlMessage::CloseSession: + HandleCloseSessionMessage(player_id, pkt); break; case ControlMessage::ChatMessage: - HandleControlChatMessage(player_id, pkt); + HandleChatMessage(player_id, pkt); break; default: @@ -715,59 +799,103 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt) } } -void Netplay::HandlePeerConnectionAsHost(ENetPeer* peer, s32 claimed_player_id) +void Netplay::HandlePeerConnectionAsHost(ENetPeer* peer) { - Log_VerbosePrint( - fmt::format("New host peer connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id) - .c_str()); + // don't do anything until they send a connect request + // TODO: we might want to put an idle timeout here... + Log_InfoPrintf(fmt::format("New peer connection from {}", PeerAddressString(peer)).c_str()); +} - PacketWrapper response = NewControlPacket(); +void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt) +{ + const ConnectRequestMessage* msg = CheckReceivedPacket(-1, pkt); + if (!msg || msg->header.type != ControlMessage::ConnectRequest) + { + Log_WarningPrintf("Received unknown packet from unknown player"); + enet_peer_reset(peer); + return; + } + + Log_VerbosePrint(fmt::format("New host peer connection from {} claiming player ID {}", PeerAddressString(peer), + msg->requested_player_id) + .c_str()); + + PacketWrapper response = NewControlPacket(); response->player_id = -1; + // TODO: Spectators shouldn't get assigned a real player ID, they should go into a separate peer list. + if (msg->mode != ConnectRequestMessage::Mode::Player) + { + response->result = ConnectResponseMessage::Result::SessionClosed; + SendControlPacket(peer, response); + return; + } + // Player is attempting to reconnect. // Hopefully both sides have disconnected completely before they reconnect. // If not, too bad. - if (claimed_player_id >= 0 && IsValidPlayerId(claimed_player_id)) + if (msg->requested_player_id >= 0 && IsValidPlayerId(msg->requested_player_id)) { - Log_ErrorPrintf("Player ID %d is already in use, rejecting connection.", claimed_player_id); - response->result = ControlConnectResponseMessage::Result::PlayerIDInUse; + Log_ErrorPrintf("Player ID %d is already in use, rejecting connection.", msg->requested_player_id); + response->result = ConnectResponseMessage::Result::PlayerIDInUse; SendControlPacket(peer, response); return; } // Any free slots? - const s32 new_player_id = (claimed_player_id >= 0) ? claimed_player_id : GetFreePlayerId(); + const s32 new_player_id = (msg->requested_player_id >= 0) ? msg->requested_player_id : GetFreePlayerId(); if (new_player_id < 0) { Log_ErrorPrintf("Server full, rejecting connection."); - response->result = ControlConnectResponseMessage::Result::ServerFull; + response->result = ConnectResponseMessage::Result::ServerFull; SendControlPacket(peer, response); return; } Log_VerbosePrint( fmt::format("New connection from {} assigned to player ID {}", PeerAddressString(peer), new_player_id).c_str()); - response->result = ControlConnectResponseMessage::Result::Success; + response->result = ConnectResponseMessage::Result::Success; response->player_id = new_player_id; SendControlPacket(peer, response); // Set up their player slot. Assert(s_num_players < MAX_PLAYERS); s_peers[new_player_id].peer = peer; + s_peers[new_player_id].nickname = msg->GetNickname(); s_num_players++; // Force everyone to resynchronize with the new player. - Resynchronize(); + Reset(); + + // Notify *after* reset so they have their nickname. + NotifyPlayerJoined(new_player_id); } void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_id) { + if (s_state == SessionState::Connecting) + { + if (peer == s_peers[s_host_player_id].peer) + { + SendConnectRequest(); + return; + } + else + { + Log_ErrorPrintf( + fmt::format("Unexpected connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id) + .c_str()); + enet_peer_disconnect_now(peer, 0); + return; + } + } + Log_VerbosePrint( fmt::format("New peer connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id) .c_str()); // shouldn't ever get a non-host connection without a valid ID - if (claimed_player_id < 0 || claimed_player_id >= MAX_PLAYERS) + if (claimed_player_id < 0 || claimed_player_id >= MAX_PLAYERS || claimed_player_id == s_player_id) { Log_ErrorPrintf("Invalid claimed_player_id %d", claimed_player_id); enet_peer_disconnect_now(peer, 0); @@ -789,59 +917,98 @@ void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_i s_peers[claimed_player_id].peer = peer; } +void Netplay::SendConnectRequest() +{ + DebugAssert(!IsHost()); + + Log_DevPrintf("Sending connect request to host with player id %d", s_player_id); + + auto pkt = NewControlPacket(); + pkt->mode = ConnectRequestMessage::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)); + StringUtil::Strlcpy(pkt->nickname, s_local_nickname, std::size(pkt->nickname)); + SendControlPacket(s_peers[s_host_player_id].peer, pkt); +} + +void Netplay::UpdateConnectingState() +{ + if (s_reset_start_time.GetTimeSeconds() >= MAX_CONNECT_TIME) + { + CloseSessionWithError(Host::TranslateStdString("Netplay", "Timed out connecting to server.")); + return; + } + + // still waiting for connection to host.. + PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16)); + Host::DisplayLoadingScreen("Connecting to host..."); + Host::PumpMessagesOnCPUThread(); +} + void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt) { - // TODO: error handling - Assert(player_id == s_host_player_id && pkt->dataLength >= sizeof(ControlConnectResponseMessage)); - - const ControlConnectResponseMessage* msg = reinterpret_cast(pkt->data); - if (msg->result != ControlConnectResponseMessage::Result::Success) + if (s_state != SessionState::Connecting) { - Log_ErrorPrintf("Connection failed %u", msg->result); - Panic("Connection failed"); + Log_ErrorPrintf("Received unexpected connect response from player %d", player_id); + return; + } + + const ConnectResponseMessage* msg = CheckReceivedPacket(player_id, pkt); + if (msg->result != ConnectResponseMessage::Result::Success) + { + CloseSessionWithError( + fmt::format("Connection rejected by server with error code {}", static_cast(msg->result))); return; } // Still need to wait for synchronization Log_InfoPrintf("Connected to host, we were assigned player ID %d", msg->player_id); s_player_id = msg->player_id; + s_state = SessionState::Resetting; + s_reset_players.reset(); + s_reset_start_time.Reset(); } void Netplay::HandlePeerDisconnectionAsHost(s32 player_id) { Log_InfoPrintf("Player %d disconnected from host, reclaiming their slot", player_id); - - Assert(s_peers[player_id].peer); - enet_peer_disconnect_now(s_peers[player_id].peer, 0); - s_peers[player_id].peer = nullptr; - s_num_players--; - - // TODO: We *could* avoid the resync here and just tell all players to drop old mate - Resynchronize(); + DropPlayer(player_id, DropPlayerReason::DisconnectedFromHost); } void Netplay::HandlePeerDisconnectionAsNonHost(s32 player_id) { - Panic("Disconnected non server peer, FIXME"); + Log_InfoPrintf("Lost connection with player %d", player_id); + if (player_id == s_host_player_id) + { + // TODO: Automatically try to reconnect to the host with our existing player ID. + CloseSessionWithError(Host::TranslateStdString("Netplay", "Lost connection to host")); + return; + } + + // tell the host we dropped a connection, let them deal with it.. + RequestReset(ResetRequestMessage::Reason::ConnectionLost, player_id); } -void Netplay::Resynchronize() +void Netplay::Reset() { Assert(IsHost()); - Log_VerbosePrintf("Resynchronizing..."); + Log_VerbosePrintf("Resetting..."); // Use the current system state, whatever that is. // TODO: This save state has the bloody path to the disc in it. We need a new save state format. + // We also want to use maximum compression. GrowableMemoryByteStream state(nullptr, System::MAX_SAVE_STATE_SIZE); if (!System::SaveStateToStream(&state, 0, SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD)) Panic("Failed to save state..."); const u32 state_data_size = static_cast(state.GetPosition()); - ControlSynchronizeSessionMessage header = {}; - header.header.type = ControlMessage::SynchronizeSession; - header.header.size = sizeof(ControlSynchronizeSessionMessage) + state_data_size; + ResetMessage header = {}; + header.header.type = ControlMessage::Reset; + header.header.size = sizeof(ResetMessage) + state_data_size; header.state_data_size = state_data_size; + header.cookie = ++s_reset_cookie; // Fill in player info. header.num_players = s_num_players; @@ -855,6 +1022,7 @@ void Netplay::Resynchronize() // TODO: This is wrong, so wrong.... header.players[i].controller_port = static_cast(i); + StringUtil::Strlcpy(header.players[i].nickname, s_peers[i].nickname, std::size(header.players[i].nickname)); if (i == s_player_id) { @@ -872,8 +1040,6 @@ void Netplay::Resynchronize() // Distribute sync request to all peers, then wait for them to reload back. // Any GGPO packets will get dropped, since the session's gone temporarily. DestroyGGPOSession(); - s_state = SessionState::Synchronizing; - s_synchronized_players = 1; for (s32 i = 0; i < MAX_PLAYERS; i++) { @@ -883,14 +1049,11 @@ void Netplay::Resynchronize() ENetPacket* pkt = enet_packet_create(nullptr, sizeof(header) + state_data_size, ENET_PACKET_FLAG_RELIABLE); std::memcpy(pkt->data, &header, sizeof(header)); 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); if (rc != 0) - { - // TODO: probably just drop them.. but the others already know their address :/ - Log_ErrorPrintf("Failed to send synchronization request to player %d", i); - Panic("Failed to send sync packet"); - enet_packet_destroy(pkt); - } + Log_ErrorPrintf("enet_peer_send() for synchronization request failed: %d", rc); } // Do a full state reload on the host as well, that way everything (including the GPU) @@ -901,29 +1064,26 @@ void Netplay::Resynchronize() if (!System::LoadStateFromStream(&state, true)) Panic("Failed to reload host state"); - // Might be done already if there's only one player. - CheckForCompleteResynchronize(); + s_state = SessionState::Resetting; + s_reset_players.reset(); + s_reset_players.set(s_player_id); + s_reset_start_time.Reset(); } -void Netplay::HandleSynchronizeSessionMessage(s32 player_id, const ENetPacket* pkt) +void Netplay::HandleResetMessage(s32 player_id, const ENetPacket* pkt) { - static bool is_synchronizing = false; - if (is_synchronizing) + if (player_id != s_host_player_id) { - // TODO: this might happen if someone drops during sync... - Panic("Double sync"); + // This shouldn't ever happen, unless someone's being cheeky. + Log_ErrorPrintf("Dropping reset from non-host player %d", player_id); + return; } - is_synchronizing = true; - // TODO: This should check that the message only comes from the host/player 1. - Assert(s_host_player_id == player_id); - - const ControlSynchronizeSessionMessage* msg = reinterpret_cast(pkt->data); - if (pkt->dataLength < sizeof(ControlSynchronizeSessionMessage) || - pkt->dataLength < (sizeof(ControlSynchronizeSessionMessage) + msg->state_data_size)) + const ResetMessage* msg = reinterpret_cast(pkt->data); + if (pkt->dataLength < sizeof(ResetMessage) || pkt->dataLength < (sizeof(ResetMessage) + msg->state_data_size)) { - // TODO: Shut down the session - Panic("Invalid synchronize session packet"); + CloseSessionWithError(fmt::format("Invalid synchronization request: expected {} bytes, got {} bytes", + sizeof(ResetMessage) + msg->state_data_size, pkt->dataLength)); return; } @@ -933,7 +1093,6 @@ void Netplay::HandleSynchronizeSessionMessage(s32 player_id, const ENetPacket* p Assert(msg->num_players > 1); Log_DevPrintf("Checking connections"); s_num_players = msg->num_players; - s_state = SessionState::Synchronizing; for (s32 i = 0; i < MAX_PLAYERS; i++) { Peer& p = s_peers[i]; @@ -943,15 +1102,22 @@ void Netplay::HandleSynchronizeSessionMessage(s32 player_id, const ENetPacket* p if (p.peer) { Log_WarningPrintf("Dropping connection to player %d", i); - enet_peer_reset(p.peer); + enet_peer_disconnect_now(p.peer, 0); p.peer = nullptr; } continue; } - // Can't connect to ourselves! Or the host, which may not contain a correct address, since it's from itself. - if (i == s_player_id || i == s_host_player_id) + // Can't connect to ourselves! + if (i == s_player_id) + continue; + + // Update nickname. + p.nickname = msg->players[i].GetNickname(); + + // Or the host, which may not contain a correct address, since it's from itself. + if (i == s_host_player_id) continue; // The host should fall into the category where we can reuse. @@ -983,68 +1149,244 @@ void Netplay::HandleSynchronizeSessionMessage(s32 player_id, const ENetPacket* p Panic("Failed to connect to peer on resynchronize"); } - if (!WaitForPeerConnections()) - { - // TODO: proper error handling here - Panic("Failed to reconnect to all peers"); - } - - if (!CreateGGPOSession()) - Panic("Failed to create GGPO session"); - // Load state from packet. - ReadOnlyMemoryByteStream stream(pkt->data + sizeof(ControlSynchronizeSessionMessage), msg->state_data_size); + Log_VerbosePrintf("Loading state from host"); + ReadOnlyMemoryByteStream stream(pkt->data + sizeof(ResetMessage), msg->state_data_size); if (!System::LoadStateFromStream(&stream, true)) Panic("Failed to load state from host"); - // We're done here. - if (!SendControlPacket(player_id, NewControlPacket())) - Panic("Failed to send sync complete control packet"); + s_state = SessionState::Resetting; + s_reset_cookie = msg->cookie; + s_reset_players.reset(); + s_reset_players.set(s_player_id); + s_reset_start_time.Reset(); +} +void Netplay::HandleResetCompleteMessage(s32 player_id, const ENetPacket* pkt) +{ + const ResetCompleteMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg) + return; + + if (s_state != SessionState::Resetting || player_id == s_host_player_id) + { + Log_ErrorPrintf("Received unexpected reset complete from player %d", player_id); + return; + } + else if (s_reset_players.test(player_id)) + { + Log_ErrorPrintf("Received double reset from player %d", player_id); + return; + } + else if (s_reset_cookie != msg->cookie) + { + Log_ErrorPrintf("Incorrect reset cookie sent from player %d", player_id); + return; + } + + Log_DevPrintf("Player %d is now reset and ready", player_id); + s_reset_players.set(player_id); +} + +void Netplay::HandleResumeSessionMessage(s32 player_id, const ENetPacket* pkt) +{ + const ResumeSessionMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg) + return; + + if (s_state != SessionState::Resetting || player_id != s_host_player_id) + { + Log_ErrorPrintf("Received unexpected resume session from player %d", player_id); + return; + } + + Log_DevPrintf("Resuming session"); + CreateGGPOSession(); s_state = SessionState::Running; - is_synchronizing = false; } -void Netplay::HandleSynchronizeCompleteMessage(s32 player_id, const ENetPacket* pkt) +void Netplay::UpdateResetState() { - if (s_state != SessionState::Synchronizing || player_id == s_host_player_id) + if (IsHost()) { - Log_ErrorPrintf("Received unexpected synchronization complete from player %d", player_id); + if (static_cast(s_reset_players.count()) == s_num_players) + { + Log_VerbosePrintf("All players synchronized, resuming!"); + SendControlPacketToAll(NewControlPacket()); + CreateGGPOSession(); + s_state = SessionState::Running; + return; + } + + // connect timeout exceeded? + if (s_reset_start_time.GetTimeSeconds() >= MAX_CONNECT_TIME) + { + // TODO: this should be tweaked, maybe only drop one at a time? + Log_InfoPrintf("Reset timeout, dropping any players who aren't connected"); + for (s32 i = 0; i < MAX_PLAYERS; i++) + { + if (!IsValidPlayerId(i) || s_reset_players.test(i)) + continue; + + // we'll check if we're done again next loop + Log_DevPrintf("Dropping player %d because they didn't connect in time", i); + DropPlayer(i, DropPlayerReason::ConnectTimeout); + } + } + } + else + { + if (static_cast(s_reset_players.count()) != s_num_players) + { + for (s32 i = 0; i < MAX_PLAYERS; i++) + { + if (!IsValidPlayerId(i) || s_reset_players.test(i)) + continue; + + if (s_peers[i].peer->state == ENET_PEER_STATE_CONNECTED) + s_reset_players.set(i); + } + + if (static_cast(s_reset_players.count()) == s_num_players) + { + // now connected to all! + Log_InfoPrintf("Connected to %d players, waiting for host...", s_num_players); + auto pkt = NewControlPacket(); + pkt->cookie = s_reset_cookie; + SendControlPacket(s_host_player_id, pkt); + } + + // cancel ourselves if we didn't get another synchronization request from the host + if (s_reset_start_time.GetTimeSeconds() >= (MAX_CONNECT_TIME * 2.0f)) + { + CloseSessionWithError(Host::TranslateStdString("Netplay", "Failed to connect within timeout")); + return; + } + } + } + + PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16)); + Host::DisplayLoadingScreen("Netplay synchronizing", 0, static_cast(s_reset_players.count()), s_num_players); + Host::PumpMessagesOnCPUThread(); +} + +void Netplay::RequestReset(ResetRequestMessage::Reason reason, s32 causing_player_id /* = 0 */) +{ + Assert(!IsHost()); + + auto pkt = NewControlPacket(); + pkt->reason = reason; + pkt->causing_player_id = causing_player_id; + + Log_DevPrintf("Requesting reset from host due to %s", pkt->ReasonToString().c_str()); + SendControlPacket(s_host_player_id, pkt); +} + +void Netplay::HandleResetRequestMessage(s32 player_id, const ENetPacket* pkt) +{ + const ResetRequestMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg) + return; + + Log_InfoPrintf("Received reset request from player %d due to %s", player_id, msg->ReasonToString().c_str()); + Reset(); +} + +void Netplay::NotifyPlayerJoined(s32 player_id) +{ + if (IsHost()) + { + auto pkt = NewControlPacket(); + pkt->player_id = player_id; + SendControlPacketToAll(pkt); + } + + Host::OnNetplayMessage( + fmt::format(Host::TranslateString("Netplay", "{} is joining the session as player {}.").GetCharArray(), + GetNicknameForPlayer(player_id), player_id)); +} + +void Netplay::HandlePlayerJoinedMessage(s32 player_id, const ENetPacket* pkt) +{ + const PlayerJoinedMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg || player_id != s_host_player_id) + return; + + NotifyPlayerJoined(msg->player_id); +} + +void Netplay::DropPlayer(s32 player_id, DropPlayerReason reason) +{ + Assert(IsValidPlayerId(player_id) && s_host_player_id != player_id && s_player_id != player_id); + DebugAssert(s_peers[player_id].peer); + + Log_InfoPrintf("Dropping player %d", player_id); + + Host::OnNetplayMessage(fmt::format(Host::TranslateString("Netplay", "{} left the session: {}").GetCharArray(), + GetNicknameForPlayer(player_id), DropPlayerReasonToString(reason))); + + enet_peer_disconnect_now(s_peers[player_id].peer, 0); + s_peers[player_id] = {}; + s_num_players--; + + if (!IsHost()) + { + // if we're not the host, the host should send a resynchronize request shortly + // but enter the state early, that way we don't keep sending ggpo stuff... + DestroyGGPOSession(); + s_state = SessionState::Resetting; + } + else + { + // tell who's left to also drop their side + auto pkt = NewControlPacket(); + pkt->reason = reason; + pkt->player_id = player_id; + SendControlPacketToAll(pkt); + + // resync with everyone who's left + Reset(); + } +} + +void Netplay::HandleDropPlayerMessage(s32 player_id, const ENetPacket* pkt) +{ + const DropPlayerMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg) + return; + + if (player_id != s_host_player_id) + { + Log_ErrorPrintf("Received unexpected drop player from player %d", player_id); return; } - // TODO: we should check that one player isn't sending multiple sync done packets... - Log_DevPrintf("Player %d completed synchronization", player_id); - s_synchronized_players++; - CheckForCompleteResynchronize(); + DropPlayer(player_id, msg->reason); } -void Netplay::HandleControlChatMessage(s32 player_id, const ENetPacket* pkt) +void Netplay::HandleCloseSessionMessage(s32 player_id, const ENetPacket* pkt) { - const ControlChatMessage* msg = reinterpret_cast(pkt->data); - if (pkt->dataLength < sizeof(ControlChatMessage) || - pkt->dataLength < (sizeof(ControlChatMessage) + msg->chat_message_size)) - { - // invalid chat message. ignore. + const CloseSessionMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg) return; - } - std::string message(pkt->data + sizeof(ControlChatMessage), - pkt->data + sizeof(ControlChatMessage) + msg->chat_message_size); - - Host::OnNetplayMessage(fmt::format("Player {}: {}", PlayerIdToGGPOHandle(player_id), message)); + Host::ReportErrorAsync(Host::TranslateString("Netplay", "Netplay Session Ended"), msg->ReasonToString()); + RequestCloseSession(msg->reason); } -void Netplay::CheckForCompleteResynchronize() +void Netplay::ShowChatMessage(s32 player_id, const std::string_view& message) { - if (s_synchronized_players == s_num_players) - { - Log_VerbosePrintf("All players synchronized, resuming!"); - if (!CreateGGPOSession()) - Panic("Failed to create GGPO session after sync"); + if (!message.empty()) + Host::OnNetplayMessage(fmt::format("{}: {}", GetNicknameForPlayer(player_id), message)); +} - s_state = SessionState::Running; - } +void Netplay::HandleChatMessage(s32 player_id, const ENetPacket* pkt) +{ + const ChatMessage* msg = CheckReceivedPacket(player_id, pkt); + if (!msg) + return; + + ShowChatMessage(player_id, msg->GetMessage()); } ////////////////////////////////////////////////////////////////////////// @@ -1072,6 +1414,8 @@ void Netplay::SetSettings() // not sure its needed but enabled for now... TODO si.SetBoolValue("GPU", "UseSoftwareRendererForReadbacks", true); + // TODO: PGXP should be the same as the host, as should overclock etc. + Host::Internal::SetNetplaySettingsLayer(&si); System::ApplySettings(false); } @@ -1143,7 +1487,7 @@ void Netplay::Throttle() } // Poll at 2ms throughout the sleep. // This way the network traffic comes through as soon as possible. - const Common::Timer::Value sleep_period = Common::Timer::ConvertMillisecondsToValue(1); + const Common::Timer::Value sleep_period = Common::Timer::ConvertMillisecondsToValue(16); while (s_state == SessionState::Running) { // TODO: make better, we can tell this function to stall until the next frame @@ -1236,35 +1580,17 @@ Netplay::Input Netplay::ReadLocalInput() return inp; } -void Netplay::SendMsg(std::string msg) +void Netplay::SendChatMessage(const std::string_view& msg) { - ControlChatMessage header{}; - const size_t msg_size = msg.size(); + if (msg.empty()) + return; - header.header.type = ControlMessage::ChatMessage; - header.header.size = sizeof(ControlChatMessage) + msg_size; - header.chat_message_size = msg_size; - - ENetPacket* pkt = enet_packet_create(nullptr, sizeof(header) + msg_size, ENET_PACKET_FLAG_RELIABLE); - std::memcpy(pkt->data, &header, sizeof(header)); - std::memcpy(pkt->data + sizeof(header), msg.c_str(), msg_size); - - for (s32 i = 0; i < MAX_PLAYERS; i++) - { - if (!s_peers[i].peer) - continue; - - const int err = enet_peer_send(s_peers[i].peer, ENET_CHANNEL_CONTROL, pkt); - if (err != 0) - { - // failed to send netplay message? just clean it up. - Log_ErrorPrint("Failed to send netplay message"); - enet_packet_destroy(pkt); - } - } + auto pkt = NewControlPacket(sizeof(ChatMessage) + static_cast(msg.length())); + std::memcpy(pkt.pkt->data + sizeof(ChatMessage), msg.data(), msg.length()); + SendControlPacketToAll(pkt); // add own netplay message locally to netplay messages - Host::OnNetplayMessage(fmt::format("Player {}: {}", s_local_handle, msg)); + ShowChatMessage(s_player_id, msg); } GGPOErrorCode Netplay::SyncInput(Netplay::Input inputs[2], int* disconnect_flags) @@ -1317,7 +1643,7 @@ void Netplay::StartNetplaySession(s32 local_handle, u16 local_port, std::string& } else if (IsHost()) { - // Load savestate if available and only when you are the host. + // Load savestate if available and only when you are the host. // the other peers will get state from the host std::string save = EmuFolders::SaveStates + "/netplay/" + System::GetRunningSerial() + ".sav"; System::LoadState(save.c_str()); @@ -1348,20 +1674,13 @@ void Netplay::ExecuteNetplay() { case SessionState::Connecting: { - // still waiting for connection to host.. - // TODO: add a timeout here. - PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16)); - Host::DisplayLoadingScreen("Connecting to host..."); - Host::PumpMessagesOnCPUThread(); + UpdateConnectingState(); continue; } - case SessionState::Synchronizing: + case SessionState::Resetting: { - // only happens on host, when waiting for clients to report back - PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16)); - Host::DisplayLoadingScreen("Netplay synchronizing", 0, s_synchronized_players, s_num_players); - Host::PumpMessagesOnCPUThread(); + UpdateResetState(); continue; } @@ -1381,6 +1700,12 @@ void Netplay::ExecuteNetplay() continue; } + case SessionState::ClosingSession: + { + CloseSession(); + break; + } + default: case SessionState::Initializing: case SessionState::Inactive: @@ -1472,17 +1797,17 @@ bool Netplay::NpOnEventCb(void* ctx, GGPOEvent* ev) switch (ev->code) { case GGPOEventCode::GGPO_EVENTCODE_CONNECTED_TO_PEER: - Host::OnNetplayMessage(fmt::format("Netplay Connected To Player: {}", ev->u.connected.player)); + Log_InfoPrintf("GGPO connected to player: %d", ev->u.connected.player); break; case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER: - Host::OnNetplayMessage( - fmt::format("Netplay Synchronzing: {}/{}", ev->u.synchronizing.count, ev->u.synchronizing.total)); + Log_InfoPrintf("GGPO synchronizing with player %d: %d/%d", ev->u.synchronizing.player, ev->u.synchronizing.count, + ev->u.synchronizing.total); break; case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER: - Host::OnNetplayMessage(fmt::format("Netplay Synchronized With Player: {}", ev->u.synchronized.player)); + Log_InfoPrintf("GGPO synchronized with player: %d", ev->u.synchronized.player); break; case GGPOEventCode::GGPO_EVENTCODE_RUNNING: - Host::OnNetplayMessage("Netplay Is Running"); + Log_InfoPrintf("GGPO running"); break; case GGPOEventCode::GGPO_EVENTCODE_TIMESYNC: HandleTimeSyncEvent(ev->u.timesync.frames_ahead, ev->u.timesync.timeSyncPeriodInFrames); @@ -1494,7 +1819,7 @@ bool Netplay::NpOnEventCb(void* ctx, GGPOEvent* ev) ev->u.desync.remoteChecksum)); break; default: - Host::OnNetplayMessage(fmt::format("Netplay Event Code: {}", static_cast(ev->code))); + Log_ErrorPrintf("Netplay Event Code: %d", static_cast(ev->code)); break; } diff --git a/src/core/netplay.h b/src/core/netplay.h index 985d5cd41..49cd8ed24 100644 --- a/src/core/netplay.h +++ b/src/core/netplay.h @@ -9,8 +9,15 @@ enum : s32 { // Maximum number of emulated controllers. MAX_PLAYERS = 2, + // Maximum netplay prediction frames MAX_ROLLBACK_FRAMES = 8, + + // Maximum length of a nickname + MAX_NICKNAME_LENGTH = 128, + + // Maximum name of password for session + MAX_SESSION_PASSWORD_LENGTH = 128, }; enum : u8 @@ -29,14 +36,14 @@ bool IsActive(); /// Frees up resources associated with the current netplay session. /// Should only be called by System::ShutdownSystem(). -void CloseSession(); +void SystemDestroyed(); /// Runs the VM and netplay loop. when the netplay loop cancels it switches to normal execute mode. void ExecuteNetplay(); void CollectInput(u32 slot, u32 bind, float value); -void SendMsg(std::string msg); +void SendChatMessage(const std::string_view& msg); s32 GetPing(); u32 GetMaxPrediction(); diff --git a/src/core/netplay_packets.h b/src/core/netplay_packets.h new file mode 100644 index 000000000..691f250c5 --- /dev/null +++ b/src/core/netplay_packets.h @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2023 Connor McLaughlin and contributors. +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once + +#include "host.h" +#include "types.h" + +#include "fmt/format.h" + +namespace Netplay { + +enum class SessionState +{ + Inactive, + Initializing, + Connecting, + Resetting, + Running, + ClosingSession, +}; + +enum class ControlMessage : u32 +{ + // host->player + ConnectResponse, + Reset, + ResumeSession, + PlayerJoined, + DropPlayer, + CloseSession, + + // player->host + ConnectRequest, + ResetComplete, + ResetRequest, + + // bi-directional + SetNickname, + ChatMessage, +}; + +enum class DropPlayerReason : u32 +{ + ConnectTimeout, + DisconnectedFromHost, +}; + +#pragma pack(push, 1) +struct ControlMessageHeader +{ + ControlMessage type; + u32 size; +}; + +struct ConnectRequestMessage +{ + enum class Mode + { + Player, + Spectator, + }; + + ControlMessageHeader header; + + Mode mode; + s32 requested_player_id; + char nickname[MAX_NICKNAME_LENGTH]; + char session_password[MAX_SESSION_PASSWORD_LENGTH]; + + std::string_view GetNickname() const + { + const size_t len = strnlen(nickname, std::size(nickname)); + return std::string_view(nickname, len); + } + + std::string_view GetSessionPassword() const + { + const size_t len = strnlen(session_password, std::size(session_password)); + return std::string_view(session_password, len); + } + + static ControlMessage MessageType() { return ControlMessage::ConnectRequest; } +}; + +struct ConnectResponseMessage +{ + enum class Result : u32 + { + Success = 0, + ServerFull, + PlayerIDInUse, + SessionClosed, + }; + + ControlMessageHeader header; + + Result result; + s32 player_id; + + static ControlMessage MessageType() { return ControlMessage::ConnectResponse; } +}; + +struct ResetMessage +{ + struct PlayerAddress + { + u32 host; + u16 port; + s16 controller_port; // -1 if not present + char nickname[MAX_NICKNAME_LENGTH]; + + std::string_view GetNickname() const + { + const size_t len = strnlen(nickname, std::size(nickname)); + return std::string_view(nickname, len); + } + }; + + ControlMessageHeader header; + u32 cookie; + s32 num_players; + PlayerAddress players[MAX_PLAYERS]; + u32 state_data_size; + // state_data_size bytes of state data follows + + static ControlMessage MessageType() { return ControlMessage::Reset; } +}; + +struct ResetCompleteMessage +{ + ControlMessageHeader header; + + u32 cookie; + + static ControlMessage MessageType() { return ControlMessage::ResetComplete; } +}; + +struct ResumeSessionMessage +{ + ControlMessageHeader header; + + static ControlMessage MessageType() { return ControlMessage::ResumeSession; } +}; + +struct PlayerJoinedMessage +{ + ControlMessageHeader header; + s32 player_id; + + static ControlMessage MessageType() { return ControlMessage::PlayerJoined; } +}; + +struct DropPlayerMessage +{ + ControlMessageHeader header; + DropPlayerReason reason; + s32 player_id; + + static ControlMessage MessageType() { return ControlMessage::DropPlayer; } +}; + +struct ResetRequestMessage +{ + enum class Reason : u32 + { + ConnectionLost, + }; + + ControlMessageHeader header; + Reason reason; + s32 causing_player_id; + + std::string ReasonToString() const + { + switch (reason) + { + case Reason::ConnectionLost: + return fmt::format(Host::TranslateString("Netplay", "Connection lost to player {}.").GetCharArray(), + causing_player_id); + default: + return "Unknown"; + } + } + + static ControlMessage MessageType() { return ControlMessage::ResetRequest; } +}; + +struct CloseSessionMessage +{ + enum class Reason : u32 + { + HostRequest, + HostShutdown, + }; + + ControlMessageHeader header; + Reason reason; + + std::string ReasonToString() const + { + switch (reason) + { + case Reason::HostRequest: + return Host::TranslateStdString("Netplay", "Session closed due to host request."); + + case Reason::HostShutdown: + return Host::TranslateStdString("Netplay", "Session closed due to host shutdown."); + + default: + return "Unknown"; + } + } + + static ControlMessage MessageType() { return ControlMessage::CloseSession; } +}; + +struct SetNicknameMessage +{ + ControlMessageHeader header; + + static ControlMessage MessageType() { return ControlMessage::SetNickname; } +}; + +struct ChatMessage +{ + ControlMessageHeader header; + + std::string_view GetMessage() const + { + return (header.size > sizeof(ChatMessage)) ? + std::string_view(reinterpret_cast(this) + sizeof(ChatMessage), + header.size - sizeof(ChatMessage)) : + std::string_view(); + } + + static ControlMessage MessageType() { return ControlMessage::ChatMessage; } +}; +#pragma pack(pop) + +} // namespace Netplay \ No newline at end of file diff --git a/src/core/system.cpp b/src/core/system.cpp index a32ac95d2..8acd5f292 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -3757,7 +3757,7 @@ void System::ShutdownSystem(bool save_resume_state) return; if (Netplay::IsActive()) - Netplay::CloseSession(); + Netplay::SystemDestroyed(); if (save_resume_state) SaveResumeState(); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index c68aea10b..ba8ef559b 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -2227,6 +2227,9 @@ int main(int argc, char* argv[]) { Host::RunOnCPUThread([]() { const bool first = (s_netplay_test == 0); + if (!first) + QtHost::RunOnUIThread([]() { g_main_window->move(g_main_window->pos() + QPoint(500, 0)); }); + const int h = first ? 1 : 2; const int nh = first ? 2 : 1; const int port_base = 31200; diff --git a/src/frontend-common/imgui_netplay.cpp b/src/frontend-common/imgui_netplay.cpp index 09a4d7ce2..65e94e122 100644 --- a/src/frontend-common/imgui_netplay.cpp +++ b/src/frontend-common/imgui_netplay.cpp @@ -97,7 +97,7 @@ void ImGuiManager::DrawNetplayMessages() const float spacing = 5.0f * scale; const float msg_spacing = 2.0f * scale; ImFont* font = ImGuiManager::GetFixedFont(); - float position_y = io.DisplaySize.y - margin - ((spacing + font->FontSize) * 2.0f) - font->FontSize; + float position_y = io.DisplaySize.y - margin - (100.0f * scale) - font->FontSize - spacing; ImDrawList* dl = ImGui::GetBackgroundDrawList(); // drop expired messages.. because of the reverse iteration below, we can't do it in there :/ @@ -121,11 +121,9 @@ void ImGuiManager::DrawNetplayMessages() const char* text_end = text_start + iter->first.length(); const ImVec2 text_size = font->CalcTextSizeA(font->FontSize, io.DisplaySize.x, 0.0f, text_start, text_end, nullptr); - dl->AddText(font, font->FontSize, - ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset), + dl->AddText(font, font->FontSize, ImVec2(margin + shadow_offset, position_y + shadow_offset), IM_COL32(0, 0, 0, shadow_alpha), text_start, text_end); - dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), - IM_COL32(255, 255, 255, alpha), text_start, text_end); + dl->AddText(font, font->FontSize, ImVec2(margin, position_y), IM_COL32(255, 255, 255, alpha), text_start, text_end); position_y -= text_size.y + msg_spacing; } @@ -144,16 +142,13 @@ void ImGuiManager::DrawNetplayStats() const float margin = 10.0f * scale; const float spacing = 5.0f * scale; ImFont* font = ImGuiManager::GetFixedFont(); - const float position_y = ImGui::GetIO().DisplaySize.y - margin - font->FontSize - spacing - font->FontSize; + const float position_y = ImGui::GetIO().DisplaySize.y - margin - (100.0f * scale); ImDrawList* dl = ImGui::GetBackgroundDrawList(); - ImVec2 text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), -1.0f, text, - text.GetCharArray() + text.GetLength(), nullptr); - dl->AddText(font, font->FontSize, - ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset), - IM_COL32(0, 0, 0, 100), text, text.GetCharArray() + text.GetLength()); - dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), - IM_COL32(255, 255, 255, 255), text, text.GetCharArray() + text.GetLength()); + dl->AddText(font, font->FontSize, ImVec2(margin + shadow_offset, position_y + shadow_offset), IM_COL32(0, 0, 0, 100), + text, text.GetCharArray() + text.GetLength()); + dl->AddText(font, font->FontSize, ImVec2(margin, position_y), IM_COL32(255, 255, 255, 255), text, + text.GetCharArray() + text.GetLength()); } void ImGuiManager::DrawNetplayChatDialog() @@ -178,7 +173,7 @@ void ImGuiManager::DrawNetplayChatDialog() // sending netplay message if (send_message && !s_netplay_chat_message.empty()) - Netplay::SendMsg(s_netplay_chat_message); + Netplay::SendChatMessage(s_netplay_chat_message); const ImGuiIO& io = ImGui::GetIO(); const ImGuiStyle& style = ImGui::GetStyle();