Netplay: Send session details to clients

This commit is contained in:
Stenzek 2023-05-16 01:01:11 +10:00
parent a6a7a1613c
commit e1e2dcd435
12 changed files with 383 additions and 192 deletions

View File

@ -376,6 +376,35 @@ std::optional<std::vector<u8>> BIOS::FindBIOSImageInDirectory(ConsoleRegion regi
return fallback_image; 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<Image> 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<std::pair<std::string, const BIOS::ImageInfo*>> BIOS::FindBIOSImagesInDirectory(const char* directory) std::vector<std::pair<std::string, const BIOS::ImageInfo*>> BIOS::FindBIOSImagesInDirectory(const char* directory)
{ {
std::vector<std::pair<std::string, const ImageInfo*>> results; std::vector<std::pair<std::string, const ImageInfo*>> results;

View File

@ -81,6 +81,9 @@ std::optional<std::vector<u8>> GetBIOSImage(ConsoleRegion region);
/// BIOS image within 512KB and 4MB will be used. /// BIOS image within 512KB and 4MB will be used.
std::optional<std::vector<u8>> FindBIOSImageInDirectory(ConsoleRegion region, const char* directory); std::optional<std::vector<u8>> 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. /// Returns a list of filenames and descriptions for BIOS images in a directory.
std::vector<std::pair<std::string, const BIOS::ImageInfo*>> FindBIOSImagesInDirectory(const char* directory); std::vector<std::pair<std::string, const BIOS::ImageInfo*>> FindBIOSImagesInDirectory(const char* directory);

View File

@ -140,6 +140,7 @@
<ClInclude Include="guncon.h" /> <ClInclude Include="guncon.h" />
<ClInclude Include="negcon.h" /> <ClInclude Include="negcon.h" />
<ClInclude Include="netplay.h" /> <ClInclude Include="netplay.h" />
<ClInclude Include="netplay_packets.h" />
<ClInclude Include="pad.h" /> <ClInclude Include="pad.h" />
<ClInclude Include="controller.h" /> <ClInclude Include="controller.h" />
<ClInclude Include="pcdrv.h" /> <ClInclude Include="pcdrv.h" />

View File

@ -129,5 +129,6 @@
<ClInclude Include="input_types.h" /> <ClInclude Include="input_types.h" />
<ClInclude Include="pcdrv.h" /> <ClInclude Include="pcdrv.h" />
<ClInclude Include="netplay.h" /> <ClInclude Include="netplay.h" />
<ClInclude Include="netplay_packets.h" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,5 @@
#include "netplay.h" #include "netplay.h"
#include "bios.h"
#include "common/byte_stream.h" #include "common/byte_stream.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/gpu_texture.h" #include "common/gpu_texture.h"
@ -13,9 +14,11 @@
#include "netplay_packets.h" #include "netplay_packets.h"
#include "pad.h" #include "pad.h"
#include "save_state_version.h" #include "save_state_version.h"
#include "settings.h"
#include "spu.h" #include "spu.h"
#include "system.h" #include "system.h"
#include <bitset> #include <bitset>
#include <cinttypes>
#include <deque> #include <deque>
#include <gsl/span> #include <gsl/span>
#include <xxhash.h> #include <xxhash.h>
@ -30,6 +33,9 @@ Log_SetChannel(Netplay);
#include "enet/enet.h" #include "enet/enet.h"
#include "ggponet.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 { namespace Netplay {
using SaveStateBuffer = std::unique_ptr<System::MemorySaveState>; using SaveStateBuffer = std::unique_ptr<System::MemorySaveState>;
@ -57,7 +63,7 @@ static void SetInputs(Input inputs[2]);
static void SetSettings(); 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 CloseSessionWithError(const std::string_view& message);
static void RequestCloseSession(CloseSessionMessage::Reason reason); static void RequestCloseSession(CloseSessionMessage::Reason reason);
@ -85,6 +91,7 @@ static void SendConnectRequest();
static void HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt); static void HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt);
static void HandleControlMessage(s32 player_id, const ENetPacket* pkt); static void HandleControlMessage(s32 player_id, const ENetPacket* pkt);
static void HandleConnectResponseMessage(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 HandleResetMessage(s32 player_id, const ENetPacket* pkt);
static void HandleResetCompleteMessage(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 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"); Log_ErrorPrintf("Can't host a netplay session without a valid VM");
return false; return false;
} }
else if (!is_hosting && !CreateSystem(std::string(), false)) else if (!is_hosting && !CreateDummySystem())
{ {
Log_ErrorPrintf("Failed to create VM for joining session"); Log_ErrorPrintf("Failed to create VM for joining session");
return false; return false;
@ -403,27 +410,16 @@ bool Netplay::IsActive()
return (s_state >= SessionState::Initializing && s_state <= SessionState::Running); 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 // close system if its already running
if (System::IsValid()) if (System::IsValid())
System::ShutdownSystem(false); System::ShutdownSystem(false);
// fast boot the selected game and wait for the other player // fast boot the selected game and wait for the other player
auto param = SystemBootParameters(std::move(game_path)); if (!System::BootSystem(SystemBootParameters()))
param.override_fast_boot = true;
if (!System::BootSystem(param))
return false; 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; return true;
} }
@ -589,7 +585,7 @@ void Netplay::HandleEnetEvent(const ENetEvent* event)
if (player_id < 0) if (player_id < 0)
{ {
// If it's a new connection, we need to handle connection request messages. // 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); HandleMessageFromNewPeer(event->peer, event->packet);
enet_packet_destroy(event->packet); enet_packet_destroy(event->packet);
return; return;
@ -786,6 +782,10 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt)
HandleConnectResponseMessage(player_id, pkt); HandleConnectResponseMessage(player_id, pkt);
break; break;
case ControlMessage::JoinResponse:
HandleJoinResponseMessage(player_id, pkt);
break;
case ControlMessage::Reset: case ControlMessage::Reset:
HandleResetMessage(player_id, pkt); HandleResetMessage(player_id, pkt);
break; break;
@ -826,15 +826,125 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt)
void Netplay::HandlePeerConnectionAsHost(ENetPeer* peer) 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... // TODO: we might want to put an idle timeout here...
Log_InfoPrintf(fmt::format("New peer connection from {}", PeerAddressString(peer)).c_str()); 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<ConnectResponseMessage>(
sizeof(ConnectResponseMessage) + static_cast<u32>(game_serial.length()) + static_cast<u32>(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<u32>(game_title.length());
pkt->game_serial_length = static_cast<u32>(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<ConnectResponseMessage>(-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<JoinRequestMessage>();
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) void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
{ {
const ConnectRequestMessage* msg = CheckReceivedPacket<ConnectRequestMessage>(-1, pkt); const JoinRequestMessage* msg = CheckReceivedPacket<JoinRequestMessage>(-1, pkt);
if (!msg || msg->header.type != ControlMessage::ConnectRequest) if (!msg || msg->header.type != ControlMessage::JoinRequest)
{ {
Log_WarningPrintf("Received unknown packet from unknown player"); Log_WarningPrintf("Received unknown packet from unknown player");
enet_peer_reset(peer); enet_peer_reset(peer);
@ -845,13 +955,13 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
msg->requested_player_id) msg->requested_player_id)
.c_str()); .c_str());
PacketWrapper<ConnectResponseMessage> response = NewControlPacket<ConnectResponseMessage>(); PacketWrapper<JoinResponseMessage> response = NewControlPacket<JoinResponseMessage>();
response->player_id = -1; response->player_id = -1;
// TODO: Spectators shouldn't get assigned a real player ID, they should go into a separate peer list. // 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); SendControlPacket(peer, response);
return; return;
} }
@ -862,7 +972,7 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
if (msg->requested_player_id >= 0 && IsValidPlayerId(msg->requested_player_id)) 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); 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); SendControlPacket(peer, response);
return; return;
} }
@ -872,14 +982,14 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
if (new_player_id < 0) if (new_player_id < 0)
{ {
Log_ErrorPrintf("Server full, rejecting connection."); Log_ErrorPrintf("Server full, rejecting connection.");
response->result = ConnectResponseMessage::Result::ServerFull; response->result = JoinResponseMessage::Result::ServerFull;
SendControlPacket(peer, response); SendControlPacket(peer, response);
return; return;
} }
Log_VerbosePrint( Log_VerbosePrint(
fmt::format("New connection from {} assigned to player ID {}", PeerAddressString(peer), new_player_id).c_str()); 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; response->player_id = new_player_id;
SendControlPacket(peer, response); SendControlPacket(peer, response);
@ -900,19 +1010,16 @@ void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_i
{ {
if (s_state == SessionState::Connecting) if (s_state == SessionState::Connecting)
{ {
if (peer == s_peers[s_host_player_id].peer) if (peer != s_peers[s_host_player_id].peer)
{
SendConnectRequest();
return;
}
else
{ {
Log_ErrorPrintf( Log_ErrorPrintf(
fmt::format("Unexpected connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id) fmt::format("Unexpected connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id)
.c_str()); .c_str());
enet_peer_disconnect_now(peer, 0); enet_peer_disconnect_now(peer, 0);
return;
} }
// wait for session details
return;
} }
Log_VerbosePrint( Log_VerbosePrint(
@ -942,45 +1049,16 @@ void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_i
s_peers[claimed_player_id].peer = peer; s_peers[claimed_player_id].peer = peer;
} }
void Netplay::SendConnectRequest() void Netplay::HandleJoinResponseMessage(s32 player_id, const ENetPacket* pkt)
{
DebugAssert(!IsHost());
Log_DevPrintf("Sending connect request to host with player id %d", s_player_id);
auto pkt = NewControlPacket<ConnectRequestMessage>();
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)
{ {
if (s_state != SessionState::Connecting) 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; return;
} }
const ConnectResponseMessage* msg = CheckReceivedPacket<ConnectResponseMessage>(player_id, pkt); const JoinResponseMessage* msg = CheckReceivedPacket<JoinResponseMessage>(player_id, pkt);
if (msg->result != ConnectResponseMessage::Result::Success) if (msg->result != JoinResponseMessage::Result::Success)
{ {
CloseSessionWithError( CloseSessionWithError(
fmt::format("Connection rejected by server with error code {}", static_cast<u32>(msg->result))); fmt::format("Connection rejected by server with error code {}", static_cast<u32>(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. // 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. // We also want to use maximum compression.
GrowableMemoryByteStream state(nullptr, System::MAX_SAVE_STATE_SIZE); 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..."); Panic("Failed to save state...");
const u32 state_data_size = static_cast<u32>(state.GetPosition()); const u32 state_data_size = static_cast<u32>(state.GetPosition());
@ -1086,7 +1164,7 @@ void Netplay::Reset()
// having a different number of cycles. // having a different number of cycles.
// CPU::CodeCache::Flush(); // CPU::CodeCache::Flush();
state.SeekAbsolute(0); state.SeekAbsolute(0);
if (!System::LoadStateFromStream(&state, true)) if (!System::LoadStateFromStream(&state, true, true))
Panic("Failed to reload host state"); Panic("Failed to reload host state");
s_state = SessionState::Resetting; s_state = SessionState::Resetting;
@ -1177,7 +1255,7 @@ void Netplay::HandleResetMessage(s32 player_id, const ENetPacket* pkt)
// Load state from packet. // Load state from packet.
Log_VerbosePrintf("Loading state from host"); Log_VerbosePrintf("Loading state from host");
ReadOnlyMemoryByteStream stream(pkt->data + sizeof(ResetMessage), msg->state_data_size); 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"); Panic("Failed to load state from host");
s_state = SessionState::Resetting; s_state = SessionState::Resetting;
@ -1429,9 +1507,25 @@ void Netplay::SetSettings()
si.SetStringValue(Controller::GetSettingsSection(i).c_str(), "Type", si.SetStringValue(Controller::GetSettingsSection(i).c_str(), "Type",
Settings::GetControllerTypeName(ControllerType::DigitalController)); 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"); // 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. // No runahead or rewind, that'd be a disaster.
si.SetIntValue("Main", "RunaheadFrameCount", 0); si.SetIntValue("Main", "RunaheadFrameCount", 0);
si.SetBoolValue("Main", "RewindEnable", false); 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) bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std::string password)
{ {
// TODO: 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; const s32 input_delay = 1;
if (!Netplay::Start(true, std::move(nickname), std::string(), port, input_delay)) 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(); CloseSession();
return false; return false;
} }
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 // the other peers will get state from the host
auto save_path = fmt::format("{}\\netplay\\{}.sav", EmuFolders::SaveStates, System::GetGameSerial()); auto save_path = fmt::format("{}\\netplay\\{}.sav", EmuFolders::SaveStates, System::GetGameSerial());
System::LoadState(save_path.c_str()); System::LoadState(save_path.c_str());
}
return true; return true;
} }

View File

@ -28,8 +28,6 @@ enum : u8
NUM_ENET_CHANNELS, 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 CreateSession(std::string nickname, s32 port, s32 max_players, std::string password);
bool JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password); bool JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password);

View File

@ -3,6 +3,7 @@
#pragma once #pragma once
#include "bios.h"
#include "host.h" #include "host.h"
#include "types.h" #include "types.h"
@ -24,6 +25,7 @@ enum class ControlMessage : u32
{ {
// host->player // host->player
ConnectResponse, ConnectResponse,
JoinResponse,
Reset, Reset,
ResumeSession, ResumeSession,
PlayerJoined, PlayerJoined,
@ -31,7 +33,7 @@ enum class ControlMessage : u32
CloseSession, CloseSession,
// player->host // player->host
ConnectRequest, JoinRequest,
ResetComplete, ResetComplete,
ResetRequest, ResetRequest,
@ -53,7 +55,39 @@ struct ControlMessageHeader
u32 size; 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;
// <char> * game_serial_length + game_title_length follows
// TODO: Include the settings overlays required to match the host config.
bool Validate() const { return static_cast<unsigned>(console_region) < static_cast<unsigned>(ConsoleRegion::Count); }
std::string_view GetGameSerial() const
{
return std::string_view(reinterpret_cast<const char*>(this) + sizeof(ConnectResponseMessage), game_serial_length);
}
std::string_view GetGameTitle() const
{
return std::string_view(reinterpret_cast<const char*>(this) + sizeof(ConnectResponseMessage) + game_serial_length,
game_title_length);
}
static ControlMessage MessageType() { return ControlMessage::ConnectResponse; }
};
struct JoinRequestMessage
{ {
enum class Mode enum class Mode
{ {
@ -80,10 +114,10 @@ struct ConnectRequestMessage
return std::string_view(session_password, len); 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 enum class Result : u32
{ {
@ -98,7 +132,7 @@ struct ConnectResponseMessage
Result result; Result result;
s32 player_id; s32 player_id;
static ControlMessage MessageType() { return ControlMessage::ConnectResponse; } static ControlMessage MessageType() { return ControlMessage::JoinResponse; }
}; };
struct ResetMessage struct ResetMessage

View File

@ -250,11 +250,12 @@ bool Pad::DoStateController(StateWrapper& sw, u32 i)
bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state) bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state)
{ {
const bool force_load = Netplay::IsActive();
bool card_present_in_state = static_cast<bool>(s_memory_cards[i]); bool card_present_in_state = static_cast<bool>(s_memory_cards[i]);
sw.Do(&card_present_in_state); 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( Host::AddFormattedOSDMessage(
20.0f, 20.0f,
@ -269,7 +270,7 @@ bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state)
if (card_present_in_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 // 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 // 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; return false;
} }
if (sw.IsWriting()) if (sw.IsWriting() || force_load)
return true; // all done as far as writes concerned. return true; // all done as far as writes concerned.
if (card_from_state) if (card_from_state)

View File

@ -1364,6 +1364,44 @@ bool System::BootSystem(SystemBootParameters parameters)
return true; return true;
} }
bool System::ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot)
{
std::optional<BIOS::Image> 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) bool System::Initialize(bool force_software_renderer)
{ {
g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK); g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK);
@ -1942,7 +1980,7 @@ std::string System::GetMediaPathFromSaveState(const char* path)
return ret; return ret;
} }
bool System::LoadStateFromStream(ByteStream* state, bool update_display) bool System::LoadStateFromStream(ByteStream* state, bool update_display, bool ignore_media)
{ {
Assert(IsValid()); Assert(IsValid());
@ -1971,6 +2009,8 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display)
return false; return false;
} }
if (!ignore_media)
{
Common::Error error; Common::Error error;
std::string media_filename; std::string media_filename;
std::unique_ptr<CDImage> media; std::unique_ptr<CDImage> media;
@ -2036,8 +2076,6 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display)
} }
} }
ClearMemorySaveStates();
CDROM::Reset(); CDROM::Reset();
if (media) if (media)
{ {
@ -2054,6 +2092,9 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display)
// ensure the correct card is loaded // ensure the correct card is loaded
if (g_settings.HasAnyPerGameMemoryCards()) if (g_settings.HasAnyPerGameMemoryCards())
UpdatePerGameMemoryCards(); UpdatePerGameMemoryCards();
}
ClearMemorySaveStates();
#ifdef WITH_CHEEVOS #ifdef WITH_CHEEVOS
// Updating game/loading settings can turn on hardcore mode. Catch this. // Updating game/loading settings can turn on hardcore mode. Catch this.
@ -2097,7 +2138,8 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display)
} }
bool System::SaveStateToStream(ByteStream* state, u32 screenshot_size /* = 256 */, 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()) if (IsShutdown())
return false; 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.title, s_running_game_title.c_str(), sizeof(header.title));
StringUtil::Strlcpy(header.serial, s_running_game_serial.c_str(), sizeof(header.serial)); 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(); const std::string& media_filename = CDROM::GetMediaFileName();
header.offset_to_media_filename = static_cast<u32>(state->GetPosition()); header.offset_to_media_filename = static_cast<u32>(state->GetPosition());

View File

@ -223,6 +223,7 @@ void ApplySettings(bool display_osd_messages);
bool ReloadGameSettings(bool display_osd_messages); bool ReloadGameSettings(bool display_osd_messages);
bool BootSystem(SystemBootParameters parameters); bool BootSystem(SystemBootParameters parameters);
bool ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot);
void PauseSystem(bool paused); void PauseSystem(bool paused);
void ResetSystem(); void ResetSystem();
@ -239,8 +240,9 @@ struct MemorySaveState
}; };
bool SaveMemoryState(MemorySaveState* mss); bool SaveMemoryState(MemorySaveState* mss);
bool LoadMemoryState(const MemorySaveState& mss); bool LoadMemoryState(const MemorySaveState& mss);
bool LoadStateFromStream(ByteStream* stream, bool update_display); bool LoadStateFromStream(ByteStream* stream, bool update_display, bool ignore_media = false);
bool SaveStateToStream(ByteStream* state, u32 screenshot_size = 256, u32 compression_method = 0); 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. /// Runs the VM until the CPU execution is canceled.
void Execute(); void Execute();

View File

@ -1078,14 +1078,15 @@ void EmuThread::createNetplaySession(const QString& nickname, qint32 port, qint3
return; return;
} }
// need a valid system to make a session
if (!System::IsValid())
return;
if (!Netplay::CreateSession(nickname.toStdString(), port, max_players, password.toStdString())) 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.")); errorReported(tr("Netplay Error"), tr("Failed to create netplay session. The log may contain more information."));
return; 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, 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.")); errorReported(tr("Netplay Error"), tr("Failed to join netplay session. The log may contain more information."));
return; return;
} }
// TODO: Fix this junk.. for some reason, it stays sleeping...
g_emu_thread->wakeThread();
} }
void EmuThread::runOnEmuThread(std::function<void()> callback) void EmuThread::runOnEmuThread(std::function<void()> callback)
@ -2228,15 +2226,22 @@ int main(int argc, char* argv[])
const bool first = (s_netplay_test == 0); const bool first = (s_netplay_test == 0);
QtHost::RunOnUIThread([first]() { g_main_window->move(QPoint(first ? 300 : 1400, 500)); }); QtHost::RunOnUIThread([first]() { g_main_window->move(QPoint(first ? 300 : 1400, 500)); });
const int h = first ? 1 : 2; const int port = 31200;
const int nh = first ? 2 : 1; const QString remote = QStringLiteral("127.0.0.1");
const int port_base = 31200;
std::string remote = "127.0.0.1";
std::string game = "D:\\PSX\\chd\\padtest.chd"; std::string game = "D:\\PSX\\chd\\padtest.chd";
Netplay::TestNetplaySession(h, port_base + h, remote, port_base + nh, 1, game); const QString nickname = QStringLiteral("NICKNAME%1").arg(s_netplay_test + 1);
if (first)
// TODO: Fix this junk.. for some reason, it stays sleeping... {
g_emu_thread->wakeThread(); auto params = std::make_shared<SystemBootParameters>(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());
}
}); });
} }

View File

@ -140,7 +140,6 @@ void ImGuiManager::DrawNetplayStats()
const float scale = ImGuiManager::GetGlobalScale(); const float scale = ImGuiManager::GetGlobalScale();
const float shadow_offset = 1.0f * scale; const float shadow_offset = 1.0f * scale;
const float margin = 10.0f * scale; const float margin = 10.0f * scale;
const float spacing = 5.0f * scale;
ImFont* font = ImGuiManager::GetFixedFont(); ImFont* font = ImGuiManager::GetFixedFont();
const float position_y = ImGui::GetIO().DisplaySize.y - margin - (100.0f * scale); const float position_y = ImGui::GetIO().DisplaySize.y - margin - (100.0f * scale);