From e1e2dcd435b44ea9d9f02cece3158272c5bd7b00 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 16 May 2023 01:01:11 +1000 Subject: [PATCH] Netplay: Send session details to clients --- src/core/bios.cpp | 29 +++ src/core/bios.h | 3 + src/core/core.vcxproj | 1 + src/core/core.vcxproj.filters | 3 +- src/core/netplay.cpp | 266 +++++++++++++++++--------- src/core/netplay.h | 2 - src/core/netplay_packets.h | 44 ++++- src/core/pad.cpp | 7 +- src/core/system.cpp | 180 ++++++++++------- src/core/system.h | 6 +- src/duckstation-qt/qthost.cpp | 33 ++-- src/frontend-common/imgui_netplay.cpp | 1 - 12 files changed, 383 insertions(+), 192 deletions(-) diff --git a/src/core/bios.cpp b/src/core/bios.cpp index 4844b155a..f27a97974 100644 --- a/src/core/bios.cpp +++ b/src/core/bios.cpp @@ -376,6 +376,35 @@ std::optional> BIOS::FindBIOSImageInDirectory(ConsoleRegion regi return fallback_image; } +std::string BIOS::FindBIOSPathWithHash(const char* directory, const Hash& hash) +{ + FileSystem::FindResultsArray files; + FileSystem::FindFiles(directory, "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &files); + + std::string ret; + + for (FILESYSTEM_FIND_DATA& fd : files) + { + if (fd.Size != BIOS_SIZE && fd.Size != BIOS_SIZE_PS2 && fd.Size != BIOS_SIZE_PS3) + continue; + + std::string full_path(Path::Combine(directory, fd.FileName)); + std::optional found_image = LoadImageFromFile(full_path.c_str()); + if (!found_image) + continue; + + const BIOS::Hash found_hash = GetImageHash(found_image.value()); + if (found_hash == hash) + { + ret = std::move(full_path); + break; + } + } + + return ret; +} + std::vector> BIOS::FindBIOSImagesInDirectory(const char* directory) { std::vector> results; diff --git a/src/core/bios.h b/src/core/bios.h index caa4d026b..477431928 100644 --- a/src/core/bios.h +++ b/src/core/bios.h @@ -81,6 +81,9 @@ std::optional> GetBIOSImage(ConsoleRegion region); /// BIOS image within 512KB and 4MB will be used. std::optional> FindBIOSImageInDirectory(ConsoleRegion region, const char* directory); +/// Returns a BIOS image which matches the specified hash. +std::string FindBIOSPathWithHash(const char* directory, const BIOS::Hash& hash); + /// Returns a list of filenames and descriptions for BIOS images in a directory. std::vector> FindBIOSImagesInDirectory(const char* directory); diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index a80cc475d..c766291c9 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -140,6 +140,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 9524a6535..d8e713efa 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -129,5 +129,6 @@ + - + \ No newline at end of file diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp index eadcd878e..1a9dbd3c4 100644 --- a/src/core/netplay.cpp +++ b/src/core/netplay.cpp @@ -1,4 +1,5 @@ #include "netplay.h" +#include "bios.h" #include "common/byte_stream.h" #include "common/file_system.h" #include "common/gpu_texture.h" @@ -13,9 +14,11 @@ #include "netplay_packets.h" #include "pad.h" #include "save_state_version.h" +#include "settings.h" #include "spu.h" #include "system.h" #include +#include #include #include #include @@ -30,6 +33,9 @@ Log_SetChannel(Netplay); #include "enet/enet.h" #include "ggponet.h" +// TODO: We don't want a core->frontend-common dependency. I'll move GameList to core at some point... +#include "frontend-common/game_list.h" + namespace Netplay { using SaveStateBuffer = std::unique_ptr; @@ -57,7 +63,7 @@ static void SetInputs(Input inputs[2]); static void SetSettings(); -static bool CreateSystem(std::string game_path, bool hosting); +static bool CreateDummySystem(); static void CloseSessionWithError(const std::string_view& message); static void RequestCloseSession(CloseSessionMessage::Reason reason); @@ -85,6 +91,7 @@ static void SendConnectRequest(); 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 HandleJoinResponseMessage(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); @@ -297,7 +304,7 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re Log_ErrorPrintf("Can't host a netplay session without a valid VM"); return false; } - else if (!is_hosting && !CreateSystem(std::string(), false)) + else if (!is_hosting && !CreateDummySystem()) { Log_ErrorPrintf("Failed to create VM for joining session"); return false; @@ -403,27 +410,16 @@ bool Netplay::IsActive() return (s_state >= SessionState::Initializing && s_state <= SessionState::Running); } -bool Netplay::CreateSystem(std::string game_path, bool is_hosting) +bool Netplay::CreateDummySystem() { // close system if its already running if (System::IsValid()) System::ShutdownSystem(false); // fast boot the selected game and wait for the other player - auto param = SystemBootParameters(std::move(game_path)); - param.override_fast_boot = true; - if (!System::BootSystem(param)) + if (!System::BootSystem(SystemBootParameters())) return false; - if (is_hosting) - { - // Fast Forward to Game Start if needed. - SPU::SetAudioOutputMuted(true); - while (System::GetInternalFrameNumber() < 2) - System::RunFrame(); - SPU::SetAudioOutputMuted(false); - } - return true; } @@ -589,7 +585,7 @@ void Netplay::HandleEnetEvent(const ENetEvent* event) if (player_id < 0) { // If it's a new connection, we need to handle connection request messages. - if (event->channelID == ENET_CHANNEL_CONTROL) + if (event->channelID == ENET_CHANNEL_CONTROL && IsHost()) HandleMessageFromNewPeer(event->peer, event->packet); enet_packet_destroy(event->packet); return; @@ -786,6 +782,10 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt) HandleConnectResponseMessage(player_id, pkt); break; + case ControlMessage::JoinResponse: + HandleJoinResponseMessage(player_id, pkt); + break; + case ControlMessage::Reset: HandleResetMessage(player_id, pkt); break; @@ -826,15 +826,125 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt) void Netplay::HandlePeerConnectionAsHost(ENetPeer* peer) { - // 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()); + + // send them the session details + const std::string& game_title = System::GetGameTitle(); + const std::string& game_serial = System::GetGameSerial(); + auto pkt = NewControlPacket( + sizeof(ConnectResponseMessage) + static_cast(game_serial.length()) + static_cast(game_title.length())); + pkt->num_players = s_num_players; + pkt->max_players = MAX_PLAYERS; + pkt->console_region = System::GetRegion(); + pkt->game_title_length = static_cast(game_title.length()); + pkt->game_serial_length = static_cast(game_serial.length()); + pkt->game_hash = System::GetGameHash(); + pkt->bios_hash = System::GetBIOSHash(); + pkt->was_fast_booted = System::WasFastBooted(); + std::memcpy(pkt.pkt->data + sizeof(ConnectResponseMessage), game_serial.c_str(), pkt->game_serial_length); + std::memcpy(pkt.pkt->data + sizeof(ConnectResponseMessage) + pkt->game_serial_length, game_title.c_str(), + pkt->game_title_length); + SendControlPacket(peer, pkt); +} + +void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt) +{ + const ConnectResponseMessage* msg = CheckReceivedPacket(-1, pkt); + if (!msg || player_id != s_host_player_id) + { + Log_ErrorPrintf("Received unexpected connect response from player %d", player_id); + return; + } + + // ignore these messages when reconnecting + if (s_state != SessionState::Connecting) + { + Log_DevPrintf("Ignoring connect response because we're not initially connecting"); + return; + } + + if (!msg->Validate()) + { + CloseSessionWithError(Host::TranslateStdString("Netplay", "Cannot join session: Invalid details recieved.")); + return; + } + + Log_InfoPrintf("Received session details from host: "); + Log_InfoPrintf(" Console Region: %s", Settings::GetConsoleRegionDisplayName(msg->console_region)); + Log_InfoPrintf(" BIOS Hash: %s%s", msg->bios_hash.ToString().c_str(), msg->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); + + // Find a matching BIOS. + const std::string bios_path = BIOS::FindBIOSPathWithHash(EmuFolders::Bios.c_str(), msg->bios_hash); + if (bios_path.empty()) + { + CloseSessionWithError(fmt::format( + Host::TranslateString("Netplay", "Cannot join session: Unable to find BIOS with hash {}.").GetCharArray(), + msg->bios_hash.ToString())); + return; + } + + // Find the matching game. + const GameList::Entry* entry = GameList::GetEntryBySerialAndHash(msg->GetGameSerial(), msg->game_hash); + if (!entry) + { + CloseSessionWithError(fmt::format( + Host::TranslateString("Netplay", "Cannot join session: Unable to find game \"{}\".\nSerial: {}\nHash: {}") + .GetCharArray(), + msg->GetGameTitle(), msg->GetGameSerial(), System::GetGameHashId(msg->game_hash))); + return; + } + + Log_InfoPrintf("Found matching BIOS: %s", bios_path.c_str()); + Log_InfoPrintf("Found matching game: %s", entry->path.c_str()); + + // Reboot created system with host details. + if (!System::ReinitializeSystem(msg->console_region, bios_path.c_str(), entry->path.c_str(), msg->was_fast_booted)) + { + CloseSessionWithError(Host::TranslateStdString("Netplay", "Cannot join session: Failed to reinitialize system.")); + return; + } + + // We're ready to go, send the connection request. + SendConnectRequest(); +} + +void Netplay::SendConnectRequest() +{ + DebugAssert(!IsHost()); + + Log_DevPrintf("Sending connect request to host with player id %d", s_player_id); + + auto pkt = NewControlPacket(); + pkt->mode = 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)); + 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::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt) { - const ConnectRequestMessage* msg = CheckReceivedPacket(-1, pkt); - if (!msg || msg->header.type != ControlMessage::ConnectRequest) + const JoinRequestMessage* msg = CheckReceivedPacket(-1, pkt); + if (!msg || msg->header.type != ControlMessage::JoinRequest) { Log_WarningPrintf("Received unknown packet from unknown player"); enet_peer_reset(peer); @@ -845,13 +955,13 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt) msg->requested_player_id) .c_str()); - PacketWrapper response = NewControlPacket(); + 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) + if (msg->mode != JoinRequestMessage::Mode::Player) { - response->result = ConnectResponseMessage::Result::SessionClosed; + response->result = JoinResponseMessage::Result::SessionClosed; SendControlPacket(peer, response); return; } @@ -862,7 +972,7 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt) if (msg->requested_player_id >= 0 && IsValidPlayerId(msg->requested_player_id)) { Log_ErrorPrintf("Player ID %d is already in use, rejecting connection.", msg->requested_player_id); - response->result = ConnectResponseMessage::Result::PlayerIDInUse; + response->result = JoinResponseMessage::Result::PlayerIDInUse; SendControlPacket(peer, response); return; } @@ -872,14 +982,14 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt) if (new_player_id < 0) { Log_ErrorPrintf("Server full, rejecting connection."); - response->result = ConnectResponseMessage::Result::ServerFull; + response->result = JoinResponseMessage::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 = ConnectResponseMessage::Result::Success; + response->result = JoinResponseMessage::Result::Success; response->player_id = new_player_id; SendControlPacket(peer, response); @@ -900,19 +1010,16 @@ void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_i { if (s_state == SessionState::Connecting) { - if (peer == s_peers[s_host_player_id].peer) - { - SendConnectRequest(); - return; - } - else + if (peer != s_peers[s_host_player_id].peer) { Log_ErrorPrintf( fmt::format("Unexpected connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id) .c_str()); enet_peer_disconnect_now(peer, 0); - return; } + + // wait for session details + return; } Log_VerbosePrint( @@ -942,45 +1049,16 @@ 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) +void Netplay::HandleJoinResponseMessage(s32 player_id, const ENetPacket* pkt) { if (s_state != SessionState::Connecting) { - Log_ErrorPrintf("Received unexpected connect response from player %d", player_id); + Log_ErrorPrintf("Received unexpected join response from player %d", player_id); return; } - const ConnectResponseMessage* msg = CheckReceivedPacket(player_id, pkt); - if (msg->result != ConnectResponseMessage::Result::Success) + const JoinResponseMessage* msg = CheckReceivedPacket(player_id, pkt); + if (msg->result != JoinResponseMessage::Result::Success) { CloseSessionWithError( fmt::format("Connection rejected by server with error code {}", static_cast(msg->result))); @@ -1025,7 +1103,7 @@ void Netplay::Reset() // 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)) + if (!System::SaveStateToStream(&state, 0, SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD, true)) Panic("Failed to save state..."); const u32 state_data_size = static_cast(state.GetPosition()); @@ -1086,7 +1164,7 @@ void Netplay::Reset() // having a different number of cycles. // CPU::CodeCache::Flush(); state.SeekAbsolute(0); - if (!System::LoadStateFromStream(&state, true)) + if (!System::LoadStateFromStream(&state, true, true)) Panic("Failed to reload host state"); s_state = SessionState::Resetting; @@ -1177,7 +1255,7 @@ void Netplay::HandleResetMessage(s32 player_id, const ENetPacket* pkt) // Load state from packet. Log_VerbosePrintf("Loading state from host"); ReadOnlyMemoryByteStream stream(pkt->data + sizeof(ResetMessage), msg->state_data_size); - if (!System::LoadStateFromStream(&stream, true)) + if (!System::LoadStateFromStream(&stream, true, true)) Panic("Failed to load state from host"); s_state = SessionState::Resetting; @@ -1429,9 +1507,25 @@ void Netplay::SetSettings() si.SetStringValue(Controller::GetSettingsSection(i).c_str(), "Type", Settings::GetControllerTypeName(ControllerType::DigitalController)); } + for (u32 i = MAX_PLAYERS; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + si.SetStringValue(Controller::GetSettingsSection(i).c_str(), "Type", + Settings::GetControllerTypeName(ControllerType::None)); + } + // We want all players to have the same memory card contents. + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + si.SetStringValue("MemoryCards", fmt::format("Card{}Type", i + 1).c_str(), + Settings::GetMemoryCardTypeName((i == 0) ? MemoryCardType::NonPersistent : MemoryCardType::None)); + } // si.SetStringValue("CPU", "ExecutionMode", "Interpreter"); + // BIOS patching must be the same. + si.SetBoolValue("BIOS", "PatchTTYEnable", false); + si.SetBoolValue("BIOS", "PatchFastBoot", true); + si.SetBoolValue("CDROM", "LoadImagePatches", false); + // No runahead or rewind, that'd be a disaster. si.SetIntValue("Main", "RunaheadFrameCount", 0); si.SetBoolValue("Main", "RewindEnable", false); @@ -1642,30 +1736,14 @@ void Netplay::SetInputs(Netplay::Input inputs[2]) } } -void Netplay::TestNetplaySession(s32 local_handle, u16 local_port, const std::string& remote_addr, u16 remote_port, - s32 input_delay, std::string game_path) -{ - const bool is_hosting = (local_handle == 1); - if (!CreateSystem(std::move(game_path), is_hosting)) - { - Log_ErrorPrintf("Failed to create system."); - return; - } - - // create session - std::string nickname = fmt::format("NICKNAME{}", local_handle); - if (!Netplay::Start(is_hosting, std::move(nickname), remote_addr, is_hosting ? local_port : remote_port, input_delay)) - { - // this'll call back to us to shut everything netplay-related down - Log_ErrorPrint("Failed to Create Netplay Session!"); - System::ShutdownSystem(false); - } -} - bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std::string password) { // TODO: Password + // 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. + const s32 input_delay = 1; if (!Netplay::Start(true, std::move(nickname), std::string(), port, input_delay)) @@ -1673,13 +1751,11 @@ bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std CloseSession(); return false; } - else if (IsHost()) - { - // Load savestate if available and only when you are the host. - // the other peers will get state from the host - auto save_path = fmt::format("{}\\netplay\\{}.sav", EmuFolders::SaveStates, System::GetGameSerial()); - System::LoadState(save_path.c_str()); - } + + // Load savestate if available and only when you are the host. + // the other peers will get state from the host + auto save_path = fmt::format("{}\\netplay\\{}.sav", EmuFolders::SaveStates, System::GetGameSerial()); + System::LoadState(save_path.c_str()); return true; } diff --git a/src/core/netplay.h b/src/core/netplay.h index acdc5b896..082b3e60b 100644 --- a/src/core/netplay.h +++ b/src/core/netplay.h @@ -28,8 +28,6 @@ enum : u8 NUM_ENET_CHANNELS, }; -void TestNetplaySession(s32 local_handle, u16 local_port, const std::string& remote_addr, u16 remote_port, - s32 input_delay, std::string game_path); 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); diff --git a/src/core/netplay_packets.h b/src/core/netplay_packets.h index 691f250c5..d53733ecf 100644 --- a/src/core/netplay_packets.h +++ b/src/core/netplay_packets.h @@ -3,6 +3,7 @@ #pragma once +#include "bios.h" #include "host.h" #include "types.h" @@ -24,6 +25,7 @@ enum class ControlMessage : u32 { // host->player ConnectResponse, + JoinResponse, Reset, ResumeSession, PlayerJoined, @@ -31,7 +33,7 @@ enum class ControlMessage : u32 CloseSession, // player->host - ConnectRequest, + JoinRequest, ResetComplete, ResetRequest, @@ -53,7 +55,39 @@ struct ControlMessageHeader u32 size; }; -struct ConnectRequestMessage +struct ConnectResponseMessage +{ + ControlMessageHeader header; + + s32 num_players; + s32 max_players; + u64 game_hash; + u32 game_serial_length; + u32 game_title_length; + ConsoleRegion console_region; + BIOS::Hash bios_hash; + bool was_fast_booted; + + // * game_serial_length + game_title_length follows + // TODO: Include the settings overlays required to match the host config. + + bool Validate() const { return static_cast(console_region) < static_cast(ConsoleRegion::Count); } + + std::string_view GetGameSerial() const + { + return std::string_view(reinterpret_cast(this) + sizeof(ConnectResponseMessage), game_serial_length); + } + + std::string_view GetGameTitle() const + { + return std::string_view(reinterpret_cast(this) + sizeof(ConnectResponseMessage) + game_serial_length, + game_title_length); + } + + static ControlMessage MessageType() { return ControlMessage::ConnectResponse; } +}; + +struct JoinRequestMessage { enum class Mode { @@ -80,10 +114,10 @@ struct ConnectRequestMessage return std::string_view(session_password, len); } - static ControlMessage MessageType() { return ControlMessage::ConnectRequest; } + static ControlMessage MessageType() { return ControlMessage::JoinRequest; } }; -struct ConnectResponseMessage +struct JoinResponseMessage { enum class Result : u32 { @@ -98,7 +132,7 @@ struct ConnectResponseMessage Result result; s32 player_id; - static ControlMessage MessageType() { return ControlMessage::ConnectResponse; } + static ControlMessage MessageType() { return ControlMessage::JoinResponse; } }; struct ResetMessage diff --git a/src/core/pad.cpp b/src/core/pad.cpp index fb366b9b7..7aef80fdd 100644 --- a/src/core/pad.cpp +++ b/src/core/pad.cpp @@ -250,11 +250,12 @@ bool Pad::DoStateController(StateWrapper& sw, u32 i) bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state) { + const bool force_load = Netplay::IsActive(); bool card_present_in_state = static_cast(s_memory_cards[i]); sw.Do(&card_present_in_state); - if (card_present_in_state && !s_memory_cards[i] && g_settings.load_devices_from_save_states) + if (card_present_in_state && !s_memory_cards[i] && g_settings.load_devices_from_save_states && !force_load) { Host::AddFormattedOSDMessage( 20.0f, @@ -269,7 +270,7 @@ bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state) if (card_present_in_state) { - if (sw.IsReading() && !g_settings.load_devices_from_save_states) + if (sw.IsReading() && !g_settings.load_devices_from_save_states && !force_load) { // load memcard into a temporary: If the card datas match, take the one from the savestate // since it has other useful non-data state information. Otherwise take the user's card @@ -281,7 +282,7 @@ bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state) return false; } - if (sw.IsWriting()) + if (sw.IsWriting() || force_load) return true; // all done as far as writes concerned. if (card_from_state) diff --git a/src/core/system.cpp b/src/core/system.cpp index 132a012f1..b56eb30a8 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1364,6 +1364,44 @@ bool System::BootSystem(SystemBootParameters parameters) return true; } +bool System::ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot) +{ + std::optional bios_image = FileSystem::ReadBinaryFile(bios_path); + if (!bios_image.has_value()) + { + Log_ErrorPrintf("Failed to read replacement BIOS at '%s'", bios_path); + return false; + } + + if (!InsertMedia(media_path)) + { + Log_ErrorPrintf("Failed to insert media at '%s'", media_path); + return false; + } + + // Replace the BIOS. + s_bios_hash = BIOS::GetImageHash(bios_image.value()); + s_bios_image_info = BIOS::GetInfoForImage(bios_image.value(), s_bios_hash); + if (s_bios_image_info) + Log_InfoPrintf("Replacing BIOS: %s", s_bios_image_info->description); + else + Log_WarningPrintf("Replacing with an unknown BIOS: %s", s_bios_hash.ToString().c_str()); + + std::memcpy(Bus::g_bios, bios_image->data(), Bus::BIOS_SIZE); + + if (s_bios_image_info && s_bios_image_info->patch_compatible) + BIOS::PatchBIOSEnableTTY(Bus::g_bios, Bus::BIOS_SIZE); + + s_was_fast_booted = false; + if (s_bios_image_info && s_bios_image_info->patch_compatible && fast_boot) + { + BIOS::PatchBIOSFastBoot(Bus::g_bios, Bus::BIOS_SIZE); + s_was_fast_booted = true; + } + + return true; +} + bool System::Initialize(bool force_software_renderer) { g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK); @@ -1942,7 +1980,7 @@ std::string System::GetMediaPathFromSaveState(const char* path) return ret; } -bool System::LoadStateFromStream(ByteStream* state, bool update_display) +bool System::LoadStateFromStream(ByteStream* state, bool update_display, bool ignore_media) { Assert(IsValid()); @@ -1971,90 +2009,93 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display) return false; } - Common::Error error; - std::string media_filename; - std::unique_ptr media; - if (header.media_filename_length > 0) + if (!ignore_media) { - media_filename.resize(header.media_filename_length); - if (!state->SeekAbsolute(header.offset_to_media_filename) || - !state->Read2(media_filename.data(), header.media_filename_length)) + Common::Error error; + std::string media_filename; + std::unique_ptr media; + if (header.media_filename_length > 0) { - return false; - } - - std::unique_ptr old_media = CDROM::RemoveMedia(false); - if (old_media && old_media->GetFileName() == media_filename) - { - Log_InfoPrintf("Re-using same media '%s'", media_filename.c_str()); - media = std::move(old_media); - } - else - { - media = CDImage::Open(media_filename.c_str(), g_settings.cdrom_load_image_patches, &error); - if (!media) + media_filename.resize(header.media_filename_length); + if (!state->SeekAbsolute(header.offset_to_media_filename) || + !state->Read2(media_filename.data(), header.media_filename_length)) { - if (old_media) + return false; + } + + std::unique_ptr old_media = CDROM::RemoveMedia(false); + if (old_media && old_media->GetFileName() == media_filename) + { + Log_InfoPrintf("Re-using same media '%s'", media_filename.c_str()); + media = std::move(old_media); + } + else + { + media = CDImage::Open(media_filename.c_str(), g_settings.cdrom_load_image_patches, &error); + if (!media) { - Host::AddFormattedOSDMessage( - 30.0f, - Host::TranslateString("OSDMessage", "Failed to open CD image from save state '%s': %s. Using " - "existing image '%s', this may result in instability."), - media_filename.c_str(), error.GetCodeAndMessage().GetCharArray(), old_media->GetFileName().c_str()); - media = std::move(old_media); - header.media_subimage_index = media->GetCurrentSubImage(); - } - else - { - Host::ReportFormattedErrorAsync( - "Error", Host::TranslateString("System", "Failed to open CD image '%s' used by save state: %s."), - media_filename.c_str(), error.GetCodeAndMessage().GetCharArray()); - return false; + if (old_media) + { + Host::AddFormattedOSDMessage( + 30.0f, + Host::TranslateString("OSDMessage", "Failed to open CD image from save state '%s': %s. Using " + "existing image '%s', this may result in instability."), + media_filename.c_str(), error.GetCodeAndMessage().GetCharArray(), old_media->GetFileName().c_str()); + media = std::move(old_media); + header.media_subimage_index = media->GetCurrentSubImage(); + } + else + { + Host::ReportFormattedErrorAsync( + "Error", Host::TranslateString("System", "Failed to open CD image '%s' used by save state: %s."), + media_filename.c_str(), error.GetCodeAndMessage().GetCharArray()); + return false; + } } } } - } - UpdateRunningGame(media_filename.c_str(), media.get(), false); + UpdateRunningGame(media_filename.c_str(), media.get(), false); - if (media && header.version >= 51) - { - const u32 num_subimages = media->HasSubImages() ? media->GetSubImageCount() : 1; - if (header.media_subimage_index >= num_subimages || - (media->HasSubImages() && media->GetCurrentSubImage() != header.media_subimage_index && - !media->SwitchSubImage(header.media_subimage_index, &error))) + if (media && header.version >= 51) { - Host::ReportFormattedErrorAsync( - "Error", - Host::TranslateString("System", "Failed to switch to subimage %u in CD image '%s' used by save state: %s."), - header.media_subimage_index + 1u, media_filename.c_str(), error.GetCodeAndMessage().GetCharArray()); - return false; + const u32 num_subimages = media->HasSubImages() ? media->GetSubImageCount() : 1; + if (header.media_subimage_index >= num_subimages || + (media->HasSubImages() && media->GetCurrentSubImage() != header.media_subimage_index && + !media->SwitchSubImage(header.media_subimage_index, &error))) + { + Host::ReportFormattedErrorAsync( + "Error", + Host::TranslateString("System", "Failed to switch to subimage %u in CD image '%s' used by save state: %s."), + header.media_subimage_index + 1u, media_filename.c_str(), error.GetCodeAndMessage().GetCharArray()); + return false; + } + else + { + Log_InfoPrintf("Switched to subimage %u in '%s'", header.media_subimage_index, media_filename.c_str()); + } + } + + CDROM::Reset(); + if (media) + { + const DiscRegion region = GetRegionForImage(media.get()); + CDROM::InsertMedia(std::move(media), region); + if (g_settings.cdrom_load_image_to_ram) + CDROM::PrecacheMedia(); } else { - Log_InfoPrintf("Switched to subimage %u in '%s'", header.media_subimage_index, media_filename.c_str()); + CDROM::RemoveMedia(false); } + + // ensure the correct card is loaded + if (g_settings.HasAnyPerGameMemoryCards()) + UpdatePerGameMemoryCards(); } ClearMemorySaveStates(); - CDROM::Reset(); - if (media) - { - const DiscRegion region = GetRegionForImage(media.get()); - CDROM::InsertMedia(std::move(media), region); - if (g_settings.cdrom_load_image_to_ram) - CDROM::PrecacheMedia(); - } - else - { - CDROM::RemoveMedia(false); - } - - // ensure the correct card is loaded - if (g_settings.HasAnyPerGameMemoryCards()) - UpdatePerGameMemoryCards(); - #ifdef WITH_CHEEVOS // Updating game/loading settings can turn on hardcore mode. Catch this. if (Achievements::ChallengeModeActive()) @@ -2097,7 +2138,8 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display) } bool System::SaveStateToStream(ByteStream* state, u32 screenshot_size /* = 256 */, - u32 compression_method /* = SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE*/) + u32 compression_method /* = SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE*/, + bool ignore_media /* = false*/) { if (IsShutdown()) return false; @@ -2114,7 +2156,7 @@ bool System::SaveStateToStream(ByteStream* state, u32 screenshot_size /* = 256 * StringUtil::Strlcpy(header.title, s_running_game_title.c_str(), sizeof(header.title)); StringUtil::Strlcpy(header.serial, s_running_game_serial.c_str(), sizeof(header.serial)); - if (CDROM::HasMedia()) + if (CDROM::HasMedia() && !ignore_media) { const std::string& media_filename = CDROM::GetMediaFileName(); header.offset_to_media_filename = static_cast(state->GetPosition()); diff --git a/src/core/system.h b/src/core/system.h index 8eda2c988..21f565746 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -223,6 +223,7 @@ void ApplySettings(bool display_osd_messages); bool ReloadGameSettings(bool display_osd_messages); bool BootSystem(SystemBootParameters parameters); +bool ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot); void PauseSystem(bool paused); void ResetSystem(); @@ -239,8 +240,9 @@ struct MemorySaveState }; bool SaveMemoryState(MemorySaveState* mss); bool LoadMemoryState(const MemorySaveState& mss); -bool LoadStateFromStream(ByteStream* stream, bool update_display); -bool SaveStateToStream(ByteStream* state, u32 screenshot_size = 256, u32 compression_method = 0); +bool LoadStateFromStream(ByteStream* stream, bool update_display, bool ignore_media = false); +bool SaveStateToStream(ByteStream* state, u32 screenshot_size = 256, u32 compression_method = 0, + bool ignore_media = false); /// Runs the VM until the CPU execution is canceled. void Execute(); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 30d90ad52..384c8713f 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1078,14 +1078,15 @@ void EmuThread::createNetplaySession(const QString& nickname, qint32 port, qint3 return; } + // need a valid system to make a session + if (!System::IsValid()) + return; + if (!Netplay::CreateSession(nickname.toStdString(), port, max_players, password.toStdString())) { errorReported(tr("Netplay Error"), tr("Failed to create netplay session. The log may contain more information.")); return; } - - // TODO: Fix this junk.. for some reason, it stays sleeping... - g_emu_thread->wakeThread(); } void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port, @@ -1103,9 +1104,6 @@ void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostn errorReported(tr("Netplay Error"), tr("Failed to join netplay session. The log may contain more information.")); return; } - - // TODO: Fix this junk.. for some reason, it stays sleeping... - g_emu_thread->wakeThread(); } void EmuThread::runOnEmuThread(std::function callback) @@ -2228,15 +2226,22 @@ int main(int argc, char* argv[]) const bool first = (s_netplay_test == 0); QtHost::RunOnUIThread([first]() { g_main_window->move(QPoint(first ? 300 : 1400, 500)); }); - const int h = first ? 1 : 2; - const int nh = first ? 2 : 1; - const int port_base = 31200; - std::string remote = "127.0.0.1"; + const int port = 31200; + const QString remote = QStringLiteral("127.0.0.1"); std::string game = "D:\\PSX\\chd\\padtest.chd"; - Netplay::TestNetplaySession(h, port_base + h, remote, port_base + nh, 1, game); - - // TODO: Fix this junk.. for some reason, it stays sleeping... - g_emu_thread->wakeThread(); + const QString nickname = QStringLiteral("NICKNAME%1").arg(s_netplay_test + 1); + if (first) + { + auto params = std::make_shared(std::move(game)); + params->override_fast_boot = true; + params->fast_forward_to_first_frame = true; + g_emu_thread->bootSystem(std::move(params)); + g_emu_thread->createNetplaySession(nickname, port, 2, QString()); + } + else + { + g_emu_thread->joinNetplaySession(nickname, remote, port, QString()); + } }); } diff --git a/src/frontend-common/imgui_netplay.cpp b/src/frontend-common/imgui_netplay.cpp index 65e94e122..8b3d23deb 100644 --- a/src/frontend-common/imgui_netplay.cpp +++ b/src/frontend-common/imgui_netplay.cpp @@ -140,7 +140,6 @@ void ImGuiManager::DrawNetplayStats() const float scale = ImGuiManager::GetGlobalScale(); const float shadow_offset = 1.0f * scale; 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 - (100.0f * scale);